From a18d9d5d43b97e2345236f4e2d16109518b25ef7 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 27 Jul 2022 22:51:04 +0800 Subject: [PATCH 001/128] fix: scroll to top always trigger when tab bar item selected issue. resolve #102 --- .../Root/MainTab/MainTabBarController.swift | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift index 5c61cf99..bfefb180 100644 --- a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift +++ b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift @@ -209,6 +209,19 @@ extension MainTabBarController { extension MainTabBarController { + // A. trigger select by MainTabBarController.tabBarController(_:didSelect:) + // The device is horizontal compact size class + // and user tap on TabBar directly. + // And navigation stack is already pop to root in + // MainTabBarController.tabBarController(_:shouldSelect:) + // B. trigger select by ContentSplitViewController.sidebarViewModel(_:active:) + // The device is horizontal regular size class and user tap on sidebar. + // And there are two conditions (true/false) for `isMainTabBarControllerActive` value. + // Only trigger pop and scroll action when main tab isActive (a.k.a secondary tab bar controller hidden) + // C. trigger select by SceneCoordinator.switchToTabBar(tab:) + // The device idiom is phone. + // Follows B. workflow with default true of `isMainTabBarControllerActive` value + // And maybe force pop to root needs func select(tab: TabBarItem, isMainTabBarControllerActive: Bool = true) { let _index = tabBar.items?.firstIndex(where: { $0.tag == tab.tag }) guard let index = _index else { @@ -243,6 +256,18 @@ extension MainTabBarController { extension MainTabBarController { + @objc private func doubleTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + switch sender.state { + case .ended: + guard let scrollViewContainer = selectedViewController?.topMost as? ScrollViewContainer else { return } + scrollViewContainer.scrollToTop(animated: true) + default: + break + } + } + + @objc private func tabBarLongPressGestureRecognizerHandler(_ sender: UILongPressGestureRecognizer) { guard sender.state == .began else { return } @@ -275,6 +300,23 @@ extension MainTabBarController { // MARK: - UITabBarControllerDelegate extension MainTabBarController: UITabBarControllerDelegate { + + func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + + // fix issue 102: https://github.com/TwidereProject/TwidereX-iOS/issues/102 + // try to pop to root when tap on the same tabBarItem and break select + if tabBarController.selectedViewController === viewController, + let navigationController = viewController as? UINavigationController, + navigationController.viewControllers.count > 1 + { + navigationController.popToRootViewController(animated: true) + return false + } + + return true + } + func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") @@ -285,4 +327,5 @@ extension MainTabBarController: UITabBarControllerDelegate { select(tab: tab) } + } From 41ae1d8243132b97fb948117c5ae7b09a5cd8c17 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 28 Jul 2022 19:18:11 +0800 Subject: [PATCH 002/128] feat: add behavior preference --- .../Entity/Twitter/TwitterUser.swift | 8 +- .../arrow.ramp.right.imageset/Contents.json | 15 ++ .../arrow.ramp.right.pdf | Bin 0 -> 3578 bytes .../Object&Tools/house.imageset/home.pdf | Bin 5021 -> 3527 bytes .../house.large.imageset/home.large.pdf | Bin 5114 -> 3553 bytes .../TwidereAsset/Generated/Assets.swift | 1 + .../TwidereCommon/Extension/Publisher.swift | 37 +++++ .../Preference/Preference+Appearance.swift | 8 +- .../Preference/Preference+Behaviors.swift | 80 ++++++++++ .../Preference/Preference+Display.swift | 2 +- .../TwidereCore/Service/ThemeService.swift | 32 ++-- .../Content/PrototypeStatusView.swift | 91 +++++++++++ .../ComposeContentViewModel+Diffable.swift | 2 +- .../PrototypeStatusViewRepresentable.swift | 103 ++++++++++++ .../Vender/TapCountRecognizerModifier.swift | 111 +++++++++++++ TwidereX.xcodeproj/project.pbxproj | 90 ++++++----- TwidereX/Coordinator/SceneCoordinator.swift | 8 +- TwidereX/Protocol/ScrollViewContainer.swift | 12 +- .../Root/ContentSplitViewController.swift | 34 +++- .../Root/MainTab/MainTabBarController.swift | 102 +++++++++++- .../SecondaryTabBarController.swift | 56 ++++++- TwidereX/Scene/Root/Sidebar/SidebarView.swift | 38 ++++- .../Scene/Root/Sidebar/SidebarViewModel.swift | 11 +- .../AppIconPreferenceView.swift | 0 .../AppearancePreferenceView.swift | 83 ---------- .../AppearancePreferenceViewModel.swift | 49 ------ .../BehaviorsPreferenceView.swift | 58 +++++++ .../BehaviorsPreferenceViewController.swift} | 20 +-- .../BehaviorsPreferenceViewModel.swift | 115 ++++++++++++++ .../DisplayPreferenceView.swift | 88 ++++++++++ .../DisplayPreferenceViewController.swift | 99 ++++++------ .../DisplayPreferenceViewModel.swift | 150 +++++------------- .../TranslateButtonPreferenceView.swift | 0 .../TranslationServicePreferenceView.swift | 0 .../Scene/Setting/List/SettingListView.swift | 61 +++++-- .../List/SettingListViewController.swift | 11 +- .../Setting/List/SettingListViewModel.swift | 9 +- .../Base/Common/TimelineViewController.swift | 11 ++ .../Base/Common/TimelineViewModel.swift | 38 ++++- .../List/ListTimelineViewController.swift | 47 ++---- .../Home/HomeTimelineViewController.swift | 15 ++ 41 files changed, 1253 insertions(+), 442 deletions(-) create mode 100644 TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Arrows/arrow.ramp.right.imageset/Contents.json create mode 100644 TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Arrows/arrow.ramp.right.imageset/arrow.ramp.right.pdf create mode 100644 TwidereSDK/Sources/TwidereCommon/Preference/Preference+Behaviors.swift create mode 100644 TwidereSDK/Sources/TwidereUI/Content/PrototypeStatusView.swift create mode 100644 TwidereSDK/Sources/TwidereUI/UIViewRepresentable/PrototypeStatusViewRepresentable.swift create mode 100644 TwidereSDK/Sources/TwidereUI/Vender/TapCountRecognizerModifier.swift rename TwidereX/Scene/Setting/{AppearancePreference => AppIconPreference}/AppIconPreferenceView.swift (100%) delete mode 100644 TwidereX/Scene/Setting/AppearancePreference/AppearancePreferenceView.swift delete mode 100644 TwidereX/Scene/Setting/AppearancePreference/AppearancePreferenceViewModel.swift create mode 100644 TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceView.swift rename TwidereX/Scene/Setting/{AppearancePreference/AppearancePreferenceViewController.swift => BehaviorsPreference/BehaviorsPreferenceViewController.swift} (69%) create mode 100644 TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewModel.swift create mode 100644 TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceView.swift rename TwidereX/Scene/Setting/{AppearancePreference => DisplayPreference/Translation}/TranslateButtonPreferenceView.swift (100%) rename TwidereX/Scene/Setting/{AppearancePreference => DisplayPreference/Translation}/TranslationServicePreferenceView.swift (100%) diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift index e1cc1204..d82ad369 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift @@ -121,9 +121,13 @@ extension TwitterUser { } set { let keyPath = #keyPath(TwitterUser.urlEntities) - let data = try? JSONEncoder().encode(newValue) willChangeValue(forKey: keyPath) - setPrimitiveValue(data, forKey: keyPath) + if let newValue = newValue { + let data = try? JSONEncoder().encode(newValue) + setPrimitiveValue(data, forKey: keyPath) + } else { + setPrimitiveValue(nil, forKey: keyPath) + } didChangeValue(forKey: keyPath) } } diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Arrows/arrow.ramp.right.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Arrows/arrow.ramp.right.imageset/Contents.json new file mode 100644 index 00000000..bef6aa4f --- /dev/null +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Arrows/arrow.ramp.right.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "arrow.ramp.right.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Arrows/arrow.ramp.right.imageset/arrow.ramp.right.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Arrows/arrow.ramp.right.imageset/arrow.ramp.right.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ab5dddc5bd7f21d639e78efc798d26953c1b8561 GIT binary patch literal 3578 zcmd5K@U@}vpa#uZpV`%?XNFsOO$5p zB%51(Sa{!v7cW@b-{HSZmH6hDw!c3;a1G!V zSBhl}*`jNa%smwuC?QbC0fcynO1Zuv4c zefA7SPBlnu()5ZGF!yyX&do!61W3^fdREf#`2kv^=9{tAo&H$`9qJNCfx zVl{h0k{OhhG0w@F7s1oIOfn%2NRlWfEE7zw2%(6O+Y&ryS8BMrc+CM}*}5x2mFNTe zz^2CFOOBAC!X7 z(&Km6I+Z$gi*L8~MF$dVUDGbRrpRMP4hI^XcTH$3=|YYT7>0v{#I7meGGC#A=|fLS zLjf&@$-__-LM2r8JpD*Dw`T~HG3s(KK9p!;n?2@;D{o+(QHnh=P2mYGCL~kUR>|5x zBM??wXmJj^lM@EI&eTX1Ha+N!HFh>^7skoJp{*Nlm6sgvOjR2Neacr#8*dEHn%PQv z7@2$l5shMAA`A&H10NV%6T+xaW8h$F1FZ{sxN%+sMAk5$WvSyEM9C;ggw4ZCXN1D( zA})nOudTGOJ*slrI_anWc#&|kLU7VKhao~!MJyB16x!K^D9%t}0 zu;Xk#5ArH#M{~R(80P!U4}CwJM*jYHnD^n?y!+?RmTzu%_iY4zZSVHGm&0fNJ|8EV zMiuePZ^_fP9fwcHrX4wcQEa2T>!CljJw(LMi3Hy6j(~9+q2i+hA^I5E{Jg&dsvI-e z@)YNK_yo3c!G8p#@C37e;RzEQDPT_gdUx7&!~MDG=VSYU3GUdugZP)%7LExAI6e|? z8+>?A5KM*VkQT}w=9Zyp_8V)(Tx_b57>;C{s=*>d_ literal 0 HcmV?d00001 diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/house.imageset/home.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/house.imageset/home.pdf index c2bb01528d14428a68e66b0ea1386d1c79254e4f..87e49883e47dca1a1c4309a7072f066c5409ec1b 100644 GIT binary patch literal 3527 zcma)9OOM+&5WeeI@UlR1h|H%b0zrUgw<(IY=+@~i=)u)08y9}Gwo|12_4$S(hqNW2 z^AV>B4U5em&cmV6 z`3$rK-x(wJz?{>%cY-n2tXgNCvAtjf3Z`>IQ(>JAK?T8@@M@A`dyEVV9Cpp8LYyt^ z8#wKySJFdsFRiyy>b*4r`H;TvItfVjeW$G;DQ)^*!Yi+Aa8e9WUl(Ku z0F71JJCd`~+D;emIdUA2VSycS4jfGu-2+UVl`5n|YgFfYBb07caJqA%Z@V5O{1v2w z?gOOw=&)pjY^y#SuL4#;CbT2T=pxkSv;guHh`s~@&^%xY9#UXc2`tIogrBrgn);Ha zVToU)-DTQb5&K5D)>`kCqdJ=k2T*DO-KOfQAuhrKa;+9^#b36{5EwGZR*B)fWUH>K zBXZ3c zC%0+bRh+maYfh~5x+K|Em|BW*0jofvj8e)MLDos8J~oHum;XB`xs;%H8ms?3C|MM( zm=3|0{0K13Oqn0h*h#Jw219Z5qDfM?skisyN$P!pdp% z5KGo*ZHqP3Q8lfW*5Eq9&4B?P3|%@Fmq{N8Qd${JmrjqliQ9*Aml_x+r|C{4aeo=- zG4CucHt1k7ruD}9-WG&upo&6YOEYA1uVyo!~&ng(U1?WLk3wE#pqn`l>&r& zI>yQ_rBdgqUFVY45GV{tX38pPMv z0BGk<3w7SUY0;jxa*a)pmvYN6mi5&)hr{u579W1Y@{Zr?*MI&T#p-eMJSO0$@oBqx zd;B6klogz(t!8@VDQjAf=i}$oFrEc{9;}nP`{Us<9$=Ba4Knz+IRO(cLDd%oLHuR0 z`eFM7s)9UNi)`oq_!(!_jQ<%(rK!8pL=K+WU~&9@bJ^^U&(}x4oW_rh6uz0VMt?=; zFtr5W^ixvACvjlNfG7PJ(pczb5LWPODWfo;ZXj*T!OLm0-HoS)viJQ1!tvsK+>bBC qi|y0PDJZM;@puvBa18M3{r2Ax?Bgfb=5&ef7$aSCclYhj@4fHyx$ zsq^JDZTHpY_4McCINg8qO5MFH{~FKLAC<0~@KH_r@|@rJTbF+GwQt(a7&Y!I9$S7n zkB6#FGSK4f&<|=4tnb^_^@$*36XSE^%1+&~(71&_BH(F5>ewt6w9+7c{!>;<2 ziPOTqg44FKy0Osg8e@k}?F96`(@ihtw1X20`Dm;gh6YgM++eJdQ*Gy51Fx($-nMEC z`Uc;453n=3^-j*2#+aea-ZOHH$GE~yID1BuNB00DXRR}7Uw3-wU8i(gYu~rdDOV2; zB>5AkG)mbR#&jA~c6jp@*@*c=$(zLTY6uuPm6NVmFcW$8G~O>7^@U2I@T z&eL5;Vt;kL#k@0EY|ue6U2QvKoyiDkpo(~mUS^62O{x%@OcWD-7w*+CAAPpBob`7H=Vke6e4u-7n2V{^Sim~sV z)fy1?bcmH%OJ$p;c9~1LhCrc5GGkU@w;lRc5Bc7dsgN~FgYJ<^puMyW+9s4+D@!(( zTkaMXWrkQFp(kU95kzRoBUc=5S>6n$OJ#|8EIo$4CBHzM%}X;k(! zp&Z|d(q~*#|KbV;=#R2cJ7X!A*&bs&p--I!)s9f7Q_AE~)^>k8V`>rVfNU@RScBY`@!6#^B}R=F~8N z>$RXYS@3e&KJKPdCE5G-HNx@qeB4jZ)YHd@XLC?C_s8Q!iNhhln|F_YPhcM&(zd5d PaK~8ht0zys{_)K}1H{`I diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/house.large.imageset/home.large.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/house.large.imageset/home.large.pdf index 53dbbe2d6eb20402d6599152bd8a10fa2c1248fd..b262c98b6c49e2e8ca76ccc3f1f0c66f2a4a8261 100644 GIT binary patch delta 705 zcmZuvJ5HQ25Ec)?;#7zg4b4!j5ELViXT}~EL`ne#&E^cDK?;dPMT117bZLEp+yFri zkSnB1i?{_9^Y60%t`=O_^5^k<5BvGxaqsSF<2DPhVw?>47NjC&AC6{Zi-icpFiPQ4 zfeY%iQZjaBU0siz2T@F>RToi_cRKc1`?9&Snx$QD{#}t~gHLN(t=$aNclU#l&;JKk zKA&wJ?M6s5`pY2kRdoBc_3?J$$UP(!Cek#ocM&D0k}@z#)>0JQffyMjP$U!63~nG{ z6)!X}EGcr0;8e*Si20p@m93hA5K`3W_9yPEhXTE#Db_*Wa3*uE$tV*%~5lut4uJJ6w_Ad;-x)6$}nQ|wDU37*HfkR@VNbR<(OkkF#SN^!(@#dlP*J4Z5i^n QYrl8Kfte|cMknLb7sx-8O#lD@ literal 5114 zcmb7IO^+Kl486~<&?P`}*zxFF0)ha|Zc`L((R9;W(1YupWVi4~Ydb~SU!O;fOv?5E zX^kM5yo|_4>XFpw+1tA}H@c5qr`4c#zyHxG_41{9^{N}U5A@H@wD{u3ar^Q3s4T#H zdh2l9kIPNBxEud|T8;PLy;isH+JCJM-Cv#Vhw@Xy>EVW-`2RFR*7ec*YzDR3bc|j< z99G*d^nrd+nLau>+VsAvxiMbnGO+uRO|<>lZ$HI-I(4b6N6EqH#sD46rhkp@LFN9(cUM5?2tpw z%Jn88JgZh@FB=nGKw#Eeoq_@c%i0Hto-@ZeTFtN{&X!-b4QtD+ZKa(+8}G)f9UKA8 zDQS;{=iUs-JJ({VH`W+y2`fP-FrV{83<5J~2nfg#th-PHbrL9`k`6EF2sLe?O&h?= z^a3iIpk)(at!$Q;&B9(^^CQ|G`KO=3yR1VGI^G1M1@uForuSH50&&EvPC!5eAz?s!wa@ux9NeD!uC4_AUmiEy}TxPenE4V`gN5T3`cB zfiT8KDWJvxo85H2n$?g%Wl*GYxs%YZm}Om_(PFFTy5xA_Y%bLTw%OuT&w^}|Tz%-a z-7o*SYfgKUB05EFvR`kSruS1nMwQn^n@+&c+cZS2m7X39Rc>9uaR^k}P+8Hwh<8hQjRw!CRkCg!F_pV|Q2lv!HM(XTp( zB27LA=!;S3%6OqS#iC&B8>?p+QLGYH%T&-dF3#&LDC^5$Ic))RIQJ$d+SH&_D^kNL`&o2XO! zF2N+Yxr~%1=_M~>d+imaN*tkB^kB;23Q}ZaVr~X?Dzrh+$vc7c{N=+k~wR_{MDo&M8$En)e0a1sD z4xPXDK70b`QqMzSsrxcD@CA}f(=nJN%m>R$ZyH*RV{A?XLXmPb;~rHvHE5%77NnYv z0j;cdariFZd=AZ|=mx~&zX9~ zH^f(ujhKndXx*UVy%jZ6YQT0qls;n@-W(F19F1G{rhV^nG6QnLnbY|Cj5ye|AwaUZ z$Ar-IMb~UGY6ah-13p#xAoMrH~A3w!vCC ziSQUjbqLCGL|qrz;^@G#or@m)XLuI=T;8JHw$(JHCKR^X@c;c5AL_gTG{ zTN`eNY3Au<>3%%yKJ8cILD7xUeKmKx+aAX)JkoX234FWU15>_2&7~7Td>OU);pqWX z6-98bPJV88pRiV6@jn1*EQK>m6ySj!O5nH4<8rv4rDNHr=yl-~RmO3)dO^ssI20 diff --git a/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift b/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift index 514efa9b..00304eec 100644 --- a/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift +++ b/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift @@ -24,6 +24,7 @@ public enum Asset { public static let accentColor = ColorAsset(name: "AccentColor") public enum Arrows { public static let arrowLeft = ImageAsset(name: "Arrows/arrow.left") + public static let arrowRampRight = ImageAsset(name: "Arrows/arrow.ramp.right") public static let arrowRight = ImageAsset(name: "Arrows/arrow.right") public static let arrowTriangle2Circlepath = ImageAsset(name: "Arrows/arrow.triangle.2.circlepath") public static let arrowTurnUpLeft = ImageAsset(name: "Arrows/arrow.turn.up.left") diff --git a/TwidereSDK/Sources/TwidereCommon/Extension/Publisher.swift b/TwidereSDK/Sources/TwidereCommon/Extension/Publisher.swift index 107c13c6..0f75ae93 100644 --- a/TwidereSDK/Sources/TwidereCommon/Extension/Publisher.swift +++ b/TwidereSDK/Sources/TwidereCommon/Extension/Publisher.swift @@ -7,6 +7,43 @@ import Combine +// Ref: https://www.swiftbysundell.com/articles/connecting-async-await-with-other-swift-code/ + +extension Publishers { + public struct MissingOutputError: Error {} +} + +extension Publisher { + public func singleOutput() async throws -> Output { + var cancellable: AnyCancellable? + var didReceiveValue = false + + return try await withCheckedThrowingContinuation { continuation in + cancellable = sink( + receiveCompletion: { completion in + switch completion { + case .failure(let error): + continuation.resume(throwing: error) + case .finished: + if !didReceiveValue { + continuation.resume( + throwing: Publishers.MissingOutputError() + ) + } + } + }, + receiveValue: { value in + guard !didReceiveValue else { return } + + didReceiveValue = true + cancellable?.cancel() + continuation.resume(returning: value) + } + ) + } + } +} + // ref: https://www.swiftbysundell.com/articles/calling-async-functions-within-a-combine-pipeline/ extension Publisher { diff --git a/TwidereSDK/Sources/TwidereCommon/Preference/Preference+Appearance.swift b/TwidereSDK/Sources/TwidereCommon/Preference/Preference+Appearance.swift index 305ca4c9..8d0dc401 100644 --- a/TwidereSDK/Sources/TwidereCommon/Preference/Preference+Appearance.swift +++ b/TwidereSDK/Sources/TwidereCommon/Preference/Preference+Appearance.swift @@ -23,12 +23,10 @@ extension UserDefaults { // Translate button - @objc public enum TranslateButtonPreference: Int, Identifiable, CaseIterable { + @objc public enum TranslateButtonPreference: Int, CaseIterable { case auto case always case off - - public var id: String { "\(rawValue)" } } @objc dynamic public var translateButtonPreference: TranslateButtonPreference { @@ -41,12 +39,10 @@ extension UserDefaults { // Service - @objc public enum TranslationServicePreference: Int, Identifiable, CaseIterable { + @objc public enum TranslationServicePreference: Int, CaseIterable { case bing case deepl case google - - public var id: String { "\(rawValue)" } } @objc dynamic public var translationServicePreference: TranslationServicePreference { diff --git a/TwidereSDK/Sources/TwidereCommon/Preference/Preference+Behaviors.swift b/TwidereSDK/Sources/TwidereCommon/Preference/Preference+Behaviors.swift new file mode 100644 index 00000000..fb8f21cf --- /dev/null +++ b/TwidereSDK/Sources/TwidereCommon/Preference/Preference+Behaviors.swift @@ -0,0 +1,80 @@ +// +// Preference+Behaviors.swift +// +// +// Created by MainasuK on 2022-7-27. +// + +import Foundation + +// MARK: - Tab bar: label +extension UserDefaults { + + @objc dynamic public var preferredTabBarLabelDisplay: Bool { + get { return bool(forKey: #function) } + set { self[#function] = newValue } + } + +} + +// MARK: - Tab bar: Tap Scroll +extension UserDefaults { + + @objc public enum TabBarTapScrollPreference: Int, Hashable, CaseIterable { + case single + case double + } + + @objc dynamic public var tabBarTapScrollPreference: TabBarTapScrollPreference { + get { + guard let rawValue: Int = self[#function] else { return .single } + return TabBarTapScrollPreference(rawValue: rawValue) ?? .single + } + set { self[#function] = newValue.rawValue } + } + +} + +// MARK: - Tab bar: Timeline Refreshing +extension UserDefaults { + + @objc dynamic public var preferredTimelineAutoRefresh: Bool { + get { + register(defaults: [#function: true]) + return bool(forKey: #function) + } + set { self[#function] = newValue } + } + + @objc public enum TimelineRefreshInterval: Int, Hashable, CaseIterable { + case _30s + case _60s + case _120s + case _300s + + public var seconds: TimeInterval { + switch self { + case ._30s: return 30 + case ._60s: return 60 + case ._120s: return 120 + case ._300s: return 300 + } + } + } + + @objc dynamic public var timelineRefreshInterval: TimelineRefreshInterval { + get { + guard let rawValue: Int = self[#function] else { return ._60s } + return TimelineRefreshInterval (rawValue: rawValue) ?? ._60s + } + set { self[#function] = newValue.rawValue } + } + + @objc dynamic public var preferredTimelineResetToTop: Bool { + get { + return bool(forKey: #function) + } + set { self[#function] = newValue } + } + +} diff --git a/TwidereSDK/Sources/TwidereCommon/Preference/Preference+Display.swift b/TwidereSDK/Sources/TwidereCommon/Preference/Preference+Display.swift index 918d8aea..b5c015ad 100644 --- a/TwidereSDK/Sources/TwidereCommon/Preference/Preference+Display.swift +++ b/TwidereSDK/Sources/TwidereCommon/Preference/Preference+Display.swift @@ -1,6 +1,6 @@ // // Preference+Display.swift -// AppShared +// TwidereCommon // // Created by Cirno MainasuK on 2021-11-3. // Copyright © 2021 Twidere. All rights reserved. diff --git a/TwidereSDK/Sources/TwidereCore/Service/ThemeService.swift b/TwidereSDK/Sources/TwidereCore/Service/ThemeService.swift index d2027e72..74ece2a1 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/ThemeService.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/ThemeService.swift @@ -37,21 +37,11 @@ public final class ThemeService { UINavigationBar.appearance().compactScrollEdgeAppearance = appearance // set tab bar appearance - let tabBarAppearance = UITabBarAppearance() - tabBarAppearance.configureWithDefaultBackground() - tabBarAppearance.stackedLayoutAppearance = { - let tabBarItemAppearance = UITabBarItemAppearance() - tabBarItemAppearance.selected.titleTextAttributes = [.foregroundColor: UIColor.clear] - tabBarItemAppearance.focused.titleTextAttributes = [.foregroundColor: UIColor.clear] - tabBarItemAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.clear] - tabBarItemAppearance.disabled.titleTextAttributes = [.foregroundColor: UIColor.clear] - return tabBarItemAppearance - }() + let tabBarAppearance = ThemeService.setupTabBarAppearance() UITabBar.appearance().standardAppearance = tabBarAppearance UITabBar.appearance().scrollEdgeAppearance = tabBarAppearance - // UITabBar.appearance().barTintColor = theme.tabBarBackgroundColor } - + } extension Theme { @@ -68,3 +58,21 @@ extension Theme { } } } + +extension ThemeService { + public static func setupTabBarAppearance() -> UITabBarAppearance { + let tabBarAppearance = UITabBarAppearance() + tabBarAppearance.configureWithDefaultBackground() + tabBarAppearance.stackedLayoutAppearance = { + let tabBarItemAppearance = UITabBarItemAppearance() + if !UserDefaults.shared.preferredTabBarLabelDisplay { + tabBarItemAppearance.selected.titleTextAttributes = [.foregroundColor: UIColor.clear] + tabBarItemAppearance.focused.titleTextAttributes = [.foregroundColor: UIColor.clear] + tabBarItemAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.clear] + tabBarItemAppearance.disabled.titleTextAttributes = [.foregroundColor: UIColor.clear] + } + return tabBarItemAppearance + }() + return tabBarAppearance + } +} diff --git a/TwidereSDK/Sources/TwidereUI/Content/PrototypeStatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/PrototypeStatusView.swift new file mode 100644 index 00000000..3eff8fc8 --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Content/PrototypeStatusView.swift @@ -0,0 +1,91 @@ +// +// PrototypeStatusView.swift +// +// +// Created by MainasuK on 2022-7-25. +// + +import UIKit +import Combine + +public protocol PrototypeStatusViewDelegate: AnyObject { + func layoutDidUpdate(_ view: PrototypeStatusView) +} + +final public class PrototypeStatusView: UIView { + + public var disposeBag = Set() + private var observations = Set() + + weak var delegate: PrototypeStatusViewDelegate? + + public let statusView = StatusView() + public private(set)var widthLayoutConstraint: NSLayoutConstraint! + + public override var intrinsicContentSize: CGSize { + let size = statusView.frame.size + defer { + self.delegate?.layoutDidUpdate(self) + } + return CGSize(width: UIView.noIntrinsicMetric, height: size.height) + } + + public override var frame: CGRect { + didSet { + guard frame != oldValue else { return } + layoutIfNeeded() + } + } + + public override func layoutSubviews() { + super.layoutSubviews() + + if frame.width != .zero { + statusView.frame.size.width = frame.width + widthLayoutConstraint.constant = frame.width + widthLayoutConstraint.isActive = true + } + + let targetSize = CGSize( + width: frame.width, + height: UIView.layoutFittingCompressedSize.height + ) + + statusView.frame.size.height = statusView.systemLayoutSizeFitting( + targetSize, + withHorizontalFittingPriority: .required, + verticalFittingPriority: .fittingSizeLevel + ).height + + invalidateIntrinsicContentSize() + } + + + public override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension PrototypeStatusView { + private func _init() { + widthLayoutConstraint = widthAnchor.constraint(equalToConstant: frame.width) + + addSubview(statusView) + + // trigger UIViewRepresentable size update + statusView + .observe(\.bounds, options: [.initial, .new]) { [weak self] statusView, _ in + guard let self = self else { return } + print(statusView.frame) + self.invalidateIntrinsicContentSize() + } + .store(in: &observations) + } +} diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel+Diffable.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel+Diffable.swift index f1efa25e..4819b278 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel+Diffable.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel+Diffable.swift @@ -1,6 +1,6 @@ // // ComposeContentViewModel+Diffable.swift -// AppShared +// TwidereUI // // Created by MainasuK on 2021/11/17. // Copyright © 2021 Twidere. All rights reserved. diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/PrototypeStatusViewRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/PrototypeStatusViewRepresentable.swift new file mode 100644 index 00000000..78268ebf --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/PrototypeStatusViewRepresentable.swift @@ -0,0 +1,103 @@ +// +// PrototypeStatusViewRepresentable.swift +// +// +// Created by MainasuK on 2022-7-25. +// + +import SwiftUI +import TwitterMeta +import TwidereCore + +public struct PrototypeStatusViewRepresentable: UIViewRepresentable { + + private let now = Date() + + let style: Style + let configurationContext: StatusView.ConfigurationContext + + @Binding var height: CGFloat + + public init( + style: Style, + configurationContext: StatusView.ConfigurationContext, + height: Binding + ) { + self.style = style + self.configurationContext = configurationContext + self._height = height + } + + public func makeUIView(context: Context) -> PrototypeStatusView { + let view = PrototypeStatusView() + switch style { + case .timeline: + view.statusView.setup(style: .inline) + view.statusView.toolbar.setup(style: .inline) + case .thread: + view.statusView.setup(style: .plain) + view.statusView.toolbar.setup(style: .plain) + } + view.delegate = context.coordinator + + view.translatesAutoresizingMaskIntoConstraints = false + view.setContentCompressionResistancePriority(.required, for: .vertical) + + view.statusView.prepareForReuse() + view.statusView.viewModel.timestamp = now + view.statusView.viewModel.dateTimeProvider = configurationContext.dateTimeProvider + + return view + } + + public func updateUIView(_ view: PrototypeStatusView, context: Context) { + let statusView = view.statusView + statusView.viewModel.authorAvatarImage = Asset.Scene.Preference.twidereAvatar.image + statusView.viewModel.authorName = PlaintextMetaContent(string: "Twidere") + statusView.viewModel.authorUsername = "TwidereProject" + + let content = TwitterContent(content: L10n.Scene.Settings.Display.Preview.thankForUsingTwidereX) + statusView.viewModel.content = TwitterMetaContent.convert( + content: content, + urlMaximumLength: 16, + twitterTextProvider: configurationContext.twitterTextProvider + ) + + view.setNeedsLayout() + view.layoutIfNeeded() + } + + public func makeCoordinator() -> Coordinator { + Coordinator(self) + } + +} + +extension PrototypeStatusViewRepresentable { + + public class Coordinator: PrototypeStatusViewDelegate { + + let representable: PrototypeStatusViewRepresentable + + init(_ representable: PrototypeStatusViewRepresentable) { + self.representable = representable + } + + public func layoutDidUpdate(_ view: PrototypeStatusView) { + DispatchQueue.main.async { + self.representable.height = view.statusView.frame.height + } + } + + } + +} + +extension PrototypeStatusViewRepresentable { + + public enum Style: Hashable, CaseIterable { + case timeline + case thread + } + +} diff --git a/TwidereSDK/Sources/TwidereUI/Vender/TapCountRecognizerModifier.swift b/TwidereSDK/Sources/TwidereUI/Vender/TapCountRecognizerModifier.swift new file mode 100644 index 00000000..2fddfa44 --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Vender/TapCountRecognizerModifier.swift @@ -0,0 +1,111 @@ +// +// TapCountRecognizerModifier.swift +// +// +// Created by MainasuK on 2022-7-28. +// + +import UIKit +import SwiftUI + +// ref: +// https://stackoverflow.com/questions/65062833/handling-both-double-and-triple-gesture-recognisers-in-swiftui/66979241#66979241 + +public struct TapCountRecognizerModifier: ViewModifier { + + let tapSensitivity: Int + let singleTapAction: (() -> Void)? + let doubleTapAction: (() -> Void)? + let tripleTapAction: (() -> Void)? + + + public init(tapSensitivity: Int = 250, singleTapAction: (() -> Void)? = nil, doubleTapAction: (() -> Void)? = nil, tripleTapAction: (() -> Void)? = nil) { + + self.tapSensitivity = ((tapSensitivity >= 0) ? tapSensitivity : 250) + self.singleTapAction = singleTapAction + self.doubleTapAction = doubleTapAction + self.tripleTapAction = tripleTapAction + + } + + @State private var tapCount: Int = Int() + @State private var currentDispatchTimeID: DispatchTime = DispatchTime.now() + + public func body(content: Content) -> some View { + + return content + .gesture(fundamentalGesture) + + } + + var fundamentalGesture: some Gesture { + + DragGesture(minimumDistance: 0.0, coordinateSpace: .local) + .onEnded() { _ in tapCount += 1; tapAnalyzerFunction() } + + } + + + + func tapAnalyzerFunction() { + + currentDispatchTimeID = dispatchTimeIdGenerator(deadline: tapSensitivity) + + if tapCount == 1 { + + let singleTapGestureDispatchTimeID: DispatchTime = currentDispatchTimeID + + DispatchQueue.main.asyncAfter(deadline: singleTapGestureDispatchTimeID) { + + if (singleTapGestureDispatchTimeID == currentDispatchTimeID) { + + if let unwrappedSingleTapAction: () -> Void = singleTapAction { unwrappedSingleTapAction() } + + tapCount = 0 + + } + + } + + } + else if tapCount == 2 { + + let doubleTapGestureDispatchTimeID: DispatchTime = currentDispatchTimeID + + DispatchQueue.main.asyncAfter(deadline: doubleTapGestureDispatchTimeID) { + + if (doubleTapGestureDispatchTimeID == currentDispatchTimeID) { + + if let unwrappedDoubleTapAction: () -> Void = doubleTapAction { unwrappedDoubleTapAction() } + + tapCount = 0 + + } + + } + + } + else { + + + if let unwrappedTripleTapAction: () -> Void = tripleTapAction { unwrappedTripleTapAction() } + + tapCount = 0 + + } + + } + + func dispatchTimeIdGenerator(deadline: Int) -> DispatchTime { return DispatchTime.now() + DispatchTimeInterval.milliseconds(deadline) } + +} + +extension View { + + public func tapCountRecognizer(tapSensitivity: Int = 250, singleTapAction: (() -> Void)? = nil, doubleTapAction: (() -> Void)? = nil, tripleTapAction: (() -> Void)? = nil) -> some View { + + return self.modifier(TapCountRecognizerModifier(tapSensitivity: tapSensitivity, singleTapAction: singleTapAction, doubleTapAction: doubleTapAction, tripleTapAction: tripleTapAction)) + + } + +} diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index cdfe207a..d3782c15 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -239,6 +239,10 @@ DB8761CC2745530200BA7EE2 /* CoverFlowStackItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8761BC2745515F00BA7EE2 /* CoverFlowStackItem.swift */; }; DB8761D02745553700BA7EE2 /* MediaSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8761CF2745553600BA7EE2 /* MediaSection.swift */; }; DB88AC3C250B26F40009E562 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB88AC3B250B26F40009E562 /* Preview Assets.xcassets */; }; + DB88E626288EA1F7009A01F5 /* DisplayPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB88E625288EA1F7009A01F5 /* DisplayPreferenceView.swift */; }; + DB88E6292890DF67009A01F5 /* BehaviorsPreferenceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB88E6282890DF67009A01F5 /* BehaviorsPreferenceViewController.swift */; }; + DB88E62C2890DF79009A01F5 /* BehaviorsPreferenceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB88E62B2890DF79009A01F5 /* BehaviorsPreferenceViewModel.swift */; }; + DB88E62E2890DF7E009A01F5 /* BehaviorsPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB88E62D2890DF7E009A01F5 /* BehaviorsPreferenceView.swift */; }; DB89F591257A2A70005ECD04 /* TimelineLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89F590257A2A70005ECD04 /* TimelineLoaderTableViewCell.swift */; }; DB8AC0F825401BA200E636BE /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AC0F725401BA200E636BE /* UIViewController.swift */; }; DB8E4FD92563CD6900459DA2 /* TwitterPinBasedAuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8E4FD82563CD6900459DA2 /* TwitterPinBasedAuthenticationViewController.swift */; }; @@ -350,13 +354,10 @@ DBF739CF275C247F00BF6AB5 /* DataSourceFacade+Mute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF739CE275C247F00BF6AB5 /* DataSourceFacade+Mute.swift */; }; DBF739D1275C3EF300BF6AB5 /* DataSourceFacade+Report.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF739D0275C3EF300BF6AB5 /* DataSourceFacade+Report.swift */; }; DBF81C7C27F6A93E00004A56 /* DataSourceFacade+Translate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF81C7B27F6A93E00004A56 /* DataSourceFacade+Translate.swift */; }; - DBF81C7E27F6E7F500004A56 /* AppearancePreferenceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF81C7D27F6E7F500004A56 /* AppearancePreferenceViewController.swift */; }; - DBF81C8027F6E80700004A56 /* AppearancePreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF81C7F27F6E80700004A56 /* AppearancePreferenceView.swift */; }; - DBF81C8327F6ECF400004A56 /* AppearancePreferenceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF81C8227F6ECF400004A56 /* AppearancePreferenceViewModel.swift */; }; - DBF81C8527F709EF00004A56 /* TranslateButtonPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF81C8427F709EF00004A56 /* TranslateButtonPreferenceView.swift */; }; - DBF81C8827F7141600004A56 /* TranslationServicePreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF81C8727F7141600004A56 /* TranslationServicePreferenceView.swift */; }; DBF81C8E27F843D700004A56 /* AppIconAssets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF81C8C27F843D700004A56 /* AppIconAssets.swift */; }; - DBF81C9127F8448C00004A56 /* AppIconPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF81C9027F8448C00004A56 /* AppIconPreferenceView.swift */; }; + DBF87AD52892A6740029A7C7 /* AppIconPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF87AD42892A6740029A7C7 /* AppIconPreferenceView.swift */; }; + DBF87AD92892A67D0029A7C7 /* TranslateButtonPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF87AD72892A67D0029A7C7 /* TranslateButtonPreferenceView.swift */; }; + DBF87ADA2892A67D0029A7C7 /* TranslationServicePreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF87AD82892A67D0029A7C7 /* TranslationServicePreferenceView.swift */; }; DBFA471C2859C33500C9FF7F /* MeLikeTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFA471B2859C33500C9FF7F /* MeLikeTimelineViewModel.swift */; }; DBFA471F2859C4AE00C9FF7F /* UserLikeTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFA471E2859C4AE00C9FF7F /* UserLikeTimelineViewController.swift */; }; DBFA47212859C4F300C9FF7F /* UserLikeTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFA47202859C4F300C9FF7F /* UserLikeTimelineViewModel.swift */; }; @@ -736,6 +737,10 @@ DB8761BC2745515F00BA7EE2 /* CoverFlowStackItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverFlowStackItem.swift; sourceTree = ""; }; DB8761CF2745553600BA7EE2 /* MediaSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSection.swift; sourceTree = ""; }; DB88AC3B250B26F40009E562 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + DB88E625288EA1F7009A01F5 /* DisplayPreferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayPreferenceView.swift; sourceTree = ""; }; + DB88E6282890DF67009A01F5 /* BehaviorsPreferenceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BehaviorsPreferenceViewController.swift; sourceTree = ""; }; + DB88E62B2890DF79009A01F5 /* BehaviorsPreferenceViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BehaviorsPreferenceViewModel.swift; sourceTree = ""; }; + DB88E62D2890DF7E009A01F5 /* BehaviorsPreferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BehaviorsPreferenceView.swift; sourceTree = ""; }; DB89F590257A2A70005ECD04 /* TimelineLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLoaderTableViewCell.swift; sourceTree = ""; }; DB8AC0F725401BA200E636BE /* UIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = ""; }; DB8AC18F2542DA9500E636BE /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveLabel.swift; sourceTree = ""; }; @@ -900,13 +905,10 @@ DBF81C7327F68AA800004A56 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = ""; }; DBF81C7A27F696E000004A56 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = gl; path = gl.lproj/InfoPlist.strings; sourceTree = ""; }; DBF81C7B27F6A93E00004A56 /* DataSourceFacade+Translate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Translate.swift"; sourceTree = ""; }; - DBF81C7D27F6E7F500004A56 /* AppearancePreferenceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePreferenceViewController.swift; sourceTree = ""; }; - DBF81C7F27F6E80700004A56 /* AppearancePreferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePreferenceView.swift; sourceTree = ""; }; - DBF81C8227F6ECF400004A56 /* AppearancePreferenceViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePreferenceViewModel.swift; sourceTree = ""; }; - DBF81C8427F709EF00004A56 /* TranslateButtonPreferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslateButtonPreferenceView.swift; sourceTree = ""; }; - DBF81C8727F7141600004A56 /* TranslationServicePreferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationServicePreferenceView.swift; sourceTree = ""; }; DBF81C8C27F843D700004A56 /* AppIconAssets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppIconAssets.swift; sourceTree = ""; }; - DBF81C9027F8448C00004A56 /* AppIconPreferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconPreferenceView.swift; sourceTree = ""; }; + DBF87AD42892A6740029A7C7 /* AppIconPreferenceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppIconPreferenceView.swift; sourceTree = ""; }; + DBF87AD72892A67D0029A7C7 /* TranslateButtonPreferenceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TranslateButtonPreferenceView.swift; sourceTree = ""; }; + DBF87AD82892A67D0029A7C7 /* TranslationServicePreferenceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TranslationServicePreferenceView.swift; sourceTree = ""; }; DBFA471B2859C33500C9FF7F /* MeLikeTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeLikeTimelineViewModel.swift; sourceTree = ""; }; DBFA471E2859C4AE00C9FF7F /* UserLikeTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLikeTimelineViewController.swift; sourceTree = ""; }; DBFA47202859C4F300C9FF7F /* UserLikeTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLikeTimelineViewModel.swift; sourceTree = ""; }; @@ -1216,8 +1218,9 @@ children = ( DB2C8739274F4B7D00CE0398 /* List */, DB580EAE288187BD00BC4A0F /* AccountPreference */, - DBF81C8127F6E84300004A56 /* AppearancePreference */, + DB88E62A2890DF6A009A01F5 /* BehaviorsPreference */, DB2C8733274F4B7D00CE0398 /* DisplayPreference */, + DBF87AD32892A6540029A7C7 /* AppIconPreference */, DB2C8736274F4B7D00CE0398 /* About */, DB2C872F274F4B7D00CE0398 /* Developer */, DBE6357B28855546001C114B /* PushNotificationScratch */, @@ -1238,8 +1241,10 @@ DB2C8733274F4B7D00CE0398 /* DisplayPreference */ = { isa = PBXGroup; children = ( + DBF87AD62892A67D0029A7C7 /* Translation */, DB2C8734274F4B7D00CE0398 /* DisplayPreferenceViewController.swift */, DB2C8735274F4B7D00CE0398 /* DisplayPreferenceViewModel.swift */, + DB88E625288EA1F7009A01F5 /* DisplayPreferenceView.swift */, ); path = DisplayPreference; sourceTree = ""; @@ -1257,9 +1262,9 @@ DB2C8739274F4B7D00CE0398 /* List */ = { isa = PBXGroup; children = ( - DB2C873A274F4B7D00CE0398 /* SettingListView.swift */, - DB235EF32834DD0900398FCA /* SettingListViewModel.swift */, DB2C873B274F4B7D00CE0398 /* SettingListViewController.swift */, + DB235EF32834DD0900398FCA /* SettingListViewModel.swift */, + DB2C873A274F4B7D00CE0398 /* SettingListView.swift */, ); path = List; sourceTree = ""; @@ -1762,6 +1767,16 @@ path = Vender; sourceTree = ""; }; + DB88E62A2890DF6A009A01F5 /* BehaviorsPreference */ = { + isa = PBXGroup; + children = ( + DB88E6282890DF67009A01F5 /* BehaviorsPreferenceViewController.swift */, + DB88E62B2890DF79009A01F5 /* BehaviorsPreferenceViewModel.swift */, + DB88E62D2890DF7E009A01F5 /* BehaviorsPreferenceView.swift */, + ); + path = BehaviorsPreference; + sourceTree = ""; + }; DB8AC1122540501A00E636BE /* Compose */ = { isa = PBXGroup; children = ( @@ -2367,41 +2382,29 @@ path = SearchDetail; sourceTree = ""; }; - DBF81C8127F6E84300004A56 /* AppearancePreference */ = { - isa = PBXGroup; - children = ( - DBF81C9227F8449800004A56 /* AppIcon */, - DBF81C8627F709F200004A56 /* Translation */, - DBF81C7D27F6E7F500004A56 /* AppearancePreferenceViewController.swift */, - DBF81C8227F6ECF400004A56 /* AppearancePreferenceViewModel.swift */, - DBF81C7F27F6E80700004A56 /* AppearancePreferenceView.swift */, - ); - path = AppearancePreference; - sourceTree = ""; - }; - DBF81C8627F709F200004A56 /* Translation */ = { + DBF81C8B27F843D700004A56 /* Generated */ = { isa = PBXGroup; children = ( - DBF81C8427F709EF00004A56 /* TranslateButtonPreferenceView.swift */, - DBF81C8727F7141600004A56 /* TranslationServicePreferenceView.swift */, + DBF81C8C27F843D700004A56 /* AppIconAssets.swift */, ); - name = Translation; + path = Generated; sourceTree = ""; }; - DBF81C8B27F843D700004A56 /* Generated */ = { + DBF87AD32892A6540029A7C7 /* AppIconPreference */ = { isa = PBXGroup; children = ( - DBF81C8C27F843D700004A56 /* AppIconAssets.swift */, + DBF87AD42892A6740029A7C7 /* AppIconPreferenceView.swift */, ); - path = Generated; + path = AppIconPreference; sourceTree = ""; }; - DBF81C9227F8449800004A56 /* AppIcon */ = { + DBF87AD62892A67D0029A7C7 /* Translation */ = { isa = PBXGroup; children = ( - DBF81C9027F8448C00004A56 /* AppIconPreferenceView.swift */, + DBF87AD72892A67D0029A7C7 /* TranslateButtonPreferenceView.swift */, + DBF87AD82892A67D0029A7C7 /* TranslationServicePreferenceView.swift */, ); - name = AppIcon; + path = Translation; sourceTree = ""; }; DBFA471D2859C33900C9FF7F /* Like */ = { @@ -2984,6 +2987,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DBF87ADA2892A67D0029A7C7 /* TranslationServicePreferenceView.swift in Sources */, DB2D36F627D5E73A00C1FBE0 /* CompositeListViewController.swift in Sources */, DB5FD9B726D8EFF700CF5439 /* UserBriefInfoView+ViewModel.swift in Sources */, DB8761BE274552F800BA7EE2 /* StatusMediaGallerySection.swift in Sources */, @@ -3013,7 +3017,6 @@ DB761E4E255935380050DC01 /* DrawerSidebarHeaderView.swift in Sources */, DBCB402E255B670C00DD8D8F /* AccountListViewController.swift in Sources */, DBDA8E9824FE075E006750DC /* MainTabBarController.swift in Sources */, - DBF81C8827F7141600004A56 /* TranslationServicePreferenceView.swift in Sources */, DB76A66F276083CB00A50673 /* MediaPreviewTransitionViewController.swift in Sources */, DBCB408A255D8C2B00DD8D8F /* SafariActivity.swift in Sources */, DBD0B4992758B58F0015A388 /* DrawerSidebarAnimatedTransitioning.swift in Sources */, @@ -3030,6 +3033,7 @@ DB2C8740274F4B7D00CE0398 /* DisplayPreferenceViewModel.swift in Sources */, DBFA471C2859C33500C9FF7F /* MeLikeTimelineViewModel.swift in Sources */, DBCE2E992591A44000926D09 /* UIViewAnimatingPosition.swift in Sources */, + DB88E626288EA1F7009A01F5 /* DisplayPreferenceView.swift in Sources */, DB262A422722970000D18EF3 /* SearchResultViewModel.swift in Sources */, DB9B3256285737FE00AC818D /* GridTimelineViewModel.swift in Sources */, DBCB4053255B6EB100DD8D8F /* AccountListViewModel.swift in Sources */, @@ -3113,7 +3117,6 @@ DB580EB9288187BD00BC4A0F /* AccountPreferenceViewModel.swift in Sources */, DB33A4A925A319A0003CED7D /* ActionToolbarContainer.swift in Sources */, DB2C873C274F4B7D00CE0398 /* DeveloperView.swift in Sources */, - DBF81C8327F6ECF400004A56 /* AppearancePreferenceViewModel.swift in Sources */, DB2C8742274F4B7D00CE0398 /* AboutViewController.swift in Sources */, DB442465285AD8660095AECF /* ListStatusTimelineViewModel+Diffable.swift in Sources */, DB5FD9B326D8D94600CF5439 /* StatusTableViewCell+ViewModel.swift in Sources */, @@ -3196,7 +3199,6 @@ DB01091526E5EB64005F67D7 /* MastodonStatusThreadViewModel.swift in Sources */, DBA2103B275A0E91000B7CB2 /* FollowerListViewController+DataSourceProvider.swift in Sources */, DB5632BC26DF503800FC893F /* TwitterStatusThreadLeafViewModel.swift in Sources */, - DBF81C8527F709EF00004A56 /* TranslateButtonPreferenceView.swift in Sources */, DB51DC1A2715581E00A0D8FB /* ProfileDashboardView.swift in Sources */, DB442471285B17730095AECF /* SearchMediaTimelineViewController.swift in Sources */, DB2C873D274F4B7D00CE0398 /* DeveloperViewModel.swift in Sources */, @@ -3207,7 +3209,6 @@ DB5632B026DCED1300FC893F /* StatusThreadRootTableViewCell.swift in Sources */, DB8761C4274552FB00BA7EE2 /* HashtagData.swift in Sources */, DBDA8E2224FCF8A3006750DC /* AppDelegate.swift in Sources */, - DBF81C7E27F6E7F500004A56 /* AppearancePreferenceViewController.swift in Sources */, DB46D11F27DB2B50003B8BA1 /* ListUserViewModel+Diffable.swift in Sources */, DB442459285A42B50095AECF /* HashtagTimelineViewController.swift in Sources */, DB56329726DCBE1600FC893F /* StatusThreadViewController.swift in Sources */, @@ -3281,23 +3282,26 @@ DB5632AB26DCCD3900FC893F /* DataSourceFacade.swift in Sources */, DB8761D02745553700BA7EE2 /* MediaSection.swift in Sources */, DB51DC1C2715588E00A0D8FB /* ProfileDashboardMeterView.swift in Sources */, + DBF87AD52892A6740029A7C7 /* AppIconPreferenceView.swift in Sources */, DBB04A352861AE4D003799CA /* TrendPlaceViewModel.swift in Sources */, DB2C8743274F4B7D00CE0398 /* SettingListView.swift in Sources */, DB76A650275F1C3200A50673 /* MediaPreviewTransitionController.swift in Sources */, - DBF81C8027F6E80700004A56 /* AppearancePreferenceView.swift in Sources */, + DB88E62E2890DF7E009A01F5 /* BehaviorsPreferenceView.swift in Sources */, DB8761C5274552FB00BA7EE2 /* HashtagItem.swift in Sources */, DB76A65B275F49AE00A50673 /* MediaInfoDescriptionView.swift in Sources */, DB76A64D275DFA0600A50673 /* TwitterStatusThreadReplyViewModel+State.swift in Sources */, + DBF87AD92892A67D0029A7C7 /* TranslateButtonPreferenceView.swift in Sources */, DB76A66A27606F6900A50673 /* MediaPreviewVideoViewController.swift in Sources */, DB148B0C281A837E00B596C7 /* SidebarView.swift in Sources */, + DB88E6292890DF67009A01F5 /* BehaviorsPreferenceViewController.swift in Sources */, DBC8E050257653E100401E20 /* SavePhotoActivity.swift in Sources */, DB1D7B4325B5938400397DCD /* TwitterAuthenticationOptionViewController.swift in Sources */, DB830209273D12E600BF5224 /* NotificationTimelineViewController+DataSourceProvider.swift in Sources */, DB8761C1274552F800BA7EE2 /* UserItem.swift in Sources */, DB5FB0102727FCC5006520FA /* SearchUserViewModel.swift in Sources */, + DB88E62C2890DF79009A01F5 /* BehaviorsPreferenceViewModel.swift in Sources */, DB5632A926DCC96C00FC893F /* ListTimelineViewController+DataSourceProvider.swift in Sources */, DB56329A26DCBE7300FC893F /* StatusThreadViewModel.swift in Sources */, - DBF81C9127F8448C00004A56 /* AppIconPreferenceView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/TwidereX/Coordinator/SceneCoordinator.swift b/TwidereX/Coordinator/SceneCoordinator.swift index 0164be02..b8b96547 100644 --- a/TwidereX/Coordinator/SceneCoordinator.swift +++ b/TwidereX/Coordinator/SceneCoordinator.swift @@ -96,7 +96,7 @@ extension SceneCoordinator { // Settings case setting(viewModel: SettingListViewModel) case accountPreference(viewModel: AccountPreferenceViewModel) - case appearancePreference + case behaviorsPreference(viewModel: BehaviorsPreferenceViewModel) case displayPreference case about @@ -363,8 +363,10 @@ private extension SceneCoordinator { let _viewController = AccountPreferenceViewController() _viewController.viewModel = viewModel viewController = _viewController - case .appearancePreference: - viewController = AppearancePreferenceViewController() + case .behaviorsPreference(let viewModel): + let _viewController = BehaviorsPreferenceViewController() + _viewController.viewModel = viewModel + viewController = _viewController case .displayPreference: viewController = DisplayPreferenceViewController() case .about: diff --git a/TwidereX/Protocol/ScrollViewContainer.swift b/TwidereX/Protocol/ScrollViewContainer.swift index cf2b5a12..65a5c63a 100644 --- a/TwidereX/Protocol/ScrollViewContainer.swift +++ b/TwidereX/Protocol/ScrollViewContainer.swift @@ -10,14 +10,22 @@ import UIKit protocol ScrollViewContainer: UIViewController { var scrollView: UIScrollView { get } - func scrollToTop(animated: Bool) + func scrollToTop(animated: Bool, option: ScrollViewContainerOption) } extension ScrollViewContainer { - func scrollToTop(animated: Bool) { + func scrollToTop(animated: Bool, option: ScrollViewContainerOption = .init()) { scrollView.scrollRectToVisible( CGRect(origin: .zero, size: CGSize(width: 1, height: 1)), animated: animated ) } } + +struct ScrollViewContainerOption { + let tryRefreshWhenStayAtTop: Bool + + init(tryRefreshWhenStayAtTop: Bool = true) { + self.tryRefreshWhenStayAtTop = tryRefreshWhenStayAtTop + } +} diff --git a/TwidereX/Scene/Root/ContentSplitViewController.swift b/TwidereX/Scene/Root/ContentSplitViewController.swift index cd06d6f5..4f8216cf 100644 --- a/TwidereX/Scene/Root/ContentSplitViewController.swift +++ b/TwidereX/Scene/Root/ContentSplitViewController.swift @@ -45,6 +45,8 @@ final class ContentSplitViewController: UIViewController, NeedsDependency { @Published var isSidebarDisplay = false @Published var isSecondaryTabBarControllerActive = false + @Published var tabBarTapScrollPreference = UserDefaults.shared.tabBarTapScrollPreference + // [Tab: HashValue] var transformNavigationStackRecord: [TabBarItem: [Int]] = [:] @@ -59,6 +61,10 @@ extension ContentSplitViewController { override func viewDidLoad() { super.viewDidLoad() + UserDefaults.shared.publisher(for: \.tabBarTapScrollPreference) + .removeDuplicates() + .assign(to: &$tabBarTapScrollPreference) + navigationController?.setNavigationBarHidden(true, animated: false) view.backgroundColor = .opaqueSeparator @@ -129,7 +135,7 @@ extension ContentSplitViewController { extension ContentSplitViewController { func select(tab: TabBarItem) { - sidebarViewController.viewModel.setActiveTab(item: tab) + sidebarViewController.viewModel.tap(item: tab) } } @@ -217,7 +223,7 @@ extension ContentSplitViewController { // MARK: - SidebarViewModelDelegate extension ContentSplitViewController: SidebarViewModelDelegate { - func sidebarViewModel(_ viewModel: SidebarViewModel, active tab: TabBarItem) { + func sidebarViewModel(_ viewModel: SidebarViewModel, didTapItem tab: TabBarItem) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") switch tab { @@ -245,5 +251,29 @@ extension ContentSplitViewController: SidebarViewModelDelegate { } } } + + func sidebarViewModel(_ viewModel: SidebarViewModel, didDoubleTapItem tab: TabBarItem) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + + switch tabBarTapScrollPreference { + case .single: return + case .double: break + } + + switch tab { + case .settings: + // do nothing + break + default: + guard viewModel.activeTab == tab else { return } + if mainTabBarController.tabs.contains(tab) { + mainTabBarController.scrollToTop(tab: tab, isMainTabBarControllerActive: !isSecondaryTabBarControllerActive) + } else if secondaryTabBarController.tabs.contains(tab) { + secondaryTabBarController.scrollToTop(tab: tab, isSecondaryTabBarControllerActive: isSecondaryTabBarControllerActive) + } else { + assertionFailure() + } + } + } } diff --git a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift index bfefb180..e071d565 100644 --- a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift +++ b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift @@ -15,6 +15,7 @@ import SwiftMessages import TwitterSDK import TwidereUI import TwidereCommon +import func QuartzCore.CACurrentMediaTime final class MainTabBarController: UITabBarController { @@ -24,7 +25,9 @@ final class MainTabBarController: UITabBarController { weak var context: AppContext! weak var coordinator: SceneCoordinator! - + + private let doubleTapGestureRecognizer = UITapGestureRecognizer.doubleTapGestureRecognizer + @Published var tabs: [TabBarItem] = [ .home, .notification, @@ -33,10 +36,18 @@ final class MainTabBarController: UITabBarController { ] @Published var currentTab: TabBarItem = .home + static var popToRootAfterActionTolerance: TimeInterval { 0.5 } + var lastPopToRootTime = CACurrentMediaTime() + @Published var tabBarTapScrollPreference = UserDefaults.shared.tabBarTapScrollPreference + init(context: AppContext, coordinator: SceneCoordinator) { self.context = context self.coordinator = coordinator super.init(nibName: nil, bundle: nil) + + UserDefaults.shared.publisher(for: \.tabBarTapScrollPreference) + .removeDuplicates() + .assign(to: &$tabBarTapScrollPreference) } required init?(coder: NSCoder) { @@ -69,12 +80,33 @@ extension MainTabBarController { viewController.tabBarItem.image = tab.image viewController.tabBarItem.accessibilityLabel = tab.title viewController.tabBarItem.largeContentSizeImage = tab.largeImage - viewController.tabBarItem.imageInsets = UIEdgeInsets(top: 6, left: 0, bottom: -6, right: 0) return viewController } setViewControllers(viewControllers, animated: false) selectedIndex = 0 + // TabBarItem appearance + configureTabBarItemAppearance() + UserDefaults.shared.publisher(for: \.preferredTabBarLabelDisplay) + .receive(on: DispatchQueue.main) + .sink { [weak self] preferredTabBarLabelDisplay in + guard let self = self else { return } + self.configureTabBarItemAppearance() + } + .store(in: &disposeBag) + + // TabBar tap gesture + doubleTapGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.doubleTapGestureRecognizerHandler(_:))) + doubleTapGestureRecognizer.delaysTouchesEnded = false + tabBar.addGestureRecognizer(doubleTapGestureRecognizer) + setupDoubleTapGestureEnabled() + UserDefaults.shared.publisher(for: \.tabBarTapScrollPreference) + .sink { [weak self] _ in + guard let self = self else { return } + self.setupDoubleTapGestureEnabled() + } + .store(in: &disposeBag) + let feedbackGenerator = UINotificationFeedbackGenerator() // post publish result observer @@ -160,6 +192,27 @@ extension MainTabBarController { return viewController(of: NotificationViewController.self) } + private func configureTabBarItemAppearance() { + let preferredTabBarLabelDisplay = UserDefaults.shared.preferredTabBarLabelDisplay + + for item in tabBar.items ?? [] { + item.imageInsets = preferredTabBarLabelDisplay ? .zero : UIEdgeInsets(top: 6, left: 0, bottom: -6, right: 0) + } + + let tabBarAppearance = ThemeService.setupTabBarAppearance() + tabBar.standardAppearance = tabBarAppearance + tabBar.scrollEdgeAppearance = tabBarAppearance + } + + func setupDoubleTapGestureEnabled() { + doubleTapGestureRecognizer.isEnabled = { + switch UserDefaults.shared.tabBarTapScrollPreference { + case .single: return false + case .double: return true + } + }() + } + private func updateTabBarDisplay() { switch traitCollection.horizontalSizeClass { case .compact: @@ -227,21 +280,60 @@ extension MainTabBarController { guard let index = _index else { return } - + defer { selectedIndex = index currentTab = tab } - // check if selected and scroll it to top or pop to top + guard popToRoot(tab: tab, isMainTabBarControllerActive: isMainTabBarControllerActive) else { return } + + // check if preferred double tap for scrollToTop + switch tabBarTapScrollPreference { + case .single: break + case .double: return + } + + scrollToTop(tab: tab, isMainTabBarControllerActive: isMainTabBarControllerActive) + } + + func popToRoot(tab: TabBarItem, isMainTabBarControllerActive: Bool = true) -> Bool { + let _index = tabBar.items?.firstIndex(where: { $0.tag == tab.tag }) + guard let index = _index else { + return false + } + guard isMainTabBarControllerActive, currentTab == tab, let viewController = viewControllers?[safe: index], let navigationController = viewController as? UINavigationController - else { return } + else { return false } guard navigationController.viewControllers.count == 1 else { navigationController.popToRootViewController(animated: true) + lastPopToRootTime = CACurrentMediaTime() + return false + } + + return true + } + + func scrollToTop(tab: TabBarItem, isMainTabBarControllerActive: Bool = true) { + let now = CACurrentMediaTime() + guard now - lastPopToRootTime > MainTabBarController.popToRootAfterActionTolerance else { return } + + let _index = tabBar.items?.firstIndex(where: { $0.tag == tab.tag }) + guard let index = _index else { + return + } + + guard isMainTabBarControllerActive, + currentTab == tab, + let viewController = viewControllers?[safe: index], + let navigationController = viewController as? UINavigationController + else { return } + + guard navigationController.viewControllers.count == 1 else { return } diff --git a/TwidereX/Scene/Root/SecondaryTab/SecondaryTabBarController.swift b/TwidereX/Scene/Root/SecondaryTab/SecondaryTabBarController.swift index 4294dba0..5f517bcf 100644 --- a/TwidereX/Scene/Root/SecondaryTab/SecondaryTabBarController.swift +++ b/TwidereX/Scene/Root/SecondaryTab/SecondaryTabBarController.swift @@ -10,6 +10,7 @@ import os.log import Foundation import UIKit import Combine +import func QuartzCore.CACurrentMediaTime final class SecondaryTabBarController: UITabBarController { @@ -27,11 +28,18 @@ final class SecondaryTabBarController: UITabBarController { } @Published var currentTab: TabBarItem? + static var popToRootAfterActionTolerance: TimeInterval { 0.5 } + var lastPopToRootTime = CACurrentMediaTime() + @Published var tabBarTapScrollPreference = UserDefaults.shared.tabBarTapScrollPreference init(context: AppContext, coordinator: SceneCoordinator) { self.context = context self.coordinator = coordinator super.init(nibName: nil, bundle: nil) + + UserDefaults.shared.publisher(for: \.tabBarTapScrollPreference) + .removeDuplicates() + .assign(to: &$tabBarTapScrollPreference) } required init?(coder: NSCoder) { @@ -70,20 +78,62 @@ extension SecondaryTabBarController { defer { selectedIndex = index currentTab = tab + } - // check if selected and scroll it to top + guard popToRoot(tab: tab, isSecondaryTabBarControllerActive: isSecondaryTabBarControllerActive) else { return } + + // check if preferred double tap for scrollToTop + switch tabBarTapScrollPreference { + case .single: break + case .double: return + } + + scrollToTop(tab: tab, isSecondaryTabBarControllerActive: isSecondaryTabBarControllerActive) + } + + func popToRoot(tab: TabBarItem, isSecondaryTabBarControllerActive: Bool = true) -> Bool { + let _index = tabBar.items?.firstIndex(where: { $0.tag == tab.tag }) + guard let index = _index else { + return false + } + + // check if selected and pop it to root guard isSecondaryTabBarControllerActive, currentTab == tab, let viewController = viewControllers?[safe: index], let navigationController = viewController as? UINavigationController - else { return } - + else { return false } + // additional prepend SecondaryTabBarRootController guard navigationController.viewControllers.count == 1 + 1 else { if let second = navigationController.viewControllers[safe: 1] { navigationController.popToViewController(second, animated: true) + lastPopToRootTime = CACurrentMediaTime() } + return false + } + + return true + } + + func scrollToTop(tab: TabBarItem, isSecondaryTabBarControllerActive: Bool = true) { + let now = CACurrentMediaTime() + guard now - lastPopToRootTime > SecondaryTabBarController.popToRootAfterActionTolerance else { return } + + let _index = tabBar.items?.firstIndex(where: { $0.tag == tab.tag }) + guard let index = _index else { + return + } + + // check if selected and scroll it to top + guard isSecondaryTabBarControllerActive, + currentTab == tab, + let viewController = viewControllers?[safe: index], + let navigationController = viewController as? UINavigationController + else { return } + + guard navigationController.viewControllers.count == 1 + 1 else { return } diff --git a/TwidereX/Scene/Root/Sidebar/SidebarView.swift b/TwidereX/Scene/Root/Sidebar/SidebarView.swift index 7671ad36..48507de8 100644 --- a/TwidereX/Scene/Root/Sidebar/SidebarView.swift +++ b/TwidereX/Scene/Root/Sidebar/SidebarView.swift @@ -10,6 +10,7 @@ import SwiftUI import TwidereAsset import TwidereLocalization import TwidereUI +import func QuartzCore.CACurrentMediaTime struct SidebarView: View { @@ -32,7 +33,9 @@ struct SidebarView: View { isActive: viewModel.activeTab == item, useAltStyle: shouldUseAltStyle(for: item) ) { item in - viewModel.setActiveTab(item: item) + viewModel.tap(item: item) + } doubleTapAction: { item in + viewModel.doubleTap(item: item) } } if !viewModel.secondaryTabBarItems.isEmpty { @@ -50,7 +53,9 @@ struct SidebarView: View { isActive: viewModel.activeTab == item, useAltStyle: shouldUseAltStyle(for: item) ) { item in - viewModel.setActiveTab(item: item) + viewModel.tap(item: item) + } doubleTapAction: { item in + viewModel.doubleTap(item: item) } } } @@ -64,7 +69,9 @@ struct SidebarView: View { isActive: false, useAltStyle: false ) { item in - viewModel.setActiveTab(item: item) + viewModel.tap(item: item) + } doubleTapAction: { item in + viewModel.doubleTap(item: item) } } .background(Color(uiColor: .systemBackground)) @@ -77,16 +84,20 @@ struct SidebarView: View { extension SidebarView { struct EntryButton: View { + + @State var lastDoubleTapTime = CACurrentMediaTime() + let item: TabBarItem let isActive: Bool let useAltStyle: Bool - let action: (TabBarItem) -> () + let tapAction: (TabBarItem) -> () + let doubleTapAction: (TabBarItem) -> () var body: some View { let dimension: CGFloat = 32 let padding: CGFloat = 16 Button { - action(item) + // do nothing } label: { VectorImageView( image: useAltStyle ? item.altImage : item.image, @@ -97,6 +108,23 @@ extension SidebarView { .frame(maxWidth: .infinity, alignment: .center) .frame(height: dimension + 2 * padding, alignment: .center) .accessibilityLabel(item.title) + .simultaneousGesture(TapGesture().onEnded { + let now = CACurrentMediaTime() + guard now - lastDoubleTapTime > 0.1 else { + return + } + tapAction(item) + }) + .simultaneousGesture(TapGesture(count: 2).onEnded { + doubleTapAction(item) + lastDoubleTapTime = CACurrentMediaTime() + }) + // note: + // SwiftUI gesture `exclusive(before:)` not works well on macCatalyst. + // So we handle single / double tap gesture simultaneous + // 1. deliver single tap without delay + // 2. deliver double tap if triggered + // 3. cancel second single tap if double tap emitted within 100ms tolerance } } } diff --git a/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift b/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift index 5bfa3449..7b576dbf 100644 --- a/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift @@ -14,7 +14,8 @@ import TwidereCore import TwidereAsset protocol SidebarViewModelDelegate: AnyObject { - func sidebarViewModel(_ viewModel: SidebarViewModel, active item: TabBarItem) + func sidebarViewModel(_ viewModel: SidebarViewModel, didTapItem item: TabBarItem) + func sidebarViewModel(_ viewModel: SidebarViewModel, didDoubleTapItem item: TabBarItem) } final class SidebarViewModel: ObservableObject { @@ -82,8 +83,12 @@ final class SidebarViewModel: ObservableObject { extension SidebarViewModel { - func setActiveTab(item: TabBarItem) { - delegate?.sidebarViewModel(self, active: item) + func tap(item: TabBarItem) { + delegate?.sidebarViewModel(self, didTapItem: item) + } + + func doubleTap(item: TabBarItem) { + delegate?.sidebarViewModel(self, didDoubleTapItem: item) } } diff --git a/TwidereX/Scene/Setting/AppearancePreference/AppIconPreferenceView.swift b/TwidereX/Scene/Setting/AppIconPreference/AppIconPreferenceView.swift similarity index 100% rename from TwidereX/Scene/Setting/AppearancePreference/AppIconPreferenceView.swift rename to TwidereX/Scene/Setting/AppIconPreference/AppIconPreferenceView.swift diff --git a/TwidereX/Scene/Setting/AppearancePreference/AppearancePreferenceView.swift b/TwidereX/Scene/Setting/AppearancePreference/AppearancePreferenceView.swift deleted file mode 100644 index 195cedcc..00000000 --- a/TwidereX/Scene/Setting/AppearancePreference/AppearancePreferenceView.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// AppearanceView.swift -// TwidereX -// -// Created by MainasuK on 2022-4-1. -// Copyright © 2022 Twidere. All rights reserved. -// - -import SwiftUI -import TwidereLocalization -import TwidereUI - -struct AppearancePreferenceView: View { - - @ObservedObject var viewModel: AppearancePreferenceViewModel - - @State private var isTranslateButtonPreferenceSheetPresented = false - - - var appIconRow: some View { - Button { - - } label: { - HStack { - Text(L10n.Scene.Settings.Appearance.appIcon) - Spacer() - Image(uiImage: UIImage(named: "\(viewModel.alternateIconNamePreference.iconName)") ?? UIImage()) - .cornerRadius(4) - } - } - .tint(Color(uiColor: .label)) - } - - var body: some View { - List { - Section { - NavigationLink { - AppIconPreferenceView() - } label: { - appIconRow - } - } header: { - EmptyView() - } - Section { - // Translate Button - NavigationLink { - TranslateButtonPreferenceView(preference: viewModel.translateButtonPreference) - } label: { - Text(L10n.Scene.Settings.Appearance.Translation.translateButton) - .tint(Color(uiColor: .label)) - .badge(viewModel.translateButtonPreference.text) - } - // Service - NavigationLink { - TranslationServicePreferenceView(preference: viewModel.translationServicePreference) - } label: { - Text(L10n.Scene.Settings.Appearance.Translation.service) - .tint(Color(uiColor: .label)) - .badge(viewModel.translationServicePreference.text) - } - } header: { - Text(L10n.Scene.Settings.Appearance.SectionHeader.translation) - } - .textCase(nil) - - - } - .listStyle(InsetGroupedListStyle()) - } -} - -#if DEBUG -struct AppearanceView_Previews: PreviewProvider { - static var previews: some View { - NavigationView { - AppearancePreferenceView(viewModel: AppearancePreferenceViewModel(context: .shared)) - .navigationBarTitle(Text(L10n.Scene.Settings.Appearance.title)) - .navigationBarTitleDisplayMode(.inline) - } - } -} -#endif diff --git a/TwidereX/Scene/Setting/AppearancePreference/AppearancePreferenceViewModel.swift b/TwidereX/Scene/Setting/AppearancePreference/AppearancePreferenceViewModel.swift deleted file mode 100644 index 01f828a2..00000000 --- a/TwidereX/Scene/Setting/AppearancePreference/AppearancePreferenceViewModel.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// AppearancePreferenceViewModel.swift -// TwidereX -// -// Created by MainasuK on 2022-4-1. -// Copyright © 2022 Twidere. All rights reserved. -// - -import os.log -import UIKit -import SwiftUI -import Combine -import CoreDataStack -import TwidereCommon -import TwidereCore -import TwitterSDK -import MastodonSDK - -final class AppearancePreferenceViewModel: ObservableObject { - - // input - let context: AppContext - - // output - // App Icon - @Published var alternateIconNamePreference = UserDefaults.shared.alternateIconNamePreference - - // Translation - @Published var translateButtonPreference = UserDefaults.shared.translateButtonPreference - @Published var translationServicePreference = UserDefaults.shared.translationServicePreference - - init( - context: AppContext - ) { - self.context = context - // end init - - // App Icon - UserDefaults.shared.publisher(for: \.alternateIconNamePreference) - .assign(to: &$alternateIconNamePreference) - - // Translation - UserDefaults.shared.publisher(for: \.translateButtonPreference) - .assign(to: &$translateButtonPreference) - UserDefaults.shared.publisher(for: \.translationServicePreference) - .assign(to: &$translationServicePreference) - } - -} diff --git a/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceView.swift b/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceView.swift new file mode 100644 index 00000000..8a7569b9 --- /dev/null +++ b/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceView.swift @@ -0,0 +1,58 @@ +// +// BehaviorsPreferenceView.swift +// TwidereX +// +// Created by MainasuK on 2022-7-27. +// Copyright © 2022 Twidere. All rights reserved. +// + +import SwiftUI +import TwidereLocalization +import TwidereUI + +struct BehaviorsPreferenceView: View { + + @ObservedObject var viewModel: BehaviorsPreferenceViewModel + + var body: some View { + List { + // Tab Bar + Section { + Toggle(isOn: $viewModel.preferredTabBarLabelDisplay) { + Text(verbatim: "Show tab bar labels") // TODO: i18n + } + Picker(selection: $viewModel.tabBarTapScrollPreference) { + ForEach(UserDefaults.TabBarTapScrollPreference.allCases, id: \.self) { preference in + Text(preference.title) + } + } label: { + Text(verbatim: "Tap tab bar scroll to top") // TODO: i18n + } + } header: { + Text(verbatim: "Tab Bar") // TODO: i18n + .textCase(nil) + } + // Timeline Refreshing + Section { + Toggle(isOn: $viewModel.preferredTimelineAutoRefresh) { + Text(verbatim: "Automatically refresh timeline") // TODO: i18n + } + Picker(selection: $viewModel.timelineRefreshInterval) { + ForEach(UserDefaults.TimelineRefreshInterval.allCases, id: \.self) { preference in + Text(preference.title) + } + } label: { + Text(verbatim: "Refresh Interval") // TODO: i18n + + } + Toggle(isOn: $viewModel.preferredTimelineResetToTop) { + Text(verbatim: "Reset to top") // TODO: i18n + } + } header: { + Text(verbatim: "Timeline Refreshing") // TODO: i18n + .textCase(nil) + } + } + } + +} diff --git a/TwidereX/Scene/Setting/AppearancePreference/AppearancePreferenceViewController.swift b/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewController.swift similarity index 69% rename from TwidereX/Scene/Setting/AppearancePreference/AppearancePreferenceViewController.swift rename to TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewController.swift index 3d756d40..3ef8deec 100644 --- a/TwidereX/Scene/Setting/AppearancePreference/AppearancePreferenceViewController.swift +++ b/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewController.swift @@ -1,8 +1,8 @@ // -// AppearancePreferenceViewController.swift +// BehaviorsPreferenceViewController.swift // TwidereX // -// Created by MainasuK on 2022-4-1. +// Created by MainasuK on 2022-7-27. // Copyright © 2022 Twidere. All rights reserved. // @@ -12,31 +12,31 @@ import Combine import SwiftUI import TwidereLocalization -final class AppearancePreferenceViewController: UIViewController, NeedsDependency { +final class BehaviorsPreferenceViewController: UIViewController, NeedsDependency { - let logger = Logger(subsystem: "AppearancePreferenceViewController", category: "ViewController") + let logger = Logger(subsystem: "BehaviorsPreferenceViewController", category: "ViewController") weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } var disposeBag = Set() - private(set) lazy var viewModel = AppearancePreferenceViewModel(context: context) - private(set) lazy var appearanceView = AppearancePreferenceView(viewModel: viewModel) + var viewModel: BehaviorsPreferenceViewModel! + private(set) lazy var behaviorsPreferenceView = BehaviorsPreferenceView(viewModel: viewModel) deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) } - + } -extension AppearancePreferenceViewController { +extension BehaviorsPreferenceViewController { override func viewDidLoad() { super.viewDidLoad() - title = L10n.Scene.Settings.Appearance.title + title = "Behaviors" // TODO: i18n - let hostingViewController = UIHostingController(rootView: appearanceView) + let hostingViewController = UIHostingController(rootView: behaviorsPreferenceView) addChild(hostingViewController) hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(hostingViewController.view) diff --git a/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewModel.swift b/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewModel.swift new file mode 100644 index 00000000..d5e04d7e --- /dev/null +++ b/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewModel.swift @@ -0,0 +1,115 @@ +// +// BehaviorsPreferenceViewModel.swift +// TwidereX +// +// Created by MainasuK on 2022-7-27. +// Copyright © 2022 Twidere. All rights reserved. +// + +import os.log +import UIKit +import SwiftUI +import Combine +import CoreDataStack +import TwidereCommon +import TwidereCore +import TwitterSDK +import MastodonSDK + +final class BehaviorsPreferenceViewModel: ObservableObject { + + var disposeBag = Set() + + // input + let context: AppContext + + // Tab Bar + @Published var preferredTabBarLabelDisplay = UserDefaults.shared.preferredTabBarLabelDisplay + @Published var tabBarTapScrollPreference = UserDefaults.shared.tabBarTapScrollPreference + + // Timeline Refreshing + @Published var preferredTimelineAutoRefresh = UserDefaults.shared.preferredTimelineAutoRefresh + @Published var timelineRefreshInterval = UserDefaults.shared.timelineRefreshInterval + @Published var preferredTimelineResetToTop = UserDefaults.shared.preferredTimelineResetToTop + + // output + + + init( + context: AppContext + ) { + self.context = context + // end init + + // preferredTabBarLabelDisplay + UserDefaults.shared.publisher(for: \.preferredTabBarLabelDisplay) + .removeDuplicates() + .assign(to: &$preferredTabBarLabelDisplay) + $preferredTabBarLabelDisplay + .sink { preferredTabBarLabelDisplay in + UserDefaults.shared.preferredTabBarLabelDisplay = preferredTabBarLabelDisplay + } + .store(in: &disposeBag) + + // tabBarTapScrollPreference + UserDefaults.shared.publisher(for: \.tabBarTapScrollPreference) + .removeDuplicates() + .assign(to: &$tabBarTapScrollPreference) + $tabBarTapScrollPreference + .sink { tabBarTapScrollPreference in + UserDefaults.shared.tabBarTapScrollPreference = tabBarTapScrollPreference + } + .store(in: &disposeBag) + + // preferredTimelineAutoRefresh + UserDefaults.shared.publisher(for: \.preferredTimelineAutoRefresh) + .removeDuplicates() + .assign(to: &$preferredTimelineAutoRefresh) + $preferredTimelineAutoRefresh + .sink { preferredTimelineAutoRefresh in + UserDefaults.shared.preferredTimelineAutoRefresh = preferredTimelineAutoRefresh + } + .store(in: &disposeBag) + + // timelineRefreshInterval + UserDefaults.shared.publisher(for: \.timelineRefreshInterval) + .removeDuplicates() + .assign(to: &$timelineRefreshInterval) + $timelineRefreshInterval + .sink { timelineRefreshInterval in + UserDefaults.shared.timelineRefreshInterval = timelineRefreshInterval + } + .store(in: &disposeBag) + + // preferredTimelineResetToTop + UserDefaults.shared.publisher(for: \.preferredTimelineResetToTop) + .removeDuplicates() + .assign(to: &$preferredTimelineResetToTop) + $preferredTimelineResetToTop + .sink { preferredTimelineResetToTop in + UserDefaults.shared.preferredTimelineResetToTop = preferredTimelineResetToTop + } + .store(in: &disposeBag) + } + +} + +extension UserDefaults.TabBarTapScrollPreference { + var title: String { + switch self { + case .single: return "Single Tap" + case .double: return "Double Tap" + } + } +} + +extension UserDefaults.TimelineRefreshInterval { + var title: String { + switch self { + case ._30s: return "30 seconds" + case ._60s: return "60 seconds" + case ._120s: return "120 seconds" + case ._300s: return "300 seconds" + } + } +} diff --git a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceView.swift b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceView.swift new file mode 100644 index 00000000..8867ea92 --- /dev/null +++ b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceView.swift @@ -0,0 +1,88 @@ +// +// DisplayPreferenceView.swift +// TwidereX +// +// Created by MainasuK on 2022-7-25. +// Copyright © 2022 Twidere. All rights reserved. +// + +import Foundation +import SwiftUI +import Combine +import TwidereCore +import TwidereUI +import TwidereLocalization +import AppShared + +struct DisplayPreferenceView: View { + + @ObservedObject var viewModel: DisplayPreferenceViewModel + + @State var timelineStatusViewHeight: CGFloat = .zero + @State var threadStatusViewHeight: CGFloat = .zero + + var body: some View { + List { + Section { + PrototypeStatusViewRepresentable( + style: .timeline, + configurationContext: StatusView.ConfigurationContext( + dateTimeProvider: DateTimeSwiftProvider(), + twitterTextProvider: OfficialTwitterTextProvider(), + authenticationContext: viewModel.$authenticationContext + ), + height: $timelineStatusViewHeight + ) + .frame(height: timelineStatusViewHeight) + } header: { + Text(verbatim: L10n.Scene.Settings.Display.SectionHeader.preview) + .textCase(nil) + } + + // Avatar + Section { + avatarStylePicker + } header: { + Text(verbatim: "Avatar") // TODO: i18n + } // end Section + + // Translation + Section { + // Translate Button + Picker(selection: $viewModel.translateButtonPreference) { + ForEach(UserDefaults.TranslateButtonPreference.allCases, id: \.self) { preference in + Text(preference.text) + } + } label: { + Text(L10n.Scene.Settings.Appearance.Translation.translateButton) + } + // Translate Service + Picker(selection: $viewModel.translationServicePreference) { + ForEach(UserDefaults.TranslationServicePreference.allCases, id: \.self) { preference in + Text(preference.text) + } + } label: { + Text(L10n.Scene.Settings.Appearance.Translation.service) + } + } header: { + Text(verbatim: L10n.Scene.Settings.Appearance.SectionHeader.translation) + .textCase(nil) + } + } + } + +} + +extension DisplayPreferenceView { + + var avatarStylePicker: some View { + Picker(selection: $viewModel.avatarStyle) { + ForEach(UserDefaults.AvatarStyle.allCases, id: \.self) { preference in + Text(preference.text) + } + } label: { + Text(L10n.Scene.Settings.Display.Text.avatarStyle) + } + } + +} diff --git a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewController.swift b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewController.swift index b43ff9fa..e380db83 100644 --- a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewController.swift +++ b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewController.swift @@ -17,6 +17,7 @@ final class DisplayPreferenceViewController: UIViewController, NeedsDependency { var disposeBag = Set() let viewModel = DisplayPreferenceViewModel() + private(set) lazy var displayPreferenceView = DisplayPreferenceView(viewModel: viewModel) private(set) lazy var tableView: UITableView = { let tableView = ControlContainableTableView(frame: .zero, style: .insetGrouped) @@ -40,61 +41,59 @@ extension DisplayPreferenceViewController { super.viewDidLoad() title = L10n.Scene.Settings.Display.title + viewModel.viewSize = view.frame.size - tableView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(tableView) + let hostingViewController = UIHostingController(rootView: displayPreferenceView) + addChild(hostingViewController) + hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(hostingViewController.view) NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: view.topAnchor), - tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + hostingViewController.view.topAnchor.constraint(equalTo: view.topAnchor), + hostingViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + hostingViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + hostingViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - - tableView.delegate = self - tableView.dataSource = viewModel - -// tableView.addGestureRecognizer(textFontSizeSliderPanGestureRecognizer) -// textFontSizeSliderPanGestureRecognizer.addTarget(self, action: #selector(DisplayPreferenceViewController.sliderPanGestureRecoginzerHandler(_:))) -// textFontSizeSliderPanGestureRecognizer.delegate = self } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) + public override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() - tableView.deselectRow(with: transitionCoordinator, animated: animated) + if viewModel.viewSize != view.frame.size { + viewModel.viewSize = view.frame.size + } } } // MARK: - UITableViewDelegate -extension DisplayPreferenceViewController: UITableViewDelegate { - - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let sectionData = viewModel.sections[section] - let header = sectionData.header - let headerView = TableViewSectionTextHeaderView() - headerView.label.text = header - return headerView - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let section = viewModel.sections[indexPath.section] - let setting = section.settings[indexPath.row] - - switch setting { - case .avatarStyle(let avatarStyle): - UserDefaults.shared.avatarStyle = avatarStyle - tableView.deselectRow(at: indexPath, animated: true) - default: - break - } - } - -} +//extension DisplayPreferenceViewController: UITableViewDelegate { +// +// func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { +// let sectionData = viewModel.sections[section] +// let header = sectionData.header +// let headerView = TableViewSectionTextHeaderView() +// headerView.label.text = header +// return headerView +// } +// +// func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { +// let section = viewModel.sections[indexPath.section] +// let setting = section.settings[indexPath.row] +// +// switch setting { +// case .avatarStyle(let avatarStyle): +// UserDefaults.shared.avatarStyle = avatarStyle +// tableView.deselectRow(at: indexPath, animated: true) +// default: +// break +// } +// } +// +//} -extension DisplayPreferenceViewController { - - @objc private func sliderPanGestureRecoginzerHandler(_ sender: UIPanGestureRecognizer) { +//extension DisplayPreferenceViewController { +// +// @objc private func sliderPanGestureRecoginzerHandler(_ sender: UIPanGestureRecognizer) { // let slider = viewModel.fontSizeSlideTableViewCell.slider // guard slider.isUserInteractionEnabled else { return } // @@ -109,13 +108,13 @@ extension DisplayPreferenceViewController { // let index = max(0, min(UserDefaults.contentSizeCategory.count - 1, Int(roundValue))) // let customContentSizeCatagory = UserDefaults.contentSizeCategory[index] // viewModel.customContentSizeCatagory.value = customContentSizeCatagory - } - -} +// } +// +//} // MARK: - UIGestureRecognizerDelegate -extension DisplayPreferenceViewController: UIGestureRecognizerDelegate { - +//extension DisplayPreferenceViewController: UIGestureRecognizerDelegate { +// // func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { // if gestureRecognizer === textFontSizeSliderPanGestureRecognizer { // return true @@ -135,5 +134,5 @@ extension DisplayPreferenceViewController: UIGestureRecognizerDelegate { // // return true // } - -} +// +//} diff --git a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewModel.swift b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewModel.swift index cd59f84b..cfc24428 100644 --- a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewModel.swift +++ b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewModel.swift @@ -15,123 +15,55 @@ import TwidereLocalization import TwidereUI import TwitterMeta -final class DisplayPreferenceViewModel: NSObject { +final class DisplayPreferenceViewModel: ObservableObject { var disposeBag = Set() - - // input -// let customContentSizeCatagory: CurrentValueSubject - - // output - let sections: [Section] = [ - Section(header: L10n.Scene.Settings.Display.SectionHeader.preview, settings: [.preview]), - // Section(header: L10n.Scene.Settings.Display.SectionHeader.text, settings: [ - // .useTheSystemFontSizeSwitch, - // .fontSizeSlider, - // ]), - Section(header: L10n.Scene.Settings.Display.Text.avatarStyle, settings: [ - .avatarStyle(.circle), - .avatarStyle(.roundedSquare), - ]), - ] - let fontSizeSlideTableViewCell = TableSlideTableViewCell() - - override init() { -// customContentSizeCatagory = CurrentValueSubject(UserDefaults.shared.customContentSizeCatagory) - super.init() - -// let feedbackGenerator = UIImpactFeedbackGenerator(style: .light) -// customContentSizeCatagory -// .dropFirst() -// .removeDuplicates() -// .sink { customContentSizeCatagory in -// feedbackGenerator.impactOccurred() -// UserDefaults.shared.customContentSizeCatagory = customContentSizeCatagory -// } -// .store(in: &disposeBag) - } - - -} - -extension DisplayPreferenceViewModel { - - enum Setting { - case preview - -// case useTheSystemFontSizeSwitch -// case fontSizeSlider - - // Avatar Style - case avatarStyle(UserDefaults.AvatarStyle) - - case dateFormat - } - - struct Section { - let header: String - let settings: [Setting] - } -} + // MARK: - layout + @Published var viewSize: CGSize = .zero -// MARK: - UITableViewDataSource -extension DisplayPreferenceViewModel: UITableViewDataSource { - - func numberOfSections(in tableView: UITableView) -> Int { - return sections.count - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return sections[section].settings.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell: UITableViewCell + // input - let section = sections[indexPath.section] - switch section.settings[indexPath.row] { - case .preview: - let _cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell - DisplayPreferenceViewModel.configure(cell: _cell) - cell = _cell - case .avatarStyle(let avatarStyle): - let _cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TableViewCheckmarkTableViewCell.self), for: indexPath) as! TableViewCheckmarkTableViewCell - let metaContent = Meta.convert(from: .plaintext(string: avatarStyle.text)) - _cell.primaryTextLabel.configure(content: metaContent) - UserDefaults.shared - .observe(\.avatarStyle, options: [.initial, .new]) { defaults, _ in - _cell.accessoryType = avatarStyle == defaults.avatarStyle ? .checkmark : .none - } - .store(in: &_cell.observations) - cell = _cell - case .dateFormat: - let _cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TableViewEntryTableViewCell.self), for: indexPath) as! TableViewEntryTableViewCell - _cell.iconImageView.isHidden = true - let metaContent = Meta.convert(from: .plaintext(string: L10n.Scene.Settings.Display.SectionHeader.dateFormat)) - _cell.primaryTextLabel.configure(content: metaContent) - cell = _cell - } - return cell - } + // avatar + @Published var avatarStyle = UserDefaults.shared.avatarStyle -} + // Translation + @Published var translateButtonPreference = UserDefaults.shared.translateButtonPreference + @Published var translationServicePreference = UserDefaults.shared.translationServicePreference -extension DisplayPreferenceViewModel { - - static func configure(cell: StatusTableViewCell) { - cell.selectionStyle = .none + // output + @Published var authenticationContext: AuthenticationContext? + + init() { + // avatar style + UserDefaults.shared.publisher(for: \.avatarStyle) + .removeDuplicates() + .assign(to: &$avatarStyle) + $avatarStyle + .sink { avatarStyle in + UserDefaults.shared.avatarStyle = avatarStyle + } + .store(in: &disposeBag) + + // Translation + UserDefaults.shared.publisher(for: \.translateButtonPreference) + .removeDuplicates() + .assign(to: &$translateButtonPreference) + $translateButtonPreference + .sink { preference in + UserDefaults.shared.translateButtonPreference = preference + } + .store(in: &disposeBag) - cell.statusView.viewModel.authorAvatarImage = Asset.Scene.Preference.twidereAvatar.image - cell.statusView.viewModel.authorName = PlaintextMetaContent(string: "Twidere") - cell.statusView.viewModel.authorUsername = "TwidereProject" - cell.statusView.viewModel.protected = false - cell.statusView.viewModel.timestamp = Date() - cell.statusView.viewModel.dateTimeProvider = DateTimeSwiftProvider() - let content = TwitterContent(content: L10n.Scene.Settings.Display.Preview.thankForUsingTwidereX) - cell.statusView.viewModel.content = TwitterMetaContent.convert(content: content, urlMaximumLength: 16, twitterTextProvider: OfficialTwitterTextProvider()) - cell.statusView.isUserInteractionEnabled = false - cell.separator.isHidden = true + // Translation service + UserDefaults.shared.publisher(for: \.translationServicePreference) + .removeDuplicates() + .assign(to: &$translationServicePreference) + $translationServicePreference + .sink { preference in + UserDefaults.shared.translationServicePreference = preference + } + .store(in: &disposeBag) } } diff --git a/TwidereX/Scene/Setting/AppearancePreference/TranslateButtonPreferenceView.swift b/TwidereX/Scene/Setting/DisplayPreference/Translation/TranslateButtonPreferenceView.swift similarity index 100% rename from TwidereX/Scene/Setting/AppearancePreference/TranslateButtonPreferenceView.swift rename to TwidereX/Scene/Setting/DisplayPreference/Translation/TranslateButtonPreferenceView.swift diff --git a/TwidereX/Scene/Setting/AppearancePreference/TranslationServicePreferenceView.swift b/TwidereX/Scene/Setting/DisplayPreference/Translation/TranslationServicePreferenceView.swift similarity index 100% rename from TwidereX/Scene/Setting/AppearancePreference/TranslationServicePreferenceView.swift rename to TwidereX/Scene/Setting/DisplayPreference/Translation/TranslationServicePreferenceView.swift diff --git a/TwidereX/Scene/Setting/List/SettingListView.swift b/TwidereX/Scene/Setting/List/SettingListView.swift index 97633ad4..d1a5e1b9 100644 --- a/TwidereX/Scene/Setting/List/SettingListView.swift +++ b/TwidereX/Scene/Setting/List/SettingListView.swift @@ -25,10 +25,11 @@ struct TextCaseEraseStyle: ViewModifier { enum SettingListEntryType: Hashable { case account - case appearance + case behaviors case display case layout case webBrowser + case appIcon case about #if DEBUG @@ -38,10 +39,11 @@ enum SettingListEntryType: Hashable { var image: Image { switch self { case .account: return Image(systemName: "person") - case .appearance: return Image(uiImage: Asset.ObjectTools.clothes.image) + case .behaviors: return Image(uiImage: Asset.Arrows.arrowRampRight.image) case .display: return Image(uiImage: Asset.TextFormatting.textHeaderRedaction.image) case .layout: return Image(uiImage: Asset.sidebarLeft.image) case .webBrowser: return Image(uiImage: Asset.window.image) + case .appIcon: return Image(uiImage: Asset.Logo.twidere.image) case .about: return Image(uiImage: Asset.Indices.infoCircle.image) #if DEBUG case .developer: return Image(systemName: "hammer") @@ -51,11 +53,12 @@ enum SettingListEntryType: Hashable { var title: String { switch self { - case .account: return "Account" // TODO: i18n - case .appearance: return L10n.Scene.Settings.Appearance.title + case .account: return L10n.Scene.Settings.SectionHeader.account + case .behaviors: return "Behaviors" // TODO: i18n case .display: return L10n.Scene.Settings.Display.title case .layout: return "Layout" case .webBrowser: return "Web Browser" + case .appIcon: return L10n.Scene.Settings.Appearance.appIcon case .about: return L10n.Scene.Settings.About.title #if DEBUG case .developer: return "Developer" @@ -95,7 +98,7 @@ struct SettingListView: View { static let generalSection: [SettingListEntry] = { let types: [SettingListEntryType] = [ - .appearance, + .behaviors, .display, // .layout, // .webBrowser @@ -105,6 +108,20 @@ struct SettingListView: View { } }() + var appIconRow: some View { + Button { + + } label: { + HStack { + Text(L10n.Scene.Settings.Appearance.appIcon) + Spacer() + Image(uiImage: UIImage(named: "\(viewModel.alternateIconNamePreference.iconName)") ?? UIImage()) + .cornerRadius(4) + } + } + .tint(Color(uiColor: .label)) + } + static let aboutSection: [SettingListEntry] = { let types: [SettingListEntryType] = [ .about, @@ -127,19 +144,19 @@ struct SettingListView: View { var body: some View { List { - Section(header: Text(verbatim: "Account")) { + // Account Section + Section { Button { viewModel.settingListEntryPublisher.send(SettingListView.accountListEntry) } label: { accountView } + } header: { + Text(verbatim: L10n.Scene.Settings.SectionHeader.account) + .textCase(nil) } - Section( - // grouped tableView get header padding since iOS 15. - // no more top padding manually - // seealso: 'UITableView.sectionHeaderTopPadding' - header: Text(verbatim: L10n.Scene.Settings.SectionHeader.general) - ) { + // General Section + Section { ForEach(SettingListView.generalSection) { entry in Button(action: { viewModel.settingListEntryPublisher.send(entry) @@ -148,9 +165,20 @@ struct SettingListView: View { .foregroundColor(Color(.label)) }) } + } header: { + Text(verbatim: L10n.Scene.Settings.SectionHeader.general) + .textCase(nil) } - .modifier(TextCaseEraseStyle()) - Section(header: Text(verbatim: L10n.Scene.Settings.SectionHeader.about)) { + // App Icon Section + Section { + NavigationLink { + AppIconPreferenceView() + } label: { + appIconRow + } + } + // About Section + Section { ForEach(SettingListView.aboutSection) { entry in Button(action: { viewModel.settingListEntryPublisher.send(entry) @@ -159,8 +187,10 @@ struct SettingListView: View { .foregroundColor(Color(.label)) }) } + } header: { + Text(verbatim: L10n.Scene.Settings.SectionHeader.about) + .textCase(nil) } - .modifier(TextCaseEraseStyle()) #if DEBUG Section { ForEach(SettingListView.developerSection) { entry in @@ -172,7 +202,6 @@ struct SettingListView: View { }) } } - .modifier(TextCaseEraseStyle()) #endif } .listStyle(InsetGroupedListStyle()) diff --git a/TwidereX/Scene/Setting/List/SettingListViewController.swift b/TwidereX/Scene/Setting/List/SettingListViewController.swift index d2c59489..cd67dc64 100644 --- a/TwidereX/Scene/Setting/List/SettingListViewController.swift +++ b/TwidereX/Scene/Setting/List/SettingListViewController.swift @@ -63,14 +63,21 @@ extension SettingListViewController { from: self, transition: .show ) - case .appearance: - self.coordinator.present(scene: .appearancePreference, from: self, transition: .show) + case .behaviors: + let behaviorsPreferenceViewModel = BehaviorsPreferenceViewModel(context: self.context) + self.coordinator.present( + scene: .behaviorsPreference(viewModel: behaviorsPreferenceViewModel), + from: self, + transition: .show + ) case .display: self.coordinator.present(scene: .displayPreference, from: self, transition: .show) case .layout: break case .webBrowser: break + case .appIcon: + break case .about: self.coordinator.present(scene: .about, from: self, transition: .show) #if DEBUG diff --git a/TwidereX/Scene/Setting/List/SettingListViewModel.swift b/TwidereX/Scene/Setting/List/SettingListViewModel.swift index a8820053..30497af3 100644 --- a/TwidereX/Scene/Setting/List/SettingListViewModel.swift +++ b/TwidereX/Scene/Setting/List/SettingListViewModel.swift @@ -29,7 +29,10 @@ final class SettingListViewModel: ObservableObject { // account @Published var user: UserObject? - + + // App Icon + @Published var alternateIconNamePreference = UserDefaults.shared.alternateIconNamePreference + init( context: AppContext, auth: AuthContext? @@ -41,6 +44,10 @@ final class SettingListViewModel: ObservableObject { Task { await setupAccountSource() } + + // App Icon + UserDefaults.shared.publisher(for: \.alternateIconNamePreference) + .assign(to: &$alternateIconNamePreference) } } diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift index 5092b8f8..fe924015 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift @@ -195,7 +195,18 @@ extension TimelineViewController { Task { @MainActor in assert(self._viewModel != nil) + await self._viewModel.loadLatest() + + if self._viewModel.preferredTimelineResetToTop, + let scrollViewContainer = self as? ScrollViewContainer + { + await _ = try self._viewModel.didLoadLatest.eraseToAnyPublisher().singleOutput() + scrollViewContainer.scrollToTop( + animated: true, + option: .init(tryRefreshWhenStayAtTop: false) + ) + } } } diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift index b9e58604..2399e01a 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift @@ -12,6 +12,7 @@ import Combine import CoreDataStack import GameplayKit import TwidereCore +import func QuartzCore.CACurrentMediaTime class TimelineViewModel: TimelineViewModelDriver { @@ -34,12 +35,21 @@ class TimelineViewModel: TimelineViewModelDriver { @Published var isRefreshControlEnabled = true @Published var isFloatyButtonDisplay = true @Published var isLoadingLatest = false - @Published var lastAutomaticFetchTimestamp: Date? + + @Published private(set) var timelineRefreshInterval = UserDefaults.shared.timelineRefreshInterval + @Published private(set) var preferredTimelineResetToTop = UserDefaults.shared.preferredTimelineResetToTop // output - // @Published var snapshot = NSDiffableDataSourceSnapshot() let didLoadLatest = PassthroughSubject() + // auto fetch + private var autoFetchLatestActionTime = CACurrentMediaTime() + let autoFetchLatestAction = PassthroughSubject() + let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common) + .autoconnect() + .share() + .eraseToAnyPublisher() + // bottom loader @MainActor private(set) lazy var stateMachine: GKStateMachine = { // exclude timeline middle fetcher state @@ -63,7 +73,31 @@ class TimelineViewModel: TimelineViewModelDriver { self.kind = kind self.feedFetchedResultsController = FeedFetchedResultsController(managedObjectContext: context.managedObjectContext) self.statusRecordFetchedResultController = StatusRecordFetchedResultController(managedObjectContext: context.managedObjectContext) + super.init() // end init + + UserDefaults.shared.publisher(for: \.timelineRefreshInterval) + .removeDuplicates() + .assign(to: &$timelineRefreshInterval) + + UserDefaults.shared.publisher(for: \.preferredTimelineResetToTop) + .removeDuplicates() + .assign(to: &$preferredTimelineResetToTop) + + timestampUpdatePublisher + .sink { [weak self] _ in + guard let self = self else { return } + let now = CACurrentMediaTime() + let elapse = now - self.autoFetchLatestActionTime + guard elapse > self.timelineRefreshInterval.seconds else { + let remains = self.timelineRefreshInterval.seconds - elapse + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): (\(String(describing: self)) auto fetch in \(remains, format: .fixed(precision: 2))s") + return + } + self.autoFetchLatestActionTime = now + self.autoFetchLatestAction.send() + } + .store(in: &disposeBag) } } diff --git a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift index cec59007..d225ec01 100644 --- a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift @@ -77,6 +77,14 @@ extension ListTimelineViewController { } .store(in: &disposeBag) + viewModel.autoFetchLatestAction + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + self.autoFetchLatest() + } + .store(in: &disposeBag) + NotificationCenter.default .publisher(for: .statusBarTapped, object: nil) .throttle(for: 0.5, scheduler: DispatchQueue.main, latest: false) @@ -108,13 +116,6 @@ extension ListTimelineViewController { tableView.deselectRow(with: transitionCoordinator, animated: animated) } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - // FIXME: use timer to auto refresh - autoFetchLatest() - } - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) @@ -144,29 +145,9 @@ extension ListTimelineViewController { !diffableDataSource.snapshot().itemIdentifiers.isEmpty // conflict with LoadOldestState else { return } - if !viewModel.isLoadingLatest { - let now = Date() - if let timestamp = viewModel.lastAutomaticFetchTimestamp { - #if DEBUG - let throttle: TimeInterval = 1 - #else - let throttle: TimeInterval = 60 - #endif - if now.timeIntervalSince(timestamp) > throttle { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Timeline] auto fetch lastest timeline…") - Task { - await _viewModel.loadLatest() - } - viewModel.lastAutomaticFetchTimestamp = now - } else { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Timeline] auto fetch lastest timeline skip. Reason: updated in recent 60s") - } - } else { - Task { - await self.viewModel.loadLatest() - } - viewModel.lastAutomaticFetchTimestamp = now - } + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Timeline] auto fetch lastest timeline…") + Task { + await _viewModel.loadLatest() } } // end func @@ -302,11 +283,13 @@ extension ListTimelineViewController: ScrollViewContainer { var scrollView: UIScrollView { return tableView } - func scrollToTop(animated: Bool) { + func scrollToTop(animated: Bool, option: ScrollViewContainerOption) { if scrollView.contentOffset.y < scrollView.frame.height, !viewModel.isLoadingLatest, (scrollView.contentOffset.y + scrollView.adjustedContentInset.top) == 0.0, - !refreshControl.isRefreshing { + !refreshControl.isRefreshing, + option.tryRefreshWhenStayAtTop + { scrollView.scrollRectToVisible(CGRect(origin: CGPoint(x: 0, y: -refreshControl.frame.height), size: CGSize(width: 1, height: 1)), animated: animated) DispatchQueue.main.async { [weak self] in guard let self = self else { return } diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController.swift index 52e0749d..605df93f 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController.swift @@ -111,6 +111,19 @@ extension HomeTimelineViewController { super.viewDidAppear(animated) unreadIndicatorView.startDisplayLink() + + DispatchQueue.once { + guard let authenticationContext = self.context.authenticationService.activeAuthenticationContext else { return } + let settingListViewModel = SettingListViewModel( + context: self.context, + auth: .init(authenticationContext: authenticationContext) + ) + self.coordinator.present( + scene: .setting(viewModel: settingListViewModel), + from: self, + transition: .modal(animated: true) + ) + } } override func viewDidDisappear(_ animated: Bool) { @@ -261,6 +274,8 @@ extension HomeTimelineViewController { } guard indexPath.row < oldIndexPath.row else { + // always update the number + viewModel.unreadItemCount = oldIndexPath.row return } From 1fb738b904835e84b4c67ffef40baa06992bad38 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 29 Jul 2022 18:57:54 +0800 Subject: [PATCH 003/128] fix: reply mastodon post not contains mention content issue --- .../AuthenticationContext.swift | 9 ++++++++ ...oseContentViewModel+MetaTextDelegate.swift | 9 ++++---- .../ComposeContentViewModel.swift | 21 +++++++++++++++++-- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/TwidereSDK/Sources/TwidereCore/Model/Authentication/AuthenticationContext.swift b/TwidereSDK/Sources/TwidereCore/Model/Authentication/AuthenticationContext.swift index 847c4815..d3f87db4 100644 --- a/TwidereSDK/Sources/TwidereCore/Model/Authentication/AuthenticationContext.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/Authentication/AuthenticationContext.swift @@ -69,6 +69,15 @@ extension AuthenticationContext { } } } + +extension AuthenticationContext { + public var platform: Platform { + switch self { + case .twitter: return .twitter + case .mastodon: return .mastodon + } + } +} public struct TwitterAuthenticationContext: Hashable { public let authenticationRecord: ManagedObjectRecord diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel+MetaTextDelegate.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel+MetaTextDelegate.swift index e453b695..753b6fbd 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel+MetaTextDelegate.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel+MetaTextDelegate.swift @@ -24,10 +24,6 @@ extension ComposeContentViewModel: MetaTextDelegate { _ metaText: MetaText, processEditing textStorage: MetaTextStorage ) -> MetaContent? { - guard let author = self.author else { - return nil - } - let kind = MetaTextViewKind(rawValue: metaText.textView.tag) ?? .none switch kind { @@ -39,7 +35,7 @@ extension ComposeContentViewModel: MetaTextDelegate { let textInput = textStorage.string self.content = textInput - switch author { + switch platform { case .twitter: let content = TwitterContent(content: textInput) let metaContent = TwitterMetaContent.convert( @@ -56,6 +52,9 @@ extension ComposeContentViewModel: MetaTextDelegate { ) let metaContent = MastodonMetaContent.convert(text: content) return metaContent + case .none: + assertionFailure() + return nil } case .contentWarning: diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift index ae6cbaad..882d92f7 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -38,7 +38,8 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { public let kind: Kind public let configurationContext: ConfigurationContext public let customEmojiPickerInputViewModel = CustomEmojiPickerInputView.ViewModel() - + public let platform: Platform + // reply-to public private(set) var replyTo: StatusObject? @@ -77,7 +78,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { } @Published public var isContentWarningEditing = false - // avatar + // avatar (me) @Published public var author: UserObject? // mention (Twitter) @@ -156,6 +157,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { ) { self.kind = kind self.configurationContext = configurationContext + self.platform = configurationContext.authenticationService.activeAuthenticationContext?.platform ?? .none super.init() // end init @@ -214,9 +216,24 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { isContentWarningComposing = true contentWarning = spoilerText } + + // set content text + var mentionAccts: [String] = [] + mentionAccts.append("@" + status.author.acct) + for mention in status.mentions { + let acct = "@" + mention.acct + guard !mentionAccts.contains(acct) else { continue } + mentionAccts.append(acct) + } + for acct in mentionAccts { + UITextChecker.learnWord(acct) + } + content = mentionAccts.joined(separator: " ") + " " } } + initialContent = content + // bind text $content .map { $0.isEmpty } From b393afa6f381de304a10cb141e4b0acd9a9e9a1b Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 29 Jul 2022 19:06:01 +0800 Subject: [PATCH 004/128] feat: escape mention self --- .../ComposeContent/ComposeContentViewModel.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift index 882d92f7..c352061a 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -219,10 +219,19 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { // set content text var mentionAccts: [String] = [] - mentionAccts.append("@" + status.author.acct) + let _authorUserIdentifier: MastodonUserIdentifier? = { + switch configurationContext.authenticationService.activeAuthenticationContext?.userIdentifier { + case .mastodon(let userIdentifier): return userIdentifier + default: return nil + } + }() + if _authorUserIdentifier?.id != status.author.id { + mentionAccts.append("@" + status.author.acct) + } for mention in status.mentions { let acct = "@" + mention.acct guard !mentionAccts.contains(acct) else { continue } + guard mention.id != _authorUserIdentifier?.id else { continue } mentionAccts.append(acct) } for acct in mentionAccts { From 78705d35e6309db4603826bced13a0bde9267647 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 29 Jul 2022 18:17:47 +0800 Subject: [PATCH 005/128] feat: add status history --- TwidereSDK/Package.swift | 5 + .../.xccurrentversion | 2 +- .../CoreDataStack 7.xcdatamodel/contents | 335 ++++++++++++++++++ .../CoreDataStack/Entity/App/History.swift | 253 +++++++++++++ .../Entity/Mastodon/MastodonStatus.swift | 1 + .../Entity/Mastodon/MastodonUser.swift | 1 + .../Entity/Twitter/TwitterStatus.swift | 3 +- .../Entity/Twitter/TwitterUser.swift | 3 +- .../Contents.json | 15 + .../clock.arrow.circlepath.pdf | Bin 0 -> 3941 bytes .../TwidereAsset/Generated/Assets.swift | 1 + .../Extension/CoreDataStack/History.swift | 24 ++ ...tatusHistoryFetchedResultsController.swift | 91 +++++ .../AuthenticationContext.swift | 9 + .../Model/Status/StatusObject.swift | 11 + .../Toolbar/StatusToolbar+ViewModel.swift | 8 + .../TwidereUI/Toolbar/StatusToolbar.swift | 1 + TwidereX.xcodeproj/project.pbxproj | 98 +++-- TwidereX/Coordinator/SceneCoordinator.swift | 12 +- .../Diffable/Misc/History/HistoryItem.swift | 16 + .../Misc/History/HistorySection.swift | 89 +++++ .../Diffable/Misc/Sidebar/SidebarItem.swift | 3 + TwidereX/Diffable/Status/StatusSection.swift | 2 +- ...oGenerateTableViewDelegate.generated.swift | 24 +- .../Provider/DataSourceFacade+History.swift | 56 +++ .../Provider/DataSourceFacade+Media.swift | 7 + .../Provider/DataSourceFacade+Status.swift | 30 ++ .../DataSourceFacade+StatusThread.swift | 8 + ...ider+StatusViewTableViewCellDelegate.swift | 16 +- ...taSourceProvider+UITableViewDelegate.swift | 11 + .../Scene/History/HistoryViewController.swift | 91 +++++ TwidereX/Scene/History/HistoryViewModel.swift | 99 ++++++ ...oryViewController+DataSourceProvider.swift | 41 +++ .../Status/StatusHistoryViewController.swift | 126 +++++++ .../StatusHistoryViewModel+Diffable.swift | 55 +++ .../Status/StatusHistoryViewModel.swift | 47 +++ .../User/UserHistoryViewController.swift | 34 ++ .../History/User/UserHistoryViewModel.swift | 44 +++ .../NotificationViewController.swift | 6 + .../Drawer/DrawerSidebarViewController.swift | 5 + .../DrawerSidebarViewModel+Diffable.swift | 4 +- .../StubTimelineCollectionViewCell.swift | 47 --- .../StubTimelineViewController.swift | 64 ---- .../StubTimeline/StubTimelineViewModel.swift | 145 -------- .../List/ListTimelineViewController.swift | 3 +- ...meTimelineViewController+DebugAction.swift | 8 - .../Home/HomeTimelineViewController.swift | 27 +- TwidereX/Supporting Files/AppDelegate.swift | 4 + 48 files changed, 1653 insertions(+), 332 deletions(-) create mode 100644 TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 7.xcdatamodel/contents create mode 100644 TwidereSDK/Sources/CoreDataStack/Entity/App/History.swift create mode 100644 TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Arrows/clock.arrow.circlepath.imageset/Contents.json create mode 100644 TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Arrows/clock.arrow.circlepath.imageset/clock.arrow.circlepath.pdf create mode 100644 TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/History.swift create mode 100644 TwidereSDK/Sources/TwidereCore/FetchedResultsController/Status/StatusHistoryFetchedResultsController.swift create mode 100644 TwidereX/Diffable/Misc/History/HistoryItem.swift create mode 100644 TwidereX/Diffable/Misc/History/HistorySection.swift create mode 100644 TwidereX/Protocol/Provider/DataSourceFacade+History.swift create mode 100644 TwidereX/Scene/History/HistoryViewController.swift create mode 100644 TwidereX/Scene/History/HistoryViewModel.swift create mode 100644 TwidereX/Scene/History/Status/StatusHistoryViewController+DataSourceProvider.swift create mode 100644 TwidereX/Scene/History/Status/StatusHistoryViewController.swift create mode 100644 TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift create mode 100644 TwidereX/Scene/History/Status/StatusHistoryViewModel.swift create mode 100644 TwidereX/Scene/History/User/UserHistoryViewController.swift create mode 100644 TwidereX/Scene/History/User/UserHistoryViewModel.swift delete mode 100644 TwidereX/Scene/StubTimeline/StubTimelineCollectionViewCell.swift delete mode 100644 TwidereX/Scene/StubTimeline/StubTimelineViewController.swift delete mode 100644 TwidereX/Scene/StubTimeline/StubTimelineViewModel.swift diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index 726e31b5..93f024d7 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -91,6 +91,11 @@ let package = Package( "MastodonSDK", .product(name: "KeychainAccess", package: "KeychainAccess"), .product(name: "ArkanaKeys", package: "ArkanaKeys"), + ], + exclude: [ + "Template/AutoGenerateProtocolDelegate.swifttemplate", + "Template/AutoGenerateProtocolRelayDelegate.swifttemplate", + "Template/AutoGenerateTableViewDelegate.stencil", ] ), .target( diff --git a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/.xccurrentversion b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/.xccurrentversion index 4998c1d6..eaddb16f 100644 --- a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/.xccurrentversion +++ b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - CoreDataStack 6.xcdatamodel + CoreDataStack 7.xcdatamodel diff --git a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 7.xcdatamodel/contents b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 7.xcdatamodel/contents new file mode 100644 index 00000000..2763b5da --- /dev/null +++ b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 7.xcdatamodel/contents @@ -0,0 +1,335 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/App/History.swift b/TwidereSDK/Sources/CoreDataStack/Entity/App/History.swift new file mode 100644 index 00000000..cfe90715 --- /dev/null +++ b/TwidereSDK/Sources/CoreDataStack/Entity/App/History.swift @@ -0,0 +1,253 @@ +// +// History.swift +// +// +// Created by MainasuK on 2022-7-29. +// + +import Foundation +import CoreData + +final public class History: NSManagedObject { + + public typealias Acct = Feed.Acct + + @NSManaged public private(set) var acctRaw: String + // sourcery: autoGenerateProperty + public var acct: Acct { + get { + Acct(rawValue: acctRaw) ?? .none + } + set { + acctRaw = newValue.rawValue + } + } + + // sourcery: autoGenerateProperty + @NSManaged public private(set) var timestamp: Date + + // sourcery: autoUpdatableObject, autoGenerateProperty + @NSManaged public private(set) var createdAt: Date + + // many-to-one relationship + // sourcery: autoUpdatableObject + @NSManaged public private(set) var twitterStatus: TwitterStatus? + // sourcery: autoUpdatableObject + @NSManaged public private(set) var twitterUser: TwitterUser? + // sourcery: autoUpdatableObject + @NSManaged public private(set) var mastodonStatus: MastodonStatus? + // sourcery: autoUpdatableObject + @NSManaged public private(set) var mastodonUser: MastodonUser? + +} + +extension History { + @objc public var sectionIdentifierByDay: String? { + get { + let keyPath = #keyPath(History.sectionIdentifierByDay) + willAccessValue(forKey: keyPath) + let _identifier = primitiveValue(forKey: keyPath) as? String + didAccessValue(forKey: keyPath) + + guard let identifier = _identifier else { + let timestamp = self.timestamp + let identifier = History.sectionIdentifier(from: timestamp) + + willChangeValue(forKey: keyPath) + setPrimitiveValue(identifier, forKey: keyPath) + didChangeValue(forKey: keyPath) + + return identifier + } + + return identifier + } + } +} + +extension History { + + @discardableResult + public static func insert( + into context: NSManagedObjectContext, + property: Property + ) -> History { + let object: History = context.insertObject() + object.configure(property: property) + return object + } + +} + +extension History: Managed { + public static var defaultSortDescriptors: [NSSortDescriptor] { + return [NSSortDescriptor(keyPath: \History.createdAt, ascending: false)] + } +} + +extension History { + + static func hasTwitterStatus() -> NSPredicate { + return NSPredicate(format: "%K != nil", #keyPath(History.twitterStatus)) + } + + static func hasTwitterUser() -> NSPredicate { + return NSPredicate(format: "%K != nil", #keyPath(History.twitterUser)) + } + + + static func hasMastodonStatus() -> NSPredicate { + return NSPredicate(format: "%K != nil", #keyPath(History.mastodonStatus)) + } + + static func hasMastodonUser() -> NSPredicate { + return NSPredicate(format: "%K != nil", #keyPath(History.mastodonUser)) + } + + static func predicate(acct: Acct) -> NSPredicate { + return NSPredicate(format: "%K == %@", #keyPath(History.acctRaw), acct.rawValue) + } + + public static func statusPredicate(acct: Acct) -> NSPredicate { + switch acct { + case .none: + return History.predicate(acct: acct) + case .twitter: + return NSCompoundPredicate(andPredicateWithSubpredicates: [ + History.predicate(acct: acct), + History.hasTwitterStatus() + ]) + case .mastodon: + return NSCompoundPredicate(andPredicateWithSubpredicates: [ + History.predicate(acct: acct), + History.hasMastodonStatus() + ]) + } + } + + public static func userPredicate(acct: Acct) -> NSPredicate { + switch acct { + case .none: + return History.predicate(acct: acct) + case .twitter: + return NSCompoundPredicate(andPredicateWithSubpredicates: [ + History.predicate(acct: acct), + History.hasTwitterUser() + ]) + case .mastodon: + return NSCompoundPredicate(andPredicateWithSubpredicates: [ + History.predicate(acct: acct), + History.hasMastodonUser() + ]) + } + } + +} + +// MARK: - AutoGenerateProperty +extension History: AutoGenerateProperty { + // sourcery:inline:History.AutoGenerateProperty + + // Generated using Sourcery + // DO NOT EDIT + public struct Property { + public let acct: Acct + public let timestamp: Date + public let createdAt: Date + + public init( + acct: Acct, + timestamp: Date, + createdAt: Date + ) { + self.acct = acct + self.timestamp = timestamp + self.createdAt = createdAt + } + } + + public func configure(property: Property) { + self.acct = property.acct + self.timestamp = property.timestamp + self.createdAt = property.createdAt + } + + public func update(property: Property) { + update(createdAt: property.createdAt) + } + + // sourcery:end +} + +// MARK: - AutoUpdatableObject +extension History: AutoUpdatableObject { + // sourcery:inline:History.AutoUpdatableObject + + // Generated using Sourcery + // DO NOT EDIT + public func update(createdAt: Date) { + if self.createdAt != createdAt { + self.createdAt = createdAt + } + } + public func update(twitterStatus: TwitterStatus?) { + if self.twitterStatus != twitterStatus { + self.twitterStatus = twitterStatus + } + } + public func update(twitterUser: TwitterUser?) { + if self.twitterUser != twitterUser { + self.twitterUser = twitterUser + } + } + public func update(mastodonStatus: MastodonStatus?) { + if self.mastodonStatus != mastodonStatus { + self.mastodonStatus = mastodonStatus + } + } + public func update(mastodonUser: MastodonUser?) { + if self.mastodonUser != mastodonUser { + self.mastodonUser = mastodonUser + } + } + // sourcery:end + + public func update(timestamp: Date) { + if self.timestamp != timestamp { + self.timestamp = timestamp + + setPrimitiveValue(nil, forKey: #keyPath(History.sectionIdentifierByDay)) + } + } +} + +extension History { + + public static func sectionIdentifier(from date: Date) -> String { + let calendar = Calendar.current + let year = calendar.component(.year, from: date) + let month = calendar.component(.month, from: date) + let day = calendar.component(.day, from: date) + + // yyyymmdd + let identifier = String(year * 10000 + month * 100 + day) + + return identifier + } + + public static func date(from sectionIdentifier: String) -> Date? { + guard let integer = Int(sectionIdentifier) else { return nil } + let year = integer / 10000 + let month = (integer - year * 10000) / 100 + let day = (integer - year * 10000 - month * 100) + + var dateComponents = DateComponents() + dateComponents.year = year + dateComponents.month = month + dateComponents.day = day + + guard let date = Calendar.current.date(from: dateComponents) else { return nil } + return date + } + +} diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonStatus.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonStatus.swift index d4161332..41f48716 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonStatus.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonStatus.swift @@ -79,6 +79,7 @@ final public class MastodonStatus: NSManagedObject { @NSManaged public private(set) var feeds: Set @NSManaged public private(set) var repostFrom: Set @NSManaged public private(set) var notifications: Set + @NSManaged public private(set) var histories: Set // many-to-one relationship // sourcery: autoGenerateRelationship diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift index eeba4028..3940e6c9 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift @@ -66,6 +66,7 @@ final public class MastodonUser: NSManagedObject { // one-to-many relationship @NSManaged public private(set) var statuses: Set @NSManaged public private(set) var notifications: Set + @NSManaged public private(set) var histories: Set // many-to-many relationship @NSManaged public private(set) var like: Set diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift index f4f1e660..aa33a4a7 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift @@ -54,7 +54,8 @@ final public class TwitterStatus: NSManagedObject { // one-to-many relationship @NSManaged public private(set) var feeds: Set - + @NSManaged public private(set) var histories: Set + // many-to-one relationship // sourcery: autoGenerateRelationship @NSManaged public private(set) var author: TwitterUser diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift index d82ad369..b717a34f 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift @@ -57,7 +57,8 @@ public final class TwitterUser: NSManagedObject { @NSManaged public private(set) var statuses: Set @NSManaged public private(set) var savedSearches: Set @NSManaged public private(set) var ownedLists: Set - + @NSManaged public private(set) var histories: Set + // many-to-many relationship @NSManaged public private(set) var like: Set @NSManaged public private(set) var reposts: Set diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Arrows/clock.arrow.circlepath.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Arrows/clock.arrow.circlepath.imageset/Contents.json new file mode 100644 index 00000000..dc0858e9 --- /dev/null +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Arrows/clock.arrow.circlepath.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "clock.arrow.circlepath.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Arrows/clock.arrow.circlepath.imageset/clock.arrow.circlepath.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Arrows/clock.arrow.circlepath.imageset/clock.arrow.circlepath.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a849d106598a43e4282c3d8391334ea466de555c GIT binary patch literal 3941 zcma)Y}F5QgvjD|iWz9J1o~2M`2klBOuyqN&qc(1R*(5*L=#T1wIO*Y_D}ms)Fe zhz!{DE0Xih@SPd*$-6gipPN2)ol#otfB&OX>ctE7@?|&dZsp(3&hgcc!|v1ZvvPnp zGV5@B7}ncv^=A0{ZZq6`_e#BfKmTuY=>FylTSZP(^4 z2Zzn>ymRh#Nzq2Nonz6LVsNYv!P*>DnP#=!8nWr&bUBU4jLdUC?lp%-lbgB85xeg` zpSk!Ki&i;vzZ*N2pK2m&RYj7~tV=wOQ4T(KdRO>c|M#VIq* zl(59?yvanP$R{YwfuK zjT1{0ndJ;5fca}Aj@?px4pxyN&Sm38HQkASOhSYUxdi1~;k=7fp_`S>cbd;zzVOK{O*e>!T5|}8*MZ!o@doCdDW1%+oqdgBayaK zbD`O5$mR-%Q%KI5AEhVRYl_z%)k;Z_8bskSIvQkT0!;&jda0Ko*-X6>5BOG!5r?NU zN>_$BX?~oNEO*5_N4g}*%Sc|LspOGA$>s^D4Yp!uGGhWtM9<*JP>5f#E-}?QJLHV@ zfc(kYh-A?r6`D-YCe_5jt}UfP)>|*BkZB0TleCtSY`rlN;<0y7%eXIj9yR$yM6A`W zD2rFo`>2V{!e^=Gi|7b#%v{I^YoGRxC`HJOYYO|Dvh=e5W!Wl0;%CbF2W+-T|m*Qq(K^}6Z%w~ko zSyN-H;Jk|`H7idEQK~S5Ir4JelAnmuO9hv>BXjN;qN+ZUl&OrODK?O)DTZFVL>LKJ=29r$Xi^?+5MlmOJeQ72{k{xHjqlVmTK7X0 z6V|%M1H_WzdUS)gObPF&A8p`{q8vIuqZFL34FJH08`w+Dl~iF3Dn+w zs~_%ep{m4#8#UQ^z5l{mv*3RKne7xG?IaE!n!&TKzz4HR1zBcIiu)ez=9y&R_@81xPr-%J^ tc&47--99@7Wp%UPAC)*<1H5{F_ul~f`XF9E9IHDdV(Fed`S$0x{{uI}8a)62 literal 0 HcmV?d00001 diff --git a/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift b/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift index 00304eec..8e1e87b0 100644 --- a/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift +++ b/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift @@ -30,6 +30,7 @@ public enum Asset { public static let arrowTurnUpLeft = ImageAsset(name: "Arrows/arrow.turn.up.left") public static let arrowTurnUpLeftMini = ImageAsset(name: "Arrows/arrow.turn.up.left.mini") public static let arrowshapeTurnUpLeftFill = ImageAsset(name: "Arrows/arrowshape.turn.up.left.fill") + public static let clockArrowCirclepath = ImageAsset(name: "Arrows/clock.arrow.circlepath") public static let squareAndArrowUp = ImageAsset(name: "Arrows/square.and.arrow.up") public static let squareAndArrowUpMini = ImageAsset(name: "Arrows/square.and.arrow.up.mini") public static let tablerChevronDown = ImageAsset(name: "Arrows/tabler.chevron.down") diff --git a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/History.swift b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/History.swift new file mode 100644 index 00000000..aff72825 --- /dev/null +++ b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/History.swift @@ -0,0 +1,24 @@ +// +// History.swift +// +// +// Created by MainasuK on 2022-7-29. +// + +import Foundation +import CoreDataStack + +extension History { + + public var statusObject: StatusObject? { + if let status = twitterStatus { + return .twitter(object: status) + } + if let status = mastodonStatus { + return .mastodon(object: status) + } + + return nil + } + +} diff --git a/TwidereSDK/Sources/TwidereCore/FetchedResultsController/Status/StatusHistoryFetchedResultsController.swift b/TwidereSDK/Sources/TwidereCore/FetchedResultsController/Status/StatusHistoryFetchedResultsController.swift new file mode 100644 index 00000000..a7f1a853 --- /dev/null +++ b/TwidereSDK/Sources/TwidereCore/FetchedResultsController/Status/StatusHistoryFetchedResultsController.swift @@ -0,0 +1,91 @@ +// +// StatusHistoryFetchedResultsController.swift +// +// +// Created by MainasuK on 2022-7-29. +// + +import os.log +import Foundation +import UIKit +import Combine +import CoreData +import CoreDataStack +import TwitterSDK +import OrderedCollections + +final public class StatusHistoryFetchedResultsController: NSObject { + + public let logger = Logger(subsystem: "StatusHistoryFetchedResultsController", category: "DB") + + var disposeBag = Set() + + public let fetchedResultsController: NSFetchedResultsController + + // input + @Published public var predicate: NSPredicate + + // output + @Published public var groupedRecords: [(String, [ManagedObjectRecord])] = [] + + public init(managedObjectContext: NSManagedObjectContext) { + self.fetchedResultsController = { + let fetchRequest = History.sortedFetchRequest + // make sure initial query return empty results + fetchRequest.predicate = History.statusPredicate(acct: .none) + fetchRequest.returnsObjectsAsFaults = false + fetchRequest.shouldRefreshRefetchedObjects = true + fetchRequest.fetchBatchSize = 15 + let controller = NSFetchedResultsController( + fetchRequest: fetchRequest, + managedObjectContext: managedObjectContext, + sectionNameKeyPath: #keyPath(History.sectionIdentifierByDay), + cacheName: nil + ) + + return controller + }() + self.predicate = History.statusPredicate(acct: .none) + super.init() + + fetchedResultsController.delegate = self + + $predicate.removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak self] predicate in + guard let self = self else { return } + self.fetchedResultsController.fetchRequest.predicate = predicate + do { + try self.fetchedResultsController.performFetch() + } catch { + assertionFailure(error.localizedDescription) + } + } + .store(in: &disposeBag) + } + + deinit { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + } + +} + +// MARK: - NSFetchedResultsControllerDelegate +extension StatusHistoryFetchedResultsController: NSFetchedResultsControllerDelegate { + public func controller( + _ controller: NSFetchedResultsController, + didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference + ) { + os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + + var groupedRecords: [(String, [ManagedObjectRecord])] = [] + for sectionInfo in controller.sections ?? [] { + guard let objects = sectionInfo.objects as? [History] else { return } + let identifier = sectionInfo.name + let records = objects.map { $0.asRecrod } + groupedRecords.append((identifier, records)) + } + self.groupedRecords = groupedRecords + } +} + diff --git a/TwidereSDK/Sources/TwidereCore/Model/Authentication/AuthenticationContext.swift b/TwidereSDK/Sources/TwidereCore/Model/Authentication/AuthenticationContext.swift index d3f87db4..9cef62dc 100644 --- a/TwidereSDK/Sources/TwidereCore/Model/Authentication/AuthenticationContext.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/Authentication/AuthenticationContext.swift @@ -68,6 +68,15 @@ extension AuthenticationContext { return .mastodon(.init(domain: authenticationContext.domain, id: authenticationContext.userID)) } } + + public var acct: Feed.Acct { + switch self { + case .twitter(let authenticationContext): + return .twitter(userID: authenticationContext.userID) + case .mastodon(let authenticationContext): + return .mastodon(domain: authenticationContext.domain, userID: authenticationContext.userID) + } + } } extension AuthenticationContext { diff --git a/TwidereSDK/Sources/TwidereCore/Model/Status/StatusObject.swift b/TwidereSDK/Sources/TwidereCore/Model/Status/StatusObject.swift index b6786aaf..e341565d 100644 --- a/TwidereSDK/Sources/TwidereCore/Model/Status/StatusObject.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/Status/StatusObject.swift @@ -71,3 +71,14 @@ extension StatusObject { } } } + +extension StatusObject { + public var histories: Set { + switch self { + case .twitter(let status): + return status.histories + case .mastodon(let status): + return status.histories + } + } +} diff --git a/TwidereSDK/Sources/TwidereUI/Toolbar/StatusToolbar+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Toolbar/StatusToolbar+ViewModel.swift index 28a93097..3bb92c95 100644 --- a/TwidereSDK/Sources/TwidereUI/Toolbar/StatusToolbar+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Toolbar/StatusToolbar+ViewModel.swift @@ -282,6 +282,14 @@ extension StatusToolbar { children.append(debugMenu) #endif + let appearEventAction = UIDeferredMenuElement.uncached { [weak self] completion in + if let self = self { + self.delegate?.statusToolbar(self, menuActionDidPressed: .appearEvent, menuButton: self.menuButton) + } + completion([]) + } + children.append(appearEventAction) + return UIMenu(title: "", options: [], children: children) }() diff --git a/TwidereSDK/Sources/TwidereUI/Toolbar/StatusToolbar.swift b/TwidereSDK/Sources/TwidereUI/Toolbar/StatusToolbar.swift index 6f375cc7..5f759356 100644 --- a/TwidereSDK/Sources/TwidereUI/Toolbar/StatusToolbar.swift +++ b/TwidereSDK/Sources/TwidereUI/Toolbar/StatusToolbar.swift @@ -172,6 +172,7 @@ extension StatusToolbar { #if DEBUG case copyID #endif + case appearEvent } public enum Style { diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index d3782c15..04667cd8 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -39,6 +39,10 @@ DB148B03281A7AB300B596C7 /* ContentSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB148B02281A7AB300B596C7 /* ContentSplitViewController.swift */; }; DB148B05281A81AE00B596C7 /* SidebarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB148B04281A81AE00B596C7 /* SidebarViewModel.swift */; }; DB148B0C281A837E00B596C7 /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB148B0B281A837E00B596C7 /* SidebarView.swift */; }; + DB1D3DEA289388CD008F0BD0 /* HistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D3DE9289388CD008F0BD0 /* HistoryViewController.swift */; }; + DB1D3DED28938ACD008F0BD0 /* HistoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D3DEC28938ACD008F0BD0 /* HistoryViewModel.swift */; }; + DB1D3DEF28938CD1008F0BD0 /* StatusHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D3DEE28938CD1008F0BD0 /* StatusHistoryViewController.swift */; }; + DB1D3DF228938CDF008F0BD0 /* StatusHistoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D3DF128938CDF008F0BD0 /* StatusHistoryViewModel.swift */; }; DB1D7B4325B5938400397DCD /* TwitterAuthenticationOptionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D7B4225B5938300397DCD /* TwitterAuthenticationOptionViewController.swift */; }; DB1D7B5525B5A87200397DCD /* TwitterAuthenticationOptionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D7B5425B5A87200397DCD /* TwitterAuthenticationOptionViewModel.swift */; }; DB1E48122772CE380074F6A0 /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB36F375257F79DB0028F81E /* SearchViewModel.swift */; }; @@ -335,9 +339,6 @@ DBE6357A28855302001C114B /* PushNotificationScratchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE6357928855302001C114B /* PushNotificationScratchViewController.swift */; }; DBE6357D2885557C001C114B /* PushNotificationScratchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE6357C2885557C001C114B /* PushNotificationScratchViewModel.swift */; }; DBE6357F288555AE001C114B /* PushNotificationScratchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE6357E288555AE001C114B /* PushNotificationScratchView.swift */; }; - DBE71B7A26B7AF5C00DFAB8E /* StubTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE71B7926B7AF5C00DFAB8E /* StubTimelineViewController.swift */; }; - DBE71B7D26B7C5FD00DFAB8E /* StubTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE71B7C26B7C5FD00DFAB8E /* StubTimelineViewModel.swift */; }; - DBE71B7F26B7D68500DFAB8E /* StubTimelineCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE71B7E26B7D68500DFAB8E /* StubTimelineCollectionViewCell.swift */; }; DBE76D1E2500E65D00DEB0FC /* HomeTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE76D1D2500E65D00DEB0FC /* HomeTimelineViewModel.swift */; }; DBEA4F842511F7460007FEC5 /* Kanna in Frameworks */ = {isa = PBXBuildFile; productRef = DBEA4F832511F7460007FEC5 /* Kanna */; }; DBED96D8253F5D7800C5383A /* NamingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBED96D7253F5D7800C5383A /* NamingState.swift */; }; @@ -368,6 +369,13 @@ DBFC0AE9276118240011E99B /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB94B6BB26C65CB000A2E8A1 /* AppShared.framework */; }; DBFC0AEA276118240011E99B /* AppShared.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DB94B6BB26C65CB000A2E8A1 /* AppShared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DBFCC44725667C620016698E /* UILabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCC44625667C620016698E /* UILabel.swift */; }; + DBFCEF1228939C9600EEBFB1 /* UserHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCEF1128939C9600EEBFB1 /* UserHistoryViewController.swift */; }; + DBFCEF1528939CAC00EEBFB1 /* UserHistoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCEF1428939CAC00EEBFB1 /* UserHistoryViewModel.swift */; }; + DBFCEF172893B92B00EEBFB1 /* StatusHistoryViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCEF162893B92B00EEBFB1 /* StatusHistoryViewModel+Diffable.swift */; }; + DBFCEF192893BA8A00EEBFB1 /* StatusHistoryViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCEF182893BA8A00EEBFB1 /* StatusHistoryViewController+DataSourceProvider.swift */; }; + DBFCEF1B2893D5C500EEBFB1 /* HistorySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCEF1A2893D5C500EEBFB1 /* HistorySection.swift */; }; + DBFCEF1E2893D5D400EEBFB1 /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCEF1D2893D5D400EEBFB1 /* HistoryItem.swift */; }; + DBFCEF202893E18400EEBFB1 /* DataSourceFacade+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCEF1F2893E18400EEBFB1 /* DataSourceFacade+History.swift */; }; DBFDCE4127F450FC00BE99E3 /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DBFDCE0327F446BB00BE99E3 /* Intents.framework */; }; DBFDCE4427F450FC00BE99E3 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFDCE4327F450FC00BE99E3 /* IntentHandler.swift */; }; DBFDCE4827F450FC00BE99E3 /* TwidereXIntent.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DBFDCE4027F450FC00BE99E3 /* TwidereXIntent.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -547,6 +555,10 @@ DB148B02281A7AB300B596C7 /* ContentSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentSplitViewController.swift; sourceTree = ""; }; DB148B04281A81AE00B596C7 /* SidebarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarViewModel.swift; sourceTree = ""; }; DB148B0B281A837E00B596C7 /* SidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarView.swift; sourceTree = ""; }; + DB1D3DE9289388CD008F0BD0 /* HistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewController.swift; sourceTree = ""; }; + DB1D3DEC28938ACD008F0BD0 /* HistoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewModel.swift; sourceTree = ""; }; + DB1D3DEE28938CD1008F0BD0 /* StatusHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusHistoryViewController.swift; sourceTree = ""; }; + DB1D3DF128938CDF008F0BD0 /* StatusHistoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusHistoryViewModel.swift; sourceTree = ""; }; DB1D7B4225B5938300397DCD /* TwitterAuthenticationOptionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterAuthenticationOptionViewController.swift; sourceTree = ""; }; DB1D7B5425B5A87200397DCD /* TwitterAuthenticationOptionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterAuthenticationOptionViewModel.swift; sourceTree = ""; }; DB1E48132772CE850074F6A0 /* SearchViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewModel+Diffable.swift"; sourceTree = ""; }; @@ -869,9 +881,6 @@ DBE635822886940A001C114B /* TwidereX-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "TwidereX-Bridging-Header.h"; sourceTree = ""; }; DBE635832886940B001C114B /* UIStatusBarManager+HandleTapAction.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIStatusBarManager+HandleTapAction.m"; sourceTree = ""; }; DBE6358528869441001C114B /* Notification+Name+HandleTapAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+Name+HandleTapAction.swift"; sourceTree = ""; }; - DBE71B7926B7AF5C00DFAB8E /* StubTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubTimelineViewController.swift; sourceTree = ""; }; - DBE71B7C26B7C5FD00DFAB8E /* StubTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubTimelineViewModel.swift; sourceTree = ""; }; - DBE71B7E26B7D68500DFAB8E /* StubTimelineCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubTimelineCollectionViewCell.swift; sourceTree = ""; }; DBE76CD2250095D900DEB0FC /* TwidereX.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TwidereX.entitlements; sourceTree = ""; }; DBE76CEA2500B29300DEB0FC /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; DBE76CEC2500B29300DEB0FC /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -915,6 +924,13 @@ DBFA4A1F25A5924C00D51703 /* ListTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ListTimelineViewModel+Diffable.swift"; sourceTree = ""; }; DBFCC44625667C620016698E /* UILabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UILabel.swift; sourceTree = ""; }; DBFCC46225668B860016698E /* DrawerSidebarPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawerSidebarPresentationController.swift; sourceTree = ""; }; + DBFCEF1128939C9600EEBFB1 /* UserHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserHistoryViewController.swift; sourceTree = ""; }; + DBFCEF1428939CAC00EEBFB1 /* UserHistoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserHistoryViewModel.swift; sourceTree = ""; }; + DBFCEF162893B92B00EEBFB1 /* StatusHistoryViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusHistoryViewModel+Diffable.swift"; sourceTree = ""; }; + DBFCEF182893BA8A00EEBFB1 /* StatusHistoryViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusHistoryViewController+DataSourceProvider.swift"; sourceTree = ""; }; + DBFCEF1A2893D5C500EEBFB1 /* HistorySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistorySection.swift; sourceTree = ""; }; + DBFCEF1D2893D5D400EEBFB1 /* HistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryItem.swift; sourceTree = ""; }; + DBFCEF1F2893E18400EEBFB1 /* DataSourceFacade+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+History.swift"; sourceTree = ""; }; DBFDCE0327F446BB00BE99E3 /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; }; DBFDCE2227F44C7100BE99E3 /* IntentsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IntentsUI.framework; path = System/Library/Frameworks/IntentsUI.framework; sourceTree = SDKROOT; }; DBFDCE4027F450FC00BE99E3 /* TwidereXIntent.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = TwidereXIntent.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1146,6 +1162,28 @@ path = View; sourceTree = ""; }; + DB1D3DEB289388CF008F0BD0 /* History */ = { + isa = PBXGroup; + children = ( + DB1D3DF028938CD4008F0BD0 /* Status */, + DBFCEF1328939C9900EEBFB1 /* User */, + DB1D3DE9289388CD008F0BD0 /* HistoryViewController.swift */, + DB1D3DEC28938ACD008F0BD0 /* HistoryViewModel.swift */, + ); + path = History; + sourceTree = ""; + }; + DB1D3DF028938CD4008F0BD0 /* Status */ = { + isa = PBXGroup; + children = ( + DB1D3DEE28938CD1008F0BD0 /* StatusHistoryViewController.swift */, + DBFCEF182893BA8A00EEBFB1 /* StatusHistoryViewController+DataSourceProvider.swift */, + DB1D3DF128938CDF008F0BD0 /* StatusHistoryViewModel.swift */, + DBFCEF162893B92B00EEBFB1 /* StatusHistoryViewModel+Diffable.swift */, + ); + path = Status; + sourceTree = ""; + }; DB1D7B5925B5A87600397DCD /* Option */ = { isa = PBXGroup; children = ( @@ -1436,6 +1474,7 @@ DB76A661275F65FE00A50673 /* DataSourceFacade+Model.swift */, DB3B906A26E8D3AB0010F64C /* DataSourceFacade+Status.swift */, DB01091F26E60756005F67D7 /* DataSourceFacade+StatusThread.swift */, + DBFCEF1F2893E18400EEBFB1 /* DataSourceFacade+History.swift */, DB262A382721621800D18EF3 /* DataSourceFacade+User.swift */, DB25C4C427798E1A00EC1435 /* DataSourceFacade+SavedSearch.swift */, DB01092126E608B7005F67D7 /* DataSourceFacade+Repost.swift */, @@ -1716,6 +1755,7 @@ DB1E48172772CEC40074F6A0 /* Search */, DB47AB1E27CCC18500CD73C7 /* List */, DB66DB912823AC400071F5F3 /* TabBar */, + DBFCEF1C2893D5C800EEBFB1 /* History */, ); path = Misc; sourceTree = ""; @@ -2184,14 +2224,14 @@ DB42411026C3E4C900B6C5F8 /* Onboarding */, DBCB4032255B670F00DD8D8F /* Account */, DB697DEB278FDB8A004EF2F7 /* Timeline */, - DB47AB1B27CCB7F100CD73C7 /* List */, - DBE71B7B26B7AF6100DFAB8E /* StubTimeline */, DB56329826DCBE1900FC893F /* StatusThread */, DB7274F0273BB25A00577D95 /* Notification */, DB747FFD251C496E000C4BD7 /* Profile */, DB8AC1122540501A00E636BE /* Compose */, DBF69E932549601C00E2A915 /* Search */, DBA5F9E42553CFEB00D2E98E /* MediaPreview */, + DB47AB1B27CCB7F100CD73C7 /* List */, + DB1D3DEB289388CF008F0BD0 /* History */, DB2C872E274F4B7D00CE0398 /* Setting */, ); path = Scene; @@ -2236,16 +2276,6 @@ path = PushNotificationScratch; sourceTree = ""; }; - DBE71B7B26B7AF6100DFAB8E /* StubTimeline */ = { - isa = PBXGroup; - children = ( - DBE71B7926B7AF5C00DFAB8E /* StubTimelineViewController.swift */, - DBE71B7C26B7C5FD00DFAB8E /* StubTimelineViewModel.swift */, - DBE71B7E26B7D68500DFAB8E /* StubTimelineCollectionViewCell.swift */, - ); - path = StubTimeline; - sourceTree = ""; - }; DBE76CE92500B29300DEB0FC /* StubMixer */ = { isa = PBXGroup; children = ( @@ -2417,6 +2447,24 @@ path = Like; sourceTree = ""; }; + DBFCEF1328939C9900EEBFB1 /* User */ = { + isa = PBXGroup; + children = ( + DBFCEF1128939C9600EEBFB1 /* UserHistoryViewController.swift */, + DBFCEF1428939CAC00EEBFB1 /* UserHistoryViewModel.swift */, + ); + path = User; + sourceTree = ""; + }; + DBFCEF1C2893D5C800EEBFB1 /* History */ = { + isa = PBXGroup; + children = ( + DBFCEF1A2893D5C500EEBFB1 /* HistorySection.swift */, + DBFCEF1D2893D5D400EEBFB1 /* HistoryItem.swift */, + ); + path = History; + sourceTree = ""; + }; DBFDCE4227F450FC00BE99E3 /* TwidereXIntent */ = { isa = PBXGroup; children = ( @@ -2996,6 +3044,7 @@ DB7274F2273BB29600577D95 /* NotificationViewModel.swift in Sources */, DB5FB0112727FD02006520FA /* SearchUserViewModel+Diffable.swift in Sources */, DBA7DD74256B96450008A95A /* UIFont.swift in Sources */, + DB1D3DF228938CDF008F0BD0 /* StatusHistoryViewModel.swift in Sources */, DB76A652275F1C3200A50673 /* MediaPreviewTransitionItem.swift in Sources */, DB697DF3278FDDF7004EF2F7 /* TimelineViewModel.swift in Sources */, DB235EF42834DD0900398FCA /* SettingListViewModel.swift in Sources */, @@ -3023,6 +3072,7 @@ DB02C76D27350D71007EA0BF /* SearchHashtagViewController.swift in Sources */, DB2C873E274F4B7D00CE0398 /* DeveloperViewController.swift in Sources */, DBFA4A2025A5924C00D51703 /* ListTimelineViewModel+Diffable.swift in Sources */, + DBFCEF1E2893D5D400EEBFB1 /* HistoryItem.swift in Sources */, DB925749251C9D48004FEFB5 /* ProfilePagingViewModel.swift in Sources */, DB932E5227FEC7390036A824 /* Intents.intentdefinition in Sources */, DBA210362759D7B1000B7CB2 /* FollowingListViewModel+State.swift in Sources */, @@ -3054,7 +3104,6 @@ DBCB4060255CAC0300DD8D8F /* TwitterAuthenticationController.swift in Sources */, DB5632B426DDE3F300FC893F /* StatusThreadViewModel+LoadThreadState.swift in Sources */, DBD0B49D2758B5F50015A388 /* SidebarSection.swift in Sources */, - DBE71B7F26B7D68500DFAB8E /* StubTimelineCollectionViewCell.swift in Sources */, DB0AD4EB28587B520002ABDB /* FederatedTimelineViewModel+Diffable.swift in Sources */, DBF739CF275C247F00BF6AB5 /* DataSourceFacade+Mute.swift in Sources */, DB578CCD254C0BDB00745336 /* UserBriefInfoView.swift in Sources */, @@ -3088,9 +3137,12 @@ DB697E0C27904C56004EF2F7 /* FederatedTimelineViewModel.swift in Sources */, DB01A9A427637FE60055FABC /* DataSourceFacade+Meta.swift in Sources */, DB30ADDD26CFD3CC00B2D2BE /* HomeTimelineViewController+DebugAction.swift in Sources */, + DBFCEF192893BA8A00EEBFB1 /* StatusHistoryViewController+DataSourceProvider.swift in Sources */, DBA5FA192553DCBC00D2E98E /* TransitioningMath.swift in Sources */, DB56329C26DCC23700FC893F /* DataSourceProvider.swift in Sources */, DB44A56226C4FEAB004C8B78 /* WelcomeViewModel.swift in Sources */, + DBFCEF1B2893D5C500EEBFB1 /* HistorySection.swift in Sources */, + DB1D3DED28938ACD008F0BD0 /* HistoryViewModel.swift in Sources */, DB522F172886A08F0088017C /* UIStatusBarManager+HandleTapAction.m in Sources */, DBC8E04B2576337F00401E20 /* DisposeBagCollectable.swift in Sources */, DBAA898E2758CF01001C273B /* DrawerSidebarHeaderView+ViewModel.swift in Sources */, @@ -3102,6 +3154,7 @@ DBA6B30E27E9CB6F004D052D /* AddListMemberViewController.swift in Sources */, DBF63A38259D7CCB009E12C8 /* CornerSmoothPreviewViewController.swift in Sources */, DB0CC4BA27D5F7BA00A051B4 /* CompositeListViewModel+Diffable.swift in Sources */, + DBFCEF172893B92B00EEBFB1 /* StatusHistoryViewModel+Diffable.swift in Sources */, DB02C77527351DA2007EA0BF /* HashtagTableViewCell+ViewModel.swift in Sources */, DB0AD4E22858734A0002ABDB /* UserMediaTimelineViewModel.swift in Sources */, DB522F1628869DAE0088017C /* Notification+Name+HandleTapAction.swift in Sources */, @@ -3109,7 +3162,9 @@ DB6BCD70277AEAC700847054 /* TrendTableViewCell.swift in Sources */, DB442461285AD8530095AECF /* ListStatusTimelineViewController.swift in Sources */, DB914C4A26C3AC7C00E85F1A /* ContentOffsetFixedCollectionView.swift in Sources */, + DB1D3DEF28938CD1008F0BD0 /* StatusHistoryViewController.swift in Sources */, DBD0B4A02758B6010015A388 /* SidebarItem.swift in Sources */, + DBFCEF1228939C9600EEBFB1 /* UserHistoryViewController.swift in Sources */, DB76A67127609A8700A50673 /* RemoteProfileViewModel.swift in Sources */, DB6BCD6B277ADBF300847054 /* TrendViewController.swift in Sources */, DB76A656275F354800A50673 /* MediaPreviewViewModel.swift in Sources */, @@ -3143,7 +3198,6 @@ DB5A2288255B9155006CA5B2 /* AccountListViewModel+Diffable.swift in Sources */, DB12BEE727329F55002AA635 /* SearchUserViewController+DataSourceProvider.swift in Sources */, DB42411426C3E55200B6C5F8 /* WelcomeView.swift in Sources */, - DBE71B7A26B7AF5C00DFAB8E /* StubTimelineViewController.swift in Sources */, DB1D7B5525B5A87200397DCD /* TwitterAuthenticationOptionViewModel.swift in Sources */, DB98DC0E2507862D0087E30F /* TimelineMiddleLoaderTableViewCell.swift in Sources */, DBA210382759EA91000B7CB2 /* FollowingListViewController+DataSourceProvider.swift in Sources */, @@ -3166,6 +3220,7 @@ DB1E48142772CE850074F6A0 /* SearchViewModel+Diffable.swift in Sources */, DB71C7D1271EB09A00BE3819 /* DataSourceFacade+Friendship.swift in Sources */, DB442473285B177B0095AECF /* SearchMediaTimelineViewModel.swift in Sources */, + DB1D3DEA289388CD008F0BD0 /* HistoryViewController.swift in Sources */, DBCE2E912591A06300926D09 /* UINavigationController.swift in Sources */, DB915406254FC8CE00613473 /* FollowActionButton.swift in Sources */, DB8761C0274552F800BA7EE2 /* StatusSection.swift in Sources */, @@ -3208,6 +3263,7 @@ DB76A657275F498F00A50673 /* MediaPreviewImageViewController.swift in Sources */, DB5632B026DCED1300FC893F /* StatusThreadRootTableViewCell.swift in Sources */, DB8761C4274552FB00BA7EE2 /* HashtagData.swift in Sources */, + DBFCEF1528939CAC00EEBFB1 /* UserHistoryViewModel.swift in Sources */, DBDA8E2224FCF8A3006750DC /* AppDelegate.swift in Sources */, DB46D11F27DB2B50003B8BA1 /* ListUserViewModel+Diffable.swift in Sources */, DB442459285A42B50095AECF /* HashtagTimelineViewController.swift in Sources */, @@ -3223,6 +3279,7 @@ DB74A16E256E50E300C5F3C9 /* UIColor.swift in Sources */, DB676161254BE580006C6798 /* MediaPreviewCollectionViewCell.swift in Sources */, DB01092026E60756005F67D7 /* DataSourceFacade+StatusThread.swift in Sources */, + DBFCEF202893E18400EEBFB1 /* DataSourceFacade+History.swift in Sources */, DBDAF244274F530B00050319 /* NeedsDependency.swift in Sources */, DB8761C3274552FB00BA7EE2 /* HashtagSection.swift in Sources */, DB9B325C2857493D00AC818D /* UserTimelineViewModel+Diffable.swift in Sources */, @@ -3236,7 +3293,6 @@ DBBF70F726D7B62F00971656 /* AccountListTableViewCell+ViewModel.swift in Sources */, DBB47DA926EB3AA2001590F7 /* ProfileFieldListView.swift in Sources */, DBF69EE12549705A00E2A915 /* ViewControllerAnimatedTransitioningDelegate.swift in Sources */, - DBE71B7D26B7C5FD00DFAB8E /* StubTimelineViewModel.swift in Sources */, DB76A658275F498F00A50673 /* MediaPreviewImageViewModel.swift in Sources */, DB47AB2E27CE097C00CD73C7 /* ListItem.swift in Sources */, DBB04A372861AF2D003799CA /* TrendPlaceView.swift in Sources */, diff --git a/TwidereX/Coordinator/SceneCoordinator.swift b/TwidereX/Coordinator/SceneCoordinator.swift index b8b96547..ca0133b3 100644 --- a/TwidereX/Coordinator/SceneCoordinator.swift +++ b/TwidereX/Coordinator/SceneCoordinator.swift @@ -93,6 +93,9 @@ extension SceneCoordinator { case trendPlace(viewModel: TrendViewModel) case searchResult(viewModel: SearchResultViewModel) + // Hisotry + case history(viewModel: HistoryViewModel) + // Settings case setting(viewModel: SettingListViewModel) case accountPreference(viewModel: AccountPreferenceViewModel) @@ -102,7 +105,6 @@ extension SceneCoordinator { #if DEBUG case developer - case stubTimeline case pushNotificationScratch #endif @@ -155,8 +157,8 @@ extension SceneCoordinator { } } - @discardableResult @MainActor + @discardableResult func present(scene: Scene, from sender: UIViewController?, transition: Transition) -> UIViewController? { guard let viewController = get(scene: scene) else { return nil @@ -355,6 +357,10 @@ private extension SceneCoordinator { _viewController.searchResultViewModel = viewModel _viewController.searchResultViewController = searchResultViewController viewController = _viewController + case .history(let viewModel): + let _viewController = HistoryViewController() + _viewController.viewModel = viewModel + viewController = _viewController case .setting(let viewModel): let _viewController = SettingListViewController() _viewController.viewModel = viewModel @@ -374,8 +380,6 @@ private extension SceneCoordinator { #if DEBUG case .developer: viewController = DeveloperViewController() - case .stubTimeline: - viewController = StubTimelineViewController() case .pushNotificationScratch: viewController = PushNotificationScratchViewController() #endif diff --git a/TwidereX/Diffable/Misc/History/HistoryItem.swift b/TwidereX/Diffable/Misc/History/HistoryItem.swift new file mode 100644 index 00000000..a17f7a76 --- /dev/null +++ b/TwidereX/Diffable/Misc/History/HistoryItem.swift @@ -0,0 +1,16 @@ +// +// HistoryItem.swift +// TwidereX +// +// Created by MainasuK on 2022-7-29. +// Copyright © 2022 Twidere. All rights reserved. +// + + +import Foundation +import CoreDataStack +import TwidereCore + +enum HistoryItem: Hashable { + case history(record: ManagedObjectRecord) +} diff --git a/TwidereX/Diffable/Misc/History/HistorySection.swift b/TwidereX/Diffable/Misc/History/HistorySection.swift new file mode 100644 index 00000000..7523c175 --- /dev/null +++ b/TwidereX/Diffable/Misc/History/HistorySection.swift @@ -0,0 +1,89 @@ +// +// HistorySection.swift +// TwidereX +// +// Created by MainasuK on 2022-7-29. +// Copyright © 2022 Twidere. All rights reserved. +// + +import os.log +import UIKit +import Combine +import CoreData +import CoreDataStack +import MetaTextKit +import TwidereUI +import AppShared +import TwitterSDK + +enum HistorySection: Hashable { + case group(identifer: String) +} + +extension HistorySection { + + static let logger = Logger(subsystem: "StatusSection", category: "Logic") + + struct Configuration { + weak var statusViewTableViewCellDelegate: StatusViewTableViewCellDelegate? + let statusViewConfigurationContext: StatusView.ConfigurationContext + } + + static func diffableDataSource( + tableView: UITableView, + context: AppContext, + configuration: Configuration + ) -> UITableViewDiffableDataSource { + tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self)) + + let diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in + // data source should dispatch in main thread + assert(Thread.isMainThread) + + switch item { + case .history(let record): + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell + StatusSection.setupStatusPollDataSource( + context: context, + statusView: cell.statusView, + configurationContext: configuration.statusViewConfigurationContext + ) + context.managedObjectContext.performAndWait { + guard let status = record.object(in: context.managedObjectContext)?.statusObject else { return } + configure( + tableView: tableView, + cell: cell, + viewModel: StatusTableViewCell.ViewModel(value: .statusObject(status)), + configuration: configuration + ) + } + return cell + } + } + + return diffableDataSource + } // end func + +} + +extension HistorySection { + + static func configure( + tableView: UITableView, + cell: StatusTableViewCell, + viewModel: StatusTableViewCell.ViewModel, + configuration: Configuration + ) { + StatusSection.configure( + tableView: tableView, + cell: cell, + viewModel: viewModel, + configuration: .init( + statusViewTableViewCellDelegate: configuration.statusViewTableViewCellDelegate, + timelineMiddleLoaderTableViewCellDelegate: nil, + statusViewConfigurationContext: configuration.statusViewConfigurationContext + ) + ) + } + +} diff --git a/TwidereX/Diffable/Misc/Sidebar/SidebarItem.swift b/TwidereX/Diffable/Misc/Sidebar/SidebarItem.swift index 40e9a1df..8c6e32e4 100644 --- a/TwidereX/Diffable/Misc/Sidebar/SidebarItem.swift +++ b/TwidereX/Diffable/Misc/Sidebar/SidebarItem.swift @@ -15,6 +15,7 @@ enum SidebarItem: Hashable { case federated // Mastodon only case messages case likes + case history case lists case trends case drafts @@ -29,6 +30,7 @@ extension SidebarItem { case .federated: return L10n.Scene.Federated.title case .messages: return L10n.Scene.Messages.title case .likes: return L10n.Scene.Likes.title + case .history: return "History" // TODO: i18n case .lists: return L10n.Scene.Lists.title case .trends: return L10n.Scene.Trends.title case .drafts: return L10n.Scene.Drafts.title @@ -42,6 +44,7 @@ extension SidebarItem { case .federated: return Asset.ObjectTools.globe.image.withRenderingMode(.alwaysTemplate) case .messages: return Asset.Communication.mail.image.withRenderingMode(.alwaysTemplate) case .likes: return Asset.Health.heart.image.withRenderingMode(.alwaysTemplate) + case .history: return Asset.Arrows.clockArrowCirclepath.image.withRenderingMode(.alwaysTemplate) case .lists: return Asset.TextFormatting.listBullet.image.withRenderingMode(.alwaysTemplate) case .trends: return Asset.Arrows.trendingUp.image.withRenderingMode(.alwaysTemplate) case .drafts: return Asset.ObjectTools.note.image.withRenderingMode(.alwaysTemplate) diff --git a/TwidereX/Diffable/Status/StatusSection.swift b/TwidereX/Diffable/Status/StatusSection.swift index 91c8059e..ca5b8c92 100644 --- a/TwidereX/Diffable/Status/StatusSection.swift +++ b/TwidereX/Diffable/Status/StatusSection.swift @@ -16,7 +16,7 @@ import TwidereUI import AppShared import TwitterSDK -enum StatusSection: Int, Hashable { +enum StatusSection: Hashable { case main case footer } diff --git a/TwidereX/Generated/AutoGenerateTableViewDelegate.generated.swift b/TwidereX/Generated/AutoGenerateTableViewDelegate.generated.swift index 5c657df6..ec0ecae9 100644 --- a/TwidereX/Generated/AutoGenerateTableViewDelegate.generated.swift +++ b/TwidereX/Generated/AutoGenerateTableViewDelegate.generated.swift @@ -99,30 +99,7 @@ func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith con aspectTableView(tableView, willPerformPreviewActionForMenuWith: configuration, animator: animator) } // sourcery:end -// sourcery:inline:ListTimelineViewController.AutoGenerateTableViewDelegate -// Generated using Sourcery -// DO NOT EDIT -func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - aspectTableView(tableView, didSelectRowAt: indexPath) -} - -func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { - return aspectTableView(tableView, contextMenuConfigurationForRowAt: indexPath, point: point) -} - -func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { - return aspectTableView(tableView, previewForHighlightingContextMenuWithConfiguration: configuration) -} - -func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { - return aspectTableView(tableView, previewForDismissingContextMenuWithConfiguration: configuration) -} - -func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { - aspectTableView(tableView, willPerformPreviewActionForMenuWith: configuration, animator: animator) -} -// sourcery:end // sourcery:inline:SearchTimelineViewController.AutoGenerateTableViewDelegate @@ -151,6 +128,7 @@ func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith con // sourcery:end + // sourcery:inline:UserTimelineViewController.AutoGenerateTableViewDelegate // Generated using Sourcery diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+History.swift b/TwidereX/Protocol/Provider/DataSourceFacade+History.swift new file mode 100644 index 00000000..0bb4a07f --- /dev/null +++ b/TwidereX/Protocol/Provider/DataSourceFacade+History.swift @@ -0,0 +1,56 @@ +// +// DataSourceFacade+History.swift +// TwidereX +// +// Created by MainasuK on 2022-7-29. +// Copyright © 2022 Twidere. All rights reserved. +// + +import Foundation +import CoreData +import CoreDataStack +import TwidereCore + +extension DataSourceFacade { + + static func recordStatusHistory( + denpendency: NeedsDependency, + status: StatusRecord + ) async { + let now = Date() + guard let authenticationContext = denpendency.context.authenticationService.activeAuthenticationContext else { return } + + let acct = authenticationContext.acct + let managedObjectContext = denpendency.context.backgroundManagedObjectContext + let _history: ManagedObjectRecord? = await managedObjectContext.perform { + guard let status = status.object(in: managedObjectContext) else { return nil } + guard let history = status.histories.first(where: { $0.acct == acct }) else { return nil } + return history.asRecrod + } + + if let history = _history { + try? await managedObjectContext.performChanges { + guard let history = history.object(in: managedObjectContext) else { return } + history.update(timestamp: now) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): update status history for: \(history.debugDescription)") + } + } else { + try? await managedObjectContext.performChanges { + guard let status = status.object(in: managedObjectContext) else { return } + let history = History.insert( + into: managedObjectContext, + property: .init(acct: acct, timestamp: now, createdAt: now) + ) + switch status { + case .twitter(let object): + history.update(twitterStatus: object) + case .mastodon(let object): + history.update(mastodonStatus: object) + } + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): create status history: \(history.debugDescription)") + + } + } + } // end func + +} diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift index 6f1454f7..0e41c280 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift @@ -56,6 +56,13 @@ extension DataSourceFacade { status: redirectRecord, mediaPreviewContext: mediaPreviewContext ) + + Task { + await recordStatusHistory( + denpendency: provider, + status: status + ) + } // end Task } } diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift index 6510cf22..f6d58b88 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift @@ -22,12 +22,25 @@ extension DataSourceFacade { sender: UIButton, authenticationContext: AuthenticationContext ) async { + defer { + Task { + await recordStatusHistory( + denpendency: provider, + status: status + ) + } // end Task + } + switch action { case .reply: guard let status = status.object(in: provider.context.managedObjectContext) else { assertionFailure() return } + + let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) + impactFeedbackGenerator.impactOccurred() + let composeViewModel = ComposeViewModel(context: provider.context) let composeContentViewModel = ComposeContentViewModel( kind: .reply(status: status), @@ -78,6 +91,9 @@ extension DataSourceFacade { } case .menu: // media menu button trigger this + let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) + impactFeedbackGenerator.impactOccurred() + await DataSourceFacade.responseToStatusShareAction( provider: provider, status: status, @@ -105,6 +121,13 @@ extension DataSourceFacade { provider: provider, status: redirectRecord ) + + Task { + await recordStatusHistory( + denpendency: provider, + status: status + ) + } // end Task } @MainActor @@ -144,6 +167,13 @@ extension DataSourceFacade { provider: provider, status: redirectRecord ) + + Task { + await recordStatusHistory( + denpendency: provider, + status: status + ) + } // end Task } @MainActor diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+StatusThread.swift b/TwidereX/Protocol/Provider/DataSourceFacade+StatusThread.swift index b8cc3519..b1cb820b 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+StatusThread.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+StatusThread.swift @@ -72,5 +72,13 @@ extension DataSourceFacade { from: provider, transition: .show ) + + Task { + guard case let .root(threadContext) = root else { return } + await recordStatusHistory( + denpendency: provider, + status: threadContext.status + ) + } // end Task } } diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift index cad39dd9..1bd9d185 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift @@ -319,7 +319,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider { guard let status = await item.status(in: self.context.managedObjectContext) else { assertionFailure("only works for status data provider") return } - + await DataSourceFacade.responseToStatusToolbar( provider: self, status: status, @@ -398,6 +398,20 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider { UIPasteboard.general.string = statusID } #endif + case .appearEvent: + let _record = await DataSourceFacade.status( + managedObjectContext: context.managedObjectContext, + status: status, + target: .status + ) + guard let record = _record else { + return + } + + await DataSourceFacade.recordStatusHistory( + denpendency: self, + status: record + ) } // end switch } // end Task } // end func diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift index fe89c78f..d2d2d588 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -64,6 +64,17 @@ extension UITableViewDelegate where Self: DataSourceProvider & MediaPreviewableV guard let cell = tableView.cellForRow(at: indexPath) as? StatusViewContainerTableViewCell else { return nil } + defer { + Task { + guard let item = await item(from: .init(tableViewCell: cell, indexPath: indexPath)) else { return } + guard let status = await item.status(in: context.managedObjectContext) else { return } + await DataSourceFacade.recordStatusHistory( + denpendency: self, + status: status + ) + } // end Task + } + // TODO: // this must call before check `isContentWarningOverlayDisplay`. otherwise, will get BadAccess exception let mediaViews = cell.statusView.mediaGridContainerView.mediaViews diff --git a/TwidereX/Scene/History/HistoryViewController.swift b/TwidereX/Scene/History/HistoryViewController.swift new file mode 100644 index 00000000..b3043e50 --- /dev/null +++ b/TwidereX/Scene/History/HistoryViewController.swift @@ -0,0 +1,91 @@ +// +// HistoryViewController.swift +// TwidereX +// +// Created by MainasuK on 2022-7-29. +// Copyright © 2022 Twidere. All rights reserved. +// + +import os.log +import UIKit +import Combine +import Tabman +import Pageboy +import TwidereCore + +final class HistoryViewController: TabmanViewController, NeedsDependency, DrawerSidebarTransitionHostViewController { + + let logger = Logger(subsystem: "HistoryViewController", category: "ViewController") + + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + + var disposeBag = Set() + var viewModel: HistoryViewModel! + + private(set) var drawerSidebarTransitionController: DrawerSidebarTransitionController! + let avatarBarButtonItem = AvatarBarButtonItem() + + private(set) lazy var pageSegmentedControl = UISegmentedControl() + + override func pageboyViewController( + _ pageboyViewController: PageboyViewController, + didScrollToPageAt index: TabmanViewController.PageIndex, + direction: PageboyViewController.NavigationDirection, + animated: Bool + ) { + super.pageboyViewController( + pageboyViewController, + didScrollToPageAt: index, + direction: direction, + animated: animated + ) + + viewModel.currentPageIndex = index + } + +} + +extension HistoryViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + isScrollEnabled = false // inner pan gesture untouchable. workaround to prevent swipe conflict + drawerSidebarTransitionController = DrawerSidebarTransitionController(hostViewController: self) + + view.backgroundColor = .systemBackground + + // TODO: + // setupSegmentedControl(scopes: viewModel.scopes) + // navigationItem.titleView = pageSegmentedControl + + title = "Hisotry" + + dataSource = viewModel + } + +} + +extension HistoryViewController { + private func setupSegmentedControl(scopes: [HistoryViewModel.Scope]) { + pageSegmentedControl.removeAllSegments() + for (i, scope) in scopes.enumerated() { + let title = scope.title(platform: viewModel.platform) + pageSegmentedControl.insertSegment(withTitle: title, at: i, animated: false) + } + + // set initial selection + guard !pageSegmentedControl.isSelected else { return } + if viewModel.currentPageIndex < pageSegmentedControl.numberOfSegments { + pageSegmentedControl.selectedSegmentIndex = viewModel.currentPageIndex + } else { + pageSegmentedControl.selectedSegmentIndex = 0 + } + + pageSegmentedControl.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + pageSegmentedControl.widthAnchor.constraint(greaterThanOrEqualToConstant: 240) + ]) + } +} diff --git a/TwidereX/Scene/History/HistoryViewModel.swift b/TwidereX/Scene/History/HistoryViewModel.swift new file mode 100644 index 00000000..0de852cc --- /dev/null +++ b/TwidereX/Scene/History/HistoryViewModel.swift @@ -0,0 +1,99 @@ +// +// HistoryViewModel.swift +// TwidereX +// +// Created by MainasuK on 2022-7-29. +// Copyright © 2022 Twidere. All rights reserved. +// + +import UIKit +import Combine +import Pageboy +import TwidereCore +import CoreDataStack + +final class HistoryViewModel { + + var disposeBag = Set() + + // input + let context: AppContext + let _coordinator: SceneCoordinator // only use for `setup` + let authContext: AuthContext + + // output + let platform: Platform + let scopes = Scope.allCases + let viewControllers: [UIViewController] + @Published var currentPageIndex = 0 + + init( + context: AppContext, + coordinator: SceneCoordinator, + authContext: AuthContext + ) { + self.context = context + self._coordinator = coordinator + self.authContext = authContext + self.platform = { + switch authContext.authenticationContext { + case .twitter: return .twitter + case .mastodon: return .mastodon + } + }() + self.viewControllers = { + // status + let statusHistoryViewController = StatusHistoryViewController() + statusHistoryViewController.context = context + statusHistoryViewController.coordinator = coordinator + statusHistoryViewController.viewModel = StatusHistoryViewModel(context: context, authContext: authContext) + // user + let userHistoryViewController = UserHistoryViewController() + userHistoryViewController.context = context + userHistoryViewController.coordinator = coordinator + userHistoryViewController.viewModel = UserHistoryViewModel(context: context, authContext: authContext) + return [statusHistoryViewController, userHistoryViewController] + }() + // end init + } + +} + +extension HistoryViewModel { + enum Scope: Hashable, CaseIterable { + case status + case user + + func title(platform: Platform) -> String { + switch self { + case .status: + switch platform { + case .twitter: return "Tweet" + case .mastodon: return "Toot" + case .none: + assertionFailure() + return "Post" + } + case .user: + return "User" // TODO: i18n + } + } + } +} + +// MARK: - PageboyViewControllerDataSource +extension HistoryViewModel: PageboyViewControllerDataSource { + + func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int { + return viewControllers.count + } + + func viewController(for pageboyViewController: PageboyViewController, at index: PageboyViewController.PageIndex) -> UIViewController? { + return viewControllers[index] + } + + func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? { + return .first + } + +} diff --git a/TwidereX/Scene/History/Status/StatusHistoryViewController+DataSourceProvider.swift b/TwidereX/Scene/History/Status/StatusHistoryViewController+DataSourceProvider.swift new file mode 100644 index 00000000..c90e3355 --- /dev/null +++ b/TwidereX/Scene/History/Status/StatusHistoryViewController+DataSourceProvider.swift @@ -0,0 +1,41 @@ +// +// StatusHistoryViewController+DataSourceProvider.swift +// TwidereX +// +// Created by MainasuK on 2022-7-29. +// Copyright © 2022 Twidere. All rights reserved. +// + +import UIKit +import TwidereCore + +extension StatusHistoryViewController: DataSourceProvider { + func item(from source: DataSourceItem.Source) async -> DataSourceItem? { + var _indexPath = source.indexPath + if _indexPath == nil, let cell = source.tableViewCell { + _indexPath = await self.indexPath(for: cell) + } + guard let indexPath = _indexPath else { return nil } + + guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { + return nil + } + + switch item { + case .history(let record): + let managedObjectContext = context.managedObjectContext + let item: DataSourceItem? = await managedObjectContext.perform { + guard let history = record.object(in: managedObjectContext) else { return nil } + guard let status = history.statusObject else { return nil } + return .status(status.asRecord) + + } + return item + } + } + + @MainActor + private func indexPath(for cell: UITableViewCell) async -> IndexPath? { + return tableView.indexPath(for: cell) + } +} diff --git a/TwidereX/Scene/History/Status/StatusHistoryViewController.swift b/TwidereX/Scene/History/Status/StatusHistoryViewController.swift new file mode 100644 index 00000000..e35ec49d --- /dev/null +++ b/TwidereX/Scene/History/Status/StatusHistoryViewController.swift @@ -0,0 +1,126 @@ +// +// StatusHistoryViewController.swift +// TwidereX +// +// Created by MainasuK on 2022-7-29. +// Copyright © 2022 Twidere. All rights reserved. +// + +import os.log +import UIKit +import Combine +import CoreDataStack +import TwidereUI + +final class StatusHistoryViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { + + let logger = Logger(subsystem: "StatusHistoryViewController", category: "ViewController") + + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + + var disposeBag = Set() + var viewModel: StatusHistoryViewModel! + + let mediaPreviewTransitionController = MediaPreviewTransitionController() + + private(set) lazy var tableView: UITableView = { + let tableView = UITableView() + tableView.backgroundColor = .systemBackground + tableView.rowHeight = UITableView.automaticDimension + tableView.separatorStyle = .none + tableView.sectionHeaderTopPadding = .zero + return tableView + }() + +} + +extension StatusHistoryViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .systemBackground + + tableView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(tableView) + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + tableView.delegate = self + viewModel.setupDiffableDataSource( + tableView: tableView, + statusViewTableViewCellDelegate: self + ) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + tableView.deselectRow(with: transitionCoordinator, animated: animated) + } + +} + +// MARK: - UITableViewDelegate +extension StatusHistoryViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { + // sourcery:inline:StatusHistoryViewController.AutoGenerateTableViewDelegate + + // Generated using Sourcery + // DO NOT EDIT + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + aspectTableView(tableView, didSelectRowAt: indexPath) + } + + func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { + return aspectTableView(tableView, contextMenuConfigurationForRowAt: indexPath, point: point) + } + + func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { + return aspectTableView(tableView, previewForHighlightingContextMenuWithConfiguration: configuration) + } + + func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { + return aspectTableView(tableView, previewForDismissingContextMenuWithConfiguration: configuration) + } + + func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { + aspectTableView(tableView, willPerformPreviewActionForMenuWith: configuration, animator: animator) + } + // sourcery:end + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + guard let diffableDataSource = viewModel.diffableDataSource else { return nil } + guard let sectionIdentifier = diffableDataSource.sectionIdentifier(for: section) else { return nil } + switch sectionIdentifier { + case .group(let identifer): + guard let date = History.date(from: identifer) else { return nil } + let formatter = DateFormatter() + formatter.dateStyle = .long + formatter.timeStyle = .none + + let title = formatter.string(from: date) + let header = TableViewSectionTextHeaderView() + header.label.text = title + + let container = UIView() + container.backgroundColor = .secondarySystemBackground + header.translatesAutoresizingMaskIntoConstraints = false + container.addSubview(header) + NSLayoutConstraint.activate([ + header.topAnchor.constraint(equalTo: container.topAnchor), + header.leadingAnchor.constraint(equalTo: container.readableContentGuide.leadingAnchor), + header.trailingAnchor.constraint(equalTo: container.readableContentGuide.trailingAnchor), + header.bottomAnchor.constraint(equalTo: container.bottomAnchor), + ]) + return container + } + } +} + +// MARK: - StatusViewTableViewCellDelegate +extension StatusHistoryViewController: StatusViewTableViewCellDelegate { } diff --git a/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift b/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift new file mode 100644 index 00000000..0b3a8961 --- /dev/null +++ b/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift @@ -0,0 +1,55 @@ +// +// StatusHistoryViewModel+Diffable.swift +// TwidereX +// +// Created by MainasuK on 2022-7-29. +// Copyright © 2022 Twidere. All rights reserved. +// + +import UIKit +import Combine +import AppShared + +extension StatusHistoryViewModel { + + func setupDiffableDataSource( + tableView: UITableView, + statusViewTableViewCellDelegate: StatusViewTableViewCellDelegate + ) { + diffableDataSource = HistorySection.diffableDataSource( + tableView: tableView, + context: context, + configuration: .init( + statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, + statusViewConfigurationContext: .init( + dateTimeProvider: DateTimeSwiftProvider(), + twitterTextProvider: OfficialTwitterTextProvider(), + authenticationContext: context.authenticationService.$activeAuthenticationContext + ) + ) + ) + + let snapshot = NSDiffableDataSourceSnapshot() + diffableDataSource?.apply(snapshot) + + statusHistoryFetchedResultsController.$groupedRecords + .receive(on: DispatchQueue.main) + .sink { [weak self] groupedRecords in + guard let self = self else { return } + guard let diffableDataSource = self.diffableDataSource else { return } + + var snapshot = NSDiffableDataSourceSnapshot() + for (identifier, records) in groupedRecords { + let section = HistorySection.group(identifer: identifier) + snapshot.appendSections([section]) + let items: [HistoryItem] = records.map { .history(record: $0) } + snapshot.appendItems(items, toSection: section) + } + + diffableDataSource.apply(snapshot, animatingDifferences: false) + } + .store(in: &disposeBag) + } + +} + diff --git a/TwidereX/Scene/History/Status/StatusHistoryViewModel.swift b/TwidereX/Scene/History/Status/StatusHistoryViewModel.swift new file mode 100644 index 00000000..f255fdbf --- /dev/null +++ b/TwidereX/Scene/History/Status/StatusHistoryViewModel.swift @@ -0,0 +1,47 @@ +// +// StatusHistoryViewModel.swift +// TwidereX +// +// Created by MainasuK on 2022-7-29. +// Copyright © 2022 Twidere. All rights reserved. +// + +import os.log +import UIKit +import Combine +import CoreDataStack +import MastodonSDK +import TwidereCore +import TwidereUI + +final class StatusHistoryViewModel { + + let logger = Logger(subsystem: "StatusHistoryViewModel", category: "ViewModel") + + var disposeBag = Set() + + // input + let context: AppContext + let authContext: AuthContext + let statusHistoryFetchedResultsController: StatusHistoryFetchedResultsController + + // output + var diffableDataSource: UITableViewDiffableDataSource? + + init( + context: AppContext, + authContext: AuthContext + ) { + self.context = context + self.authContext = authContext + self.statusHistoryFetchedResultsController = StatusHistoryFetchedResultsController(managedObjectContext: context.managedObjectContext) + // end init + + statusHistoryFetchedResultsController.predicate = History.statusPredicate(acct: authContext.authenticationContext.acct) + } + + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function) + } + +} diff --git a/TwidereX/Scene/History/User/UserHistoryViewController.swift b/TwidereX/Scene/History/User/UserHistoryViewController.swift new file mode 100644 index 00000000..c9b6f967 --- /dev/null +++ b/TwidereX/Scene/History/User/UserHistoryViewController.swift @@ -0,0 +1,34 @@ +// +// UserHistoryViewController.swift +// TwidereX +// +// Created by MainasuK on 2022-7-29. +// Copyright © 2022 Twidere. All rights reserved. +// + +import os.log +import UIKit +import Combine +import TwidereUI + +final class UserHistoryViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { + + let logger = Logger(subsystem: "UserHistoryViewController", category: "ViewController") + + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + + var disposeBag = Set() + var viewModel: UserHistoryViewModel! + + let mediaPreviewTransitionController = MediaPreviewTransitionController() + + private(set) lazy var tableView: UITableView = { + let tableView = UITableView() + tableView.backgroundColor = .systemBackground + tableView.rowHeight = UITableView.automaticDimension + tableView.separatorStyle = .none + return tableView + }() + +} diff --git a/TwidereX/Scene/History/User/UserHistoryViewModel.swift b/TwidereX/Scene/History/User/UserHistoryViewModel.swift new file mode 100644 index 00000000..e7231f0a --- /dev/null +++ b/TwidereX/Scene/History/User/UserHistoryViewModel.swift @@ -0,0 +1,44 @@ +// +// UserHistoryViewModel.swift +// TwidereX +// +// Created by MainasuK on 2022-7-29. +// Copyright © 2022 Twidere. All rights reserved. +// + +import os.log +import UIKit +import Combine +import CoreDataStack +import MastodonSDK +import TwidereCore +import TwidereUI + +final class UserHistoryViewModel { + + let logger = Logger(subsystem: "UserHistoryViewModel", category: "ViewModel") + + var disposeBag = Set() + + // input + let context: AppContext + let authContext: AuthContext + + // output + // var diffableDataSource: UITableViewDiffableDataSource? + // var didLoadLatest = PassthroughSubject() + + init( + context: AppContext, + authContext: AuthContext + ) { + self.context = context + self.authContext = authContext + // end init + } + + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function) + } + +} diff --git a/TwidereX/Scene/Notification/NotificationViewController.swift b/TwidereX/Scene/Notification/NotificationViewController.swift index 520e984f..c8ea1eae 100644 --- a/TwidereX/Scene/Notification/NotificationViewController.swift +++ b/TwidereX/Scene/Notification/NotificationViewController.swift @@ -51,6 +51,7 @@ final class NotificationViewController: TabmanViewController, NeedsDependency, D } extension NotificationViewController { + override func viewDidLoad() { super.viewDidLoad() @@ -175,6 +176,11 @@ extension NotificationViewController { } else { pageSegmentedControl.selectedSegmentIndex = 0 } + + pageSegmentedControl.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + pageSegmentedControl.widthAnchor.constraint(greaterThanOrEqualToConstant: 240) + ]) } } diff --git a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift index f28b1d51..316881ef 100644 --- a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift +++ b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift @@ -215,6 +215,11 @@ extension DrawerSidebarViewController: UICollectionViewDelegate { case .likes: let meLikeTimelineViewModel = MeLikeTimelineViewModel(context: context) coordinator.present(scene: .userLikeTimeline(viewModel: meLikeTimelineViewModel), from: presentingViewController, transition: .show) + case .history: + guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext else { return } + let authContext = AuthContext(authenticationContext: authenticationContext) + let historyViewModel = HistoryViewModel(context: context, coordinator: coordinator, authContext: authContext) + coordinator.present(scene: .history(viewModel: historyViewModel), from: presentingViewController, transition: .show) case .lists: guard let me = context.authenticationService.activeAuthenticationContext?.user(in: context.managedObjectContext)?.asRecord else { return } diff --git a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewModel+Diffable.swift b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewModel+Diffable.swift index 65ca80f7..e3e8c3df 100644 --- a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewModel+Diffable.swift +++ b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewModel+Diffable.swift @@ -27,9 +27,9 @@ extension DrawerSidebarViewModel { snapshot.appendSections([.main]) switch authenticationContext { case .twitter: - snapshot.appendItems([.likes, .lists], toSection: .main) + snapshot.appendItems([.likes, .history, .lists], toSection: .main) case .mastodon: - snapshot.appendItems([.local, .federated, .likes, .lists], toSection: .main) + snapshot.appendItems([.local, .federated, .likes, .history, .lists], toSection: .main) case .none: break } diff --git a/TwidereX/Scene/StubTimeline/StubTimelineCollectionViewCell.swift b/TwidereX/Scene/StubTimeline/StubTimelineCollectionViewCell.swift deleted file mode 100644 index 8536b012..00000000 --- a/TwidereX/Scene/StubTimeline/StubTimelineCollectionViewCell.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// StubTimelineCollectionViewCell.swift -// StubTimelineCollectionViewCell -// -// Created by Cirno MainasuK on 2021-8-2. -// Copyright © 2021 Twidere. All rights reserved. -// - -import UIKit - -final class StubTimelineCollectionViewCell: UICollectionViewCell { - - let primaryLabel = UILabel() - - override init(frame: CGRect) { - super.init(frame: frame) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension StubTimelineCollectionViewCell { - - private func _init() { - primaryLabel.translatesAutoresizingMaskIntoConstraints = false - addSubview(primaryLabel) - NSLayoutConstraint.activate([ - primaryLabel.topAnchor.constraint(equalTo: topAnchor), - primaryLabel.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), - primaryLabel.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), - primaryLabel.bottomAnchor.constraint(equalTo: bottomAnchor), - primaryLabel.heightAnchor.constraint(equalToConstant: 44).priority(.required - 1), - ]) - } - -} - -extension StubTimelineCollectionViewCell { - struct ViewModel: Hashable { - let title: String - } -} diff --git a/TwidereX/Scene/StubTimeline/StubTimelineViewController.swift b/TwidereX/Scene/StubTimeline/StubTimelineViewController.swift deleted file mode 100644 index b9129388..00000000 --- a/TwidereX/Scene/StubTimeline/StubTimelineViewController.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// StubTimelineViewController.swift -// TwidereX -// -// Created by Cirno MainasuK on 2021-8-2. -// Copyright © 2021 Twidere. All rights reserved. -// - -import UIKit -import Combine - -final class StubTimelineViewController: UIViewController { - - var disposeBag = Set() - - private(set) lazy var viewModel = StubTimelineViewModel() - - private(set) lazy var collectionView: UICollectionView = { - let layoutConfiguration = UICollectionLayoutListConfiguration(appearance: .plain) - let layout = UICollectionViewCompositionalLayout.list(using: layoutConfiguration) - let collectionView = ContentOffsetFixedCollectionView(frame: .zero, collectionViewLayout: layout) - return collectionView - }() - - private(set) lazy var refreshControl = UIRefreshControl() - - - -} - -extension StubTimelineViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .systemBackground - - collectionView.backgroundColor = .systemBackground - collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - collectionView.frame = view.bounds - view.addSubview(collectionView) - viewModel.setupDiffableDataSource(collectionView: collectionView) - - collectionView.refreshControl = refreshControl - refreshControl.addTarget(self, action: #selector(StubTimelineViewController.refreshControlValueDidChanged(_:)), for: .valueChanged) - viewModel.didLoadLatest - .receive(on: DispatchQueue.main) - .sink { [weak self] in - guard let self = self else { return } - self.refreshControl.endRefreshing() - } - .store(in: &disposeBag) - } - -} - -extension StubTimelineViewController { - @objc private func refreshControlValueDidChanged(_ sender: UIRefreshControl) { - Task(priority: .userInitiated) { - await self.viewModel.loadLatest() - } - } -} - diff --git a/TwidereX/Scene/StubTimeline/StubTimelineViewModel.swift b/TwidereX/Scene/StubTimeline/StubTimelineViewModel.swift deleted file mode 100644 index b0c791db..00000000 --- a/TwidereX/Scene/StubTimeline/StubTimelineViewModel.swift +++ /dev/null @@ -1,145 +0,0 @@ -// -// StubTimelineViewModel.swift -// StubTimelineViewModel -// -// Created by Cirno MainasuK on 2021-8-2. -// Copyright © 2021 Twidere. All rights reserved. -// - -import os.log -import UIKit -import Combine - -@MainActor -final class StubTimelineViewModel { - - let logger = Logger(subsystem: "StubTimelineViewModel", category: "Logic") - - // input - weak var collectionView: UICollectionView? - - // output - var diffableDataSource: UICollectionViewDiffableDataSource? - var didLoadLatest = PassthroughSubject() -} - -extension StubTimelineViewModel { - enum Section: Hashable { - case main - } - - enum Item: Hashable { - case stub(id: Int) - } - - func setupDiffableDataSource(collectionView: UICollectionView) { - self.collectionView = collectionView - diffableDataSource = StubTimelineViewModel.diffableDataSource(collectionView: collectionView) - - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.main]) - diffableDataSource?.apply(snapshot) - } - - static func diffableDataSource( - collectionView: UICollectionView - ) -> UICollectionViewDiffableDataSource { - let stubCellRegistration = UICollectionView.CellRegistration { cell, indexPath, viewModel in - cell.primaryLabel.text = viewModel.title - } - - return UICollectionViewDiffableDataSource( - collectionView: collectionView - ) { collectionView, indexPath, item in - switch item { - case .stub(let id): - let viewModel = StubTimelineCollectionViewCell.ViewModel(title: "\(id)") - return collectionView.dequeueConfiguredReusableCell(using: stubCellRegistration, for: indexPath, item: viewModel) - } - } - } - - struct Difference { - let item: T - let sourceIndexPath: IndexPath - let sourceDistanceToTop: CGFloat - let targetIndexPath: IndexPath - } - - private func calculateReloadSnapshotDifference( - collectionView: UICollectionView, - oldSnapshot: NSDiffableDataSourceSnapshot, - newSnapshot: NSDiffableDataSourceSnapshot - ) -> Difference? { - guard let sourceIndexPath = collectionView.indexPathsForVisibleItems.sorted().first else { return nil } - guard let layoutAttributes = collectionView.layoutAttributesForItem(at: sourceIndexPath) else { return nil } - - let sourceDistanceToTop = layoutAttributes.frame.origin.y - collectionView.bounds.origin.y - - guard sourceIndexPath.section < oldSnapshot.numberOfSections, - sourceIndexPath.row < oldSnapshot.numberOfItems(inSection: oldSnapshot.sectionIdentifiers[sourceIndexPath.section]) - else { return nil } - - let sectionIdentifier = oldSnapshot.sectionIdentifiers[sourceIndexPath.section] - let item = oldSnapshot.itemIdentifiers(inSection: sectionIdentifier)[sourceIndexPath.row] - - guard let targetIndexPathRow = newSnapshot.indexOfItem(item), - let newSectionIdentifier = newSnapshot.sectionIdentifier(containingItem: item), - let targetIndexPathSection = newSnapshot.indexOfSection(newSectionIdentifier) - else { return nil } - - let targetIndexPath = IndexPath(row: targetIndexPathRow, section: targetIndexPathSection) - - return Difference( - item: item, - sourceIndexPath: sourceIndexPath, - sourceDistanceToTop: sourceDistanceToTop, - targetIndexPath: targetIndexPath - ) - } - - func loadLatest() async { - guard let collectionView = self.collectionView else { return } - guard let diffableDataSource = self.diffableDataSource else { return } - let oldSnapshot = diffableDataSource.snapshot() - let count = oldSnapshot.numberOfItems - let start = count + 1 - - var items: [Item] = oldSnapshot.itemIdentifiers - let newItems = (start.. = { - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.main]) - snapshot.appendItems(items, toSection: .main) - return snapshot - }() - - await Task.sleep(2_000_000_000) // 2s - - let difference = calculateReloadSnapshotDifference(collectionView: collectionView, oldSnapshot: oldSnapshot, newSnapshot: newSnapshot) - - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - let animatingDifferences = difference == nil - diffableDataSource.apply(newSnapshot, animatingDifferences: animatingDifferences) { - self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): applied new snapshot") - defer { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.33) { [weak self] in - self?.didLoadLatest.send() - } - } - guard let difference = difference else { return } - guard let layoutAttributes = collectionView.layoutAttributesForItem(at: difference.targetIndexPath) else { return } - let targetDistanceToTop = layoutAttributes.frame.origin.y - collectionView.bounds.origin.y - let offset = targetDistanceToTop - difference.sourceDistanceToTop - var contentOffset = collectionView.contentOffset - contentOffset.y += offset - collectionView.setContentOffset(contentOffset, animated: false) - } - } - } - -} diff --git a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift index d225ec01..fc59b6a8 100644 --- a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift @@ -237,7 +237,8 @@ extension ListTimelineViewController { // MARK: - UITableViewDelegate extension ListTimelineViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { - // sourcery:inline:ListTimelineView + // sourcery:inline:ListTimelineViewController.AutoGenerateTableViewDelegate + // Generated using Sourcery // DO NOT EDIT func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift index d4a09fce..9306e40b 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift @@ -100,10 +100,6 @@ extension HomeTimelineViewController { guard let self = self else { return } self.showAccountListAction(action) }), - UIAction(title: "Stub Timeline", image: nil, attributes: [], handler: { [weak self] action in - guard let self = self else { return } - self.showStubTimelineAction(action) - }), UIAction(title: "Local Timeline", image: nil, attributes: [], handler: { [weak self] action in guard let self = self else { return } self.showLocalTimelineAction(action) @@ -312,10 +308,6 @@ extension HomeTimelineViewController { ) } - @objc private func showStubTimelineAction(_ sender: UIAction) { - coordinator.present(scene: .stubTimeline, from: self, transition: .show) - } - @objc private func showLocalTimelineAction(_ sender: UIAction) { let federatedTimelineViewModel = FederatedTimelineViewModel(context: context, isLocal: true) coordinator.present(scene: .federatedTimeline(viewModel: federatedTimelineViewModel), from: self, transition: .show) diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController.swift index 605df93f..6665a7dd 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController.swift @@ -112,18 +112,21 @@ extension HomeTimelineViewController { unreadIndicatorView.startDisplayLink() - DispatchQueue.once { - guard let authenticationContext = self.context.authenticationService.activeAuthenticationContext else { return } - let settingListViewModel = SettingListViewModel( - context: self.context, - auth: .init(authenticationContext: authenticationContext) - ) - self.coordinator.present( - scene: .setting(viewModel: settingListViewModel), - from: self, - transition: .modal(animated: true) - ) - } +// #if DEBUG +// DispatchQueue.once { +// guard let authenticationContext = self.context.authenticationService.activeAuthenticationContext else { return } +// let historyViewModel = HistoryViewModel( +// context: self.context, +// coordinator: coordinator, +// authContext: .init(authenticationContext: authenticationContext) +// ) +// self.coordinator.present( +// scene: .history(viewModel: historyViewModel), +// from: self, +// transition: .show +// ) +// } +// #endif } override func viewDidDisappear(_ animated: Bool) { diff --git a/TwidereX/Supporting Files/AppDelegate.swift b/TwidereX/Supporting Files/AppDelegate.swift index 8ef813a9..a8dd65cd 100644 --- a/TwidereX/Supporting Files/AppDelegate.swift +++ b/TwidereX/Supporting Files/AppDelegate.swift @@ -12,6 +12,7 @@ import Combine import Floaty import Firebase import FirebaseMessaging +import FirebaseCrashlytics import Kingfisher import AppShared import TwidereCommon @@ -39,6 +40,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { FirebaseApp.configure() Crashlytics.crashlytics().setCustomValue(Locale.preferredLanguages.first ?? "nil", forKey: "preferredLanguage") Messaging.messaging().delegate = self + #if DEBUG + Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(false) + #endif // configure AudioSession try? AVAudioSession.sharedInstance().setCategory(.ambient) From 85eb45fa2a002008424b5498087b660111a4f1c2 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 29 Jul 2022 19:11:51 +0800 Subject: [PATCH 006/128] chore: update version to 1.4.3 (118) --- NotificationService/Info.plist | 4 +-- ShareExtension/Info.plist | 4 +-- TwidereX.xcodeproj/project.pbxproj | 48 +++++++++++++++--------------- TwidereX/Info.plist | 4 +-- TwidereXIntent/Info.plist | 4 +-- TwidereXTests/Info.plist | 4 +-- TwidereXUITests/Info.plist | 4 +-- 7 files changed, 36 insertions(+), 36 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 8fea3554..8c410563 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -3,7 +3,7 @@ CFBundleShortVersionString - 1.4.2 + 1.4.3 CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 117 + 118 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index c02f1cde..301b7e36 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.2 + 1.4.3 CFBundleVersion - 117 + 118 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 04667cd8..210dc016 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3610,7 +3610,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 117; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3636,7 +3636,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 117; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3659,7 +3659,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 117; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3685,11 +3685,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 117; + CURRENT_PROJECT_VERSION = 118; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 117; + DYLIB_CURRENT_VERSION = 118; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3717,7 +3717,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 117; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3744,7 +3744,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 117; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; @@ -3774,7 +3774,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 117; + CURRENT_PROJECT_VERSION = 118; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; @@ -3804,7 +3804,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 117; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3833,7 +3833,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 117; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3862,11 +3862,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 117; + CURRENT_PROJECT_VERSION = 118; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 117; + DYLIB_CURRENT_VERSION = 118; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3896,11 +3896,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 117; + CURRENT_PROJECT_VERSION = 118; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 117; + DYLIB_CURRENT_VERSION = 118; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3927,7 +3927,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 117; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3954,7 +3954,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 117; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -4108,7 +4108,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 117; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -4139,7 +4139,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 117; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -4165,7 +4165,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 117; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4190,7 +4190,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 117; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4213,7 +4213,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 117; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4236,7 +4236,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 117; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4260,7 +4260,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 117; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; @@ -4288,7 +4288,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 117; + CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index e8195054..ea111c39 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -38,9 +38,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.2 + 1.4.3 CFBundleVersion - 117 + 118 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index 74bdbc2e..f55d373c 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.2 + 1.4.3 CFBundleVersion - 117 + 118 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index 2ee4462a..bbc26090 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.2 + 1.4.3 CFBundleVersion - 117 + 118 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index 2ee4462a..bbc26090 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.2 + 1.4.3 CFBundleVersion - 117 + 118 From bba8bb4e2c6098b0a98f7d327f2ffb0ee8db6d06 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 2 Aug 2022 10:27:55 +0800 Subject: [PATCH 007/128] feat: add user history record --- .../CoreDataStack/Entity/App/History.swift | 2 +- .../Extension/CoreDataStack/History.swift | 11 +++ .../HistoryFetchedResultsController.swift} | 10 +- .../TwidereCore/Model/User/UserObject.swift | 11 +++ TwidereX.xcodeproj/project.pbxproj | 8 ++ .../Misc/History/HistorySection.swift | 75 ++++++++++++--- ...oGenerateTableViewDelegate.generated.swift | 1 + .../Provider/DataSourceFacade+History.swift | 40 ++++++++ .../Provider/DataSourceFacade+Profile.swift | 7 ++ .../Scene/History/HistoryViewController.swift | 19 +++- .../Status/StatusHistoryViewController.swift | 1 + .../StatusHistoryViewModel+Diffable.swift | 7 +- .../Status/StatusHistoryViewModel.swift | 6 +- ...oryViewController+DataSourceProvider.swift | 40 ++++++++ .../User/UserHistoryViewController.swift | 94 +++++++++++++++++++ .../User/UserHistoryViewModel+Diffable.swift | 59 ++++++++++++ .../History/User/UserHistoryViewModel.swift | 9 +- 17 files changed, 367 insertions(+), 33 deletions(-) rename TwidereSDK/Sources/TwidereCore/FetchedResultsController/{Status/StatusHistoryFetchedResultsController.swift => History/HistoryFetchedResultsController.swift} (89%) create mode 100644 TwidereX/Scene/History/User/UserHistoryViewController+DataSourceProvider.swift create mode 100644 TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/App/History.swift b/TwidereSDK/Sources/CoreDataStack/Entity/App/History.swift index cfe90715..5ffe0898 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/App/History.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/App/History.swift @@ -104,7 +104,7 @@ extension History { return NSPredicate(format: "%K != nil", #keyPath(History.mastodonUser)) } - static func predicate(acct: Acct) -> NSPredicate { + public static func predicate(acct: Acct) -> NSPredicate { return NSPredicate(format: "%K == %@", #keyPath(History.acctRaw), acct.rawValue) } diff --git a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/History.swift b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/History.swift index aff72825..e0da785e 100644 --- a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/History.swift +++ b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/History.swift @@ -20,5 +20,16 @@ extension History { return nil } + + public var userObject: UserObject? { + if let user = twitterUser { + return .twitter(object: user) + } + if let user = mastodonUser { + return .mastodon(object: user) + } + + return nil + } } diff --git a/TwidereSDK/Sources/TwidereCore/FetchedResultsController/Status/StatusHistoryFetchedResultsController.swift b/TwidereSDK/Sources/TwidereCore/FetchedResultsController/History/HistoryFetchedResultsController.swift similarity index 89% rename from TwidereSDK/Sources/TwidereCore/FetchedResultsController/Status/StatusHistoryFetchedResultsController.swift rename to TwidereSDK/Sources/TwidereCore/FetchedResultsController/History/HistoryFetchedResultsController.swift index a7f1a853..4bcab913 100644 --- a/TwidereSDK/Sources/TwidereCore/FetchedResultsController/Status/StatusHistoryFetchedResultsController.swift +++ b/TwidereSDK/Sources/TwidereCore/FetchedResultsController/History/HistoryFetchedResultsController.swift @@ -1,5 +1,5 @@ // -// StatusHistoryFetchedResultsController.swift +// HistoryFetchedResultsController.swift // // // Created by MainasuK on 2022-7-29. @@ -14,7 +14,7 @@ import CoreDataStack import TwitterSDK import OrderedCollections -final public class StatusHistoryFetchedResultsController: NSObject { +final public class HistoryFetchedResultsController: NSObject { public let logger = Logger(subsystem: "StatusHistoryFetchedResultsController", category: "DB") @@ -32,7 +32,7 @@ final public class StatusHistoryFetchedResultsController: NSObject { self.fetchedResultsController = { let fetchRequest = History.sortedFetchRequest // make sure initial query return empty results - fetchRequest.predicate = History.statusPredicate(acct: .none) + fetchRequest.predicate = History.predicate(acct: .none) fetchRequest.returnsObjectsAsFaults = false fetchRequest.shouldRefreshRefetchedObjects = true fetchRequest.fetchBatchSize = 15 @@ -45,7 +45,7 @@ final public class StatusHistoryFetchedResultsController: NSObject { return controller }() - self.predicate = History.statusPredicate(acct: .none) + self.predicate = History.predicate(acct: .none) super.init() fetchedResultsController.delegate = self @@ -71,7 +71,7 @@ final public class StatusHistoryFetchedResultsController: NSObject { } // MARK: - NSFetchedResultsControllerDelegate -extension StatusHistoryFetchedResultsController: NSFetchedResultsControllerDelegate { +extension HistoryFetchedResultsController: NSFetchedResultsControllerDelegate { public func controller( _ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference diff --git a/TwidereSDK/Sources/TwidereCore/Model/User/UserObject.swift b/TwidereSDK/Sources/TwidereCore/Model/User/UserObject.swift index c72b48a1..28c1376b 100644 --- a/TwidereSDK/Sources/TwidereCore/Model/User/UserObject.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/User/UserObject.swift @@ -87,3 +87,14 @@ extension UserObject { } } } + +extension UserObject { + public var histories: Set { + switch self { + case .twitter(let user): + return user.histories + case .mastodon(let user): + return user.histories + } + } +} diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 210dc016..eff430e0 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -256,6 +256,8 @@ DB92570A251C8FE0004FEFB5 /* ProfileHeaderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB925709251C8FE0004FEFB5 /* ProfileHeaderViewController.swift */; }; DB925744251C9CD6004FEFB5 /* ProfilePagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB925743251C9CD6004FEFB5 /* ProfilePagingViewController.swift */; }; DB925749251C9D48004FEFB5 /* ProfilePagingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB925748251C9D47004FEFB5 /* ProfilePagingViewModel.swift */; }; + DB92DB3F2898026B0011B564 /* UserHistoryViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB92DB3E2898026B0011B564 /* UserHistoryViewController+DataSourceProvider.swift */; }; + DB92DB41289804440011B564 /* UserHistoryViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB92DB40289804440011B564 /* UserHistoryViewModel+Diffable.swift */; }; DB932E5227FEC7390036A824 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = DB932E5527FEC7390036A824 /* Intents.intentdefinition */; settings = {ATTRIBUTES = (codegen, ); }; }; DB932E5327FEC7390036A824 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = DB932E5527FEC7390036A824 /* Intents.intentdefinition */; }; DB94B6B426C65BE100A2E8A1 /* MastodonAuthenticationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB94B6B326C65BE100A2E8A1 /* MastodonAuthenticationController.swift */; }; @@ -763,6 +765,8 @@ DB925709251C8FE0004FEFB5 /* ProfileHeaderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderViewController.swift; sourceTree = ""; }; DB925743251C9CD6004FEFB5 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = ""; }; DB925748251C9D47004FEFB5 /* ProfilePagingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewModel.swift; sourceTree = ""; }; + DB92DB3E2898026B0011B564 /* UserHistoryViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserHistoryViewController+DataSourceProvider.swift"; sourceTree = ""; }; + DB92DB40289804440011B564 /* UserHistoryViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserHistoryViewModel+Diffable.swift"; sourceTree = ""; }; DB932E5427FEC7390036A824 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/Intents.intentdefinition; sourceTree = ""; }; DB932E5727FEC73B0036A824 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Intents.strings; sourceTree = ""; }; DB932E5927FEC73C0036A824 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Intents.strings; sourceTree = ""; }; @@ -2451,7 +2455,9 @@ isa = PBXGroup; children = ( DBFCEF1128939C9600EEBFB1 /* UserHistoryViewController.swift */, + DB92DB3E2898026B0011B564 /* UserHistoryViewController+DataSourceProvider.swift */, DBFCEF1428939CAC00EEBFB1 /* UserHistoryViewModel.swift */, + DB92DB40289804440011B564 /* UserHistoryViewModel+Diffable.swift */, ); path = User; sourceTree = ""; @@ -3221,6 +3227,7 @@ DB71C7D1271EB09A00BE3819 /* DataSourceFacade+Friendship.swift in Sources */, DB442473285B177B0095AECF /* SearchMediaTimelineViewModel.swift in Sources */, DB1D3DEA289388CD008F0BD0 /* HistoryViewController.swift in Sources */, + DB92DB3F2898026B0011B564 /* UserHistoryViewController+DataSourceProvider.swift in Sources */, DBCE2E912591A06300926D09 /* UINavigationController.swift in Sources */, DB915406254FC8CE00613473 /* FollowActionButton.swift in Sources */, DB8761C0274552F800BA7EE2 /* StatusSection.swift in Sources */, @@ -3291,6 +3298,7 @@ DB2D36F927D5E74D00C1FBE0 /* CompositeListViewModel.swift in Sources */, DBB47DAB26EB3BF8001590F7 /* ProfileFieldContentView.swift in Sources */, DBBF70F726D7B62F00971656 /* AccountListTableViewCell+ViewModel.swift in Sources */, + DB92DB41289804440011B564 /* UserHistoryViewModel+Diffable.swift in Sources */, DBB47DA926EB3AA2001590F7 /* ProfileFieldListView.swift in Sources */, DBF69EE12549705A00E2A915 /* ViewControllerAnimatedTransitioningDelegate.swift in Sources */, DB76A658275F498F00A50673 /* MediaPreviewImageViewModel.swift in Sources */, diff --git a/TwidereX/Diffable/Misc/History/HistorySection.swift b/TwidereX/Diffable/Misc/History/HistorySection.swift index 7523c175..29bba199 100644 --- a/TwidereX/Diffable/Misc/History/HistorySection.swift +++ b/TwidereX/Diffable/Misc/History/HistorySection.swift @@ -27,6 +27,9 @@ extension HistorySection { struct Configuration { weak var statusViewTableViewCellDelegate: StatusViewTableViewCellDelegate? let statusViewConfigurationContext: StatusView.ConfigurationContext + + weak var userViewTableViewCellDelegate: UserViewTableViewCellDelegate? + let userViewConfigurationContext: UserView.ConfigurationContext } static func diffableDataSource( @@ -35,27 +38,54 @@ extension HistorySection { configuration: Configuration ) -> UITableViewDiffableDataSource { tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self)) - + tableView.register(UserRelationshipStyleTableViewCell.self, forCellReuseIdentifier: String(describing: UserRelationshipStyleTableViewCell.self)) + let diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in // data source should dispatch in main thread assert(Thread.isMainThread) switch item { case .history(let record): - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell - StatusSection.setupStatusPollDataSource( - context: context, - statusView: cell.statusView, - configurationContext: configuration.statusViewConfigurationContext - ) - context.managedObjectContext.performAndWait { - guard let status = record.object(in: context.managedObjectContext)?.statusObject else { return } - configure( - tableView: tableView, - cell: cell, - viewModel: StatusTableViewCell.ViewModel(value: .statusObject(status)), - configuration: configuration - ) + let cell: UITableViewCell = context.managedObjectContext.performAndWait { + guard let history = record.object(in: context.managedObjectContext) else { + assertionFailure() + return UITableViewCell() + } + // status + if let status = history.statusObject { + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell + StatusSection.setupStatusPollDataSource( + context: context, + statusView: cell.statusView, + configurationContext: configuration.statusViewConfigurationContext + ) + configure( + tableView: tableView, + cell: cell, + viewModel: StatusTableViewCell.ViewModel(value: .statusObject(status)), + configuration: configuration + ) + return cell + } + // user + if let user = history.userObject { + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserRelationshipStyleTableViewCell.self), for: indexPath) as! UserRelationshipStyleTableViewCell + let authenticationContext = context.authenticationService.activeAuthenticationContext + let me = authenticationContext?.user(in: context.managedObjectContext) + let viewModel = UserTableViewCell.ViewModel( + user: user, + me: me, + notification: nil + ) + configure( + cell: cell, + viewModel: viewModel, + configuration: configuration + ) + return cell + } + + return UITableViewCell() } return cell } @@ -86,4 +116,19 @@ extension HistorySection { ) } + static func configure( + cell: UserTableViewCell, + viewModel: UserTableViewCell.ViewModel, + configuration: Configuration + ) { + UserSection.configure( + cell: cell, + viewModel: viewModel, + configuration: .init( + userViewTableViewCellDelegate: configuration.userViewTableViewCellDelegate, + userViewConfigurationContext: configuration.userViewConfigurationContext + ) + ) + } + } diff --git a/TwidereX/Generated/AutoGenerateTableViewDelegate.generated.swift b/TwidereX/Generated/AutoGenerateTableViewDelegate.generated.swift index ec0ecae9..30985739 100644 --- a/TwidereX/Generated/AutoGenerateTableViewDelegate.generated.swift +++ b/TwidereX/Generated/AutoGenerateTableViewDelegate.generated.swift @@ -129,6 +129,7 @@ func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith con + // sourcery:inline:UserTimelineViewController.AutoGenerateTableViewDelegate // Generated using Sourcery diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+History.swift b/TwidereX/Protocol/Provider/DataSourceFacade+History.swift index 0bb4a07f..15589a17 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+History.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+History.swift @@ -53,4 +53,44 @@ extension DataSourceFacade { } } // end func + static func recordUserHistory( + denpendency: NeedsDependency, + user: UserRecord + ) async { + let now = Date() + guard let authenticationContext = denpendency.context.authenticationService.activeAuthenticationContext else { return } + + let acct = authenticationContext.acct + let managedObjectContext = denpendency.context.backgroundManagedObjectContext + let _history: ManagedObjectRecord? = await managedObjectContext.perform { + guard let user = user.object(in: managedObjectContext) else { return nil } + guard let history = user.histories.first(where: { $0.acct == acct }) else { return nil } + return history.asRecrod + } + + if let history = _history { + try? await managedObjectContext.performChanges { + guard let history = history.object(in: managedObjectContext) else { return } + history.update(timestamp: now) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): update user history for: \(history.debugDescription)") + } + } else { + try? await managedObjectContext.performChanges { + guard let user = user.object(in: managedObjectContext) else { return } + let history = History.insert( + into: managedObjectContext, + property: .init(acct: acct, timestamp: now, createdAt: now) + ) + switch user { + case .twitter(let object): + history.update(twitterUser: object) + case .mastodon(let object): + history.update(mastodonUser: object) + } + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): create user history: \(history.debugDescription)") + + } + } + } + } diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Profile.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Profile.swift index e633e8b6..02f4fdfb 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Profile.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Profile.swift @@ -46,6 +46,13 @@ extension DataSourceFacade { from: provider, transition: .show ) + + Task { + await recordUserHistory( + denpendency: provider, + user: user + ) + } // end Task } } diff --git a/TwidereX/Scene/History/HistoryViewController.swift b/TwidereX/Scene/History/HistoryViewController.swift index b3043e50..2830a556 100644 --- a/TwidereX/Scene/History/HistoryViewController.swift +++ b/TwidereX/Scene/History/HistoryViewController.swift @@ -56,12 +56,10 @@ extension HistoryViewController { view.backgroundColor = .systemBackground - // TODO: - // setupSegmentedControl(scopes: viewModel.scopes) - // navigationItem.titleView = pageSegmentedControl + setupSegmentedControl(scopes: viewModel.scopes) + navigationItem.titleView = pageSegmentedControl + pageSegmentedControl.addTarget(self, action: #selector(HistoryViewController.pageSegmentedControlValueChanged(_:)), for: .valueChanged) - title = "Hisotry" - dataSource = viewModel } @@ -89,3 +87,14 @@ extension HistoryViewController { ]) } } + +extension HistoryViewController { + + @objc private func pageSegmentedControlValueChanged(_ sender: UISegmentedControl) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + + let index = sender.selectedSegmentIndex + scrollToPage(.at(index: index), animated: true, completion: nil) + } + +} diff --git a/TwidereX/Scene/History/Status/StatusHistoryViewController.swift b/TwidereX/Scene/History/Status/StatusHistoryViewController.swift index e35ec49d..b86e34fc 100644 --- a/TwidereX/Scene/History/Status/StatusHistoryViewController.swift +++ b/TwidereX/Scene/History/Status/StatusHistoryViewController.swift @@ -120,6 +120,7 @@ extension StatusHistoryViewController: UITableViewDelegate, AutoGenerateTableVie return container } } + } // MARK: - StatusViewTableViewCellDelegate diff --git a/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift b/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift index 0b3a8961..d5b8a627 100644 --- a/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift +++ b/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift @@ -25,6 +25,11 @@ extension StatusHistoryViewModel { dateTimeProvider: DateTimeSwiftProvider(), twitterTextProvider: OfficialTwitterTextProvider(), authenticationContext: context.authenticationService.$activeAuthenticationContext + ), + userViewTableViewCellDelegate: nil, + userViewConfigurationContext: .init( + listMembershipViewModel: nil, + authenticationContext: context.authenticationService.activeAuthenticationContext ) ) ) @@ -32,7 +37,7 @@ extension StatusHistoryViewModel { let snapshot = NSDiffableDataSourceSnapshot() diffableDataSource?.apply(snapshot) - statusHistoryFetchedResultsController.$groupedRecords + historyFetchedResultsController.$groupedRecords .receive(on: DispatchQueue.main) .sink { [weak self] groupedRecords in guard let self = self else { return } diff --git a/TwidereX/Scene/History/Status/StatusHistoryViewModel.swift b/TwidereX/Scene/History/Status/StatusHistoryViewModel.swift index f255fdbf..7790afb6 100644 --- a/TwidereX/Scene/History/Status/StatusHistoryViewModel.swift +++ b/TwidereX/Scene/History/Status/StatusHistoryViewModel.swift @@ -23,7 +23,7 @@ final class StatusHistoryViewModel { // input let context: AppContext let authContext: AuthContext - let statusHistoryFetchedResultsController: StatusHistoryFetchedResultsController + let historyFetchedResultsController: HistoryFetchedResultsController // output var diffableDataSource: UITableViewDiffableDataSource? @@ -34,10 +34,10 @@ final class StatusHistoryViewModel { ) { self.context = context self.authContext = authContext - self.statusHistoryFetchedResultsController = StatusHistoryFetchedResultsController(managedObjectContext: context.managedObjectContext) + self.historyFetchedResultsController = HistoryFetchedResultsController(managedObjectContext: context.managedObjectContext) // end init - statusHistoryFetchedResultsController.predicate = History.statusPredicate(acct: authContext.authenticationContext.acct) + historyFetchedResultsController.predicate = History.statusPredicate(acct: authContext.authenticationContext.acct) } deinit { diff --git a/TwidereX/Scene/History/User/UserHistoryViewController+DataSourceProvider.swift b/TwidereX/Scene/History/User/UserHistoryViewController+DataSourceProvider.swift new file mode 100644 index 00000000..510833cf --- /dev/null +++ b/TwidereX/Scene/History/User/UserHistoryViewController+DataSourceProvider.swift @@ -0,0 +1,40 @@ +// +// UserHistoryViewController+DataSourceProvider.swift +// TwidereX +// +// Created by MainasuK on 2022-8-1. +// Copyright © 2022 Twidere. All rights reserved. +// + +import UIKit + +extension UserHistoryViewController: DataSourceProvider { + func item(from source: DataSourceItem.Source) async -> DataSourceItem? { + var _indexPath = source.indexPath + if _indexPath == nil, let cell = source.tableViewCell { + _indexPath = await self.indexPath(for: cell) + } + guard let indexPath = _indexPath else { return nil } + + guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else { + return nil + } + + switch item { + case .history(let record): + let managedObjectContext = context.managedObjectContext + let item: DataSourceItem? = await managedObjectContext.perform { + guard let history = record.object(in: managedObjectContext) else { return nil } + guard let user = history.userObject else { return nil } + return .user(user.asRecord) + + } + return item + } + } + + @MainActor + private func indexPath(for cell: UITableViewCell) async -> IndexPath? { + return tableView.indexPath(for: cell) + } +} diff --git a/TwidereX/Scene/History/User/UserHistoryViewController.swift b/TwidereX/Scene/History/User/UserHistoryViewController.swift index c9b6f967..f3293c6d 100644 --- a/TwidereX/Scene/History/User/UserHistoryViewController.swift +++ b/TwidereX/Scene/History/User/UserHistoryViewController.swift @@ -9,6 +9,7 @@ import os.log import UIKit import Combine +import CoreDataStack import TwidereUI final class UserHistoryViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { @@ -28,7 +29,100 @@ final class UserHistoryViewController: UIViewController, NeedsDependency, MediaP tableView.backgroundColor = .systemBackground tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .none + tableView.sectionHeaderTopPadding = .zero return tableView }() } + +extension UserHistoryViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .systemBackground + + tableView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(tableView) + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + tableView.delegate = self + viewModel.setupDiffableDataSource( + tableView: tableView, + userViewTableViewCellDelegate: self + ) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + tableView.deselectRow(with: transitionCoordinator, animated: animated) + } + +} + +// MARK: - UITableViewDelegate +extension UserHistoryViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { + // sourcery:inline:UserHistoryViewController.AutoGenerateTableViewDelegate + + // Generated using Sourcery + // DO NOT EDIT + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + aspectTableView(tableView, didSelectRowAt: indexPath) + } + + func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { + return aspectTableView(tableView, contextMenuConfigurationForRowAt: indexPath, point: point) + } + + func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { + return aspectTableView(tableView, previewForHighlightingContextMenuWithConfiguration: configuration) + } + + func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { + return aspectTableView(tableView, previewForDismissingContextMenuWithConfiguration: configuration) + } + + func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { + aspectTableView(tableView, willPerformPreviewActionForMenuWith: configuration, animator: animator) + } + + // sourcery:end + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + guard let diffableDataSource = viewModel.diffableDataSource else { return nil } + guard let sectionIdentifier = diffableDataSource.sectionIdentifier(for: section) else { return nil } + switch sectionIdentifier { + case .group(let identifer): + guard let date = History.date(from: identifer) else { return nil } + let formatter = DateFormatter() + formatter.dateStyle = .long + formatter.timeStyle = .none + + let title = formatter.string(from: date) + let header = TableViewSectionTextHeaderView() + header.label.text = title + + let container = UIView() + container.backgroundColor = .secondarySystemBackground + header.translatesAutoresizingMaskIntoConstraints = false + container.addSubview(header) + NSLayoutConstraint.activate([ + header.topAnchor.constraint(equalTo: container.topAnchor), + header.leadingAnchor.constraint(equalTo: container.readableContentGuide.leadingAnchor), + header.trailingAnchor.constraint(equalTo: container.readableContentGuide.trailingAnchor), + header.bottomAnchor.constraint(equalTo: container.bottomAnchor), + ]) + return container + } + } + +} + +// MARK: - UserViewTableViewCellDelegate +extension UserHistoryViewController: UserViewTableViewCellDelegate { } diff --git a/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift b/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift new file mode 100644 index 00000000..655d0e22 --- /dev/null +++ b/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift @@ -0,0 +1,59 @@ +// +// UserHistoryViewModel+Diffable.swift +// TwidereX +// +// Created by MainasuK on 2022-8-1. +// Copyright © 2022 Twidere. All rights reserved. +// + +import UIKit +import Combine +import AppShared + +extension UserHistoryViewModel { + + func setupDiffableDataSource( + tableView: UITableView, + userViewTableViewCellDelegate: UserViewTableViewCellDelegate + ) { + diffableDataSource = HistorySection.diffableDataSource( + tableView: tableView, + context: context, + configuration: .init( + statusViewTableViewCellDelegate: nil, + statusViewConfigurationContext: .init( + dateTimeProvider: DateTimeSwiftProvider(), + twitterTextProvider: OfficialTwitterTextProvider(), + authenticationContext: context.authenticationService.$activeAuthenticationContext + ), + userViewTableViewCellDelegate: userViewTableViewCellDelegate, + userViewConfigurationContext: .init( + listMembershipViewModel: nil, + authenticationContext: context.authenticationService.activeAuthenticationContext + ) + ) + ) + + let snapshot = NSDiffableDataSourceSnapshot() + diffableDataSource?.apply(snapshot) + + historyFetchedResultsController.$groupedRecords + .receive(on: DispatchQueue.main) + .sink { [weak self] groupedRecords in + guard let self = self else { return } + guard let diffableDataSource = self.diffableDataSource else { return } + + var snapshot = NSDiffableDataSourceSnapshot() + for (identifier, records) in groupedRecords { + let section = HistorySection.group(identifer: identifier) + snapshot.appendSections([section]) + let items: [HistoryItem] = records.map { .history(record: $0) } + snapshot.appendItems(items, toSection: section) + } + + diffableDataSource.apply(snapshot, animatingDifferences: false) + } + .store(in: &disposeBag) + } + +} diff --git a/TwidereX/Scene/History/User/UserHistoryViewModel.swift b/TwidereX/Scene/History/User/UserHistoryViewModel.swift index e7231f0a..e186b0c6 100644 --- a/TwidereX/Scene/History/User/UserHistoryViewModel.swift +++ b/TwidereX/Scene/History/User/UserHistoryViewModel.swift @@ -23,10 +23,10 @@ final class UserHistoryViewModel { // input let context: AppContext let authContext: AuthContext - + let historyFetchedResultsController: HistoryFetchedResultsController + // output - // var diffableDataSource: UITableViewDiffableDataSource? - // var didLoadLatest = PassthroughSubject() + var diffableDataSource: UITableViewDiffableDataSource? init( context: AppContext, @@ -34,7 +34,10 @@ final class UserHistoryViewModel { ) { self.context = context self.authContext = authContext + self.historyFetchedResultsController = HistoryFetchedResultsController(managedObjectContext: context.managedObjectContext) // end init + + historyFetchedResultsController.predicate = History.userPredicate(acct: authContext.authenticationContext.acct) } deinit { From 8145f674e7771401872f6bfa72bd98318aacb714 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 2 Aug 2022 15:43:32 +0800 Subject: [PATCH 008/128] feat: add History entry for iPad sidebar --- .../TwidereCore/State/AuthContext.swift | 12 ++++ TwidereX/Coordinator/SceneCoordinator.swift | 65 +++++++++++-------- .../Diffable/Misc/TabBar/TabBarItem.swift | 13 +++- .../Provider/DataSourceFacade+User.swift | 1 - .../Root/ContentSplitViewController.swift | 27 ++++++-- .../Root/MainTab/MainTabBarController.swift | 10 ++- .../SecondaryTabBarController.swift | 7 +- .../Scene/Root/Sidebar/SidebarViewModel.swift | 4 +- .../Base/Common/TimelineViewModel.swift | 1 + TwidereX/Supporting Files/SceneDelegate.swift | 1 - 10 files changed, 99 insertions(+), 42 deletions(-) diff --git a/TwidereSDK/Sources/TwidereCore/State/AuthContext.swift b/TwidereSDK/Sources/TwidereCore/State/AuthContext.swift index f985704c..dbb3f588 100644 --- a/TwidereSDK/Sources/TwidereCore/State/AuthContext.swift +++ b/TwidereSDK/Sources/TwidereCore/State/AuthContext.swift @@ -8,6 +8,7 @@ import Foundation import CoreData import CoreDataStack +import TwidereCommon public class AuthContext { @@ -17,4 +18,15 @@ public class AuthContext { self.authenticationContext = authenticationContext } + public convenience init?(authenticationIndex: AuthenticationIndex) { + let _authenticationContext = AuthenticationContext( + authenticationIndex: authenticationIndex, + secret: AppSecret.default.secret + ) + guard let authenticationContext = _authenticationContext else { + return nil + } + self.init(authenticationContext: authenticationContext) + } + } diff --git a/TwidereX/Coordinator/SceneCoordinator.swift b/TwidereX/Coordinator/SceneCoordinator.swift index ca0133b3..e2f15a32 100644 --- a/TwidereX/Coordinator/SceneCoordinator.swift +++ b/TwidereX/Coordinator/SceneCoordinator.swift @@ -117,44 +117,53 @@ extension SceneCoordinator { extension SceneCoordinator { + @MainActor func setup() { let rootViewController: UIViewController - switch UIDevice.current.userInterfaceIdiom { - case .phone: - let viewController = MainTabBarController(context: context, coordinator: self) - rootViewController = viewController - needsSetupAvatarBarButtonItem = true - default: - let contentSplitViewController = ContentSplitViewController() - contentSplitViewController.context = context - contentSplitViewController.coordinator = self - rootViewController = contentSplitViewController - contentSplitViewController.$isSidebarDisplay - .sink { [weak self] isSidebarDisplay in - guard let self = self else { return } - self.needsSetupAvatarBarButtonItem = !isSidebarDisplay - } - .store(in: &contentSplitViewController.disposeBag) - } - sceneDelegate.window?.rootViewController = rootViewController - } - - func setupWelcomeIfNeeds() { do { let request = AuthenticationIndex.sortedFetchRequest - let count = try context.managedObjectContext.count(for: request) - if count == 0 { - DispatchQueue.main.async { - let configuration = WelcomeViewModel.Configuration(allowDismissModal: false) - let welcomeViewModel = WelcomeViewModel(context: self.context, configuration: configuration) - self.present(scene: .welcome(viewModel: welcomeViewModel), from: nil, transition: .modal(animated: false, completion: nil)) - } + request.fetchLimit = 1 + let _authenticationIndex = try context.managedObjectContext.fetch(request).first + guard let authenticationIndex = _authenticationIndex, + let authContext = AuthContext(authenticationIndex: authenticationIndex) + else { + let configuration = WelcomeViewModel.Configuration(allowDismissModal: false) + let welcomeViewModel = WelcomeViewModel(context: context, configuration: configuration) + let welcomeViewController = WelcomeViewController() + welcomeViewController.viewModel = welcomeViewModel + welcomeViewController.context = context + welcomeViewController.coordinator = self + rootViewController = welcomeViewController + sceneDelegate.window?.rootViewController = rootViewController // entry #1: Welcome + return + } + + switch UIDevice.current.userInterfaceIdiom { + case .phone: + let viewController = MainTabBarController(context: context, coordinator: self, authContext: authContext) + rootViewController = viewController + needsSetupAvatarBarButtonItem = true + default: + let contentSplitViewController = ContentSplitViewController(context: context, coordinator: self, authContext: authContext) + rootViewController = contentSplitViewController + contentSplitViewController.$isSidebarDisplay + .sink { [weak self] isSidebarDisplay in + guard let self = self else { return } + self.needsSetupAvatarBarButtonItem = !isSidebarDisplay + } + .store(in: &contentSplitViewController.disposeBag) } + sceneDelegate.window?.rootViewController = rootViewController // entry #2: main app } catch { assertionFailure(error.localizedDescription) + Task { + try? await Task.sleep(nanoseconds: .second * 2) + setup() // entry #3: retry + } // end Task } + } @MainActor diff --git a/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift b/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift index d4934b6e..218fd8e8 100644 --- a/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift +++ b/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift @@ -20,6 +20,7 @@ enum TabBarItem: Int, Hashable { case federated // Mastodon only case messages case likes + case history case lists case trends case drafts @@ -42,6 +43,7 @@ extension TabBarItem { case .federated: return L10n.Scene.Federated.title case .messages: return L10n.Scene.Messages.title case .likes: return L10n.Scene.Likes.title + case .history: return "History" // TODO: i18n case .lists: return L10n.Scene.Lists.title case .trends: return L10n.Scene.Trends.title case .drafts: return L10n.Scene.Drafts.title @@ -59,6 +61,7 @@ extension TabBarItem { case .federated: return Asset.ObjectTools.globe.image.withRenderingMode(.alwaysTemplate) case .messages: return Asset.Communication.mail.image.withRenderingMode(.alwaysTemplate) case .likes: return Asset.Health.heart.image.withRenderingMode(.alwaysTemplate) + case .history: return Asset.Arrows.clockArrowCirclepath.image.withRenderingMode(.alwaysTemplate) case .lists: return Asset.TextFormatting.listBullet.image.withRenderingMode(.alwaysTemplate) case .trends: return Asset.Arrows.trendingUp.image.withRenderingMode(.alwaysTemplate) case .drafts: return Asset.ObjectTools.note.image.withRenderingMode(.alwaysTemplate) @@ -80,7 +83,7 @@ extension TabBarItem { } extension TabBarItem { - func viewController(context: AppContext, coordinator: SceneCoordinator) -> UIViewController { + func viewController(context: AppContext, coordinator: SceneCoordinator, authContext: AuthContext) -> UIViewController { let viewController: UIViewController switch self { case .home: @@ -113,6 +116,14 @@ extension TabBarItem { let _viewController = UserLikeTimelineViewController() _viewController.viewModel = MeLikeTimelineViewModel(context: context) viewController = _viewController + case .history: + let _viewController = HistoryViewController() + _viewController.viewModel = HistoryViewModel( + context: context, + coordinator: coordinator, + authContext: authContext + ) + viewController = _viewController case .lists: guard let me = context.authenticationService.activeAuthenticationContext?.user(in: context.managedObjectContext)?.asRecord else { return AdaptiveStatusBarStyleNavigationController(rootViewController: UIViewController()) diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+User.swift b/TwidereX/Protocol/Provider/DataSourceFacade+User.swift index c1137e77..3ef8123b 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+User.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+User.swift @@ -374,7 +374,6 @@ extension DataSourceFacade { guard isSignOut else { return } dependency.coordinator.setup() - dependency.coordinator.setupWelcomeIfNeeds() } // end Task } alertController.addAction(signOutAction) diff --git a/TwidereX/Scene/Root/ContentSplitViewController.swift b/TwidereX/Scene/Root/ContentSplitViewController.swift index 4f8216cf..0dbacf27 100644 --- a/TwidereX/Scene/Root/ContentSplitViewController.swift +++ b/TwidereX/Scene/Root/ContentSplitViewController.swift @@ -9,6 +9,7 @@ import os.log import UIKit import Combine +import TwidereCore final class ContentSplitViewController: UIViewController, NeedsDependency { @@ -20,7 +21,8 @@ final class ContentSplitViewController: UIViewController, NeedsDependency { weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } - + let authContext: AuthContext + private(set) lazy var sidebarViewController: SidebarViewController = { let sidebarViewController = SidebarViewController() sidebarViewController.context = context @@ -31,12 +33,12 @@ final class ContentSplitViewController: UIViewController, NeedsDependency { }() private(set) lazy var mainTabBarController: MainTabBarController = { - let mainTabBarController = MainTabBarController(context: context, coordinator: coordinator) + let mainTabBarController = MainTabBarController(context: context, coordinator: coordinator, authContext: authContext) return mainTabBarController }() private(set) lazy var secondaryTabBarController: SecondaryTabBarController = { - let secondaryTabBarController = SecondaryTabBarController(context: context, coordinator: coordinator) + let secondaryTabBarController = SecondaryTabBarController(context: context, coordinator: coordinator, authContext: authContext) return secondaryTabBarController }() @@ -49,7 +51,22 @@ final class ContentSplitViewController: UIViewController, NeedsDependency { // [Tab: HashValue] var transformNavigationStackRecord: [TabBarItem: [Int]] = [:] - + + init( + context: AppContext, + coordinator: SceneCoordinator, + authContext: AuthContext + ) { + self.context = context + self.coordinator = coordinator + self.authContext = authContext + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) } @@ -211,7 +228,7 @@ extension ContentSplitViewController { for tab in secondaryTabBarController.tabs { guard let secondaryTabBarNavigationController = secondaryTabBarController.navigationController(for: tab) else { continue } if secondaryTabBarNavigationController.viewControllers.count == 1 { - let viewController = tab.viewController(context: context, coordinator: coordinator) + let viewController = tab.viewController(context: context, coordinator: coordinator, authContext: authContext) viewController.navigationItem.hidesBackButton = true secondaryTabBarNavigationController.pushViewController(viewController, animated: false) } diff --git a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift index e071d565..59f808d6 100644 --- a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift +++ b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift @@ -25,6 +25,7 @@ final class MainTabBarController: UITabBarController { weak var context: AppContext! weak var coordinator: SceneCoordinator! + let authContext: AuthContext private let doubleTapGestureRecognizer = UITapGestureRecognizer.doubleTapGestureRecognizer @@ -40,9 +41,14 @@ final class MainTabBarController: UITabBarController { var lastPopToRootTime = CACurrentMediaTime() @Published var tabBarTapScrollPreference = UserDefaults.shared.tabBarTapScrollPreference - init(context: AppContext, coordinator: SceneCoordinator) { + init( + context: AppContext, + coordinator: SceneCoordinator, + authContext: AuthContext + ) { self.context = context self.coordinator = coordinator + self.authContext = authContext super.init(nibName: nil, bundle: nil) UserDefaults.shared.publisher(for: \.tabBarTapScrollPreference) @@ -73,7 +79,7 @@ extension MainTabBarController { view.backgroundColor = .systemBackground let viewControllers: [UIViewController] = tabs.map { tab in - let rootViewController = tab.viewController(context: context, coordinator: coordinator) + let rootViewController = tab.viewController(context: context, coordinator: coordinator, authContext: authContext) let viewController = AdaptiveStatusBarStyleNavigationController(rootViewController: rootViewController) viewController.tabBarItem.tag = tab.tag viewController.tabBarItem.title = tab.title diff --git a/TwidereX/Scene/Root/SecondaryTab/SecondaryTabBarController.swift b/TwidereX/Scene/Root/SecondaryTab/SecondaryTabBarController.swift index 5f517bcf..dc77b2d7 100644 --- a/TwidereX/Scene/Root/SecondaryTab/SecondaryTabBarController.swift +++ b/TwidereX/Scene/Root/SecondaryTab/SecondaryTabBarController.swift @@ -10,6 +10,7 @@ import os.log import Foundation import UIKit import Combine +import TwidereCore import func QuartzCore.CACurrentMediaTime final class SecondaryTabBarController: UITabBarController { @@ -20,6 +21,7 @@ final class SecondaryTabBarController: UITabBarController { weak var context: AppContext! weak var coordinator: SceneCoordinator! + let authContext: AuthContext @Published var tabs: [TabBarItem] = [] { didSet { @@ -32,9 +34,10 @@ final class SecondaryTabBarController: UITabBarController { var lastPopToRootTime = CACurrentMediaTime() @Published var tabBarTapScrollPreference = UserDefaults.shared.tabBarTapScrollPreference - init(context: AppContext, coordinator: SceneCoordinator) { + init(context: AppContext, coordinator: SceneCoordinator, authContext: AuthContext) { self.context = context self.coordinator = coordinator + self.authContext = authContext super.init(nibName: nil, bundle: nil) UserDefaults.shared.publisher(for: \.tabBarTapScrollPreference) @@ -159,7 +162,7 @@ extension SecondaryTabBarController { private func update(tabs: [TabBarItem]) { let viewControllers: [UIViewController] = tabs.map { tab in let viewController = AdaptiveStatusBarStyleNavigationController(rootViewController: SecondaryTabBarRootController()) - let _rootViewController = tab.viewController(context: context, coordinator: coordinator) + let _rootViewController = tab.viewController(context: context, coordinator: coordinator, authContext: authContext) _rootViewController.navigationItem.hidesBackButton = true viewController.pushViewController(_rootViewController, animated: false) viewController.tabBarItem.tag = tab.tag diff --git a/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift b/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift index 7b576dbf..f1d051e0 100644 --- a/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift @@ -46,9 +46,9 @@ final class SidebarViewModel: ObservableObject { var items: [TabBarItem] = [] switch authenticationContext { case .twitter: - items.append(contentsOf: [.likes, .lists]) + items.append(contentsOf: [.likes, .history, .lists]) case .mastodon: - items.append(contentsOf: [.local, .federated, .likes, .lists]) + items.append(contentsOf: [.local, .federated, .likes, .history, .lists]) case .none: break } diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift index 2399e01a..5e005a60 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift @@ -87,6 +87,7 @@ class TimelineViewModel: TimelineViewModelDriver { timestampUpdatePublisher .sink { [weak self] _ in guard let self = self else { return } + guard self.enableAutoFetchLatest else { return } let now = CACurrentMediaTime() let elapse = now - self.autoFetchLatestActionTime guard elapse > self.timelineRefreshInterval.seconds else { diff --git a/TwidereX/Supporting Files/SceneDelegate.swift b/TwidereX/Supporting Files/SceneDelegate.swift index 410817dd..b0046f7e 100644 --- a/TwidereX/Supporting Files/SceneDelegate.swift +++ b/TwidereX/Supporting Files/SceneDelegate.swift @@ -61,7 +61,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { self.coordinator = sceneCoordinator sceneCoordinator.setup() - sceneCoordinator.setupWelcomeIfNeeds() window.makeKeyAndVisible() From 211a33f9a6f2a696d38419af3452aab0b42b2235 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 3 Aug 2022 15:16:34 +0800 Subject: [PATCH 009/128] feat: add history toggle and clear action --- .../Preference/Preference+Behaviors.swift | 12 +++++- .../Scene/History/HistoryViewController.swift | 23 ++++++++++ .../DrawerSidebarViewModel+Diffable.swift | 41 +++++++++++------- .../Scene/Root/Sidebar/SidebarViewModel.swift | 42 +++++++++++++------ .../BehaviorsPreferenceView.swift | 22 +++++++--- .../BehaviorsPreferenceViewModel.swift | 16 ++++++- 6 files changed, 121 insertions(+), 35 deletions(-) diff --git a/TwidereSDK/Sources/TwidereCommon/Preference/Preference+Behaviors.swift b/TwidereSDK/Sources/TwidereCommon/Preference/Preference+Behaviors.swift index fb8f21cf..5d9cdd87 100644 --- a/TwidereSDK/Sources/TwidereCommon/Preference/Preference+Behaviors.swift +++ b/TwidereSDK/Sources/TwidereCommon/Preference/Preference+Behaviors.swift @@ -65,7 +65,7 @@ extension UserDefaults { @objc dynamic public var timelineRefreshInterval: TimelineRefreshInterval { get { guard let rawValue: Int = self[#function] else { return ._60s } - return TimelineRefreshInterval (rawValue: rawValue) ?? ._60s + return TimelineRefreshInterval(rawValue: rawValue) ?? ._60s } set { self[#function] = newValue.rawValue } } @@ -78,3 +78,13 @@ extension UserDefaults { } } + +// MARK: - History +extension UserDefaults { + + @objc dynamic public var preferredEnableHistory: Bool { + get { return bool(forKey: #function) } + set { self[#function] = newValue } + } + +} diff --git a/TwidereX/Scene/History/HistoryViewController.swift b/TwidereX/Scene/History/HistoryViewController.swift index 2830a556..a4c71b54 100644 --- a/TwidereX/Scene/History/HistoryViewController.swift +++ b/TwidereX/Scene/History/HistoryViewController.swift @@ -9,6 +9,8 @@ import os.log import UIKit import Combine +import CoreData +import CoreDataStack import Tabman import Pageboy import TwidereCore @@ -28,6 +30,8 @@ final class HistoryViewController: TabmanViewController, NeedsDependency, Drawer private(set) lazy var pageSegmentedControl = UISegmentedControl() + let optionBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle")) + override func pageboyViewController( _ pageboyViewController: PageboyViewController, didScrollToPageAt index: TabmanViewController.PageIndex, @@ -60,6 +64,25 @@ extension HistoryViewController { navigationItem.titleView = pageSegmentedControl pageSegmentedControl.addTarget(self, action: #selector(HistoryViewController.pageSegmentedControlValueChanged(_:)), for: .valueChanged) + navigationItem.rightBarButtonItem = optionBarButtonItem + optionBarButtonItem.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: [ + UIAction(title: "Clear", image: UIImage(systemName: "minus.circle"), identifier: nil, discoverabilityTitle: nil, attributes: [.destructive], state: .off, handler: { [weak self] action in + guard let self = self else { return } + Task { + let managedObjectContext = self.context.backgroundManagedObjectContext + let acct = self.viewModel.authContext.authenticationContext.acct + try await managedObjectContext.performChanges { + let request = History.sortedFetchRequest + request.predicate = History.predicate(acct: acct) + let histories = try managedObjectContext.fetch(request) + for history in histories { + managedObjectContext.delete(history) + } + } + } // end Task + }) + ]) + dataSource = viewModel } diff --git a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewModel+Diffable.swift b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewModel+Diffable.swift index e3e8c3df..ef901786 100644 --- a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewModel+Diffable.swift +++ b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewModel+Diffable.swift @@ -7,6 +7,7 @@ // import UIKit +import Combine import TwidereAsset extension DrawerSidebarViewModel { @@ -20,22 +21,34 @@ extension DrawerSidebarViewModel { sidebarSnapshot.appendSections([.main]) sidebarDiffableDataSource?.applySnapshotUsingReloadData(sidebarSnapshot) - context.authenticationService.$activeAuthenticationContext - .sink { [weak self] authenticationContext in - guard let self = self else { return } - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.main]) - switch authenticationContext { - case .twitter: - snapshot.appendItems([.likes, .history, .lists], toSection: .main) - case .mastodon: - snapshot.appendItems([.local, .federated, .likes, .history, .lists], toSection: .main) - case .none: - break + Publishers.CombineLatest( + context.authenticationService.$activeAuthenticationContext.removeDuplicates(), + UserDefaults.shared.publisher(for: \.preferredEnableHistory).removeDuplicates() + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] authenticationContext, preferredEnableHistory in + guard let self = self else { return } + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + switch authenticationContext { + case .twitter: + snapshot.appendItems([.likes], toSection: .main) + if preferredEnableHistory { + snapshot.appendItems([.history], toSection: .main) } - self.sidebarDiffableDataSource?.applySnapshotUsingReloadData(snapshot) + snapshot.appendItems([.lists], toSection: .main) + case .mastodon: + snapshot.appendItems([.local, .federated, .likes], toSection: .main) + if preferredEnableHistory { + snapshot.appendItems([.history], toSection: .main) + } + snapshot.appendItems([.lists], toSection: .main) + case .none: + break } - .store(in: &disposeBag) + self.sidebarDiffableDataSource?.applySnapshotUsingReloadData(snapshot) + } + .store(in: &disposeBag) // setting settingDiffableDataSource = setupDiffableDataSource(collectionView: settingCollectionView) diff --git a/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift b/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift index f1d051e0..959d3779 100644 --- a/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift @@ -39,21 +39,39 @@ final class SidebarViewModel: ObservableObject { init(context: AppContext) { self.context = context + Publishers.CombineLatest( + context.authenticationService.$activeAuthenticationContext.removeDuplicates(), + UserDefaults.shared.publisher(for: \.preferredEnableHistory).removeDuplicates() + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] authenticationContext, preferredEnableHistory in + guard let self = self else { return } + + var items: [TabBarItem] = [] + switch authenticationContext { + case .twitter: + items.append(contentsOf: [.likes]) + if preferredEnableHistory { + items.append(contentsOf: [.history]) + } + items.append(contentsOf: [.lists]) + case .mastodon: + items.append(contentsOf: [.local, .federated, .likes]) + if preferredEnableHistory { + items.append(contentsOf: [.history]) + } + items.append(contentsOf: [.lists]) + case .none: + break + } + self.secondaryTabBarItems = items + } + .store(in: &disposeBag) + context.authenticationService.$activeAuthenticationContext .sink { [weak self] authenticationContext in guard let self = self else { return } - - var items: [TabBarItem] = [] - switch authenticationContext { - case .twitter: - items.append(contentsOf: [.likes, .history, .lists]) - case .mastodon: - items.append(contentsOf: [.local, .federated, .likes, .history, .lists]) - case .none: - break - } - self.secondaryTabBarItems = items - + let user = authenticationContext?.user(in: context.managedObjectContext) switch user { case .twitter(let object): diff --git a/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceView.swift b/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceView.swift index 8a7569b9..6b8df627 100644 --- a/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceView.swift +++ b/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceView.swift @@ -37,13 +37,14 @@ struct BehaviorsPreferenceView: View { Toggle(isOn: $viewModel.preferredTimelineAutoRefresh) { Text(verbatim: "Automatically refresh timeline") // TODO: i18n } - Picker(selection: $viewModel.timelineRefreshInterval) { - ForEach(UserDefaults.TimelineRefreshInterval.allCases, id: \.self) { preference in - Text(preference.title) + if viewModel.preferredTimelineAutoRefresh { + Picker(selection: $viewModel.timelineRefreshInterval) { + ForEach(UserDefaults.TimelineRefreshInterval.allCases, id: \.self) { preference in + Text(preference.title) + } + } label: { + Text(verbatim: "Refresh Interval") // TODO: i18n } - } label: { - Text(verbatim: "Refresh Interval") // TODO: i18n - } Toggle(isOn: $viewModel.preferredTimelineResetToTop) { Text(verbatim: "Reset to top") // TODO: i18n @@ -52,6 +53,15 @@ struct BehaviorsPreferenceView: View { Text(verbatim: "Timeline Refreshing") // TODO: i18n .textCase(nil) } + // History + Section { + Toggle(isOn: $viewModel.preferredEnableHistory) { + Text(verbatim: "Enable History Record") // TODO: i18n + } + } header: { + Text(verbatim: "History") // TODO: i18n + .textCase(nil) + } } } diff --git a/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewModel.swift b/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewModel.swift index d5e04d7e..006738cd 100644 --- a/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewModel.swift +++ b/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewModel.swift @@ -32,8 +32,10 @@ final class BehaviorsPreferenceViewModel: ObservableObject { @Published var timelineRefreshInterval = UserDefaults.shared.timelineRefreshInterval @Published var preferredTimelineResetToTop = UserDefaults.shared.preferredTimelineResetToTop - // output - + // History + @Published var preferredEnableHistory = UserDefaults.shared.preferredEnableHistory + + // output init( context: AppContext @@ -90,6 +92,16 @@ final class BehaviorsPreferenceViewModel: ObservableObject { UserDefaults.shared.preferredTimelineResetToTop = preferredTimelineResetToTop } .store(in: &disposeBag) + + // preferredEnableHistory + UserDefaults.shared.publisher(for: \.preferredEnableHistory) + .removeDuplicates() + .assign(to: &$preferredEnableHistory) + $preferredEnableHistory + .sink { preferredEnableHistory in + UserDefaults.shared.preferredEnableHistory = preferredEnableHistory + } + .store(in: &disposeBag) } } From be2f3d199a4e9360cff80d832b125333d8b9b459 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 3 Aug 2022 17:23:54 +0800 Subject: [PATCH 010/128] chore: update i18n resources --- .../Generated/Strings.swift | 64 ++++++++++++++++++- .../Resources/ar.lproj/Localizable.strings | 25 +++++++- .../Resources/ca.lproj/Localizable.strings | 25 +++++++- .../Resources/de.lproj/Localizable.strings | 25 +++++++- .../Resources/en.lproj/Localizable.strings | 25 +++++++- .../Resources/es.lproj/Localizable.strings | 25 +++++++- .../Resources/eu.lproj/Localizable.strings | 25 +++++++- .../Resources/gl.lproj/Localizable.strings | 47 ++++++++++---- .../gl.lproj/Localizable.stringsdict | 4 +- .../Resources/ja.lproj/Localizable.strings | 25 +++++++- .../Resources/ko.lproj/Localizable.strings | 25 +++++++- .../Resources/pt-BR.lproj/Localizable.strings | 47 ++++++++++---- .../pt-BR.lproj/Localizable.stringsdict | 4 +- .../Resources/tr.lproj/Localizable.strings | 51 ++++++++++----- .../tr.lproj/Localizable.stringsdict | 4 +- .../zh-Hans.lproj/Localizable.strings | 23 ++++++- .../Diffable/Misc/Sidebar/SidebarItem.swift | 2 +- .../Diffable/Misc/TabBar/TabBarItem.swift | 2 +- .../Provider/DataSourceFacade+Report.swift | 3 +- TwidereX/Scene/History/HistoryViewModel.swift | 6 +- .../AccountPreferenceView.swift | 2 +- .../MastodonNotificationSectionView.swift | 2 +- .../BehaviorsPreferenceView.swift | 18 +++--- .../BehaviorsPreferenceViewController.swift | 2 +- .../BehaviorsPreferenceViewModel.swift | 2 +- .../DisplayPreferenceView.swift | 2 +- .../Scene/Setting/List/SettingListView.swift | 2 +- TwidereXIntent/tr.lproj/Intents.strings | 34 +++++----- 28 files changed, 415 insertions(+), 106 deletions(-) diff --git a/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift b/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift index 9b34eb7a..27709f53 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift +++ b/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift @@ -954,7 +954,7 @@ public enum L10n { } } public enum ReplySettings { - /// Everyone can peply + /// Everyone can reply public static let everyoneCanReply = L10n.tr("Localizable", "Scene.Compose.ReplySettings.EveryoneCanReply") /// Only people you mention can reply public static let onlyPeopleYouMentionCanReply = L10n.tr("Localizable", "Scene.Compose.ReplySettings.OnlyPeopleYouMentionCanReply") @@ -1056,6 +1056,20 @@ public enum L10n { /// Following public static let title = L10n.tr("Localizable", "Scene.Following.Title") } + public enum History { + /// Clear + public static let clear = L10n.tr("Localizable", "Scene.History.Clear") + /// History + public static let title = L10n.tr("Localizable", "Scene.History.Title") + public enum Scope { + /// Toot + public static let toot = L10n.tr("Localizable", "Scene.History.Scope.Toot") + /// Tweet + public static let tweet = L10n.tr("Localizable", "Scene.History.Scope.Tweet") + /// User + public static let user = L10n.tr("Localizable", "Scene.History.Scope.User") + } + } public enum Likes { /// Likes public static let title = L10n.tr("Localizable", "Scene.Likes.Title") @@ -1293,8 +1307,6 @@ public enum L10n { } } public enum Account { - /// Account Settings - public static let accountSettings = L10n.tr("Localizable", "Scene.Settings.Account.AccountSettings") /// Blocked People public static let blockedPeople = L10n.tr("Localizable", "Scene.Settings.Account.BlockedPeople") /// Mute and Block @@ -1360,6 +1372,50 @@ public enum L10n { public static let translateButton = L10n.tr("Localizable", "Scene.Settings.Appearance.Translation.TranslateButton") } } + public enum Behaviors { + /// Behaviors + public static let title = L10n.tr("Localizable", "Scene.Settings.Behaviors.Title") + public enum HistorySection { + /// Enable History Record + public static let enableHistoryRecord = L10n.tr("Localizable", "Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord") + /// History + public static let history = L10n.tr("Localizable", "Scene.Settings.Behaviors.HistorySection.History") + } + public enum TabBarSection { + /// Show tab bar labels + public static let showTabBarLabels = L10n.tr("Localizable", "Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels") + /// Tab Bar + public static let tabBar = L10n.tr("Localizable", "Scene.Settings.Behaviors.TabBarSection.TabBar") + /// Tap tab bar scroll to top + public static let tapTabBarScrollToTop = L10n.tr("Localizable", "Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop") + } + public enum TimelineRefreshingSection { + /// Automatically refresh timeline + public static let automaticallyRefreshTimeline = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline") + /// Refresh interval + public static let refreshInterval = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval") + /// Reset to top + public static let resetToTop = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop") + /// Timeline Refreshing + public static let timelineRefreshing = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing") + public enum RefreshIntervalOption { + /// 120 seconds + public static let _120Seconds = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds") + /// 300 seconds + public static let _300Seconds = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds") + /// 30 seconds + public static let _30Seconds = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds") + /// 60 seconds + public static let _60Seconds = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds") + } + public enum ResetToTopOption { + /// Double Tap + public static let doubleTap = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap") + /// Single Tap + public static let singleTap = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap") + } + } + } public enum Display { /// Display public static let title = L10n.tr("Localizable", "Scene.Settings.Display.Title") @@ -1390,6 +1446,8 @@ public enum L10n { public static let thankForUsingTwidereX = L10n.tr("Localizable", "Scene.Settings.Display.Preview.ThankForUsingTwidereX") } public enum SectionHeader { + /// Avatar + public static let avatar = L10n.tr("Localizable", "Scene.Settings.Display.SectionHeader.Avatar") /// Date Format public static let dateFormat = L10n.tr("Localizable", "Scene.Settings.Display.SectionHeader.DateFormat") /// Media diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings index 1d635c1c..f4437829 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings @@ -285,7 +285,7 @@ "Scene.Compose.Media.Remove" = "أزِل"; "Scene.Compose.OthersInThisConversation" = "آخرون في هذه المحادثة:"; "Scene.Compose.Placeholder" = "ماذا يحدث ؟"; -"Scene.Compose.ReplySettings.EveryoneCanReply" = "Everyone can peply"; +"Scene.Compose.ReplySettings.EveryoneCanReply" = "Everyone can reply"; "Scene.Compose.ReplySettings.OnlyPeopleYouMentionCanReply" = "Only people you mention can reply"; "Scene.Compose.ReplySettings.PeopleYouFollowCanReply" = "People you follow can reply"; "Scene.Compose.ReplyTo" = "رد على …"; @@ -322,6 +322,11 @@ "Scene.Federated.Title" = "الشبكة الموحدة"; "Scene.Followers.Title" = "المتابِعون"; "Scene.Following.Title" = "المتابَعون"; +"Scene.History.Clear" = "Clear"; +"Scene.History.Scope.Toot" = "تبويقة"; +"Scene.History.Scope.Tweet" = "تغريدة"; +"Scene.History.Scope.User" = "User"; +"Scene.History.Title" = "التأريخ"; "Scene.Likes.Title" = "الإعجابات"; "Scene.Listed.Title" = "مدرج"; "Scene.Lists.Icons.Create" = "أنشئ قائمة"; @@ -398,7 +403,6 @@ "Scene.Settings.About.Logo.BackgroundShadow" = "تظليل شعار الخلفية لصفحة 'حول'"; "Scene.Settings.About.Title" = "حول"; "Scene.Settings.About.Version" = "النسخة %@"; -"Scene.Settings.Account.AccountSettings" = "Account Settings"; "Scene.Settings.Account.BlockedPeople" = "Blocked People"; "Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; "Scene.Settings.Account.MutedPeople" = "Muted People"; @@ -425,6 +429,22 @@ "Scene.Settings.Appearance.Translation.Off" = "إيقاف"; "Scene.Settings.Appearance.Translation.Service" = "الخدمة"; "Scene.Settings.Appearance.Translation.TranslateButton" = "زر الترجمة"; +"Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord" = "Enable History Record"; +"Scene.Settings.Behaviors.HistorySection.History" = "التأريخ"; +"Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels" = "Show tab bar labels"; +"Scene.Settings.Behaviors.TabBarSection.TabBar" = "Tab Bar"; +"Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop" = "Tap tab bar scroll to top"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline" = "Automatically refresh timeline"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval" = "Refresh interval"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds" = "120 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds" = "300 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds" = "30 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds" = "60 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop" = "Reset to top"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap" = "Double Tap"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap" = "Single Tap"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing" = "Timeline Refreshing"; +"Scene.Settings.Behaviors.Title" = "Behaviors"; "Scene.Settings.Display.DateFormat.Absolute" = "مُطلق"; "Scene.Settings.Display.DateFormat.Relative" = "نسبي"; "Scene.Settings.Display.Media.Always" = "دائماً"; @@ -434,6 +454,7 @@ "Scene.Settings.Display.Media.MuteByDefault" = "مكتوم افتراضيًا"; "Scene.Settings.Display.Media.Off" = "إيقاف"; "Scene.Settings.Display.Preview.ThankForUsingTwidereX" = "شكرا على استخدام @TwidereProject!"; +"Scene.Settings.Display.SectionHeader.Avatar" = "Avatar"; "Scene.Settings.Display.SectionHeader.DateFormat" = "صيغة التاريخ"; "Scene.Settings.Display.SectionHeader.Media" = "الوسائط"; "Scene.Settings.Display.SectionHeader.Preview" = "معاينة"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings index 58d211d5..9e1e722a 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings @@ -285,7 +285,7 @@ "Scene.Compose.Media.Remove" = "Elimina"; "Scene.Compose.OthersInThisConversation" = "Altres en aquesta conversa:"; "Scene.Compose.Placeholder" = "Què està passant?"; -"Scene.Compose.ReplySettings.EveryoneCanReply" = "Everyone can peply"; +"Scene.Compose.ReplySettings.EveryoneCanReply" = "Everyone can reply"; "Scene.Compose.ReplySettings.OnlyPeopleYouMentionCanReply" = "Only people you mention can reply"; "Scene.Compose.ReplySettings.PeopleYouFollowCanReply" = "People you follow can reply"; "Scene.Compose.ReplyTo" = "Resposta a …"; @@ -322,6 +322,11 @@ "Scene.Federated.Title" = "Federated"; "Scene.Followers.Title" = "Seguidors"; "Scene.Following.Title" = "Seguint"; +"Scene.History.Clear" = "Clear"; +"Scene.History.Scope.Toot" = "Toot"; +"Scene.History.Scope.Tweet" = "Piulada"; +"Scene.History.Scope.User" = "User"; +"Scene.History.Title" = "Historial"; "Scene.Likes.Title" = "M'agrada"; "Scene.Listed.Title" = "Llistat"; "Scene.Lists.Icons.Create" = "Crea llista"; @@ -398,7 +403,6 @@ Encara en una fase inicial."; "Scene.Settings.About.Logo.BackgroundShadow" = "Quant al ombrejat del logo del fons de la pàgina"; "Scene.Settings.About.Title" = "Quant a"; "Scene.Settings.About.Version" = "Versió %@"; -"Scene.Settings.Account.AccountSettings" = "Account Settings"; "Scene.Settings.Account.BlockedPeople" = "Blocked People"; "Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; "Scene.Settings.Account.MutedPeople" = "Muted People"; @@ -425,6 +429,22 @@ Encara en una fase inicial."; "Scene.Settings.Appearance.Translation.Off" = "Desactivada"; "Scene.Settings.Appearance.Translation.Service" = "Service"; "Scene.Settings.Appearance.Translation.TranslateButton" = "Translate button"; +"Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord" = "Enable History Record"; +"Scene.Settings.Behaviors.HistorySection.History" = "Historial"; +"Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels" = "Show tab bar labels"; +"Scene.Settings.Behaviors.TabBarSection.TabBar" = "Tab Bar"; +"Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop" = "Tap tab bar scroll to top"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline" = "Automatically refresh timeline"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval" = "Refresh interval"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds" = "120 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds" = "300 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds" = "30 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds" = "60 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop" = "Reset to top"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap" = "Double Tap"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap" = "Single Tap"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing" = "Timeline Refreshing"; +"Scene.Settings.Behaviors.Title" = "Behaviors"; "Scene.Settings.Display.DateFormat.Absolute" = "Absoluta"; "Scene.Settings.Display.DateFormat.Relative" = "Relativa"; "Scene.Settings.Display.Media.Always" = "Sempre"; @@ -434,6 +454,7 @@ Encara en una fase inicial."; "Scene.Settings.Display.Media.MuteByDefault" = "Mute by default"; "Scene.Settings.Display.Media.Off" = "Desactivada"; "Scene.Settings.Display.Preview.ThankForUsingTwidereX" = "Gràcies per utilitzar @TwidereProject!"; +"Scene.Settings.Display.SectionHeader.Avatar" = "Avatar"; "Scene.Settings.Display.SectionHeader.DateFormat" = "Format de la data"; "Scene.Settings.Display.SectionHeader.Media" = "Multimèdia"; "Scene.Settings.Display.SectionHeader.Preview" = "Previsualitza"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings index bbdd38bd..ac10f855 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings @@ -285,7 +285,7 @@ "Scene.Compose.Media.Remove" = "Entfernen"; "Scene.Compose.OthersInThisConversation" = "Andere in dieser Konversation:"; "Scene.Compose.Placeholder" = "Was ist passiert?"; -"Scene.Compose.ReplySettings.EveryoneCanReply" = "Everyone can peply"; +"Scene.Compose.ReplySettings.EveryoneCanReply" = "Everyone can reply"; "Scene.Compose.ReplySettings.OnlyPeopleYouMentionCanReply" = "Only people you mention can reply"; "Scene.Compose.ReplySettings.PeopleYouFollowCanReply" = "People you follow can reply"; "Scene.Compose.ReplyTo" = "Antworten auf …"; @@ -322,6 +322,11 @@ "Scene.Federated.Title" = "Federated"; "Scene.Followers.Title" = "Follower"; "Scene.Following.Title" = "Folgen"; +"Scene.History.Clear" = "Löschen"; +"Scene.History.Scope.Toot" = "Toot"; +"Scene.History.Scope.Tweet" = "Tweet"; +"Scene.History.Scope.User" = "User"; +"Scene.History.Title" = "Verlauf"; "Scene.Likes.Title" = "Likes"; "Scene.Listed.Title" = "Listet"; "Scene.Lists.Icons.Create" = "Create list"; @@ -398,7 +403,6 @@ Still in early stage."; "Scene.Settings.About.Logo.BackgroundShadow" = "About page background logo shadow"; "Scene.Settings.About.Title" = "Über"; "Scene.Settings.About.Version" = "Ver %@"; -"Scene.Settings.Account.AccountSettings" = "Account Settings"; "Scene.Settings.Account.BlockedPeople" = "Blocked People"; "Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; "Scene.Settings.Account.MutedPeople" = "Muted People"; @@ -425,6 +429,22 @@ Still in early stage."; "Scene.Settings.Appearance.Translation.Off" = "Aus"; "Scene.Settings.Appearance.Translation.Service" = "Dienste"; "Scene.Settings.Appearance.Translation.TranslateButton" = "Übersetzen-Schaltfläche"; +"Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord" = "Enable History Record"; +"Scene.Settings.Behaviors.HistorySection.History" = "Verlauf"; +"Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels" = "Show tab bar labels"; +"Scene.Settings.Behaviors.TabBarSection.TabBar" = "Tab Bar"; +"Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop" = "Tap tab bar scroll to top"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline" = "Automatically refresh timeline"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval" = "Refresh interval"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds" = "120 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds" = "300 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds" = "30 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds" = "60 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop" = "Reset to top"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap" = "Double Tap"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap" = "Single Tap"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing" = "Timeline Refreshing"; +"Scene.Settings.Behaviors.Title" = "Behaviors"; "Scene.Settings.Display.DateFormat.Absolute" = "Absolut"; "Scene.Settings.Display.DateFormat.Relative" = "Relativ"; "Scene.Settings.Display.Media.Always" = "Immer"; @@ -434,6 +454,7 @@ Still in early stage."; "Scene.Settings.Display.Media.MuteByDefault" = "Standardmäßig stummschalten"; "Scene.Settings.Display.Media.Off" = "Aus"; "Scene.Settings.Display.Preview.ThankForUsingTwidereX" = "Vielen Dank für die Verwendung von @TwidereProject!"; +"Scene.Settings.Display.SectionHeader.Avatar" = "Avatar"; "Scene.Settings.Display.SectionHeader.DateFormat" = "Datumsformat"; "Scene.Settings.Display.SectionHeader.Media" = "Medien"; "Scene.Settings.Display.SectionHeader.Preview" = "Vorschau"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings index fc3fa357..e9fd3d73 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings @@ -285,7 +285,7 @@ "Scene.Compose.Media.Remove" = "Remove"; "Scene.Compose.OthersInThisConversation" = "Others in this conversation:"; "Scene.Compose.Placeholder" = "What’s happening?"; -"Scene.Compose.ReplySettings.EveryoneCanReply" = "Everyone can peply"; +"Scene.Compose.ReplySettings.EveryoneCanReply" = "Everyone can reply"; "Scene.Compose.ReplySettings.OnlyPeopleYouMentionCanReply" = "Only people you mention can reply"; "Scene.Compose.ReplySettings.PeopleYouFollowCanReply" = "People you follow can reply"; "Scene.Compose.ReplyTo" = "Reply to …"; @@ -322,6 +322,11 @@ "Scene.Federated.Title" = "Federated"; "Scene.Followers.Title" = "Followers"; "Scene.Following.Title" = "Following"; +"Scene.History.Clear" = "Clear"; +"Scene.History.Scope.Toot" = "Toot"; +"Scene.History.Scope.Tweet" = "Tweet"; +"Scene.History.Scope.User" = "User"; +"Scene.History.Title" = "History"; "Scene.Likes.Title" = "Likes"; "Scene.Listed.Title" = "Listed"; "Scene.Lists.Icons.Create" = "Create list"; @@ -398,7 +403,6 @@ Still in early stage."; "Scene.Settings.About.Logo.BackgroundShadow" = "About page background logo shadow"; "Scene.Settings.About.Title" = "About"; "Scene.Settings.About.Version" = "Ver %@"; -"Scene.Settings.Account.AccountSettings" = "Account Settings"; "Scene.Settings.Account.BlockedPeople" = "Blocked People"; "Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; "Scene.Settings.Account.MutedPeople" = "Muted People"; @@ -425,6 +429,22 @@ Still in early stage."; "Scene.Settings.Appearance.Translation.Off" = "Off"; "Scene.Settings.Appearance.Translation.Service" = "Service"; "Scene.Settings.Appearance.Translation.TranslateButton" = "Translate button"; +"Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord" = "Enable History Record"; +"Scene.Settings.Behaviors.HistorySection.History" = "History"; +"Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels" = "Show tab bar labels"; +"Scene.Settings.Behaviors.TabBarSection.TabBar" = "Tab Bar"; +"Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop" = "Tap tab bar scroll to top"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline" = "Automatically refresh timeline"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval" = "Refresh interval"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds" = "120 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds" = "300 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds" = "30 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds" = "60 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop" = "Reset to top"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap" = "Double Tap"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap" = "Single Tap"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing" = "Timeline Refreshing"; +"Scene.Settings.Behaviors.Title" = "Behaviors"; "Scene.Settings.Display.DateFormat.Absolute" = "Absolute"; "Scene.Settings.Display.DateFormat.Relative" = "Relative"; "Scene.Settings.Display.Media.Always" = "Always"; @@ -434,6 +454,7 @@ Still in early stage."; "Scene.Settings.Display.Media.MuteByDefault" = "Mute by default"; "Scene.Settings.Display.Media.Off" = "Off"; "Scene.Settings.Display.Preview.ThankForUsingTwidereX" = "Thanks for using @TwidereProject!"; +"Scene.Settings.Display.SectionHeader.Avatar" = "Avatar"; "Scene.Settings.Display.SectionHeader.DateFormat" = "Date Format"; "Scene.Settings.Display.SectionHeader.Media" = "Media"; "Scene.Settings.Display.SectionHeader.Preview" = "Preview"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings index 7073335d..8804ee70 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings @@ -285,7 +285,7 @@ "Scene.Compose.Media.Remove" = "Eliminar"; "Scene.Compose.OthersInThisConversation" = "Usuarios en esta conversación:"; "Scene.Compose.Placeholder" = "¿Qué está pasando?"; -"Scene.Compose.ReplySettings.EveryoneCanReply" = "Everyone can peply"; +"Scene.Compose.ReplySettings.EveryoneCanReply" = "Everyone can reply"; "Scene.Compose.ReplySettings.OnlyPeopleYouMentionCanReply" = "Only people you mention can reply"; "Scene.Compose.ReplySettings.PeopleYouFollowCanReply" = "People you follow can reply"; "Scene.Compose.ReplyTo" = "Responder a …"; @@ -322,6 +322,11 @@ "Scene.Federated.Title" = "Federada"; "Scene.Followers.Title" = "Seguidores"; "Scene.Following.Title" = "Siguiendo"; +"Scene.History.Clear" = "Clear"; +"Scene.History.Scope.Toot" = "Toot"; +"Scene.History.Scope.Tweet" = "Tweet"; +"Scene.History.Scope.User" = "User"; +"Scene.History.Title" = "Historial"; "Scene.Likes.Title" = "Me gusta"; "Scene.Listed.Title" = "Listado"; "Scene.Lists.Icons.Create" = "Crear lista"; @@ -398,7 +403,6 @@ Todavía en fase temprana."; "Scene.Settings.About.Logo.BackgroundShadow" = "Acerca de la sombra del logo del fondo de la página"; "Scene.Settings.About.Title" = "Acerca de"; "Scene.Settings.About.Version" = "Ver %@"; -"Scene.Settings.Account.AccountSettings" = "Account Settings"; "Scene.Settings.Account.BlockedPeople" = "Blocked People"; "Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; "Scene.Settings.Account.MutedPeople" = "Muted People"; @@ -425,6 +429,22 @@ Todavía en fase temprana."; "Scene.Settings.Appearance.Translation.Off" = "Desactivado"; "Scene.Settings.Appearance.Translation.Service" = "Servicio"; "Scene.Settings.Appearance.Translation.TranslateButton" = "Botón de traducción"; +"Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord" = "Enable History Record"; +"Scene.Settings.Behaviors.HistorySection.History" = "Historial"; +"Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels" = "Show tab bar labels"; +"Scene.Settings.Behaviors.TabBarSection.TabBar" = "Tab Bar"; +"Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop" = "Tap tab bar scroll to top"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline" = "Automatically refresh timeline"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval" = "Refresh interval"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds" = "120 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds" = "300 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds" = "30 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds" = "60 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop" = "Reset to top"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap" = "Double Tap"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap" = "Single Tap"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing" = "Timeline Refreshing"; +"Scene.Settings.Behaviors.Title" = "Behaviors"; "Scene.Settings.Display.DateFormat.Absolute" = "Absoluto"; "Scene.Settings.Display.DateFormat.Relative" = "Relativo"; "Scene.Settings.Display.Media.Always" = "Siempre"; @@ -434,6 +454,7 @@ Todavía en fase temprana."; "Scene.Settings.Display.Media.MuteByDefault" = "Silenciar por defecto"; "Scene.Settings.Display.Media.Off" = "Desactivado"; "Scene.Settings.Display.Preview.ThankForUsingTwidereX" = "¡Gracias por usar @TwidereProject!"; +"Scene.Settings.Display.SectionHeader.Avatar" = "Avatar"; "Scene.Settings.Display.SectionHeader.DateFormat" = "Formato de fecha"; "Scene.Settings.Display.SectionHeader.Media" = "Multimedia"; "Scene.Settings.Display.SectionHeader.Preview" = "Vista previa"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings index 5437d511..0c34fb72 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings @@ -285,7 +285,7 @@ "Scene.Compose.Media.Remove" = "Ezabatu"; "Scene.Compose.OthersInThisConversation" = "Beste batzuk elkarrizketa honetan:"; "Scene.Compose.Placeholder" = "Zer gertatzen ari da?"; -"Scene.Compose.ReplySettings.EveryoneCanReply" = "Everyone can peply"; +"Scene.Compose.ReplySettings.EveryoneCanReply" = "Everyone can reply"; "Scene.Compose.ReplySettings.OnlyPeopleYouMentionCanReply" = "Only people you mention can reply"; "Scene.Compose.ReplySettings.PeopleYouFollowCanReply" = "People you follow can reply"; "Scene.Compose.ReplyTo" = "… erantzun"; @@ -322,6 +322,11 @@ "Scene.Federated.Title" = "Federatua"; "Scene.Followers.Title" = "Jarratzaileak"; "Scene.Following.Title" = "Jarraitzen"; +"Scene.History.Clear" = "Clear"; +"Scene.History.Scope.Toot" = "Toota"; +"Scene.History.Scope.Tweet" = "Txiokatu"; +"Scene.History.Scope.User" = "User"; +"Scene.History.Title" = "Historia"; "Scene.Likes.Title" = "Atsegite"; "Scene.Listed.Title" = "Zerrendatua"; "Scene.Lists.Icons.Create" = "Sortu zerrenda"; @@ -398,7 +403,6 @@ Oraindik hasierako fasean dago."; "Scene.Settings.About.Logo.BackgroundShadow" = "Orrialdearen hondoko logotipoaren itzalari buruz"; "Scene.Settings.About.Title" = "Honi buruz"; "Scene.Settings.About.Version" = "%@ ikusi"; -"Scene.Settings.Account.AccountSettings" = "Account Settings"; "Scene.Settings.Account.BlockedPeople" = "Blocked People"; "Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; "Scene.Settings.Account.MutedPeople" = "Muted People"; @@ -425,6 +429,22 @@ Oraindik hasierako fasean dago."; "Scene.Settings.Appearance.Translation.Off" = "Itzalita"; "Scene.Settings.Appearance.Translation.Service" = "Service"; "Scene.Settings.Appearance.Translation.TranslateButton" = "Translate button"; +"Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord" = "Enable History Record"; +"Scene.Settings.Behaviors.HistorySection.History" = "Historia"; +"Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels" = "Show tab bar labels"; +"Scene.Settings.Behaviors.TabBarSection.TabBar" = "Tab Bar"; +"Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop" = "Tap tab bar scroll to top"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline" = "Automatically refresh timeline"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval" = "Refresh interval"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds" = "120 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds" = "300 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds" = "30 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds" = "60 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop" = "Reset to top"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap" = "Double Tap"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap" = "Single Tap"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing" = "Timeline Refreshing"; +"Scene.Settings.Behaviors.Title" = "Behaviors"; "Scene.Settings.Display.DateFormat.Absolute" = "Absolutua"; "Scene.Settings.Display.DateFormat.Relative" = "Erlatiboa"; "Scene.Settings.Display.Media.Always" = "Beti"; @@ -434,6 +454,7 @@ Oraindik hasierako fasean dago."; "Scene.Settings.Display.Media.MuteByDefault" = "Isilarazi lehenetsita"; "Scene.Settings.Display.Media.Off" = "Itzalita"; "Scene.Settings.Display.Preview.ThankForUsingTwidereX" = "Eskerrik asko @TwidereProject erabiltzeagatik!"; +"Scene.Settings.Display.SectionHeader.Avatar" = "Avatar"; "Scene.Settings.Display.SectionHeader.DateFormat" = "Data-formatua"; "Scene.Settings.Display.SectionHeader.Media" = "Multimedia"; "Scene.Settings.Display.SectionHeader.Preview" = "Aurrebista"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings index 101effd7..a250cb41 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings @@ -192,8 +192,8 @@ "Common.Controls.Friendship.Actions.Unfollow" = "Deixar de seguir"; "Common.Controls.Friendship.Actions.Unmute" = "Non acalar"; "Common.Controls.Friendship.BlockUser" = "Bloquear a %@"; -"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Do you want to report and block %@"; -"Common.Controls.Friendship.DoYouWantToReportUser" = "Do you want to report %@"; +"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Queres denunciar e bloquear a %@"; +"Common.Controls.Friendship.DoYouWantToReportUser" = "Queres denunciar a %@"; "Common.Controls.Friendship.Follower" = "seguidora"; "Common.Controls.Friendship.Followers" = "seguidores"; "Common.Controls.Friendship.FollowsYou" = "Séguete"; @@ -285,7 +285,7 @@ "Scene.Compose.Media.Remove" = "Eliminar"; "Scene.Compose.OthersInThisConversation" = "Máis persoas nesta conversa:"; "Scene.Compose.Placeholder" = "Que está a pasar?"; -"Scene.Compose.ReplySettings.EveryoneCanReply" = "Todos poden responder"; +"Scene.Compose.ReplySettings.EveryoneCanReply" = "Everyone can reply"; "Scene.Compose.ReplySettings.OnlyPeopleYouMentionCanReply" = "Só as persoas que segues poden responder"; "Scene.Compose.ReplySettings.PeopleYouFollowCanReply" = "As persoas que segues poden responder"; "Scene.Compose.ReplyTo" = "Responder a …"; @@ -322,6 +322,11 @@ "Scene.Federated.Title" = "Federada"; "Scene.Followers.Title" = "Seguidores"; "Scene.Following.Title" = "Seguindo"; +"Scene.History.Clear" = "Limpar"; +"Scene.History.Scope.Toot" = "Toot"; +"Scene.History.Scope.Tweet" = "Chío"; +"Scene.History.Scope.User" = "User"; +"Scene.History.Title" = "Historial"; "Scene.Likes.Title" = "Favoritos"; "Scene.Listed.Title" = "Listado"; "Scene.Lists.Icons.Create" = "Xerar lista"; @@ -398,10 +403,9 @@ Aínda en fase previa."; "Scene.Settings.About.Logo.BackgroundShadow" = "Sobre a sombra do logo de fondo da páxina"; "Scene.Settings.About.Title" = "Sobre"; "Scene.Settings.About.Version" = "Ver %@"; -"Scene.Settings.Account.AccountSettings" = "Account Settings"; -"Scene.Settings.Account.BlockedPeople" = "Blocked People"; -"Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; -"Scene.Settings.Account.MutedPeople" = "Muted People"; +"Scene.Settings.Account.BlockedPeople" = "Persoas bloqueadas"; +"Scene.Settings.Account.MuteAndBlock" = "Silenciar e bloquear"; +"Scene.Settings.Account.MutedPeople" = "Persoas silenciadas"; "Scene.Settings.Account.Title" = "Conta"; "Scene.Settings.Appearance.AmoledOptimizedMode" = "Modo otimizado para AMOLED"; "Scene.Settings.Appearance.AppIcon" = "Icona da app"; @@ -425,6 +429,22 @@ Aínda en fase previa."; "Scene.Settings.Appearance.Translation.Off" = "Apagada"; "Scene.Settings.Appearance.Translation.Service" = "Servizo"; "Scene.Settings.Appearance.Translation.TranslateButton" = "Botón traducir"; +"Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord" = "Enable History Record"; +"Scene.Settings.Behaviors.HistorySection.History" = "Historial"; +"Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels" = "Show tab bar labels"; +"Scene.Settings.Behaviors.TabBarSection.TabBar" = "Tab Bar"; +"Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop" = "Tap tab bar scroll to top"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline" = "Automatically refresh timeline"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval" = "Refresh interval"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds" = "120 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds" = "300 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds" = "30 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds" = "60 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop" = "Reset to top"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap" = "Double Tap"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap" = "Single Tap"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing" = "Timeline Refreshing"; +"Scene.Settings.Behaviors.Title" = "Behaviors"; "Scene.Settings.Display.DateFormat.Absolute" = "Absoluta"; "Scene.Settings.Display.DateFormat.Relative" = "Relativa"; "Scene.Settings.Display.Media.Always" = "Sempre"; @@ -434,6 +454,7 @@ Aínda en fase previa."; "Scene.Settings.Display.Media.MuteByDefault" = "Acalar por defecto"; "Scene.Settings.Display.Media.Off" = "Apagada"; "Scene.Settings.Display.Preview.ThankForUsingTwidereX" = "Grazas por usar @TwidereProject!"; +"Scene.Settings.Display.SectionHeader.Avatar" = "Avatar"; "Scene.Settings.Display.SectionHeader.DateFormat" = "Formato da data"; "Scene.Settings.Display.SectionHeader.Media" = "Multimedia"; "Scene.Settings.Display.SectionHeader.Preview" = "Vista previa"; @@ -473,13 +494,13 @@ Aínda en fase previa."; "Scene.Settings.Misc.Proxy.Username" = "Nome de usuaria"; "Scene.Settings.Misc.Title" = "Miscelânea"; "Scene.Settings.Notification.Accounts" = "Contas"; -"Scene.Settings.Notification.Mastodon.Favorite" = "Favorite"; -"Scene.Settings.Notification.Mastodon.Mention" = "Mention"; -"Scene.Settings.Notification.Mastodon.NewFollow" = "New Follow"; -"Scene.Settings.Notification.Mastodon.Poll" = "poll"; -"Scene.Settings.Notification.Mastodon.Reblog" = "Reblog"; +"Scene.Settings.Notification.Mastodon.Favorite" = "Favorito"; +"Scene.Settings.Notification.Mastodon.Mention" = "Mención"; +"Scene.Settings.Notification.Mastodon.NewFollow" = "Novo seguimento"; +"Scene.Settings.Notification.Mastodon.Poll" = "enquisa"; +"Scene.Settings.Notification.Mastodon.Reblog" = "Promover"; "Scene.Settings.Notification.NotificationSwitch" = "Amosar notificación"; -"Scene.Settings.Notification.PushNotification" = "Push Notification"; +"Scene.Settings.Notification.PushNotification" = "Notificacións emerxentes"; "Scene.Settings.Notification.Title" = "Notificación"; "Scene.Settings.SectionHeader.About" = "Sobre"; "Scene.Settings.SectionHeader.Account" = "Conta"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.stringsdict index 230796c1..d49ef0cc 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.stringsdict @@ -13,9 +13,9 @@ NSStringFormatValueTypeKey ld one - 1 notification + 1 notificación other - %ld notifications + %ld notificacións count.media diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings index 3c8699fb..b9154387 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings @@ -285,7 +285,7 @@ "Scene.Compose.Media.Remove" = "削除"; "Scene.Compose.OthersInThisConversation" = "この会話の他の人:"; "Scene.Compose.Placeholder" = "いまどうしてる?"; -"Scene.Compose.ReplySettings.EveryoneCanReply" = "誰でもリプライすることができます。"; +"Scene.Compose.ReplySettings.EveryoneCanReply" = "Everyone can reply"; "Scene.Compose.ReplySettings.OnlyPeopleYouMentionCanReply" = "返信できるのはあなたがメンションした人のみです"; "Scene.Compose.ReplySettings.PeopleYouFollowCanReply" = "フォローしている人が返信できます"; "Scene.Compose.ReplyTo" = "返信 ..."; @@ -322,6 +322,11 @@ "Scene.Federated.Title" = "連合"; "Scene.Followers.Title" = "フォロワー"; "Scene.Following.Title" = "フォロー中"; +"Scene.History.Clear" = "削除"; +"Scene.History.Scope.Toot" = "トゥート"; +"Scene.History.Scope.Tweet" = "ツイート"; +"Scene.History.Scope.User" = "User"; +"Scene.History.Title" = "履歴"; "Scene.Likes.Title" = "いいね"; "Scene.Listed.Title" = "リスト"; "Scene.Lists.Icons.Create" = "リストを作成"; @@ -398,7 +403,6 @@ "Scene.Settings.About.Logo.BackgroundShadow" = "情報ページの背景のロゴの影"; "Scene.Settings.About.Title" = "情報"; "Scene.Settings.About.Version" = "Ver %@"; -"Scene.Settings.Account.AccountSettings" = "Account Settings"; "Scene.Settings.Account.BlockedPeople" = "Blocked People"; "Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; "Scene.Settings.Account.MutedPeople" = "Muted People"; @@ -425,6 +429,22 @@ "Scene.Settings.Appearance.Translation.Off" = "オフ"; "Scene.Settings.Appearance.Translation.Service" = "サービス"; "Scene.Settings.Appearance.Translation.TranslateButton" = "翻訳ボタン"; +"Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord" = "Enable History Record"; +"Scene.Settings.Behaviors.HistorySection.History" = "履歴"; +"Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels" = "Show tab bar labels"; +"Scene.Settings.Behaviors.TabBarSection.TabBar" = "Tab Bar"; +"Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop" = "Tap tab bar scroll to top"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline" = "Automatically refresh timeline"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval" = "Refresh interval"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds" = "120 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds" = "300 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds" = "30 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds" = "60 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop" = "Reset to top"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap" = "Double Tap"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap" = "Single Tap"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing" = "Timeline Refreshing"; +"Scene.Settings.Behaviors.Title" = "Behaviors"; "Scene.Settings.Display.DateFormat.Absolute" = "絶対"; "Scene.Settings.Display.DateFormat.Relative" = "相対"; "Scene.Settings.Display.Media.Always" = "常に"; @@ -434,6 +454,7 @@ "Scene.Settings.Display.Media.MuteByDefault" = "デフォルトでミュート"; "Scene.Settings.Display.Media.Off" = "オフ"; "Scene.Settings.Display.Preview.ThankForUsingTwidereX" = "@TwidereProject をご利用いただきありがとうございます!"; +"Scene.Settings.Display.SectionHeader.Avatar" = "Avatar"; "Scene.Settings.Display.SectionHeader.DateFormat" = "日付フォーマット"; "Scene.Settings.Display.SectionHeader.Media" = "メディア"; "Scene.Settings.Display.SectionHeader.Preview" = "プレビュー"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings index 3d835bb7..a191b91b 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings @@ -285,7 +285,7 @@ "Scene.Compose.Media.Remove" = "지우기"; "Scene.Compose.OthersInThisConversation" = "이 대화에 있는 다른 사람"; "Scene.Compose.Placeholder" = "무슨 일이 일어나고 있나요?"; -"Scene.Compose.ReplySettings.EveryoneCanReply" = "Everyone can peply"; +"Scene.Compose.ReplySettings.EveryoneCanReply" = "Everyone can reply"; "Scene.Compose.ReplySettings.OnlyPeopleYouMentionCanReply" = "Only people you mention can reply"; "Scene.Compose.ReplySettings.PeopleYouFollowCanReply" = "People you follow can reply"; "Scene.Compose.ReplyTo" = "…에 답글"; @@ -322,6 +322,11 @@ "Scene.Federated.Title" = "연합"; "Scene.Followers.Title" = "팔로워"; "Scene.Following.Title" = "팔로우 중"; +"Scene.History.Clear" = "Clear"; +"Scene.History.Scope.Toot" = "툿"; +"Scene.History.Scope.Tweet" = "트윗"; +"Scene.History.Scope.User" = "User"; +"Scene.History.Title" = "기록"; "Scene.Likes.Title" = "좋아요"; "Scene.Listed.Title" = "담긴 리스트"; "Scene.Lists.Icons.Create" = "리스트 만들기"; @@ -398,7 +403,6 @@ "Scene.Settings.About.Logo.BackgroundShadow" = "페이지 바탕 로고의 그림자에 대하여"; "Scene.Settings.About.Title" = "정보"; "Scene.Settings.About.Version" = "%@ 버전"; -"Scene.Settings.Account.AccountSettings" = "Account Settings"; "Scene.Settings.Account.BlockedPeople" = "Blocked People"; "Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; "Scene.Settings.Account.MutedPeople" = "Muted People"; @@ -425,6 +429,22 @@ "Scene.Settings.Appearance.Translation.Off" = "끄기"; "Scene.Settings.Appearance.Translation.Service" = "Service"; "Scene.Settings.Appearance.Translation.TranslateButton" = "Translate button"; +"Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord" = "Enable History Record"; +"Scene.Settings.Behaviors.HistorySection.History" = "기록"; +"Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels" = "Show tab bar labels"; +"Scene.Settings.Behaviors.TabBarSection.TabBar" = "Tab Bar"; +"Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop" = "Tap tab bar scroll to top"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline" = "Automatically refresh timeline"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval" = "Refresh interval"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds" = "120 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds" = "300 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds" = "30 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds" = "60 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop" = "Reset to top"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap" = "Double Tap"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap" = "Single Tap"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing" = "Timeline Refreshing"; +"Scene.Settings.Behaviors.Title" = "Behaviors"; "Scene.Settings.Display.DateFormat.Absolute" = "절대 시간"; "Scene.Settings.Display.DateFormat.Relative" = "상대적 시간 (~분 전)"; "Scene.Settings.Display.Media.Always" = "언제나"; @@ -434,6 +454,7 @@ "Scene.Settings.Display.Media.MuteByDefault" = "기본으로 숨기기"; "Scene.Settings.Display.Media.Off" = "끄기"; "Scene.Settings.Display.Preview.ThankForUsingTwidereX" = "@TwidereProject를 써주셔서 고맙습니다!"; +"Scene.Settings.Display.SectionHeader.Avatar" = "Avatar"; "Scene.Settings.Display.SectionHeader.DateFormat" = "날짜 형식"; "Scene.Settings.Display.SectionHeader.Media" = "미디어"; "Scene.Settings.Display.SectionHeader.Preview" = "미리보기"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings index caba44f9..4e93c32a 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings @@ -192,8 +192,8 @@ "Common.Controls.Friendship.Actions.Unfollow" = "Deixar de seguir"; "Common.Controls.Friendship.Actions.Unmute" = "Dessilenciar"; "Common.Controls.Friendship.BlockUser" = "Bloquear %@"; -"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Do you want to report and block %@"; -"Common.Controls.Friendship.DoYouWantToReportUser" = "Do you want to report %@"; +"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Deseja reportar e bloquear %@"; +"Common.Controls.Friendship.DoYouWantToReportUser" = "Você quer reportar %@"; "Common.Controls.Friendship.Follower" = "seguidor"; "Common.Controls.Friendship.Followers" = "seguidores"; "Common.Controls.Friendship.FollowsYou" = "Segue você"; @@ -285,7 +285,7 @@ "Scene.Compose.Media.Remove" = "Remover"; "Scene.Compose.OthersInThisConversation" = "Outros nesta conversa:"; "Scene.Compose.Placeholder" = "O que está acontecendo?"; -"Scene.Compose.ReplySettings.EveryoneCanReply" = "Todos podem responder"; +"Scene.Compose.ReplySettings.EveryoneCanReply" = "Everyone can reply"; "Scene.Compose.ReplySettings.OnlyPeopleYouMentionCanReply" = "Apenas as pessoas mencionadas podem responder"; "Scene.Compose.ReplySettings.PeopleYouFollowCanReply" = "As pessoas que você segue podem responder"; "Scene.Compose.ReplyTo" = "Resposta para …"; @@ -322,6 +322,11 @@ "Scene.Federated.Title" = "Federado"; "Scene.Followers.Title" = "Seguidores"; "Scene.Following.Title" = "Seguindo"; +"Scene.History.Clear" = "Limpar"; +"Scene.History.Scope.Toot" = "Toot"; +"Scene.History.Scope.Tweet" = "Tweet"; +"Scene.History.Scope.User" = "User"; +"Scene.History.Title" = "Histórico"; "Scene.Likes.Title" = "Curtidas"; "Scene.Listed.Title" = "Listado"; "Scene.Lists.Icons.Create" = "Criar lista"; @@ -398,10 +403,9 @@ Ainda na fase inicial."; "Scene.Settings.About.Logo.BackgroundShadow" = "Sobre a sombra do logo da página de fundo"; "Scene.Settings.About.Title" = "Sobre"; "Scene.Settings.About.Version" = "Ver %@"; -"Scene.Settings.Account.AccountSettings" = "Account Settings"; -"Scene.Settings.Account.BlockedPeople" = "Blocked People"; -"Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; -"Scene.Settings.Account.MutedPeople" = "Muted People"; +"Scene.Settings.Account.BlockedPeople" = "Pessoas Bloqueadas"; +"Scene.Settings.Account.MuteAndBlock" = "Silenciar e Bloquear"; +"Scene.Settings.Account.MutedPeople" = "Pessoas Silenciadas"; "Scene.Settings.Account.Title" = "Conta"; "Scene.Settings.Appearance.AmoledOptimizedMode" = "Modo otimizado para AMOLED"; "Scene.Settings.Appearance.AppIcon" = "Ícone do Aplicativo"; @@ -425,6 +429,22 @@ Ainda na fase inicial."; "Scene.Settings.Appearance.Translation.Off" = "Desligado"; "Scene.Settings.Appearance.Translation.Service" = "Serviço"; "Scene.Settings.Appearance.Translation.TranslateButton" = "Botão de tradução"; +"Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord" = "Enable History Record"; +"Scene.Settings.Behaviors.HistorySection.History" = "Histórico"; +"Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels" = "Show tab bar labels"; +"Scene.Settings.Behaviors.TabBarSection.TabBar" = "Tab Bar"; +"Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop" = "Tap tab bar scroll to top"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline" = "Automatically refresh timeline"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval" = "Refresh interval"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds" = "120 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds" = "300 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds" = "30 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds" = "60 seconds"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop" = "Reset to top"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap" = "Double Tap"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap" = "Single Tap"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing" = "Timeline Refreshing"; +"Scene.Settings.Behaviors.Title" = "Behaviors"; "Scene.Settings.Display.DateFormat.Absolute" = "Absoluto"; "Scene.Settings.Display.DateFormat.Relative" = "Relativo"; "Scene.Settings.Display.Media.Always" = "Sempre"; @@ -434,6 +454,7 @@ Ainda na fase inicial."; "Scene.Settings.Display.Media.MuteByDefault" = "Silenciar por padrão"; "Scene.Settings.Display.Media.Off" = "Desligado"; "Scene.Settings.Display.Preview.ThankForUsingTwidereX" = "Obrigado por usar o @TwidereProject!"; +"Scene.Settings.Display.SectionHeader.Avatar" = "Avatar"; "Scene.Settings.Display.SectionHeader.DateFormat" = "Formato da Data"; "Scene.Settings.Display.SectionHeader.Media" = "Mídia"; "Scene.Settings.Display.SectionHeader.Preview" = "Pré-visualização"; @@ -473,13 +494,13 @@ Ainda na fase inicial."; "Scene.Settings.Misc.Proxy.Username" = "Nome de usuário"; "Scene.Settings.Misc.Title" = "Miscelânea"; "Scene.Settings.Notification.Accounts" = "Contas"; -"Scene.Settings.Notification.Mastodon.Favorite" = "Favorite"; -"Scene.Settings.Notification.Mastodon.Mention" = "Mention"; -"Scene.Settings.Notification.Mastodon.NewFollow" = "New Follow"; -"Scene.Settings.Notification.Mastodon.Poll" = "poll"; -"Scene.Settings.Notification.Mastodon.Reblog" = "Reblog"; +"Scene.Settings.Notification.Mastodon.Favorite" = "Favorito"; +"Scene.Settings.Notification.Mastodon.Mention" = "Menção"; +"Scene.Settings.Notification.Mastodon.NewFollow" = "Novo Seguidor"; +"Scene.Settings.Notification.Mastodon.Poll" = "enquete"; +"Scene.Settings.Notification.Mastodon.Reblog" = "Reblogar"; "Scene.Settings.Notification.NotificationSwitch" = "Mostrar Notificação"; -"Scene.Settings.Notification.PushNotification" = "Push Notification"; +"Scene.Settings.Notification.PushNotification" = "Notificação Push"; "Scene.Settings.Notification.Title" = "Notificação"; "Scene.Settings.SectionHeader.About" = "Sobre"; "Scene.Settings.SectionHeader.Account" = "Conta"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.stringsdict index 149368ad..96ae236b 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.stringsdict @@ -13,9 +13,9 @@ NSStringFormatValueTypeKey ld one - 1 notification + 1 notificação other - %ld notifications + %ld notificações count.media diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings index 44f70bba..653d981a 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings @@ -192,8 +192,8 @@ "Common.Controls.Friendship.Actions.Unfollow" = "Takibi bırak"; "Common.Controls.Friendship.Actions.Unmute" = "Sessizden çıkar"; "Common.Controls.Friendship.BlockUser" = "%@ Engelle"; -"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Do you want to report and block %@"; -"Common.Controls.Friendship.DoYouWantToReportUser" = "Do you want to report %@"; +"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "%@ kullanıcısını bildirmek ve engellemek istiyor musunuz"; +"Common.Controls.Friendship.DoYouWantToReportUser" = "%@ kullanıcısını bildirmek istiyor musunuz"; "Common.Controls.Friendship.Follower" = "takipçi"; "Common.Controls.Friendship.Followers" = "takipçiler"; "Common.Controls.Friendship.FollowsYou" = "Sizi takip ediyor"; @@ -226,8 +226,8 @@ "Common.Controls.Status.Poll.TotalPerson" = "%@ kişi"; "Common.Controls.Status.Poll.TotalVote" = "%@ oy"; "Common.Controls.Status.Poll.TotalVotes" = "%@ oylar"; -"Common.Controls.Status.ReplySettings.PeopleUserFollowsOrMentionedCanReply" = "People %@ follows or mentioned can reply."; -"Common.Controls.Status.ReplySettings.PeopleUserMentionedCanReply" = "People %@ mentioned can reply."; +"Common.Controls.Status.ReplySettings.PeopleUserFollowsOrMentionedCanReply" = "%@ takip eden veya adı geçen kişiler cevap verebilir."; +"Common.Controls.Status.ReplySettings.PeopleUserMentionedCanReply" = "%@ bahsi geçen kişiler yanıt verebilir."; "Common.Controls.Status.Thread.Show" = "Bu konuyu göster"; "Common.Controls.Status.UserBoosted" = "%@ artırıldı"; "Common.Controls.Status.UserRetweeted" = "%@ retweetledi"; @@ -322,6 +322,11 @@ "Scene.Federated.Title" = "Federe olmuş"; "Scene.Followers.Title" = "Takipçiler"; "Scene.Following.Title" = "Takip ediliyor"; +"Scene.History.Clear" = "Temizle"; +"Scene.History.Scope.Toot" = "Toot"; +"Scene.History.Scope.Tweet" = "Tweet'le"; +"Scene.History.Scope.User" = "Kullanıcı"; +"Scene.History.Title" = "Geçmiş"; "Scene.Likes.Title" = "Beğeniler"; "Scene.Listed.Title" = "Sıralanan"; "Scene.Lists.Icons.Create" = "Liste oluştur"; @@ -386,7 +391,7 @@ "Scene.Search.ShowMore" = "Daha fazla göster"; "Scene.Search.Tabs.Hashtag" = "Etiket"; "Scene.Search.Tabs.Media" = "Medya"; -"Scene.Search.Tabs.People" = "İnsanlar"; +"Scene.Search.Tabs.People" = "Kişiler"; "Scene.Search.Tabs.Toots" = "Tootlar"; "Scene.Search.Tabs.Tweets" = "Tweetler"; "Scene.Search.Tabs.Users" = "Kullanıcılar"; @@ -398,10 +403,9 @@ Hala erken aşamada."; "Scene.Settings.About.Logo.BackgroundShadow" = "Sayfa arka plan logosu gölgesi hakkında"; "Scene.Settings.About.Title" = "Hakkında"; "Scene.Settings.About.Version" = "Sür %@"; -"Scene.Settings.Account.AccountSettings" = "Account Settings"; -"Scene.Settings.Account.BlockedPeople" = "Blocked People"; -"Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; -"Scene.Settings.Account.MutedPeople" = "Muted People"; +"Scene.Settings.Account.BlockedPeople" = "Engellenmiş kişiler"; +"Scene.Settings.Account.MuteAndBlock" = "Sustur ve Engelle"; +"Scene.Settings.Account.MutedPeople" = "Susturulmuş kişiler"; "Scene.Settings.Account.Title" = "Hesabım"; "Scene.Settings.Appearance.AmoledOptimizedMode" = "AMOLED optimize edilmiş mod"; "Scene.Settings.Appearance.AppIcon" = "Uygulama Simgesi"; @@ -425,6 +429,22 @@ Hala erken aşamada."; "Scene.Settings.Appearance.Translation.Off" = "Kapalı"; "Scene.Settings.Appearance.Translation.Service" = "Hizmet"; "Scene.Settings.Appearance.Translation.TranslateButton" = "Çevir butonu"; +"Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord" = "Enable History Record"; +"Scene.Settings.Behaviors.HistorySection.History" = "Geçmiş"; +"Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels" = "Sekme çubuğu etiketlerini göster"; +"Scene.Settings.Behaviors.TabBarSection.TabBar" = "Sekme Çubuğu"; +"Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop" = "Yukarı kaydırmak için sekme çubuğuna dokun"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline" = "Otomatik olarak zaman çizelgesini yenile"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval" = "Yenileme aralığı"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds" = "120 saniye"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds" = "300 saniye"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds" = "30 saniye"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds" = "60 saniye"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop" = "Başa sıfırla"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap" = "Çift dokunuş"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap" = "Tek dokunuş"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing" = "Zaman Çizelgesi Yenileniyor"; +"Scene.Settings.Behaviors.Title" = "Davranışlar"; "Scene.Settings.Display.DateFormat.Absolute" = "Kesin"; "Scene.Settings.Display.DateFormat.Relative" = "Göreceli"; "Scene.Settings.Display.Media.Always" = "Daima"; @@ -434,6 +454,7 @@ Hala erken aşamada."; "Scene.Settings.Display.Media.MuteByDefault" = "Varsayılan olarak sustur"; "Scene.Settings.Display.Media.Off" = "Kapalı"; "Scene.Settings.Display.Preview.ThankForUsingTwidereX" = "@TwidereProject kullandığınız için teşekkürler!"; +"Scene.Settings.Display.SectionHeader.Avatar" = "Profil Resmi"; "Scene.Settings.Display.SectionHeader.DateFormat" = "Tarih biçimi"; "Scene.Settings.Display.SectionHeader.Media" = "Medya"; "Scene.Settings.Display.SectionHeader.Preview" = "Önizleme"; @@ -473,13 +494,13 @@ Hala erken aşamada."; "Scene.Settings.Misc.Proxy.Username" = "Kullanıcı adı"; "Scene.Settings.Misc.Title" = "Diğer"; "Scene.Settings.Notification.Accounts" = "Hesaplar"; -"Scene.Settings.Notification.Mastodon.Favorite" = "Favorite"; -"Scene.Settings.Notification.Mastodon.Mention" = "Mention"; -"Scene.Settings.Notification.Mastodon.NewFollow" = "New Follow"; -"Scene.Settings.Notification.Mastodon.Poll" = "poll"; -"Scene.Settings.Notification.Mastodon.Reblog" = "Reblog"; +"Scene.Settings.Notification.Mastodon.Favorite" = "Favori"; +"Scene.Settings.Notification.Mastodon.Mention" = "Bahset"; +"Scene.Settings.Notification.Mastodon.NewFollow" = "Yeni takip"; +"Scene.Settings.Notification.Mastodon.Poll" = "anket"; +"Scene.Settings.Notification.Mastodon.Reblog" = "Yeniden yayınla"; "Scene.Settings.Notification.NotificationSwitch" = "Bildirim göster"; -"Scene.Settings.Notification.PushNotification" = "Push Notification"; +"Scene.Settings.Notification.PushNotification" = "Anlık bildirim"; "Scene.Settings.Notification.Title" = "Bildirim"; "Scene.Settings.SectionHeader.About" = "Hakkında"; "Scene.Settings.SectionHeader.Account" = "Hesabım"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.stringsdict index d90abaa6..6bcab9ac 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.stringsdict @@ -13,9 +13,9 @@ NSStringFormatValueTypeKey ld one - 1 notification + 1 bildirim other - %ld notifications + %ld bildirim count.media diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings index e6bdafcb..fcadef52 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings @@ -322,6 +322,11 @@ "Scene.Federated.Title" = "跨站"; "Scene.Followers.Title" = "关注者"; "Scene.Following.Title" = "已关注"; +"Scene.History.Clear" = "清除"; +"Scene.History.Scope.Toot" = "嘟文"; +"Scene.History.Scope.Tweet" = "推文"; +"Scene.History.Scope.User" = "用户"; +"Scene.History.Title" = "历史"; "Scene.Likes.Title" = "喜欢"; "Scene.Listed.Title" = "列表中"; "Scene.Lists.Icons.Create" = "创建列表"; @@ -398,7 +403,6 @@ "Scene.Settings.About.Logo.BackgroundShadow" = "关于页面背景徽标阴影"; "Scene.Settings.About.Title" = "关于"; "Scene.Settings.About.Version" = "版本 %@"; -"Scene.Settings.Account.AccountSettings" = "账户设置"; "Scene.Settings.Account.BlockedPeople" = "已屏蔽用户"; "Scene.Settings.Account.MuteAndBlock" = "静音和屏蔽"; "Scene.Settings.Account.MutedPeople" = "已静音用户"; @@ -425,6 +429,22 @@ "Scene.Settings.Appearance.Translation.Off" = "关闭"; "Scene.Settings.Appearance.Translation.Service" = "服务"; "Scene.Settings.Appearance.Translation.TranslateButton" = "翻译按钮"; +"Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord" = "启用历史记录"; +"Scene.Settings.Behaviors.HistorySection.History" = "历史"; +"Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels" = "显示标签栏文本"; +"Scene.Settings.Behaviors.TabBarSection.TabBar" = "标签栏"; +"Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop" = "点击标签栏时滚动到顶部"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline" = "自动刷新时间线"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval" = "刷新间隔"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds" = "120 秒"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds" = "300 秒"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds" = "30 秒"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds" = "60 秒"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop" = "重置到顶部"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap" = "双击"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap" = "单击"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing" = "时间线刷新"; +"Scene.Settings.Behaviors.Title" = "行为"; "Scene.Settings.Display.DateFormat.Absolute" = "绝对的"; "Scene.Settings.Display.DateFormat.Relative" = "相对的"; "Scene.Settings.Display.Media.Always" = "总是"; @@ -434,6 +454,7 @@ "Scene.Settings.Display.Media.MuteByDefault" = "默认静音"; "Scene.Settings.Display.Media.Off" = "关闭"; "Scene.Settings.Display.Preview.ThankForUsingTwidereX" = "感谢您使用 @TwidereProject !"; +"Scene.Settings.Display.SectionHeader.Avatar" = "头像"; "Scene.Settings.Display.SectionHeader.DateFormat" = "时间格式"; "Scene.Settings.Display.SectionHeader.Media" = "媒体"; "Scene.Settings.Display.SectionHeader.Preview" = "预览"; diff --git a/TwidereX/Diffable/Misc/Sidebar/SidebarItem.swift b/TwidereX/Diffable/Misc/Sidebar/SidebarItem.swift index 8c6e32e4..dfba6ad7 100644 --- a/TwidereX/Diffable/Misc/Sidebar/SidebarItem.swift +++ b/TwidereX/Diffable/Misc/Sidebar/SidebarItem.swift @@ -30,7 +30,7 @@ extension SidebarItem { case .federated: return L10n.Scene.Federated.title case .messages: return L10n.Scene.Messages.title case .likes: return L10n.Scene.Likes.title - case .history: return "History" // TODO: i18n + case .history: return L10n.Scene.History.title case .lists: return L10n.Scene.Lists.title case .trends: return L10n.Scene.Trends.title case .drafts: return L10n.Scene.Drafts.title diff --git a/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift b/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift index 218fd8e8..5599b015 100644 --- a/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift +++ b/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift @@ -43,7 +43,7 @@ extension TabBarItem { case .federated: return L10n.Scene.Federated.title case .messages: return L10n.Scene.Messages.title case .likes: return L10n.Scene.Likes.title - case .history: return "History" // TODO: i18n + case .history: return L10n.Scene.History.title case .lists: return L10n.Scene.Lists.title case .trends: return L10n.Scene.Trends.title case .drafts: return L10n.Scene.Drafts.title diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Report.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Report.swift index d9b53b91..07534c06 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Report.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Report.swift @@ -85,8 +85,7 @@ extension DataSourceFacade { } } - // TODO: i18n - let alertControllerTitle = performBlock ? "Do you want to report and block \(reportAlertContext.name)" : "Do you want to report \(reportAlertContext.name)" + let alertControllerTitle = performBlock ? L10n.Common.Controls.Friendship.doYouWantToReportAndBlockUser(reportAlertContext.name) : L10n.Common.Controls.Friendship.doYouWantToReportUser(reportAlertContext.name) let alertController = UIAlertController( title: alertControllerTitle, diff --git a/TwidereX/Scene/History/HistoryViewModel.swift b/TwidereX/Scene/History/HistoryViewModel.swift index 0de852cc..ff97eeb5 100644 --- a/TwidereX/Scene/History/HistoryViewModel.swift +++ b/TwidereX/Scene/History/HistoryViewModel.swift @@ -68,14 +68,14 @@ extension HistoryViewModel { switch self { case .status: switch platform { - case .twitter: return "Tweet" - case .mastodon: return "Toot" + case .twitter: return L10n.Scene.History.Scope.tweet + case .mastodon: return L10n.Scene.History.Scope.toot case .none: assertionFailure() return "Post" } case .user: - return "User" // TODO: i18n + return L10n.Scene.History.Scope.user } } } diff --git a/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceView.swift b/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceView.swift index f185906c..50fa7d4b 100644 --- a/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceView.swift +++ b/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceView.swift @@ -20,7 +20,7 @@ enum AccountPreferenceListEntry: Hashable { switch self { case .muted: return L10n.Scene.Settings.Account.mutedPeople case .blocked: return L10n.Scene.Settings.Account.blockedPeople - case .accountSettings: return L10n.Scene.Settings.Account.accountSettings + case .accountSettings: return "Account Settings" // TODO: i18n case .signout: return L10n.Common.Controls.Actions.signOut } } diff --git a/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionView.swift b/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionView.swift index ec17566a..2fc5c9f8 100644 --- a/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionView.swift +++ b/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionView.swift @@ -136,7 +136,7 @@ extension MastodonNotificationSectionView { extension MastodonNotificationSubscription.MentionPreference.Preference { fileprivate var title: String { switch self { - case .everyone: return "Everyone" + case .everyone: return "Everyone" // TODO: i18n case .follows: return "Follows" } } diff --git a/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceView.swift b/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceView.swift index 6b8df627..dc712773 100644 --- a/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceView.swift +++ b/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceView.swift @@ -19,23 +19,23 @@ struct BehaviorsPreferenceView: View { // Tab Bar Section { Toggle(isOn: $viewModel.preferredTabBarLabelDisplay) { - Text(verbatim: "Show tab bar labels") // TODO: i18n + Text(verbatim: L10n.Scene.Settings.Behaviors.TabBarSection.showTabBarLabels) } Picker(selection: $viewModel.tabBarTapScrollPreference) { ForEach(UserDefaults.TabBarTapScrollPreference.allCases, id: \.self) { preference in Text(preference.title) } } label: { - Text(verbatim: "Tap tab bar scroll to top") // TODO: i18n + Text(verbatim: L10n.Scene.Settings.Behaviors.TabBarSection.tapTabBarScrollToTop) } } header: { - Text(verbatim: "Tab Bar") // TODO: i18n + Text(verbatim: L10n.Scene.Settings.Behaviors.TabBarSection.tabBar) .textCase(nil) } // Timeline Refreshing Section { Toggle(isOn: $viewModel.preferredTimelineAutoRefresh) { - Text(verbatim: "Automatically refresh timeline") // TODO: i18n + Text(verbatim: L10n.Scene.Settings.Behaviors.TimelineRefreshingSection.automaticallyRefreshTimeline) } if viewModel.preferredTimelineAutoRefresh { Picker(selection: $viewModel.timelineRefreshInterval) { @@ -43,23 +43,23 @@ struct BehaviorsPreferenceView: View { Text(preference.title) } } label: { - Text(verbatim: "Refresh Interval") // TODO: i18n + Text(verbatim: L10n.Scene.Settings.Behaviors.TimelineRefreshingSection.refreshInterval) } } Toggle(isOn: $viewModel.preferredTimelineResetToTop) { - Text(verbatim: "Reset to top") // TODO: i18n + Text(verbatim: L10n.Scene.Settings.Behaviors.TimelineRefreshingSection.resetToTop) } } header: { - Text(verbatim: "Timeline Refreshing") // TODO: i18n + Text(verbatim: L10n.Scene.Settings.Behaviors.TimelineRefreshingSection.timelineRefreshing) .textCase(nil) } // History Section { Toggle(isOn: $viewModel.preferredEnableHistory) { - Text(verbatim: "Enable History Record") // TODO: i18n + Text(verbatim: L10n.Scene.Settings.Behaviors.HistorySection.enableHistoryRecord) } } header: { - Text(verbatim: "History") // TODO: i18n + Text(verbatim: L10n.Scene.Settings.Behaviors.HistorySection.history) .textCase(nil) } } diff --git a/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewController.swift b/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewController.swift index 3ef8deec..2f07f53f 100644 --- a/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewController.swift +++ b/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewController.swift @@ -34,7 +34,7 @@ extension BehaviorsPreferenceViewController { override func viewDidLoad() { super.viewDidLoad() - title = "Behaviors" // TODO: i18n + title = L10n.Scene.Settings.Behaviors.title let hostingViewController = UIHostingController(rootView: behaviorsPreferenceView) addChild(hostingViewController) diff --git a/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewModel.swift b/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewModel.swift index 006738cd..d0e9b9d5 100644 --- a/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewModel.swift +++ b/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewModel.swift @@ -35,7 +35,7 @@ final class BehaviorsPreferenceViewModel: ObservableObject { // History @Published var preferredEnableHistory = UserDefaults.shared.preferredEnableHistory - // output + // output init( context: AppContext diff --git a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceView.swift b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceView.swift index 8867ea92..6b5058ba 100644 --- a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceView.swift +++ b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceView.swift @@ -43,7 +43,7 @@ struct DisplayPreferenceView: View { Section { avatarStylePicker } header: { - Text(verbatim: "Avatar") // TODO: i18n + Text(verbatim: L10n.Scene.Settings.Display.SectionHeader.avatar) } // end Section // Translation diff --git a/TwidereX/Scene/Setting/List/SettingListView.swift b/TwidereX/Scene/Setting/List/SettingListView.swift index d1a5e1b9..b3899106 100644 --- a/TwidereX/Scene/Setting/List/SettingListView.swift +++ b/TwidereX/Scene/Setting/List/SettingListView.swift @@ -54,7 +54,7 @@ enum SettingListEntryType: Hashable { var title: String { switch self { case .account: return L10n.Scene.Settings.SectionHeader.account - case .behaviors: return "Behaviors" // TODO: i18n + case .behaviors: return L10n.Scene.Settings.Behaviors.title case .display: return L10n.Scene.Settings.Display.title case .layout: return "Layout" case .webBrowser: return "Web Browser" diff --git a/TwidereXIntent/tr.lproj/Intents.strings b/TwidereXIntent/tr.lproj/Intents.strings index 3c51c9c2..36846d61 100644 --- a/TwidereXIntent/tr.lproj/Intents.strings +++ b/TwidereXIntent/tr.lproj/Intents.strings @@ -1,14 +1,14 @@ -"0e8JfP" = "Just to confirm, you wanted ‘${account}’?"; +"0e8JfP" = "Onaylamak için, \"${account}\" mu istediniz?"; -"1iPE2d-BS59z4" = "Just to confirm, you wanted ‘Public’?"; +"1iPE2d-BS59z4" = "Sadece onaylamak için \"Halka açık\" olarak mı istediniz?"; -"1iPE2d-aV5Ezl" = "Just to confirm, you wanted ‘Default’?"; +"1iPE2d-aV5Ezl" = "Sadece onaylamak için \"Varsayılan\" olarak mı istediniz?"; -"1iPE2d-aV8f0g" = "Just to confirm, you wanted ‘Private’?"; +"1iPE2d-aV8f0g" = "Sadece onaylamak için \"Özel\" olarak mı istediniz?"; -"1iPE2d-aeaw8w" = "Just to confirm, you wanted ‘Direct’?"; +"1iPE2d-aeaw8w" = "Sadece onaylamak için \"Direkt\" olarak mı istediniz?"; -"1iPE2d-jEAHra" = "Just to confirm, you wanted ‘Unlisted’?"; +"1iPE2d-jEAHra" = "Sadece onaylamak için \"Listelenmemiş\" olarak mı istediniz?"; "8WWS78" = "Kullanıcı adı"; @@ -26,7 +26,7 @@ "G5v3xr" = "Hesabı etkinleştir"; -"GILN5g" = "There are ${count} options matching ‘${accounts}’."; +"GILN5g" = "\"${accounts}\" ile eşleşen ${count} seçenek var."; "N4GVJI" = "${errorDescription}"; @@ -36,7 +36,7 @@ "SnCJhk" = "Gönderiyi Yayınla"; -"SqRrlA" = "Just to confirm, you wanted ‘${accounts}’?"; +"SqRrlA" = "Sadece onaylamak için, \"${accounts}\" mu istediniz?"; "SrV2FP" = "Hata Açıklaması"; @@ -46,7 +46,7 @@ "Xs6a35" = "Toot Görünürlüğü"; -"YNUo2I" = "There are ${count} options matching ‘${account}’."; +"YNUo2I" = "\"${account}\" ile eşleşen ${count} seçenek var."; "Yb4tzx" = "Hesaplar"; @@ -56,27 +56,27 @@ "aeaw8w" = "Direkt"; -"ch7ADX" = "Compose new post"; +"ch7ADX" = "Yeni bir gönderi oluştur"; "f1YNIs" = "Gönderi İçeriği"; "jEAHra" = "Liste dışı"; -"kWl9zU" = "Post ${content}with ${accounts}"; +"kWl9zU" = "${content} ${accounts} ile yayınla"; "noeHVX" = "Gönderi"; -"vbgnbQ" = "Post ${content}with ${accounts}"; +"vbgnbQ" = "${content} ${accounts} ile yayınla"; -"xeqcBa-BS59z4" = "There are ${count} options matching ‘Public’."; +"xeqcBa-BS59z4" = "\"Herkese açık\" olarak eşleşen ${count} seçenek var."; -"xeqcBa-aV5Ezl" = "There are ${count} options matching ‘Default’."; +"xeqcBa-aV5Ezl" = "\"Varsayılan\" olarak eşleşen ${count} seçenek var."; -"xeqcBa-aV8f0g" = "There are ${count} options matching ‘Private’."; +"xeqcBa-aV8f0g" = "\"Özel\" olarak eşleşen ${count} seçenek var."; -"xeqcBa-aeaw8w" = "There are ${count} options matching ‘Direct’."; +"xeqcBa-aeaw8w" = "\"Direkt\" olarak eşleşen ${count} seçenek var."; -"xeqcBa-jEAHra" = "There are ${count} options matching ‘Unlisted’."; +"xeqcBa-jEAHra" = "\"Liste dışı\" olarak eşleşen ${count} seçenek var."; "z3DAP7" = "${account} hesabına geç"; From f9b0dcf4986306728197458003c6b77ed3937bf1 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 5 Aug 2022 11:47:49 +0800 Subject: [PATCH 011/128] fix: add optional checking to fix setter crash issue --- .../CoreDataStack/Entity/Twitter/TwitterStatus.swift | 8 ++++++-- .../CoreDataStack/Entity/Twitter/TwitterUser.swift | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift index aa33a4a7..b3d8c125 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift @@ -146,9 +146,13 @@ extension TwitterStatus { } set { let keyPath = #keyPath(TwitterStatus.entities) - let data = try? JSONEncoder().encode(newValue) willChangeValue(forKey: keyPath) - setPrimitiveValue(data, forKey: keyPath) + if let newValue = newValue { + let data = try? JSONEncoder().encode(newValue) + setPrimitiveValue(data, forKey: keyPath) + } else { + setPrimitiveValue(nil, forKey: keyPath) + } didChangeValue(forKey: keyPath) } } diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift index b717a34f..451e42fe 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift @@ -97,9 +97,13 @@ extension TwitterUser { } set { let keyPath = #keyPath(TwitterUser.bioEntities) - let data = try? JSONEncoder().encode(newValue) willChangeValue(forKey: keyPath) - setPrimitiveValue(data, forKey: keyPath) + if let newValue = newValue { + let data = try? JSONEncoder().encode(newValue) + setPrimitiveValue(data, forKey: keyPath) + } else { + setPrimitiveValue(nil, forKey: keyPath) + } didChangeValue(forKey: keyPath) } } From e788e5f056aeb4aadda9b5a57c8658018378713c Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 5 Aug 2022 17:10:31 +0800 Subject: [PATCH 012/128] fix: add token expired alert banner --- .../Banner/NotificationBannerView.swift | 1 + .../Provider/DataSourceFacade+Banner.swift | 34 ++++++++++++++++++- .../DataSourceFacade+Friendship.swift | 7 ++++ .../Provider/DataSourceFacade+Like.swift | 7 ++++ .../Welcome/WelcomeViewController.swift | 9 +++++ .../Root/MainTab/MainTabBarController.swift | 18 +++++++--- 6 files changed, 71 insertions(+), 5 deletions(-) diff --git a/TwidereSDK/Sources/TwidereUI/Banner/NotificationBannerView.swift b/TwidereSDK/Sources/TwidereUI/Banner/NotificationBannerView.swift index a6028da1..5dd98a04 100644 --- a/TwidereSDK/Sources/TwidereUI/Banner/NotificationBannerView.swift +++ b/TwidereSDK/Sources/TwidereUI/Banner/NotificationBannerView.swift @@ -23,6 +23,7 @@ public final class NotificationBannerView: UIView { let imageView = UIImageView() imageView.image = Asset.Indices.exclamationmarkCircle.image.withRenderingMode(.alwaysTemplate) imageView.tintColor = .white + imageView.contentMode = .scaleAspectFit return imageView }() diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Banner.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Banner.swift index 15af84a5..1d9767c6 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Banner.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Banner.swift @@ -7,7 +7,7 @@ // import os.log -import Foundation +import UIKit import SwiftMessages extension DataSourceFacade { @@ -43,3 +43,35 @@ extension DataSourceFacade { } } + +extension DataSourceFacade { + + @MainActor + public static func presentForbiddenBanner( + error: Error, + dependency: NeedsDependency + ) { + var config = SwiftMessages.defaultConfig + config.duration = .seconds(seconds: 15) + config.interactiveHide = true + let bannerView = NotificationBannerView() + bannerView.configure(style: .warning) + bannerView.titleLabel.text = "Forbidden" + bannerView.messageLabel.text = "Application token expired. Please sign in the app again to reactive." + bannerView.messageLabel.numberOfLines = 0 + bannerView.actionButtonTapHandler = { [weak dependency] _ in + guard let dependency = dependency else { return } + let welcomeViewModel = WelcomeViewModel( + context: dependency.context, + configuration: WelcomeViewModel.Configuration(allowDismissModal: true) + ) + dependency.coordinator.present( + scene: .welcome(viewModel: welcomeViewModel), + from: nil, + transition: .modal(animated: true, completion: nil) + ) + } + SwiftMessages.show(config: config, view: bannerView) + } + +} diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Friendship.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Friendship.swift index 754a3004..ec433444 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Friendship.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Friendship.swift @@ -10,6 +10,7 @@ import UIKit import CoreData import CoreDataStack import TwidereCore +import TwitterSDK import MastodonSDK import TwidereUI import SwiftMessages @@ -30,6 +31,12 @@ extension DataSourceFacade { authenticationContext: authenticationContext ) await notificationFeedbackGenerator.notificationOccurred(.success) + } catch let error as Twitter.API.Error.ResponseError where error.httpResponseStatus == .forbidden { + await notificationFeedbackGenerator.notificationOccurred(.error) + await presentForbiddenBanner( + error: error, + dependency: provider + ) } catch { await notificationFeedbackGenerator.notificationOccurred(.error) } diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Like.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Like.swift index a9102995..5557ba53 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Like.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Like.swift @@ -9,6 +9,7 @@ import UIKit import CoreData import CoreDataStack +import TwitterSDK extension DataSourceFacade { static func responseToStatusLikeAction( @@ -26,6 +27,12 @@ extension DataSourceFacade { authenticationContext: authenticationContext ) await notificationFeedbackGenerator.notificationOccurred(.success) + } catch let error as Twitter.API.Error.ResponseError where error.httpResponseStatus == .forbidden { + await notificationFeedbackGenerator.notificationOccurred(.error) + await presentForbiddenBanner( + error: error, + dependency: provider + ) } catch { await notificationFeedbackGenerator.notificationOccurred(.error) } diff --git a/TwidereX/Scene/Onboarding/Welcome/WelcomeViewController.swift b/TwidereX/Scene/Onboarding/Welcome/WelcomeViewController.swift index 37dd501c..c80075f6 100644 --- a/TwidereX/Scene/Onboarding/Welcome/WelcomeViewController.swift +++ b/TwidereX/Scene/Onboarding/Welcome/WelcomeViewController.swift @@ -75,6 +75,15 @@ extension WelcomeViewController { .sink { [weak self] error in guard let self = self else { return } let alertController = UIAlertController.standardAlert(of: error) + let action = UIAlertAction(title: "Contact", style: .default) { _ in + let url = URL(string: "https://twitter.com/twidereproject")! + self.coordinator.present( + scene: .safari(url: url.absoluteString), + from: nil, + transition: .safariPresent(animated: true, completion: nil) + ) + } + alertController.addAction(action) self.present(alertController, animated: true) } .store(in: &disposeBag) diff --git a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift index 59f808d6..98f68624 100644 --- a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift +++ b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift @@ -17,14 +17,14 @@ import TwidereUI import TwidereCommon import func QuartzCore.CACurrentMediaTime -final class MainTabBarController: UITabBarController { +final class MainTabBarController: UITabBarController, NeedsDependency { let logger = Logger(subsystem: "MainTabBarController", category: "TabBar") var disposeBag = Set() - weak var context: AppContext! - weak var coordinator: SceneCoordinator! + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } let authContext: AuthContext private let doubleTapGestureRecognizer = UITapGestureRecognizer.doubleTapGestureRecognizer @@ -119,7 +119,7 @@ extension MainTabBarController { context.publisherService.statusPublishResult .receive(on: DispatchQueue.main) .sink { [weak self] result in - guard let _ = self else { return } + guard let self = self else { return } switch result { case .success(let result): var config = SwiftMessages.defaultConfig @@ -146,6 +146,16 @@ extension MainTabBarController { return } + if let error = error as? Twitter.API.Error.ResponseError { + Task { @MainActor in + DataSourceFacade.presentForbiddenBanner( + error: error, + dependency: self + ) + } // end Task + return + } + var config = SwiftMessages.defaultConfig config.duration = .seconds(seconds: 3) config.interactiveHide = true From 274cd19370781c34ed1fd02f1ca871b84e8cfa2a Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 9 Aug 2022 14:32:28 +0800 Subject: [PATCH 013/128] fix: remove 30s entry for timeline auto refresh interval --- .../CoreDataStack 7.xcdatamodel/contents | 23 +------------------ .../Preference/Preference+Behaviors.swift | 2 -- .../Twitter/Persistence+Twitter.swift | 2 +- .../Twitter/Persistence+TwitterUser+V2.swift | 2 +- .../MetaLabelRepresentable.swift | 1 + .../Entity/Twitter+Entity+User.swift | 2 +- ...> Twitter+Entity+V2+ReferencedTweet.swift} | 2 +- TwidereX/Coordinator/SceneCoordinator.swift | 1 - .../BehaviorsPreferenceViewModel.swift | 11 ++++----- 9 files changed, 11 insertions(+), 35 deletions(-) rename TwidereSDK/Sources/TwitterSDK/Entity/V2/{Twiiter+Entity+V2+ReferencedTweet.swift => Twitter+Entity+V2+ReferencedTweet.swift} (94%) diff --git a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 7.xcdatamodel/contents b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 7.xcdatamodel/contents index 2763b5da..5e397ba5 100644 --- a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 7.xcdatamodel/contents +++ b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 7.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -311,25 +311,4 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/TwidereSDK/Sources/TwidereCommon/Preference/Preference+Behaviors.swift b/TwidereSDK/Sources/TwidereCommon/Preference/Preference+Behaviors.swift index 5d9cdd87..bccfa3eb 100644 --- a/TwidereSDK/Sources/TwidereCommon/Preference/Preference+Behaviors.swift +++ b/TwidereSDK/Sources/TwidereCommon/Preference/Preference+Behaviors.swift @@ -47,14 +47,12 @@ extension UserDefaults { } @objc public enum TimelineRefreshInterval: Int, Hashable, CaseIterable { - case _30s case _60s case _120s case _300s public var seconds: TimeInterval { switch self { - case ._30s: return 30 case ._60s: return 60 case ._120s: return 120 case ._300s: return 300 diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+Twitter.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+Twitter.swift index 7339210b..9de13c69 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+Twitter.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+Twitter.swift @@ -1,5 +1,5 @@ // -// .swift +// Persistence+Twitter.swift // Persistence+TwiPersistence+Twittertter // // Created by Cirno MainasuK on 2021-8-31. diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser+V2.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser+V2.swift index 696253be..5feac52a 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser+V2.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser+V2.swift @@ -6,11 +6,11 @@ // Copyright © 2021 Twidere. All rights reserved. // +import os.log import CoreData import CoreDataStack import Foundation import TwitterSDK -import os.log extension Persistence.TwitterUser { diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/MetaLabelRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/MetaLabelRepresentable.swift index babf6ff1..bbaf148c 100644 --- a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/MetaLabelRepresentable.swift +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/MetaLabelRepresentable.swift @@ -9,6 +9,7 @@ import UIKit import SwiftUI import TwidereCore import MetaTextKit +import MetaLabel public struct MetaLabelRepresentable: UIViewRepresentable { diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+User.swift b/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+User.swift index 8abacecb..f99f91d2 100644 --- a/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+User.swift +++ b/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+User.swift @@ -1,5 +1,5 @@ // -// Twitter+User.swift +// Twitter+Entity+User.swift // TwitterAPI // // Created by Cirno MainasuK on 2020-9-3. diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twiiter+Entity+V2+ReferencedTweet.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+ReferencedTweet.swift similarity index 94% rename from TwidereSDK/Sources/TwitterSDK/Entity/V2/Twiiter+Entity+V2+ReferencedTweet.swift rename to TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+ReferencedTweet.swift index deff7ff7..7d00d032 100644 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twiiter+Entity+V2+ReferencedTweet.swift +++ b/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+ReferencedTweet.swift @@ -1,5 +1,5 @@ // -// Twiiter+Entity+V2+ReferencedTweet.swift +// Twitter+Entity+V2+ReferencedTweet.swift // // // Created by Cirno MainasuK on 2020-10-19. diff --git a/TwidereX/Coordinator/SceneCoordinator.swift b/TwidereX/Coordinator/SceneCoordinator.swift index e2f15a32..f4f2a204 100644 --- a/TwidereX/Coordinator/SceneCoordinator.swift +++ b/TwidereX/Coordinator/SceneCoordinator.swift @@ -163,7 +163,6 @@ extension SceneCoordinator { setup() // entry #3: retry } // end Task } - } @MainActor diff --git a/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewModel.swift b/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewModel.swift index d0e9b9d5..dc7c43e8 100644 --- a/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewModel.swift +++ b/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewModel.swift @@ -109,8 +109,8 @@ final class BehaviorsPreferenceViewModel: ObservableObject { extension UserDefaults.TabBarTapScrollPreference { var title: String { switch self { - case .single: return "Single Tap" - case .double: return "Double Tap" + case .single: return L10n.Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.singleTap + case .double: return L10n.Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.doubleTap } } } @@ -118,10 +118,9 @@ extension UserDefaults.TabBarTapScrollPreference { extension UserDefaults.TimelineRefreshInterval { var title: String { switch self { - case ._30s: return "30 seconds" - case ._60s: return "60 seconds" - case ._120s: return "120 seconds" - case ._300s: return "300 seconds" + case ._60s: return L10n.Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption._60Seconds + case ._120s: return L10n.Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption._120Seconds + case ._300s: return L10n.Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption._300Seconds } } } From 1012d8acbc5cf70b36f0009649274fec6113df8c Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 9 Aug 2022 16:05:02 +0800 Subject: [PATCH 014/128] feat: update to MetaTextKit v4 --- TwidereSDK/Package.swift | 6 +- .../MetaTextKit/PlaintextMetaContent.swift | 23 ------ .../Protocol/TextStyleConfigurable.swift | 73 +------------------ .../TwidereUI/Content/PollOptionView.swift | 1 + .../TwidereUI/Content/StatusView.swift | 2 + .../Sources/TwidereUI/Content/UserView.swift | 1 + .../MentionPickViewModel+Diffable.swift | 2 +- .../TableViewCheckmarkTableViewCell.swift | 2 +- .../TableViewEntryTableViewCell.swift | 1 + .../TableView/TableViewPlainCell.swift | 1 + .../xcshareddata/swiftpm/Package.resolved | 4 +- ...der+MediaInfoDescriptionViewDelegate.swift | 1 + .../View/MediaInfoDescriptionView.swift | 1 + .../Header/ProfileHeaderViewController.swift | 1 + .../View/ProfileFieldCollectionViewCell.swift | 1 + .../Header/View/ProfileFieldContentView.swift | 3 +- .../Header/View/ProfileFieldListView.swift | 1 + .../Header/View/ProfileHeaderView.swift | 1 + .../Scene/Profile/ProfileViewController.swift | 3 +- .../Drawer/View/DrawerSidebarHeaderView.swift | 1 + .../Search/Cell/TrendTableViewCell.swift | 1 + .../Hashtag/Cell/HashtagTableViewCell.swift | 1 + .../View/Content/TimelineHeaderView.swift | 1 + .../View/Content/UserBriefInfoView.swift | 1 + 24 files changed, 32 insertions(+), 101 deletions(-) delete mode 100644 TwidereSDK/Sources/TwidereCore/Extension/MetaTextKit/PlaintextMetaContent.swift diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index 93f024d7..d4cd4ee3 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -30,7 +30,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-nio.git", from: "2.34.0"), .package(url: "https://github.com/Flipboard/FLAnimatedImage.git", from: "1.0.0"), .package(url: "https://github.com/MainasuK/CommonOSLog", from: "0.1.1"), - .package(url: "https://github.com/TwidereProject/MetaTextKit.git", .exact("3.3.2")), + .package(url: "https://github.com/TwidereProject/MetaTextKit.git", exact: "4.0.0"), .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.5.0"), .package(url: "https://github.com/Alamofire/AlamofireImage.git", from: "4.1.0"), .package(url: "https://github.com/Alamofire/AlamofireNetworkActivityIndicator.git", from: "3.1.0"), @@ -43,7 +43,7 @@ let package = Package( .package(url: "https://github.com/SwiftKickMobile/SwiftMessages.git", from: "9.0.5"), .package(url: "https://github.com/aheze/Popovers.git", from: "1.3.2"), .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", from: "4.2.2"), - .package(name: "Introspect", url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.4"), + .package(url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.4"), .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"), ], targets: [ @@ -124,7 +124,7 @@ let package = Package( "TwidereCore", .product(name: "CropViewController", package: "TOCropViewController"), .product(name: "FLAnimatedImage", package: "FLAnimatedImage"), - .product(name: "Introspect", package: "Introspect"), + .product(name: "Introspect", package: "SwiftUI-Introspect"), .product(name: "KeyboardLayoutGuide", package: "KeyboardLayoutGuide"), .product(name: "Kingfisher", package: "Kingfisher"), .product(name: "Popovers", package: "Popovers"), diff --git a/TwidereSDK/Sources/TwidereCore/Extension/MetaTextKit/PlaintextMetaContent.swift b/TwidereSDK/Sources/TwidereCore/Extension/MetaTextKit/PlaintextMetaContent.swift deleted file mode 100644 index ed62cf37..00000000 --- a/TwidereSDK/Sources/TwidereCore/Extension/MetaTextKit/PlaintextMetaContent.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// PlaintextMetaContent.swift -// PlaintextMetaContent -// -// Created by Cirno MainasuK on 2021-8-19. -// Copyright © 2021 Twidere. All rights reserved. -// - -import Foundation -import Meta - -public struct PlaintextMetaContent: MetaContent { - public let string: String - public let entities: [Meta.Entity] = [] - - public init(string: String) { - self.string = string - } - - public func metaAttachment(for entity: Meta.Entity) -> MetaAttachment? { - return nil - } -} diff --git a/TwidereSDK/Sources/TwidereCore/Protocol/TextStyleConfigurable.swift b/TwidereSDK/Sources/TwidereCore/Protocol/TextStyleConfigurable.swift index 3584f717..8522392e 100644 --- a/TwidereSDK/Sources/TwidereCore/Protocol/TextStyleConfigurable.swift +++ b/TwidereSDK/Sources/TwidereCore/Protocol/TextStyleConfigurable.swift @@ -9,6 +9,7 @@ import UIKit import Meta import MetaTextKit +import MetaLabel import MetaTextArea import TwidereAsset @@ -201,7 +202,8 @@ extension TextStyle { case .profileFieldValue: return .label case .mediaDescriptionAuthorName: - return .label + // force white due to media view controller override to dark mode + return .white case .hashtagTitle: return .label case .hashtagDescription: @@ -235,80 +237,13 @@ extension MetaLabel: TextStyleConfigurable { } public func setupLayout(style: TextStyle) { - lineBreakMode = .byTruncatingTail - textContainer.lineBreakMode = .byTruncatingTail - textContainer.lineFragmentPadding = 0 - - numberOfLines = style.numberOfLines - - switch style { - case .statusHeader: - break - case .statusAuthorName: - break - case .statusAuthorUsername: - break - case .statusTimestamp: - break - case .statusLocation: - break - case .statusContent: - break - case .statusMetrics: - break - case .pollOptionTitle: - break - case .pollOptionPercentage: - break - case .pollVoteDescription: - break - case .userAuthorName: - break - case .userAuthorUsername: - break - case .userDescription: - break - case .profileAuthorName, .profileAuthorUsername: - textAlignment = .center - paragraphStyle.alignment = .center - case .profileAuthorBio: - break - case .profileFieldKey: - break - case .profileFieldValue: - break - case .mediaDescriptionAuthorName: - break - case .hashtagTitle: - break - case .hashtagDescription: - break - case .listPrimaryText: - break - case .searchHistoryTitle: - break - case .searchTrendTitle: - break - case .searchTrendSubtitle: - break - case .searchTrendCount: - break - case .sidebarAuthorName: - break - case .sidebarAuthorUsername: - break - case .custom: - break - } + // do nothing due to cannot tweak TextKit 2 } public func setupAttributes(style: TextStyle) { let font = style.font let textColor = style.textColor - self.font = font - self.textColor = textColor - textAttributes = [ .font: font, .foregroundColor: textColor diff --git a/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift b/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift index b4a25523..d28e5d74 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift @@ -8,6 +8,7 @@ import UIKit import Combine import MetaTextKit +import MetaLabel import TwidereLocalization import UITextView_Placeholder import TwidereCore diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index 18ed6c54..fd521bdc 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -11,6 +11,7 @@ import Combine import UIKit import MetaTextKit import MetaTextArea +import MetaLabel import TwidereCommon import TwidereCore import NIOPosix @@ -432,6 +433,7 @@ extension StatusView.Style { // authorNameLabel authorContentStackView.addArrangedSubview(statusView.authorNameLabel) + statusView.authorNameLabel.setContentHuggingPriority(.required - 10, for: .horizontal) statusView.authorNameLabel.setContentCompressionResistancePriority(.required - 10, for: .horizontal) // lockImageView statusView.lockImageView.translatesAutoresizingMaskIntoConstraints = false diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift index e24dde5a..420f7661 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift @@ -10,6 +10,7 @@ import os.log import UIKit import Combine import MetaTextKit +import MetaLabel import TwidereCore protocol UserViewDelegate: AnyObject { diff --git a/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewModel+Diffable.swift b/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewModel+Diffable.swift index 4bb8b36e..8066caba 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewModel+Diffable.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewModel+Diffable.swift @@ -7,7 +7,7 @@ import UIKit import CoreData -import TwidereUI +import MetaTextKit extension MentionPickViewModel { struct DataSourceConfiguration { diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/TableView/TableViewCheckmarkTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/TableView/TableViewCheckmarkTableViewCell.swift index f545be10..ef6c1646 100644 --- a/TwidereSDK/Sources/TwidereUI/TableViewCell/TableView/TableViewCheckmarkTableViewCell.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/TableView/TableViewCheckmarkTableViewCell.swift @@ -7,7 +7,6 @@ // import UIKit - public final class TableViewCheckmarkTableViewCell: TableViewPlainCell { override func _init() { @@ -21,6 +20,7 @@ public final class TableViewCheckmarkTableViewCell: TableViewPlainCell { #if canImport(SwiftUI) && DEBUG import SwiftUI import TwidereCore +import MetaTextKit struct ListCheckmarkTableViewCell_Previews: PreviewProvider { diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/TableView/TableViewEntryTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/TableView/TableViewEntryTableViewCell.swift index 4d29e168..f1561dae 100644 --- a/TwidereSDK/Sources/TwidereUI/TableViewCell/TableView/TableViewEntryTableViewCell.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/TableView/TableViewEntryTableViewCell.swift @@ -22,6 +22,7 @@ public final class TableViewEntryTableViewCell: TableViewPlainCell { #if canImport(SwiftUI) && DEBUG import SwiftUI import TwidereCore +import MetaTextKit struct ListEntryTableViewCell_Previews: PreviewProvider { diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/TableView/TableViewPlainCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/TableView/TableViewPlainCell.swift index 05c83be3..736640df 100644 --- a/TwidereSDK/Sources/TwidereUI/TableViewCell/TableView/TableViewPlainCell.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/TableView/TableViewPlainCell.swift @@ -8,6 +8,7 @@ import UIKit import MetaTextKit +import MetaLabel public class TableViewPlainCell: UITableViewCell { diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2287d087..b4445b22 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -123,8 +123,8 @@ "repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git", "state": { "branch": null, - "revision": "f5af64a22d5a4839d68efe74e41216f039194d12", - "version": "3.3.2" + "revision": "eb333883076f5d469fcf738d00d71f66f92a5e98", + "version": "4.0.0" } }, { diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift index a92a905f..1218162a 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift @@ -13,6 +13,7 @@ import TwidereCore import TwidereUI import AppShared import MetaTextKit +import MetaLabel extension MediaInfoDescriptionViewDelegate where Self: DataSourceProvider { func mediaInfoDescriptionView(_ mediaInfoDescriptionView: MediaInfoDescriptionView, avatarButtonDidPressed button: UIButton) { diff --git a/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView.swift b/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView.swift index e3216b54..19f5c40b 100644 --- a/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView.swift +++ b/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView.swift @@ -11,6 +11,7 @@ import UIKit import Combine import MetaTextKit import MetaTextArea +import MetaLabel import TwidereUI protocol MediaInfoDescriptionViewDelegate: AnyObject { diff --git a/TwidereX/Scene/Profile/Header/ProfileHeaderViewController.swift b/TwidereX/Scene/Profile/Header/ProfileHeaderViewController.swift index 24bae4b7..9acad42e 100644 --- a/TwidereX/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/TwidereX/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -11,6 +11,7 @@ import Combine import TabBarPager import MetaTextKit import MetaTextArea +import MetaLabel import Meta protocol ProfileHeaderViewControllerDelegate: AnyObject { diff --git a/TwidereX/Scene/Profile/Header/View/ProfileFieldCollectionViewCell.swift b/TwidereX/Scene/Profile/Header/View/ProfileFieldCollectionViewCell.swift index a8bc4ac7..3ca3f0b8 100644 --- a/TwidereX/Scene/Profile/Header/View/ProfileFieldCollectionViewCell.swift +++ b/TwidereX/Scene/Profile/Header/View/ProfileFieldCollectionViewCell.swift @@ -8,6 +8,7 @@ import UIKit import MetaTextKit +import MetaLabel import Meta protocol ProfileFieldCollectionViewCellDelegate: AnyObject { diff --git a/TwidereX/Scene/Profile/Header/View/ProfileFieldContentView.swift b/TwidereX/Scene/Profile/Header/View/ProfileFieldContentView.swift index 0263efdf..2bd8d1b5 100644 --- a/TwidereX/Scene/Profile/Header/View/ProfileFieldContentView.swift +++ b/TwidereX/Scene/Profile/Header/View/ProfileFieldContentView.swift @@ -9,6 +9,7 @@ import os.log import UIKit import MetaTextKit +import MetaLabel protocol ProfileFieldContentViewDelegate: AnyObject { func profileFieldContentView(_ contentView: ProfileFieldContentView, metaLabel: MetaLabel, didSelectMeta meta: Meta) @@ -110,7 +111,7 @@ extension ProfileFieldContentView { valueMetaLabel.setContentHuggingPriority(.required - 10, for: .horizontal) valueMetaLabel.setContentCompressionResistancePriority(.required - 10, for: .horizontal) - valueMetaLabel.linkDelegate = self + valueMetaLabel.delegate = self } private func apply(configuration: ContentConfiguration) { diff --git a/TwidereX/Scene/Profile/Header/View/ProfileFieldListView.swift b/TwidereX/Scene/Profile/Header/View/ProfileFieldListView.swift index 7354e294..0bbf2852 100644 --- a/TwidereX/Scene/Profile/Header/View/ProfileFieldListView.swift +++ b/TwidereX/Scene/Profile/Header/View/ProfileFieldListView.swift @@ -9,6 +9,7 @@ import os.log import UIKit import MetaTextKit +import MetaLabel import Meta protocol ProfileFieldListViewDelegate: AnyObject { diff --git a/TwidereX/Scene/Profile/Header/View/ProfileHeaderView.swift b/TwidereX/Scene/Profile/Header/View/ProfileHeaderView.swift index b5011e77..65f026d8 100644 --- a/TwidereX/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/TwidereX/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -11,6 +11,7 @@ import UIKit import Combine import MetaTextKit import MetaTextArea +import MetaLabel import TwidereUI protocol ProfileHeaderViewDelegate: AnyObject { diff --git a/TwidereX/Scene/Profile/ProfileViewController.swift b/TwidereX/Scene/Profile/ProfileViewController.swift index 6f5ec431..a173685a 100644 --- a/TwidereX/Scene/Profile/ProfileViewController.swift +++ b/TwidereX/Scene/Profile/ProfileViewController.swift @@ -13,8 +13,9 @@ import CoreDataStack import AppShared import Floaty import Meta -import MetaTextArea import MetaTextKit +import MetaTextArea +import MetaLabel import TabBarPager import XLPagerTabStrip import TwidereUI diff --git a/TwidereX/Scene/Root/Drawer/View/DrawerSidebarHeaderView.swift b/TwidereX/Scene/Root/Drawer/View/DrawerSidebarHeaderView.swift index 5d5b0f51..be8c9049 100644 --- a/TwidereX/Scene/Root/Drawer/View/DrawerSidebarHeaderView.swift +++ b/TwidereX/Scene/Root/Drawer/View/DrawerSidebarHeaderView.swift @@ -10,6 +10,7 @@ import os.log import UIKit import TwidereCore import MetaTextKit +import MetaLabel import TwidereUI protocol DrawerSidebarHeaderViewDelegate: AnyObject { diff --git a/TwidereX/Scene/Search/Search/Cell/TrendTableViewCell.swift b/TwidereX/Scene/Search/Search/Cell/TrendTableViewCell.swift index 12ddd460..7aca31b3 100644 --- a/TwidereX/Scene/Search/Search/Cell/TrendTableViewCell.swift +++ b/TwidereX/Scene/Search/Search/Cell/TrendTableViewCell.swift @@ -8,6 +8,7 @@ import UIKit import MetaTextKit +import MetaLabel import TwidereCore final class TrendTableViewCell: UITableViewCell { diff --git a/TwidereX/Scene/Search/SearchResult/Hashtag/Cell/HashtagTableViewCell.swift b/TwidereX/Scene/Search/SearchResult/Hashtag/Cell/HashtagTableViewCell.swift index 31b7e355..99628e4c 100644 --- a/TwidereX/Scene/Search/SearchResult/Hashtag/Cell/HashtagTableViewCell.swift +++ b/TwidereX/Scene/Search/SearchResult/Hashtag/Cell/HashtagTableViewCell.swift @@ -8,6 +8,7 @@ import UIKit import MetaTextKit +import MetaLabel final class HashtagTableViewCell: UITableViewCell { diff --git a/TwidereX/Scene/Share/View/Content/TimelineHeaderView.swift b/TwidereX/Scene/Share/View/Content/TimelineHeaderView.swift index c710a78e..dd3856bf 100644 --- a/TwidereX/Scene/Share/View/Content/TimelineHeaderView.swift +++ b/TwidereX/Scene/Share/View/Content/TimelineHeaderView.swift @@ -8,6 +8,7 @@ import UIKit import MetaTextKit +import MetaLabel final class TimelineHeaderView: UIView { diff --git a/TwidereX/Scene/Share/View/Content/UserBriefInfoView.swift b/TwidereX/Scene/Share/View/Content/UserBriefInfoView.swift index 44861cde..d2e8caa3 100644 --- a/TwidereX/Scene/Share/View/Content/UserBriefInfoView.swift +++ b/TwidereX/Scene/Share/View/Content/UserBriefInfoView.swift @@ -9,6 +9,7 @@ import UIKit import Combine import MetaTextKit +import MetaLabel final class UserBriefInfoView: UIView { From 4bb648d39acbe5792a5c5944ae7cb381d217b910 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 12 Aug 2022 14:32:30 +0800 Subject: [PATCH 015/128] chore: update MetaTextKit version --- TwidereSDK/Package.swift | 2 +- TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index d4cd4ee3..dbade0f9 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -30,7 +30,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-nio.git", from: "2.34.0"), .package(url: "https://github.com/Flipboard/FLAnimatedImage.git", from: "1.0.0"), .package(url: "https://github.com/MainasuK/CommonOSLog", from: "0.1.1"), - .package(url: "https://github.com/TwidereProject/MetaTextKit.git", exact: "4.0.0"), + .package(url: "https://github.com/TwidereProject/MetaTextKit.git", exact: "4.1.1"), .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.5.0"), .package(url: "https://github.com/Alamofire/AlamofireImage.git", from: "4.1.0"), .package(url: "https://github.com/Alamofire/AlamofireNetworkActivityIndicator.git", from: "3.1.0"), diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index b4445b22..79396534 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -123,8 +123,8 @@ "repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git", "state": { "branch": null, - "revision": "eb333883076f5d469fcf738d00d71f66f92a5e98", - "version": "4.0.0" + "revision": "a435a39c279746ffb59694a75bef376802ff0a06", + "version": "4.1.1" } }, { From a8addee7ed6a534f2e80cf82364dbaa68ac960a5 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 12 Aug 2022 14:34:17 +0800 Subject: [PATCH 016/128] feat: default disable timeline auto refresh to respect Twitter Rules --- .../Sources/TwidereCommon/Preference/Preference+Behaviors.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/TwidereSDK/Sources/TwidereCommon/Preference/Preference+Behaviors.swift b/TwidereSDK/Sources/TwidereCommon/Preference/Preference+Behaviors.swift index bccfa3eb..a117fb12 100644 --- a/TwidereSDK/Sources/TwidereCommon/Preference/Preference+Behaviors.swift +++ b/TwidereSDK/Sources/TwidereCommon/Preference/Preference+Behaviors.swift @@ -40,7 +40,6 @@ extension UserDefaults { @objc dynamic public var preferredTimelineAutoRefresh: Bool { get { - register(defaults: [#function: true]) return bool(forKey: #function) } set { self[#function] = newValue } From 4d1908b473340e1dbf83e872dd86eefb12bde2ba Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 12 Aug 2022 17:13:20 +0800 Subject: [PATCH 017/128] feat: add rate limit for tweet publish --- .../Sources/TwidereCore/Error/AppError.swift | 4 +- ...thenticationContext+TwitterRateLimit.swift | 75 +++++++++++++++++++ .../APIService+Status+Publish.swift | 24 ++++++ .../Generated/Strings.swift | 6 ++ .../Resources/ar.lproj/Localizable.strings | 2 + .../Resources/ca.lproj/Localizable.strings | 2 + .../Resources/de.lproj/Localizable.strings | 2 + .../Resources/en.lproj/Localizable.strings | 2 + .../Resources/es.lproj/Localizable.strings | 2 + .../Resources/eu.lproj/Localizable.strings | 2 + .../Resources/gl.lproj/Localizable.strings | 2 + .../Resources/ja.lproj/Localizable.strings | 2 + .../Resources/ko.lproj/Localizable.strings | 18 +++-- .../Resources/pt-BR.lproj/Localizable.strings | 2 + .../Resources/tr.lproj/Localizable.strings | 4 +- .../zh-Hans.lproj/Localizable.strings | 2 + .../Root/MainTab/MainTabBarController.swift | 2 +- TwidereX/Supporting Files/SceneDelegate.swift | 4 +- 18 files changed, 143 insertions(+), 14 deletions(-) create mode 100644 TwidereSDK/Sources/TwidereCore/Model/Authentication/AuthenticationContext+TwitterRateLimit.swift diff --git a/TwidereSDK/Sources/TwidereCore/Error/AppError.swift b/TwidereSDK/Sources/TwidereCore/Error/AppError.swift index 0b44955c..3772dff0 100644 --- a/TwidereSDK/Sources/TwidereCore/Error/AppError.swift +++ b/TwidereSDK/Sources/TwidereCore/Error/AppError.swift @@ -63,7 +63,7 @@ extension AppError.ErrorReason: LocalizedError { case .badRequest: return "Bad Request" case .requestThrottle: - return "Request Throttle" + return L10n.Common.Alerts.RequestThrottle.title case .twitterResponseError(let error): guard let twitterAPIError = error.twitterAPIError else { return error.httpResponseStatus.reasonPhrase @@ -90,7 +90,7 @@ extension AppError.ErrorReason: LocalizedError { case .badRequest: return "The request is invalid" case .requestThrottle: - return "The requests are too frequent" + return L10n.Common.Alerts.RequestThrottle.message case .twitterResponseError(let error): guard let twitterAPIError = error.twitterAPIError else { return nil diff --git a/TwidereSDK/Sources/TwidereCore/Model/Authentication/AuthenticationContext+TwitterRateLimit.swift b/TwidereSDK/Sources/TwidereCore/Model/Authentication/AuthenticationContext+TwitterRateLimit.swift new file mode 100644 index 00000000..3cc2a5b5 --- /dev/null +++ b/TwidereSDK/Sources/TwidereCore/Model/Authentication/AuthenticationContext+TwitterRateLimit.swift @@ -0,0 +1,75 @@ +// +// AuthenticationContext+TwitterRateLimit.swift +// +// +// Created by MainasuK on 2022-8-12. +// + +import Foundation +import TwidereCommon + +extension AuthenticationContext { + + /// Client side rate limit + public struct RateLimit: Codable { + public var remaining: Int + public let limit: Int + public let reset: Date + + public init(limit: Int, remaining: Int, reset: Date) { + self.limit = limit + self.remaining = remaining + self.reset = reset + } + + /// Rate limit scope + public enum Scope: String, Hashable { + /// Post publish endpoint + case publish + } + + } + + private func key(scope: RateLimit.Scope) -> String { + return "com.twidere.TwidereCore.TwitterRateLimit.\(scope.rawValue).\(acct.rawValue)" + } + + public func rateLimit(scope: RateLimit.Scope) -> RateLimit? { + let key = key(scope: scope) + guard let encoded = AppSecret.keychain[key], + let data = Data(base64Encoded: encoded), + let rateLimit = try? JSONDecoder().decode(AuthenticationContext.RateLimit.self, from: data) + else { + return nil + } + + return rateLimit + } + + @discardableResult + public func updateRateLimit(scope: RateLimit.Scope, now: Date) -> RateLimit { + if var rateLimit = rateLimit(scope: scope), now < rateLimit.reset { + rateLimit.remaining = max(0, rateLimit.remaining - 1) + return rateLimit + } else { + let reset = Calendar.current.date(byAdding: .minute, value: 10, to: now) ?? now.addingTimeInterval(10 * 60) // 10 min + + let limit = 5 + let rateLimit = RateLimit( + limit: limit, + remaining: limit - 1, + reset: reset + ) + + let key = key(scope: scope) + AppSecret.keychain[key] = { + guard let data = try? JSONEncoder().encode(rateLimit) else { return nil } + let encoded = data.base64EncodedString() + return encoded + }() + + return rateLimit + } + } + +} diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Publish.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Publish.swift index 06a6c6bd..f6be0443 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Publish.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Publish.swift @@ -95,12 +95,24 @@ extension APIService { query: Twitter.API.V2.Status.PublishQuery, twitterAuthenticationContext: TwitterAuthenticationContext ) async throws -> Twitter.Response.Content { + let authenticationContext = AuthenticationContext.twitter(authenticationContext: twitterAuthenticationContext) + + let now = Date() + if let rateLimit = authenticationContext.rateLimit(scope: .publish), now < rateLimit.reset, rateLimit.remaining <= 0 { + throw AppError.explicit(.requestThrottle) + } + let response = try await Twitter.API.V2.Status.publish( session: session, query: query, authorization: twitterAuthenticationContext.authorization ) + authenticationContext.updateRateLimit( + scope: .publish, + now: response.networkDate + ) + return response } @@ -112,6 +124,13 @@ extension APIService { excludeReplyUserIDs: [TwitterUser.ID]?, twitterAuthenticationContext: TwitterAuthenticationContext ) async throws -> Twitter.Response.Content { + let authenticationContext = AuthenticationContext.twitter(authenticationContext: twitterAuthenticationContext) + + let now = Date() + if let rateLimit = authenticationContext.rateLimit(scope: .publish), now < rateLimit.reset, rateLimit.remaining <= 0 { + throw AppError.explicit(.requestThrottle) + } + let authorization = twitterAuthenticationContext.authorization let managedObjectContext = backgroundManagedObjectContext @@ -140,6 +159,11 @@ extension APIService { authorization: authorization ) + authenticationContext.updateRateLimit( + scope: .publish, + now: response.networkDate + ) + return response } diff --git a/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift b/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift index 27709f53..5ac559d8 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift +++ b/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift @@ -448,6 +448,12 @@ public enum L10n { return L10n.tr("Localizable", "Common.Alerts.ReportUserSuccess.Title", String(describing: p1)) } } + public enum RequestThrottle { + /// Operation too frequent. Please try again later + public static let message = L10n.tr("Localizable", "Common.Alerts.RequestThrottle.Message") + /// Request Throttle + public static let title = L10n.tr("Localizable", "Common.Alerts.RequestThrottle.Title") + } public enum SignOutUserConfirm { /// Do you want to sign out? public static let message = L10n.tr("Localizable", "Common.Alerts.SignOutUserConfirm.Message") diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings index f4437829..08da8f12 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings @@ -133,6 +133,8 @@ "Common.Alerts.RateLimitExceeded.Title" = "اجتزت الحد المسموح به"; "Common.Alerts.ReportAndBlockUserSuccess.Title" = "أُبلغ عن %@ وحجب بسبب الازعاج"; "Common.Alerts.ReportUserSuccess.Title" = "أُبلغ عن %@ بسبب الازعاج"; +"Common.Alerts.RequestThrottle.Message" = "Operation too frequent. Please try again later"; +"Common.Alerts.RequestThrottle.Title" = "Request Throttle"; "Common.Alerts.SignOutUserConfirm.Message" = "هل تريد الخروج؟"; "Common.Alerts.SignOutUserConfirm.Title" = "خروج"; "Common.Alerts.TooManyRequests.Title" = "طلبات كثيرة"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings index 9e1e722a..11ce2c25 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings @@ -133,6 +133,8 @@ "Common.Alerts.RateLimitExceeded.Title" = "Taxa d'ús excedida"; "Common.Alerts.ReportAndBlockUserSuccess.Title" = "%@ ha estat denunciat per publicitat abusiva i blocat"; "Common.Alerts.ReportUserSuccess.Title" = "%@ ha estat denunciat per publicitat abusiva"; +"Common.Alerts.RequestThrottle.Message" = "Operation too frequent. Please try again later"; +"Common.Alerts.RequestThrottle.Title" = "Request Throttle"; "Common.Alerts.SignOutUserConfirm.Message" = "Do you want to sign out?"; "Common.Alerts.SignOutUserConfirm.Title" = "Sign out"; "Common.Alerts.TooManyRequests.Title" = "Massa soŀlicituds"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings index ac10f855..2878035b 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings @@ -133,6 +133,8 @@ "Common.Alerts.RateLimitExceeded.Title" = "Kappungsgrenze überschritten"; "Common.Alerts.ReportAndBlockUserSuccess.Title" = "%@ wurde wegen Spam gemeldet und blockiert"; "Common.Alerts.ReportUserSuccess.Title" = "%@ wurde wegen Spam gemeldet"; +"Common.Alerts.RequestThrottle.Message" = "Operation too frequent. Please try again later"; +"Common.Alerts.RequestThrottle.Title" = "Request Throttle"; "Common.Alerts.SignOutUserConfirm.Message" = "Möchtest du dich abmelden?"; "Common.Alerts.SignOutUserConfirm.Title" = "Abmelden"; "Common.Alerts.TooManyRequests.Title" = "Zu viele Anfragen"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings index e9fd3d73..964a9fd6 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings @@ -133,6 +133,8 @@ "Common.Alerts.RateLimitExceeded.Title" = "Rate Limit Exceeded"; "Common.Alerts.ReportAndBlockUserSuccess.Title" = "%@ has been reported for spam and blocked"; "Common.Alerts.ReportUserSuccess.Title" = "%@ has been reported for spam"; +"Common.Alerts.RequestThrottle.Message" = "Operation too frequent. Please try again later"; +"Common.Alerts.RequestThrottle.Title" = "Request Throttle"; "Common.Alerts.SignOutUserConfirm.Message" = "Do you want to sign out?"; "Common.Alerts.SignOutUserConfirm.Title" = "Sign out"; "Common.Alerts.TooManyRequests.Title" = "Too Many Requests"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings index 8804ee70..1bbc4c1a 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings @@ -133,6 +133,8 @@ "Common.Alerts.RateLimitExceeded.Title" = "Límite de transferencia excedido"; "Common.Alerts.ReportAndBlockUserSuccess.Title" = "%@ ha sido reportado por spam y bloqueado"; "Common.Alerts.ReportUserSuccess.Title" = "%@ ha sido reportado por spam"; +"Common.Alerts.RequestThrottle.Message" = "Operation too frequent. Please try again later"; +"Common.Alerts.RequestThrottle.Title" = "Request Throttle"; "Common.Alerts.SignOutUserConfirm.Message" = "¿Desea cerrar sesión?"; "Common.Alerts.SignOutUserConfirm.Title" = "Cerrar sesión"; "Common.Alerts.TooManyRequests.Title" = "Demasiadas solicitudes"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings index 0c34fb72..9c81387c 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings @@ -133,6 +133,8 @@ "Common.Alerts.RateLimitExceeded.Title" = "Tasaren muga gainditu da"; "Common.Alerts.ReportAndBlockUserSuccess.Title" = "%@ spam bidez salatu da eta blokeatu da"; "Common.Alerts.ReportUserSuccess.Title" = "%@ spam bidez salatu da"; +"Common.Alerts.RequestThrottle.Message" = "Operation too frequent. Please try again later"; +"Common.Alerts.RequestThrottle.Title" = "Request Throttle"; "Common.Alerts.SignOutUserConfirm.Message" = "Saiotik irten nahi duzu?"; "Common.Alerts.SignOutUserConfirm.Title" = "Amaitu saioa"; "Common.Alerts.TooManyRequests.Title" = "Eskaera gehiegi"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings index a250cb41..0bb78b34 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings @@ -133,6 +133,8 @@ "Common.Alerts.RateLimitExceeded.Title" = "Taxa de uso superada"; "Common.Alerts.ReportAndBlockUserSuccess.Title" = "Denunciaches e bloqueaches a %@ por spam"; "Common.Alerts.ReportUserSuccess.Title" = "Denunciaches a %@ por spam"; +"Common.Alerts.RequestThrottle.Message" = "Operation too frequent. Please try again later"; +"Common.Alerts.RequestThrottle.Title" = "Request Throttle"; "Common.Alerts.SignOutUserConfirm.Message" = "Queres pechar a sesión?"; "Common.Alerts.SignOutUserConfirm.Title" = "Pechar sesión"; "Common.Alerts.TooManyRequests.Title" = "Demasiadas peticións"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings index b9154387..9c880cb2 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings @@ -133,6 +133,8 @@ "Common.Alerts.RateLimitExceeded.Title" = "レート制限を超えました"; "Common.Alerts.ReportAndBlockUserSuccess.Title" = "%@ はスパムとして報告されブロックされました"; "Common.Alerts.ReportUserSuccess.Title" = "%@ はスパムとして報告されました"; +"Common.Alerts.RequestThrottle.Message" = "Operation too frequent. Please try again later"; +"Common.Alerts.RequestThrottle.Title" = "Request Throttle"; "Common.Alerts.SignOutUserConfirm.Message" = "サインアウトしますか?"; "Common.Alerts.SignOutUserConfirm.Title" = "サインアウト"; "Common.Alerts.TooManyRequests.Title" = "リクエストが多すぎます"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings index a191b91b..23724451 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings @@ -133,6 +133,8 @@ "Common.Alerts.RateLimitExceeded.Title" = "한도를 넘어서 제한이 걸렸습니다."; "Common.Alerts.ReportAndBlockUserSuccess.Title" = "%@는 스팸으로 신고받아 차단되었습니다."; "Common.Alerts.ReportUserSuccess.Title" = "%@는 스팸으로 신고됐습니다."; +"Common.Alerts.RequestThrottle.Message" = "Operation too frequent. Please try again later"; +"Common.Alerts.RequestThrottle.Title" = "Request Throttle"; "Common.Alerts.SignOutUserConfirm.Message" = "로그아웃 할까요?"; "Common.Alerts.SignOutUserConfirm.Title" = "로그아웃 됐습니다."; "Common.Alerts.TooManyRequests.Title" = "요청이 몰렸습니다."; @@ -229,8 +231,8 @@ "Common.Controls.Status.ReplySettings.PeopleUserFollowsOrMentionedCanReply" = "People %@ follows or mentioned can reply."; "Common.Controls.Status.ReplySettings.PeopleUserMentionedCanReply" = "People %@ mentioned can reply."; "Common.Controls.Status.Thread.Show" = "이 타래 보기"; -"Common.Controls.Status.UserBoosted" = "%@이 부스트했습니다."; -"Common.Controls.Status.UserRetweeted" = "%@가 리트윗함"; +"Common.Controls.Status.UserBoosted" = "%@ 님이 부스트했습니다."; +"Common.Controls.Status.UserRetweeted" = "%@ 님이 리트윗함"; "Common.Controls.Status.YouBoosted" = "내가 부스트했습니다."; "Common.Controls.Status.YouRetweeted" = "내가 리트윗했습니다."; "Common.Controls.Timeline.LoadMore" = "더 불러오기"; @@ -254,19 +256,19 @@ "Common.Countable.Tweet.Multiple" = "%@개 트윗"; "Common.Countable.Tweet.Single" = "%@개 트윗"; "Common.Notification.Favourite" = "%@가 내 툿에 좋아요를 눌렀습니다"; -"Common.Notification.Follow" = "%@가 나를 팔로합니다."; -"Common.Notification.FollowRequest" = "%@가 팔로 요청을 보냈습니다"; +"Common.Notification.Follow" = "%@ 님이 나를 팔로우합니다."; +"Common.Notification.FollowRequest" = "%@ 님이 팔로우 요청을 보냈습니다"; "Common.Notification.FollowRequestAction.Approve" = "Approve"; "Common.Notification.FollowRequestAction.Deny" = "Deny"; "Common.Notification.FollowRequestResponse.FollowRequestApproved" = "Follow Request Approved"; "Common.Notification.FollowRequestResponse.FollowRequestDenied" = "Follow Request Denied"; -"Common.Notification.Mentions" = "%@가 언급했습니다."; -"Common.Notification.Messages.Content" = "%@님이 메시지를 보냈습니다"; +"Common.Notification.Mentions" = "%@ 님이 언급했습니다."; +"Common.Notification.Messages.Content" = "%@ 님이 메시지를 보냈습니다"; "Common.Notification.Messages.Title" = "새 쪽지"; "Common.Notification.OwnPoll" = "투표가 끝났습니다"; "Common.Notification.Poll" = "투표한 것의 결과가 나왔습니다"; -"Common.Notification.Reblog" = "%@가 내 툿을 부스트 했습니다"; -"Common.Notification.Status" = "%@가 글을 올렸습니다"; +"Common.Notification.Reblog" = "%@ 님이 내 툿을 부스트 했습니다"; +"Common.Notification.Status" = "%@ 님이 글을 올렸습니다"; "Common.NotificationChannel.BackgroundProgresses.Name" = "백그라운드에서 작동"; "Common.NotificationChannel.ContentInteractions.Description" = "답글과 리트윗 같은 알림"; "Common.NotificationChannel.ContentInteractions.Name" = "알림"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings index 4e93c32a..baf55acd 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings @@ -133,6 +133,8 @@ "Common.Alerts.RateLimitExceeded.Title" = "Limite de Acesso Excedido"; "Common.Alerts.ReportAndBlockUserSuccess.Title" = "%@ foi reportado por spam e bloqueado"; "Common.Alerts.ReportUserSuccess.Title" = "%@ foi reportado por spam"; +"Common.Alerts.RequestThrottle.Message" = "Operation too frequent. Please try again later"; +"Common.Alerts.RequestThrottle.Title" = "Request Throttle"; "Common.Alerts.SignOutUserConfirm.Message" = "Você quer encerrar esta sessão?"; "Common.Alerts.SignOutUserConfirm.Title" = "Encerrar sessão"; "Common.Alerts.TooManyRequests.Title" = "Muitos Pedidos"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings index 653d981a..cececc80 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings @@ -133,6 +133,8 @@ "Common.Alerts.RateLimitExceeded.Title" = "Kullanım limiti aşıldı"; "Common.Alerts.ReportAndBlockUserSuccess.Title" = "%@ spam için şikayet edildi ve engellendi"; "Common.Alerts.ReportUserSuccess.Title" = "%@ spam için şikayet edildi"; +"Common.Alerts.RequestThrottle.Message" = "Operation too frequent. Please try again later"; +"Common.Alerts.RequestThrottle.Title" = "Request Throttle"; "Common.Alerts.SignOutUserConfirm.Message" = "Çıkış yapmak istiyor musunuz?"; "Common.Alerts.SignOutUserConfirm.Title" = "Çıkış Yap"; "Common.Alerts.TooManyRequests.Title" = "Çok Fazla İstek"; @@ -429,7 +431,7 @@ Hala erken aşamada."; "Scene.Settings.Appearance.Translation.Off" = "Kapalı"; "Scene.Settings.Appearance.Translation.Service" = "Hizmet"; "Scene.Settings.Appearance.Translation.TranslateButton" = "Çevir butonu"; -"Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord" = "Enable History Record"; +"Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord" = "Geçmiş Kaydını Etkinleştir"; "Scene.Settings.Behaviors.HistorySection.History" = "Geçmiş"; "Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels" = "Sekme çubuğu etiketlerini göster"; "Scene.Settings.Behaviors.TabBarSection.TabBar" = "Sekme Çubuğu"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings index fcadef52..8632282f 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings @@ -133,6 +133,8 @@ "Common.Alerts.RateLimitExceeded.Title" = "超出用量限制"; "Common.Alerts.ReportAndBlockUserSuccess.Title" = "%@ 已被举报为垃圾信息用户并被屏蔽"; "Common.Alerts.ReportUserSuccess.Title" = "%@ 已被举报为垃圾信息用户"; +"Common.Alerts.RequestThrottle.Message" = "操作过于频繁,请稍后再试"; +"Common.Alerts.RequestThrottle.Title" = "请求受限"; "Common.Alerts.SignOutUserConfirm.Message" = "确定要登出吗?"; "Common.Alerts.SignOutUserConfirm.Title" = "登出"; "Common.Alerts.TooManyRequests.Title" = "请求次数过多"; diff --git a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift index 98f68624..a587c60c 100644 --- a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift +++ b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift @@ -157,7 +157,7 @@ extension MainTabBarController { } var config = SwiftMessages.defaultConfig - config.duration = .seconds(seconds: 3) + config.duration = .seconds(seconds: 10) config.interactiveHide = true let bannerView = NotificationBannerView() diff --git a/TwidereX/Supporting Files/SceneDelegate.swift b/TwidereX/Supporting Files/SceneDelegate.swift index b0046f7e..77d8cf79 100644 --- a/TwidereX/Supporting Files/SceneDelegate.swift +++ b/TwidereX/Supporting Files/SceneDelegate.swift @@ -23,7 +23,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? var coordinator: SceneCoordinator? - #if PROFILE + #if DEBUG var fpsIndicator: FPSIndicator? #endif @@ -64,7 +64,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window.makeKeyAndVisible() - #if PROFILE + #if DEBUG fpsIndicator = FPSIndicator(windowScene: windowScene) #endif } From ed396254aba46653fe8010f07de96f97ec969b28 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 12 Aug 2022 21:53:38 +0800 Subject: [PATCH 018/128] fix: Mastodon user timeline refresh not correctly reload data issue --- .../Base/Common/TimelineViewModel+LoadOldestState.swift | 6 ++---- .../Scene/Timeline/Base/Common/TimelineViewModel.swift | 8 +++++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift index fb6b80b8..0ba53692 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift @@ -133,10 +133,8 @@ extension TimelineViewModel.LoadOldestState { authenticationContext: authenticationContext, kind: viewModel.kind, position: { - guard let record = record else { - return .top(anchor: nil) - } - return .bottom(anchor: record) + // always reload at top when nextInput is nil + return .top(anchor: nil) }(), filter: StatusFetchViewModel.Timeline.Filter(rule: .empty) ) diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift index 5e005a60..a59d2c6d 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift @@ -153,9 +153,11 @@ extension TimelineViewModel { assertionFailure("do not support refresh for search") return .top(anchor: nil) case .user: - // FIXME: use anchor with minID or reset the data source - // the like timeline gap may missing - return .top(anchor: nil) + let anchor: StatusRecord? = { + guard let record = statusRecordFetchedResultController.records.first else { return nil } + return record + }() + return .top(anchor: anchor) } }(), filter: StatusFetchViewModel.Timeline.Filter(rule: .empty) From a0c0c6020285babb7e96839e52ae47c9d2c23ab5 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 12 Aug 2022 21:56:43 +0800 Subject: [PATCH 019/128] fix: TextKit 2 different behavior cause cannot layout inline emoji on iOS 15 issue --- TwidereSDK/Package.swift | 2 +- TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index dbade0f9..7a47ac7e 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -30,7 +30,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-nio.git", from: "2.34.0"), .package(url: "https://github.com/Flipboard/FLAnimatedImage.git", from: "1.0.0"), .package(url: "https://github.com/MainasuK/CommonOSLog", from: "0.1.1"), - .package(url: "https://github.com/TwidereProject/MetaTextKit.git", exact: "4.1.1"), + .package(url: "https://github.com/TwidereProject/MetaTextKit.git", exact: "4.1.2"), .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.5.0"), .package(url: "https://github.com/Alamofire/AlamofireImage.git", from: "4.1.0"), .package(url: "https://github.com/Alamofire/AlamofireNetworkActivityIndicator.git", from: "3.1.0"), diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index 79396534..164d87a9 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -123,8 +123,8 @@ "repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git", "state": { "branch": null, - "revision": "a435a39c279746ffb59694a75bef376802ff0a06", - "version": "4.1.1" + "revision": "f788c7ebb359ee6f7bbd99e0c2c3904fda7a84ea", + "version": "4.1.2" } }, { From d96f5440e1ede0bf45b55eebe5af7fae79464a39 Mon Sep 17 00:00:00 2001 From: CMK Date: Sun, 14 Aug 2022 05:07:43 +0800 Subject: [PATCH 020/128] fix: persist on multiple background context may raise cause due to concurrency issue --- TwidereSDK/Sources/CoreDataStack/CoreDataStack.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.swift b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.swift index 422a6317..0a61fa1f 100644 --- a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.swift +++ b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.swift @@ -24,6 +24,10 @@ public final class CoreDataStack { // output @Published public var didFinishLoad = false + + private lazy var persistHistoryManagedObjectContext: NSManagedObjectContext = { + return self.newTaskContext() + }() /// A persistent history token used for fetching transactions from the store. private var lastHistoryToken: NSPersistentHistoryToken? @@ -168,7 +172,7 @@ extension CoreDataStack { // seealso: `NSPersistentStoreRemoteChangeNotificationPostOptionKey` private func processRemoteStoreChange() async throws { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - let context = self.newTaskContext() + let context = persistHistoryManagedObjectContext context.transactionAuthor = "PersistentHistoryContext" context.name = "PersistentHistoryContext" From d9de39d13012882401dd0ecdd5c2665ff3ab73c7 Mon Sep 17 00:00:00 2001 From: CMK Date: Sat, 28 Jan 2023 17:02:11 +0800 Subject: [PATCH 021/128] fix: add SQL thread assert and try to fix concurrency issue --- .../Service/NotificationService.swift | 13 ++++++++--- .../ViewModel/RelationshipViewModel.swift | 1 + .../ComposeContentViewModel.swift | 7 +++++- .../Publisher/MastodonStatusPublisher.swift | 10 ++++----- .../Toolbar/ComposeContentToolbarView.swift | 3 +-- ...swift => Twitter+Entity+Coordinates.swift} | 2 +- .../xcshareddata/xcschemes/TwidereX.xcscheme | 22 +++++++++++++++++++ .../Provider/DataSourceFacade+Status.swift | 5 +++-- .../Root/MainTab/MainTabBarController.swift | 2 +- .../Base/Common/TimelineViewModel.swift | 1 + .../List/ListTimelineViewController.swift | 13 +++++++++++ .../Base/List/ListTimelineViewModel.swift | 8 +++++++ ...meTimelineViewController+DebugAction.swift | 18 +++++++++------ .../UserTimelineViewModel+Diffable.swift | 2 +- 14 files changed, 84 insertions(+), 23 deletions(-) rename TwidereSDK/Sources/TwitterSDK/Entity/{Twitter+Coordinates.swift => Twitter+Entity+Coordinates.swift} (88%) diff --git a/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift b/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift index 08703fe5..448c1b77 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift @@ -58,16 +58,23 @@ final public actor NotificationService { .sink { [weak self] authenticationIndexes in guard let self = self else { return } + let managedObjectContext = authenticationService.managedObjectContext // request permission when sign-in account Task { - if !authenticationIndexes.isEmpty { + let isEmpty = await managedObjectContext.perform { + return authenticationIndexes.isEmpty + } + if !isEmpty { await self.requestNotificationPermission() } } // end Task Task { - let authenticationContexts = authenticationIndexes.compactMap { authenticationIndex in - AuthenticationContext(authenticationIndex: authenticationIndex, secret: self.appSecret.secret) + let authenticationContexts: [AuthenticationContext] = await managedObjectContext.perform { + let authenticationContexts = authenticationIndexes.compactMap { authenticationIndex in + AuthenticationContext(authenticationIndex: authenticationIndex, secret: self.appSecret.secret) + } + return authenticationContexts } await self.updateSubscribers(authenticationContexts) } // end Task diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/RelationshipViewModel.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/RelationshipViewModel.swift index 32200cde..8cd9d8e4 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/RelationshipViewModel.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/RelationshipViewModel.swift @@ -109,6 +109,7 @@ public final class RelationshipViewModel { $me, relationshipUpdatePublisher ) + .receive(on: DispatchQueue.main) .sink { [weak self] user, me, _ in guard let self = self else { return } self.update(user: user, me: me) diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift index c352061a..1715ab6a 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -40,6 +40,8 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { public let customEmojiPickerInputViewModel = CustomEmojiPickerInputView.ViewModel() public let platform: Platform + @Published var authContext: AuthContext? + // reply-to public private(set) var replyTo: StatusObject? @@ -721,7 +723,9 @@ extension ComposeContentViewModel { } public func statusPublisher() throws -> StatusPublisher { - guard let author = self.author else { + guard let authContext = self.authContext, + let author = self.author + else { throw AppError.implicit(.authenticationMissing) } @@ -766,6 +770,7 @@ extension ComposeContentViewModel { ) case .mastodon(let author): return MastodonStatusPublisher( + authContext: authContext, author: author, replyTo: { guard case let .mastodon(status) = replyTo else { return nil } diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift index 306ca6e0..45499341 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift @@ -17,6 +17,7 @@ public final class MastodonStatusPublisher: NSObject, ProgressReporting { let logger = Logger(subsystem: "MastodonStatusPublisher", category: "Publisher") // Input + public let authContext: AuthContext // author public let author: MastodonUser @@ -45,6 +46,7 @@ public final class MastodonStatusPublisher: NSObject, ProgressReporting { public var state: Published.Publisher { $_state } public init( + authContext: AuthContext, author: MastodonUser, replyTo: ManagedObjectRecord?, isContentWarningComposing: Bool, @@ -58,6 +60,7 @@ public final class MastodonStatusPublisher: NSObject, ProgressReporting { pollMultipleConfiguration: PollComposeItem.MultipleConfiguration, visibility: Mastodon.Entity.Status.Visibility ) { + self.authContext = authContext self.author = author self.replyTo = replyTo self.isContentWarningComposing = isContentWarningComposing @@ -96,11 +99,8 @@ extension MastodonStatusPublisher: StatusPublisher { progress.totalUnitCount = taskCount progress.completedUnitCount = 0 - let _authenticationContext: MastodonAuthenticationContext? = await api.backgroundManagedObjectContext.perform { - guard let authentication = self.author.mastodonAuthentication else { return nil } - return MastodonAuthenticationContext(authentication: authentication) - } - guard let authenticationContext = _authenticationContext else { + guard case let .mastodon(authenticationContext) = authContext.authenticationContext else { + assertionFailure() throw AppError.implicit(.authenticationMissing) } diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.swift index d4ea966f..50cf4d80 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.swift @@ -31,8 +31,7 @@ public struct ComposeContentToolbarView: View { // replySettings | visibility menu button switch viewModel.author { case .twitter: - EmptyView() - twitterReplySettingsMenuButton + twitterReplySettingsMenuButton case .mastodon: mastodonVisibilityMenuButton case .none: diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Coordinates.swift b/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+Coordinates.swift similarity index 88% rename from TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Coordinates.swift rename to TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+Coordinates.swift index 425c081a..54a7971c 100644 --- a/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Coordinates.swift +++ b/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+Coordinates.swift @@ -1,5 +1,5 @@ // -// Twitter+Coordinates.swift +// Twitter+Entity+Coordinates.swift // TwitterAPI // // Created by Cirno MainasuK on 2020-9-16. diff --git a/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX.xcscheme b/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX.xcscheme index 9c63e642..3ab0eef7 100644 --- a/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX.xcscheme +++ b/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX.xcscheme @@ -89,6 +89,28 @@ ReferencedContainer = "container:TwidereX.xcodeproj"> + + + + + + + + + + + + (Void()) @Published var enableAutoFetchLatest = false + @Published var didAutoFetchLatest = false @Published var isRefreshControlEnabled = true @Published var isFloatyButtonDisplay = true @Published var isLoadingLatest = false diff --git a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift index fc59b6a8..205f8bcc 100644 --- a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift @@ -72,7 +72,20 @@ extension ListTimelineViewController { .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self = self else { return } + // do not fetch more when on background guard self.isDisplaying else { return } + + switch self.viewModel.kind { + case .home: + // do not fetch more when empty on home timeline + // otherwise the fetchLatest will be override at app launch + guard let snapshot = self.viewModel.diffableDataSource?.snapshot(), + snapshot.numberOfItems > 0 + else { return } + default: + break + } + self.viewModel.stateMachine.enter(TimelineViewModel.LoadOldestState.Loading.self) } .store(in: &disposeBag) diff --git a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift index e6485cb1..6ca31a44 100644 --- a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift @@ -22,6 +22,10 @@ class ListTimelineViewModel: TimelineViewModel { animatingDifferences: Bool ) { diffableDataSource?.apply(snapshot, animatingDifferences: animatingDifferences) + + if enableAutoFetchLatest, !didAutoFetchLatest { + autoFetchLatestAction.send() + } } @MainActor @@ -29,6 +33,10 @@ class ListTimelineViewModel: TimelineViewModel { snapshot: NSDiffableDataSourceSnapshot ) { diffableDataSource?.applySnapshotUsingReloadData(snapshot) + + if enableAutoFetchLatest, !didAutoFetchLatest { + autoFetchLatestAction.send() + } } } diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift index 9306e40b..2c678063 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift @@ -526,14 +526,18 @@ extension HomeTimelineViewController { let droppingObjectIDs = snapshot.itemIdentifiers.prefix(count).compactMap { item -> [NSManagedObjectID]? in switch item { case .feed(let record): - var ids: [NSManagedObjectID] = [record.objectID] - if let feed = record.object(in: context.apiService.backgroundManagedObjectContext) { - if let objectID = feed.twitterStatus?.objectID { - ids.append(objectID) - } - if let objectID = feed.mastodonStatus?.objectID { - ids.append(objectID) + let managedObjectContext = context.managedObjectContext + let ids: [NSManagedObjectID] = managedObjectContext.performAndWait { + var ids: [NSManagedObjectID] = [record.objectID] + if let feed = record.object(in: managedObjectContext) { + if let objectID = feed.twitterStatus?.objectID { + ids.append(objectID) + } + if let objectID = feed.mastodonStatus?.objectID { + ids.append(objectID) + } } + return ids } return ids default: diff --git a/TwidereX/Scene/Timeline/User/Status/UserTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/User/Status/UserTimelineViewModel+Diffable.swift index 3e3f39b0..efbb3c29 100644 --- a/TwidereX/Scene/Timeline/User/Status/UserTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/User/Status/UserTimelineViewModel+Diffable.swift @@ -83,7 +83,7 @@ extension UserTimelineViewModel { self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): snapshot has changes") } - await self.updateDataSource(snapshot: newSnapshot, animatingDifferences: false) + self.updateDataSource(snapshot: newSnapshot, animatingDifferences: false) self.didLoadLatest.send() self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): applied new snapshot") From b11f871b40da60ca3661468496699b46d3c0e4f8 Mon Sep 17 00:00:00 2001 From: CMK Date: Sat, 28 Jan 2023 17:13:54 +0800 Subject: [PATCH 022/128] fix: the first launch may get wrong layout on iOS 16 issue --- TwidereX/Coordinator/SceneCoordinator.swift | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/TwidereX/Coordinator/SceneCoordinator.swift b/TwidereX/Coordinator/SceneCoordinator.swift index f4f2a204..23c9107f 100644 --- a/TwidereX/Coordinator/SceneCoordinator.swift +++ b/TwidereX/Coordinator/SceneCoordinator.swift @@ -122,20 +122,25 @@ extension SceneCoordinator { let rootViewController: UIViewController do { + // check AuthContext let request = AuthenticationIndex.sortedFetchRequest request.fetchLimit = 1 let _authenticationIndex = try context.managedObjectContext.fetch(request).first guard let authenticationIndex = _authenticationIndex, let authContext = AuthContext(authenticationIndex: authenticationIndex) else { + // no AuthContext, use empty ViewController as root and show welcome via modal let configuration = WelcomeViewModel.Configuration(allowDismissModal: false) let welcomeViewModel = WelcomeViewModel(context: context, configuration: configuration) - let welcomeViewController = WelcomeViewController() - welcomeViewController.viewModel = welcomeViewModel - welcomeViewController.context = context - welcomeViewController.coordinator = self - rootViewController = welcomeViewController - sceneDelegate.window?.rootViewController = rootViewController // entry #1: Welcome + sceneDelegate.window?.rootViewController = UIViewController() + // use async without animation modal to fix the UIKit safe-area not take effect issue + DispatchQueue.main.async { + self.present( + scene: .welcome(viewModel: welcomeViewModel), + from: nil, + transition: .modal(animated: false) + ) // entry #1: Welcome + } return } From 30260b4f310f34ab770f63b4af4080c6d89337e9 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 2 Feb 2023 14:30:18 +0800 Subject: [PATCH 023/128] chore: inject AuthContext --- ShareExtension/ComposeViewController.swift | 196 ++++++++++++------ ShareExtension/ComposeViewModel.swift | 15 +- .../TwidereCore/State/AuthContext.swift | 15 ++ .../StatusFetchViewModel+Timeline.swift | 8 + .../Control/UnreadIndicatorView.swift | 2 + .../ComposeContentViewModel.swift | 19 +- TwidereX.xcodeproj/project.pbxproj | 8 - TwidereX/Coordinator/SceneCoordinator.swift | 13 +- .../Diffable/Misc/TabBar/TabBarItem.swift | 14 +- .../Provider/DataSourceFacade+LIst.swift | 10 +- .../Provider/DataSourceFacade+Media.swift | 7 +- .../Provider/DataSourceFacade+Meta.swift | 10 +- .../Provider/DataSourceFacade+Profile.swift | 12 +- .../DataSourceFacade+SavedSearch.swift | 6 +- .../Provider/DataSourceFacade+Status.swift | 4 +- .../DataSourceFacade+StatusThread.swift | 5 +- .../Provider/DataSourceFacade+User.swift | 8 +- ...der+MediaInfoDescriptionViewDelegate.swift | 6 +- ...ider+StatusViewTableViewCellDelegate.swift | 14 +- ...taSourceProvider+UITableViewDelegate.swift | 4 +- .../Scene/Compose/ComposeViewController.swift | 6 +- TwidereX/Scene/Compose/ComposeViewModel.swift | 9 - .../Scene/History/HistoryViewController.swift | 5 + .../Status/StatusHistoryViewController.swift | 5 + .../User/UserHistoryViewController.swift | 5 + .../AddListMemberViewController.swift | 1 + .../AddListMemberViewModel.swift | 7 +- .../CompositeListViewController.swift | 2 +- .../CompositeListViewModel.swift | 15 +- .../Scene/List/List/ListViewController.swift | 2 +- TwidereX/Scene/List/List/ListViewModel.swift | 3 + .../ListUser/ListUserViewController.swift | 7 +- .../List/ListUser/ListUserViewModel.swift | 3 + .../MediaPreviewViewController.swift | 5 + .../MediaPreview/MediaPreviewViewModel.swift | 3 + .../NotificationTimelineViewController.swift | 5 + ...tificationTimelineViewModel+Diffable.swift | 2 +- .../NotificationTimelineViewModel.swift | 10 +- .../NotificationViewController.swift | 8 +- .../Notification/NotificationViewModel.swift | 18 +- .../FollowerListViewController.swift | 5 + .../FollowingListViewController.swift | 5 + .../FriendshipListViewModel.swift | 10 +- .../Scene/Profile/LocalProfileViewModel.swift | 11 +- .../Scene/Profile/MeProfileViewModel.swift | 47 ----- .../Scene/Profile/ProfileViewController.swift | 17 +- TwidereX/Scene/Profile/ProfileViewModel.swift | 7 +- .../Profile/RemoteProfileViewModel.swift | 8 +- .../Root/ContentSplitViewController.swift | 4 +- .../Drawer/DrawerSidebarViewController.swift | 22 +- .../Root/Drawer/DrawerSidebarViewModel.swift | 7 +- TwidereX/Scene/Root/Sidebar/SidebarView.swift | 7 +- .../Scene/Root/Sidebar/SidebarViewModel.swift | 7 +- .../SavedSearchViewController.swift | 5 + .../SavedSearch/SavedSearchViewModel.swift | 12 +- .../Search/Search/SearchViewController.swift | 9 +- .../Scene/Search/Search/SearchViewModel.swift | 11 +- .../Hashtag/SearchHashtagViewController.swift | 7 +- .../Hashtag/SearchHashtagViewModel.swift | 7 +- .../SearchResult/SearchResultViewModel.swift | 24 +-- .../User/SearchUserViewController.swift | 5 + .../User/SearchUserViewModel.swift | 3 + .../Search/Trend/TrendViewController.swift | 6 + .../Scene/Search/Trend/TrendViewModel.swift | 7 +- TwidereX/Scene/Setting/About/AboutView.swift | 26 +-- .../Setting/About/AboutViewController.swift | 3 +- .../Scene/Setting/About/AboutViewModel.swift | 5 +- .../AccountPreferenceViewModel.swift | 8 +- ...MastodonNotificationSectionViewModel.swift | 8 +- .../Scene/Setting/List/SettingListView.swift | 20 +- .../List/SettingListViewController.swift | 7 +- .../Setting/List/SettingListViewModel.swift | 8 +- .../StatusThreadViewController.swift | 4 + .../StatusThread/StatusThreadViewModel.swift | 5 + .../Base/Common/TimelineViewController.swift | 9 +- .../Base/Common/TimelineViewModel.swift | 3 + .../FederatedTimelineViewModel.swift | 2 + .../Hashtag/HashtagTimelineViewModel.swift | 8 +- ...meTimelineViewController+DebugAction.swift | 60 +++++- .../Timeline/Home/HomeTimelineViewModel.swift | 14 +- .../List/ListStatusTimelineViewModel.swift | 2 + .../Media/SearchMediaTimelineViewModel.swift | 4 +- .../Status/SearchTimelineViewModel.swift | 4 +- .../User/Like/MeLikeTimelineViewModel.swift | 29 --- .../Media/UserMediaTimelineViewModel.swift | 14 +- .../User/Status/UserTimelineViewModel.swift | 12 +- .../DrawerSidebarTransitionController.swift | 8 +- TwidereX/Supporting Files/SceneDelegate.swift | 45 ++-- 88 files changed, 668 insertions(+), 390 deletions(-) delete mode 100644 TwidereX/Scene/Profile/MeProfileViewModel.swift delete mode 100644 TwidereX/Scene/Timeline/User/Like/MeLikeTimelineViewModel.swift diff --git a/ShareExtension/ComposeViewController.swift b/ShareExtension/ComposeViewController.swift index ae692b16..030677ef 100644 --- a/ShareExtension/ComposeViewController.swift +++ b/ShareExtension/ComposeViewController.swift @@ -11,6 +11,7 @@ import UIKit import Combine import AppShared import TwidereUI +import CoreDataStack import UniformTypeIdentifiers class ComposeViewController: UIViewController { @@ -25,26 +26,29 @@ class ComposeViewController: UIViewController { private(set) lazy var sendBarButtonItem = UIBarButtonItem(image: Asset.Transportation.paperAirplane.image, style: .plain, target: self, action: #selector(ComposeViewController.sendBarButtonItemPressed(_:))) - lazy var composeContentViewModel: ComposeContentViewModel = { - return ComposeContentViewModel( - kind: .post, - configurationContext: ComposeContentViewModel.ConfigurationContext( - apiService: context.apiService, - authenticationService: context.authenticationService, - mastodonEmojiService: context.mastodonEmojiService, - statusViewConfigureContext: .init( - dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - authenticationContext: context.authenticationService.$activeAuthenticationContext - ) - ) - ) - }() - private(set) lazy var composeContentViewController: ComposeContentViewController = { - let composeContentViewController = ComposeContentViewController() - composeContentViewController.viewModel = composeContentViewModel - return composeContentViewController - }() + private var composeContentViewModel: ComposeContentViewModel? + private var composeContentViewController: ComposeContentViewController? + +// lazy var composeContentViewModel: ComposeContentViewModel = { +// return ComposeContentViewModel( +// kind: .post, +// configurationContext: ComposeContentViewModel.ConfigurationContext( +// apiService: context.apiService, +// authenticationService: context.authenticationService, +// mastodonEmojiService: context.mastodonEmojiService, +// statusViewConfigureContext: .init( +// dateTimeProvider: DateTimeSwiftProvider(), +// twitterTextProvider: OfficialTwitterTextProvider(), +// authenticationContext: context.authenticationService.$activeAuthenticationContext +// ) +// ) +// ) +// }() +// private(set) lazy var composeContentViewController: ComposeContentViewController = { +// let composeContentViewController = ComposeContentViewController() +// composeContentViewController.viewModel = composeContentViewModel +// return composeContentViewController +// }() let activityIndicatorBarButtonItem: UIBarButtonItem = { let indicatorView = UIActivityIndicatorView(style: .medium) @@ -75,6 +79,7 @@ extension ComposeViewController { navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .close, target: self, action: #selector(ComposeViewController.closeBarButtonItemPressed(_:))) navigationItem.rightBarButtonItem = sendBarButtonItem + viewModel.$isBusy .receive(on: DispatchQueue.main) .sink { [weak self] isBusy in @@ -88,59 +93,102 @@ extension ComposeViewController { await load(inputItems: inputItems) } // end Task - addChild(composeContentViewController) - composeContentViewController.view.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(composeContentViewController.view) - NSLayoutConstraint.activate([ - composeContentViewController.view.topAnchor.constraint(equalTo: view.topAnchor), - composeContentViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), - composeContentViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), - composeContentViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - composeContentViewController.didMove(toParent: self) - - // layout publish progress - publishProgressView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(publishProgressView) - NSLayoutConstraint.activate([ - publishProgressView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), - publishProgressView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - publishProgressView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - ]) - - // bind compose bar button item - composeContentViewModel.$isComposeBarButtonEnabled - .receive(on: DispatchQueue.main) - .assign(to: \.isEnabled, on: sendBarButtonItem) - .store(in: &disposeBag) - - // bind author - viewModel.$author.assign(to: &composeContentViewModel.$author) - - // bind progress bar - viewModel.$currentPublishProgress - .receive(on: DispatchQueue.main) - .sink { [weak self] progress in - guard let self = self else { return } - let progress = Float(progress) - let withAnimation = progress > self.publishProgressView.progress - self.publishProgressView.setProgress(progress, animated: withAnimation) - - if progress == 1 { - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in - guard let self = self else { return } - self.publishProgressView.setProgress(0, animated: false) + do { + guard let authContext = try setupAuthContext() else { + // setupHintLabel() + return + } + viewModel.authContext = authContext + + let composeContentViewModel = ComposeContentViewModel( + context: context, + authContext: authContext, + kind: .post, + configurationContext: ComposeContentViewModel.ConfigurationContext( + apiService: context.apiService, + authenticationService: context.authenticationService, + mastodonEmojiService: context.mastodonEmojiService, + statusViewConfigureContext: .init( + dateTimeProvider: DateTimeSwiftProvider(), + twitterTextProvider: OfficialTwitterTextProvider(), + authenticationContext: context.authenticationService.$activeAuthenticationContext + ) + ) + ) + let composeContentViewController = ComposeContentViewController() + composeContentViewController.viewModel = composeContentViewModel + + addChild(composeContentViewController) + composeContentViewController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(composeContentViewController.view) + NSLayoutConstraint.activate([ + composeContentViewController.view.topAnchor.constraint(equalTo: view.topAnchor), + composeContentViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + composeContentViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + composeContentViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + composeContentViewController.didMove(toParent: self) + + // layout publish progress + publishProgressView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(publishProgressView) + NSLayoutConstraint.activate([ + publishProgressView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), + publishProgressView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + publishProgressView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ]) + + // bind compose bar button item + composeContentViewModel.$isComposeBarButtonEnabled + .receive(on: DispatchQueue.main) + .assign(to: \.isEnabled, on: sendBarButtonItem) + .store(in: &disposeBag) + + // bind progress bar + viewModel.$currentPublishProgress + .receive(on: DispatchQueue.main) + .sink { [weak self] progress in + guard let self = self else { return } + let progress = Float(progress) + let withAnimation = progress > self.publishProgressView.progress + self.publishProgressView.setProgress(progress, animated: withAnimation) + + if progress == 1 { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in + guard let self = self else { return } + self.publishProgressView.setProgress(0, animated: false) + } } } - } - .store(in: &disposeBag) - - // set delegate - composeContentViewController.delegate = self + .store(in: &disposeBag) + + // set delegate + composeContentViewController.delegate = self + + self.composeContentViewModel = composeContentViewModel + self.composeContentViewController = composeContentViewController + + Task { @MainActor in + let inputItems = self.extensionContext?.inputItems.compactMap { $0 as? NSExtensionItem } ?? [] + await load(inputItems: inputItems) + } // end Task + + } catch { + + } } } +extension ComposeViewController { + private func setupAuthContext() throws -> AuthContext? { + let request = AuthenticationIndex.sortedFetchRequest + let _authenticationIndex = try context.managedObjectContext.fetch(request).first + let _authContext = _authenticationIndex.flatMap { AuthContext(authenticationIndex: $0) } + return _authContext + } +} + extension ComposeViewController { @objc private func closeBarButtonItemPressed(_ sender: UIBarButtonItem) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") @@ -154,7 +202,10 @@ extension ComposeViewController { Task { @MainActor in do { await self.setBusy(true) - let statusPublisher = try composeContentViewModel.statusPublisher() + guard let statusPublisher = try self.composeContentViewModel?.statusPublisher() else { + await self.setBusy(false) + return + } // setup progress self.viewModel.currentPublishProgressObservation = statusPublisher.progress @@ -195,6 +246,13 @@ extension ComposeViewController { extension ComposeViewController { private func load(inputItems: [NSExtensionItem]) async { + guard let composeContentViewModel = self.composeContentViewModel, + let authContext = viewModel.authContext + else { + assertionFailure() + return + } + var itemProviders: [NSItemProvider] = [] for item in inputItems { @@ -281,7 +339,7 @@ extension ComposeViewController: ComposeContentViewControllerDelegate { extension ComposeViewController: UIAdaptivePresentationControllerDelegate { func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { - return composeContentViewModel.canDismissDirectly + return composeContentViewModel?.canDismissDirectly ?? true } func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) { diff --git a/ShareExtension/ComposeViewModel.swift b/ShareExtension/ComposeViewModel.swift index 840ff5e9..b53e4a8f 100644 --- a/ShareExtension/ComposeViewModel.swift +++ b/ShareExtension/ComposeViewModel.swift @@ -14,28 +14,21 @@ final class ComposeViewModel { var disposeBag = Set() + var currentPublishProgressObservation: NSKeyValueObservation? + // input let context: AppContext + + @Published var authContext: AuthContext? @Published var isBusy = false @Published var didLoad = false - var currentPublishProgressObservation: NSKeyValueObservation? - // output - @Published var author: UserObject? @Published var currentPublishProgress: Double = 0 init(context: AppContext) { self.context = context // end init - - context.authenticationService.activeAuthenticationIndex - .receive(on: DispatchQueue.main) - .sink { [weak self] authenticationIndex in - guard let self = self else { return } - self.author = authenticationIndex?.user - } - .store(in: &disposeBag) } } diff --git a/TwidereSDK/Sources/TwidereCore/State/AuthContext.swift b/TwidereSDK/Sources/TwidereCore/State/AuthContext.swift index dbb3f588..ef665ff0 100644 --- a/TwidereSDK/Sources/TwidereCore/State/AuthContext.swift +++ b/TwidereSDK/Sources/TwidereCore/State/AuthContext.swift @@ -10,6 +10,10 @@ import CoreData import CoreDataStack import TwidereCommon +public protocol AuthContextProvider { + var authContext: AuthContext { get } +} + public class AuthContext { public let authenticationContext: AuthenticationContext @@ -30,3 +34,14 @@ public class AuthContext { } } + +#if DEBUG +extension AuthContext { + public static func mock(context: AppContext) -> AuthContext? { + let request = AuthenticationIndex.sortedFetchRequest + let _authenticationIndex = try? context.managedObjectContext.fetch(request).first + let _authContext = _authenticationIndex.flatMap { AuthContext(authenticationIndex: $0) } + return _authContext + } +} +#endif diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift index 38f48e09..bd8fef76 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift @@ -61,6 +61,14 @@ extension StatusFetchViewModel.Timeline.Kind { } } + public init( + timelineKind: TimelineKind, + userIdentifier: UserIdentifier? + ) { + self.timelineKind = timelineKind + self.userIdentifier = userIdentifier + } + public enum TimelineKind { case status case media diff --git a/TwidereSDK/Sources/TwidereUI/Control/UnreadIndicatorView.swift b/TwidereSDK/Sources/TwidereUI/Control/UnreadIndicatorView.swift index f13af2ac..9a8a892a 100644 --- a/TwidereSDK/Sources/TwidereUI/Control/UnreadIndicatorView.swift +++ b/TwidereSDK/Sources/TwidereUI/Control/UnreadIndicatorView.swift @@ -30,7 +30,9 @@ final public class UnreadIndicatorView: UIView { } } + // input public var count = 0 + private var currentCount = 0 var displayLink: CADisplayLink? diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift index 1715ab6a..944133f2 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -35,12 +35,15 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { @Published var viewSize: CGSize = .zero // input + let context: AppContext public let kind: Kind public let configurationContext: ConfigurationContext public let customEmojiPickerInputViewModel = CustomEmojiPickerInputView.ViewModel() public let platform: Platform - @Published var authContext: AuthContext? + // Author (Me) + @Published public private(set) var authContext: AuthContext? + @Published public private(set) var author: UserObject? // reply-to public private(set) var replyTo: StatusObject? @@ -79,9 +82,6 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { } } @Published public var isContentWarningEditing = false - - // avatar (me) - @Published public var author: UserObject? // mention (Twitter) @Published public private(set) var isMentionPickDisplay = false @@ -153,10 +153,14 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { let viewLayoutMarginDidUpdate = CurrentValueSubject(Void()) public init( + context: AppContext, + authContext: AuthContext, kind: Kind, settings: Settings = Settings(), configurationContext: ConfigurationContext ) { + self.context = context + self.authContext = authContext self.kind = kind self.configurationContext = configurationContext self.platform = configurationContext.authenticationService.activeAuthenticationContext?.platform ?? .none @@ -245,6 +249,13 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { initialContent = content + // bind author +// $authContext +// .receive(on: DispatchQueue.main) +// .map { authContext in +// authContext?.authenticationContext.user(in: configurationContext.apiService.) +// } + // bind text $content .map { $0.isEmpty } diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index eff430e0..8872b3da 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -84,7 +84,6 @@ DB30ADDC26CFC7EE00B2D2BE /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB30ADDB26CFC7EE00B2D2BE /* StatusTableViewCell.swift */; }; DB30ADDD26CFD3CC00B2D2BE /* HomeTimelineViewController+DebugAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB2513D2599DBAB0064A876 /* HomeTimelineViewController+DebugAction.swift */; }; DB33A4A925A319A0003CED7D /* ActionToolbarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB33A4A825A319A0003CED7D /* ActionToolbarContainer.swift */; }; - DB34029D2521BE8B009EFADF /* MeProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB34029C2521BE8B009EFADF /* MeProfileViewModel.swift */; }; DB36F35E257F74C10028F81E /* ScrollViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB36F35D257F74C00028F81E /* ScrollViewContainer.swift */; }; DB37F6A0274B556B0081603F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB37F69F274B556B0081603F /* Assets.xcassets */; }; DB3B905F26E8A9650010F64C /* StatusThreadViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3B905E26E8A9650010F64C /* StatusThreadViewController+DataSourceProvider.swift */; }; @@ -361,7 +360,6 @@ DBF87AD52892A6740029A7C7 /* AppIconPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF87AD42892A6740029A7C7 /* AppIconPreferenceView.swift */; }; DBF87AD92892A67D0029A7C7 /* TranslateButtonPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF87AD72892A67D0029A7C7 /* TranslateButtonPreferenceView.swift */; }; DBF87ADA2892A67D0029A7C7 /* TranslationServicePreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF87AD82892A67D0029A7C7 /* TranslationServicePreferenceView.swift */; }; - DBFA471C2859C33500C9FF7F /* MeLikeTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFA471B2859C33500C9FF7F /* MeLikeTimelineViewModel.swift */; }; DBFA471F2859C4AE00C9FF7F /* UserLikeTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFA471E2859C4AE00C9FF7F /* UserLikeTimelineViewController.swift */; }; DBFA47212859C4F300C9FF7F /* UserLikeTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFA47202859C4F300C9FF7F /* UserLikeTimelineViewModel.swift */; }; DBFA4A2025A5924C00D51703 /* ListTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFA4A1F25A5924C00D51703 /* ListTimelineViewModel+Diffable.swift */; }; @@ -601,7 +599,6 @@ DB30ADDB26CFC7EE00B2D2BE /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = ""; }; DB33A4A825A319A0003CED7D /* ActionToolbarContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionToolbarContainer.swift; sourceTree = ""; }; DB33E5B42719637400EC2225 /* CoverFlowStackCollectionViewLayout */ = {isa = PBXFileReference; lastKnownFileType = folder; path = CoverFlowStackCollectionViewLayout; sourceTree = ""; }; - DB34029C2521BE8B009EFADF /* MeProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeProfileViewModel.swift; sourceTree = ""; }; DB36F35D257F74C00028F81E /* ScrollViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewContainer.swift; sourceTree = ""; }; DB36F375257F79DB0028F81E /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; DB37F69F274B556B0081603F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -922,7 +919,6 @@ DBF87AD42892A6740029A7C7 /* AppIconPreferenceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppIconPreferenceView.swift; sourceTree = ""; }; DBF87AD72892A67D0029A7C7 /* TranslateButtonPreferenceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TranslateButtonPreferenceView.swift; sourceTree = ""; }; DBF87AD82892A67D0029A7C7 /* TranslationServicePreferenceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TranslationServicePreferenceView.swift; sourceTree = ""; }; - DBFA471B2859C33500C9FF7F /* MeLikeTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeLikeTimelineViewModel.swift; sourceTree = ""; }; DBFA471E2859C4AE00C9FF7F /* UserLikeTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLikeTimelineViewController.swift; sourceTree = ""; }; DBFA47202859C4F300C9FF7F /* UserLikeTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLikeTimelineViewModel.swift; sourceTree = ""; }; DBFA4A1F25A5924C00D51703 /* ListTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ListTimelineViewModel+Diffable.swift"; sourceTree = ""; }; @@ -1659,7 +1655,6 @@ DB6DF3DF252060AA00E8A273 /* ProfileViewModel.swift */, DB3B906826E8D1D80010F64C /* LocalProfileViewModel.swift */, DB76A67027609A8700A50673 /* RemoteProfileViewModel.swift */, - DB34029C2521BE8B009EFADF /* MeProfileViewModel.swift */, ); path = Profile; sourceTree = ""; @@ -2446,7 +2441,6 @@ children = ( DBFA471E2859C4AE00C9FF7F /* UserLikeTimelineViewController.swift */, DBFA47202859C4F300C9FF7F /* UserLikeTimelineViewModel.swift */, - DBFA471B2859C33500C9FF7F /* MeLikeTimelineViewModel.swift */, ); path = Like; sourceTree = ""; @@ -3063,7 +3057,6 @@ DB8302142742180C00BF5224 /* NotificationTimelineViewController+DebugAction.swift in Sources */, DB71C7D3271EB71800BE3819 /* ProfileViewController+DataSourceProvider.swift in Sources */, DB8301FA273CED2E00BF5224 /* NotificationTimelineViewModel.swift in Sources */, - DB34029D2521BE8B009EFADF /* MeProfileViewModel.swift in Sources */, DB36F35E257F74C10028F81E /* ScrollViewContainer.swift in Sources */, DB76A65D275F52D500A50673 /* MediaInfoDescriptionView+ViewModel.swift in Sources */, DB66DB902823AC3C0071F5F3 /* TabBarItem.swift in Sources */, @@ -3087,7 +3080,6 @@ DB76A64F275F1C3200A50673 /* MediaPreviewingViewController.swift in Sources */, DB42411626C3EB9100B6C5F8 /* ReadabilityPadding.swift in Sources */, DB2C8740274F4B7D00CE0398 /* DisplayPreferenceViewModel.swift in Sources */, - DBFA471C2859C33500C9FF7F /* MeLikeTimelineViewModel.swift in Sources */, DBCE2E992591A44000926D09 /* UIViewAnimatingPosition.swift in Sources */, DB88E626288EA1F7009A01F5 /* DisplayPreferenceView.swift in Sources */, DB262A422722970000D18EF3 /* SearchResultViewModel.swift in Sources */, diff --git a/TwidereX/Coordinator/SceneCoordinator.swift b/TwidereX/Coordinator/SceneCoordinator.swift index 23c9107f..d848353e 100644 --- a/TwidereX/Coordinator/SceneCoordinator.swift +++ b/TwidereX/Coordinator/SceneCoordinator.swift @@ -101,7 +101,7 @@ extension SceneCoordinator { case accountPreference(viewModel: AccountPreferenceViewModel) case behaviorsPreference(viewModel: BehaviorsPreferenceViewModel) case displayPreference - case about + case about(viewModel: AboutViewModel) #if DEBUG case developer @@ -388,8 +388,10 @@ private extension SceneCoordinator { viewController = _viewController case .displayPreference: viewController = DisplayPreferenceViewController() - case .about: - viewController = AboutViewController() + case .about(let viewModel): + let _viewController = AboutViewController() + _viewController.viewModel = viewModel + viewController = _viewController #if DEBUG case .developer: viewController = DeveloperViewController() @@ -455,6 +457,9 @@ extension SceneCoordinator { return } + let mastodonAuthenticationContext = MastodonAuthenticationContext(authentication: authentication) + let authConext = AuthContext(authenticationContext: .mastodon(authenticationContext: mastodonAuthenticationContext)) + // 1. active notification account guard let currentAuthenticationContext = context.authenticationService.activeAuthenticationContext else { // discard task if no available account @@ -496,6 +501,7 @@ extension SceneCoordinator { case .follow: let remoteProfileViewModel = RemoteProfileViewModel( context: context, + authContext: authConext, profileContext: .mastodon(.userID(notification.account.id)) ) present( @@ -522,6 +528,7 @@ extension SceneCoordinator { } let statusThreadViewModel = StatusThreadViewModel( context: context, + authContext: authConext, root: .root(context: .init(status: .mastodon(record: root.asRecrod))) ) present( diff --git a/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift b/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift index 5599b015..2f8b4e62 100644 --- a/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift +++ b/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift @@ -88,33 +88,33 @@ extension TabBarItem { switch self { case .home: let _viewController = HomeTimelineViewController() - _viewController.viewModel = HomeTimelineViewModel(context: context) + _viewController.viewModel = HomeTimelineViewModel(context: context, authContext: authContext) viewController = _viewController case .notification: let _viewController = NotificationViewController() viewController = _viewController case .search: let _viewController = SearchViewController() - _viewController.viewModel = SearchViewModel(context: context) + _viewController.viewModel = SearchViewModel(context: context, authContext: authContext) viewController = _viewController case .me: let _viewController = ProfileViewController() - let profileViewModel = MeProfileViewModel(context: context) + let profileViewModel = ProfileViewModel(context: context, authContext: authContext) _viewController.viewModel = profileViewModel viewController = _viewController case .local: let _viewController = FederatedTimelineViewController() - _viewController.viewModel = FederatedTimelineViewModel(context: context, isLocal: true) + _viewController.viewModel = FederatedTimelineViewModel(context: context, authContext: authContext, isLocal: true) viewController = _viewController case .federated: let _viewController = FederatedTimelineViewController() - _viewController.viewModel = FederatedTimelineViewModel(context: context, isLocal: false) + _viewController.viewModel = FederatedTimelineViewModel(context: context, authContext: authContext, isLocal: false) viewController = _viewController case .messages: fatalError() case .likes: let _viewController = UserLikeTimelineViewController() - _viewController.viewModel = MeLikeTimelineViewModel(context: context) + _viewController.viewModel = UserLikeTimelineViewModel(context: context, authContext: authContext, timelineContext: .init(timelineKind: .like, userIdentifier: authContext.authenticationContext.userIdentifier)) viewController = _viewController case .history: let _viewController = HistoryViewController() @@ -129,7 +129,7 @@ extension TabBarItem { return AdaptiveStatusBarStyleNavigationController(rootViewController: UIViewController()) } let _viewController = CompositeListViewController() - _viewController.viewModel = CompositeListViewModel(context: context, kind: .lists(me)) + _viewController.viewModel = CompositeListViewModel(context: context, authContext: authContext, kind: .lists(me)) viewController = _viewController case .trends: fatalError() diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+LIst.swift b/TwidereX/Protocol/Provider/DataSourceFacade+LIst.swift index 3720c62f..2e0759d0 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+LIst.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+LIst.swift @@ -17,11 +17,12 @@ import SwiftMessages extension DataSourceFacade { static func coordinateToListMemberScene( - dependency: NeedsDependency & UIViewController, + dependency: NeedsDependency & AuthContextProvider & UIViewController, list: ListRecord ) async { let listUserViewModel = ListUserViewModel( context: dependency.context, + authContext: dependency.authContext, kind: .members(list: list) ) await dependency.coordinator.present( @@ -32,11 +33,12 @@ extension DataSourceFacade { } static func coordinateToListSubscriberScene( - dependency: NeedsDependency & UIViewController, + dependency: NeedsDependency & AuthContextProvider & UIViewController, list: ListRecord ) async { let listUserViewModel = ListUserViewModel( context: dependency.context, + authContext: dependency.authContext, kind: .subscribers(list: list) ) await dependency.coordinator.present( @@ -51,7 +53,7 @@ extension DataSourceFacade { extension DataSourceFacade { static func createMenuForList( - dependency: NeedsDependency & UIViewController, + dependency: NeedsDependency & AuthContextProvider & UIViewController, list: ListRecord, authenticationContext: AuthenticationContext ) async throws -> UIMenu { @@ -218,7 +220,7 @@ extension DataSourceFacade { ) { _ in Task { @MainActor [weak dependency] in guard let dependency = dependency else { return } - let profileViewModel = LocalProfileViewModel(context: dependency.context, userRecord: owner) + let profileViewModel = LocalProfileViewModel(context: dependency.context, authContext: dependency.authContext, userRecord: owner) dependency.coordinator.present( scene: .profile(viewModel: profileViewModel), from: dependency, diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift index 0e41c280..acd47032 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift @@ -39,7 +39,7 @@ extension DataSourceFacade { } static func coordinateToMediaPreviewScene( - provider: DataSourceProvider & MediaPreviewableViewController, + provider: DataSourceProvider & AuthContextProvider & MediaPreviewableViewController, target: StatusTarget, status: StatusRecord, mediaPreviewContext: MediaPreviewContext @@ -71,7 +71,7 @@ extension DataSourceFacade { @MainActor static func coordinateToMediaPreviewScene( - provider: DataSourceProvider & MediaPreviewableViewController, + provider: DataSourceProvider & AuthContextProvider & MediaPreviewableViewController, status: StatusRecord, mediaPreviewContext: MediaPreviewContext ) async { @@ -171,7 +171,7 @@ extension DataSourceFacade { @MainActor static func coordinateToMediaPreviewScene( - provider: DataSourceProvider & MediaPreviewableViewController, + provider: DataSourceProvider & AuthContextProvider & MediaPreviewableViewController, status: StatusRecord, mediaPreviewItem: MediaPreviewViewModel.Item, mediaPreviewTransitionItem: MediaPreviewTransitionItem, @@ -179,6 +179,7 @@ extension DataSourceFacade { ) async { let mediaPreviewViewModel = MediaPreviewViewModel( context: provider.context, + authContext: provider.authContext, item: mediaPreviewItem, transitionItem: mediaPreviewTransitionItem ) diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Meta.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Meta.swift index 8f788617..91d23615 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Meta.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Meta.swift @@ -14,7 +14,7 @@ import Meta extension DataSourceFacade { static func responseToMetaTextAreaView( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, target: StatusTarget, status: StatusRecord, metaTextAreaView: MetaTextAreaView, @@ -36,7 +36,7 @@ extension DataSourceFacade { } static func responseToMetaTextAreaView( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, status: StatusRecord, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta @@ -45,7 +45,7 @@ extension DataSourceFacade { case .url(_, _, let url, _): await provider.coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) case .hashtag(_, let hashtag, _): - let hashtagViewModel = HashtagTimelineViewModel(context: provider.context, hashtag: hashtag) + let hashtagViewModel = HashtagTimelineViewModel(context: provider.context, authContext: provider.authContext, hashtag: hashtag) await provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagViewModel), from: provider, transition: .show) case .mention(_, let mention, let userInfo): await DataSourceFacade.coordinateToProfileScene( @@ -62,7 +62,7 @@ extension DataSourceFacade { extension DataSourceFacade { static func responseToMetaTextAreaView( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, user: UserRecord, didSelectMeta meta: Meta ) async { @@ -70,7 +70,7 @@ extension DataSourceFacade { case .url(_, _, let url, _): await provider.coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) case .hashtag(_, let hashtag, _): - let hashtagViewModel = HashtagTimelineViewModel(context: provider.context, hashtag: hashtag) + let hashtagViewModel = HashtagTimelineViewModel(context: provider.context, authContext: provider.authContext, hashtag: hashtag) await provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagViewModel), from: provider, transition: .show) case .mention(_, let mention, let userInfo): await coordinateToProfileScene( diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Profile.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Profile.swift index 02f4fdfb..f1d24520 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Profile.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Profile.swift @@ -13,7 +13,7 @@ import CoreDataStack extension DataSourceFacade { static func coordinateToProfileScene( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, target: StatusTarget, status: StatusRecord ) async { @@ -34,11 +34,12 @@ extension DataSourceFacade { @MainActor static func coordinateToProfileScene( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, user: UserRecord ) async { let profileViewModel = LocalProfileViewModel( context: provider.context, + authContext: provider.authContext, userRecord: user ) provider.coordinator.present( @@ -61,7 +62,7 @@ extension DataSourceFacade { extension DataSourceFacade { static func coordinateToProfileScene( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, status: StatusRecord, mention: String, // username, userInfo: [AnyHashable: Any]? @@ -128,7 +129,7 @@ extension DataSourceFacade { } static func coordinateToProfileScene( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, user: UserRecord, mention: String, // username, userInfo: [AnyHashable: Any]? @@ -178,11 +179,12 @@ extension DataSourceFacade { @MainActor static func coordinateToProfileScene( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, profileContext: RemoteProfileViewModel.ProfileContext ) async { let profileViewModel = RemoteProfileViewModel( context: provider.context, + authContext: provider.authContext, profileContext: profileContext ) provider.coordinator.present( diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+SavedSearch.swift b/TwidereX/Protocol/Provider/DataSourceFacade+SavedSearch.swift index 8f8f7177..197937d1 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+SavedSearch.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+SavedSearch.swift @@ -14,13 +14,14 @@ extension DataSourceFacade { @MainActor static func coordinateToSearchResult( - dependency: NeedsDependency & UIViewController, + dependency: NeedsDependency & AuthContextProvider & UIViewController, savedSearch: SavedSearchRecord ) { guard let savedResult = savedSearch.object(in: dependency.context.managedObjectContext) else { return } let searchResultViewModel = SearchResultViewModel( context: dependency.context, + authContext: dependency.authContext, coordinator: dependency.coordinator ) searchResultViewModel.searchText = savedResult.query @@ -33,11 +34,12 @@ extension DataSourceFacade { @MainActor static func coordinateToSearchResult( - dependency: NeedsDependency & UIViewController, + dependency: NeedsDependency & AuthContextProvider & UIViewController, trend object: TrendObject ) { let searchResultViewModel = SearchResultViewModel( context: dependency.context, + authContext: dependency.authContext, coordinator: dependency.coordinator ) searchResultViewModel.searchText = object.query diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift index be1e8e94..48767fd3 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift @@ -16,7 +16,7 @@ import SwiftMessages extension DataSourceFacade { @MainActor static func responseToStatusToolbar( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, status: StatusRecord, action: StatusToolbar.Action, sender: UIButton, @@ -43,6 +43,8 @@ extension DataSourceFacade { let composeViewModel = ComposeViewModel(context: provider.context) let composeContentViewModel = ComposeContentViewModel( + context: provider.context, + authContext: provider.authContext, kind: .reply(status: status), configurationContext: ComposeContentViewModel.ConfigurationContext( apiService: provider.context.apiService, diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+StatusThread.swift b/TwidereX/Protocol/Provider/DataSourceFacade+StatusThread.swift index b1cb820b..61bb41b8 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+StatusThread.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+StatusThread.swift @@ -12,7 +12,7 @@ import CoreDataStack extension DataSourceFacade { static func coordinateToStatusThreadScene( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, target: StatusTarget, status: StatusRecord ) async { @@ -60,11 +60,12 @@ extension DataSourceFacade { @MainActor static func coordinateToStatusThreadScene( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, root: StatusItem.Thread ) async { let statusThreadViewModel = StatusThreadViewModel( context: provider.context, + authContext: provider.authContext, root: root ) provider.coordinator.present( diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+User.swift b/TwidereX/Protocol/Provider/DataSourceFacade+User.swift index 3ef8123b..f5eaf808 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+User.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+User.swift @@ -14,7 +14,7 @@ import TwidereCore extension DataSourceFacade { static func createMenuForUser( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, user: UserRecord, authenticationContext: AuthenticationContext ) async throws -> UIMenu { @@ -102,7 +102,7 @@ extension DataSourceFacade { @MainActor private static func createMenuViewListsActionForUser( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, record: UserRecord, authenticationContext: AuthenticationContext ) async -> UIAction? { @@ -138,6 +138,7 @@ extension DataSourceFacade { let compositeListViewModel = CompositeListViewModel( context: provider.context, + authContext: provider.authContext, kind: .lists(record) ) provider.coordinator.present( @@ -151,7 +152,7 @@ extension DataSourceFacade { @MainActor private static func createMenuViewListedActionForUser( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, record: UserRecord, authenticationContext: AuthenticationContext ) async -> UIAction? { @@ -172,6 +173,7 @@ extension DataSourceFacade { let compositeListViewModel = CompositeListViewModel( context: provider.context, + authContext: provider.authContext, kind: .listed(record) ) provider.coordinator.present( diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift index 1218162a..4162c8ac 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift @@ -15,7 +15,7 @@ import AppShared import MetaTextKit import MetaLabel -extension MediaInfoDescriptionViewDelegate where Self: DataSourceProvider { +extension MediaInfoDescriptionViewDelegate where Self: DataSourceProvider & AuthContextProvider { func mediaInfoDescriptionView(_ mediaInfoDescriptionView: MediaInfoDescriptionView, avatarButtonDidPressed button: UIButton) { Task { let source = DataSourceItem.Source(tableViewCell: nil, indexPath: nil) @@ -45,7 +45,7 @@ extension MediaInfoDescriptionViewDelegate where Self: DataSourceProvider { assertionFailure("only works for status data provider") return } - + await DataSourceFacade.coordinateToStatusThreadScene( provider: self, target: .repost, // keep repost wrapper @@ -64,7 +64,7 @@ extension MediaInfoDescriptionViewDelegate where Self: DataSourceProvider { assertionFailure("only works for status data provider") return } - + await DataSourceFacade.coordinateToStatusThreadScene( provider: self, target: .repost, // keep repost wrapper diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift index 1bd9d185..a40d415a 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift @@ -13,7 +13,7 @@ import MetaTextArea import Meta // MARK: - header -extension StatusViewTableViewCellDelegate where Self: DataSourceProvider { +extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell( _ cell: UITableViewCell, statusView: StatusView, @@ -39,7 +39,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider { } // MARK: - avatar button -extension StatusViewTableViewCellDelegate where Self: DataSourceProvider { +extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell( _ cell: UITableViewCell, @@ -111,7 +111,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider { } // MARK: - content -extension StatusViewTableViewCellDelegate where Self: DataSourceProvider { +extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) { Task { let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) @@ -157,7 +157,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider { // MARK: - media -extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & MediaPreviewableViewController { +extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider & MediaPreviewableViewController { func tableViewCell( _ cell: UITableViewCell, statusView: StatusView, @@ -279,7 +279,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider { } // MARK: - quote -extension StatusViewTableViewCellDelegate where Self: DataSourceProvider { +extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, quoteStatusViewDidPressed quoteStatusView: StatusView) { Task { let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) @@ -301,7 +301,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider { // MARK: - toolbar -extension StatusViewTableViewCellDelegate where Self: DataSourceProvider { +extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell( _ cell: UITableViewCell, statusView: StatusView, @@ -438,7 +438,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider { } // MARK: - a11y -extension StatusViewTableViewCellDelegate where Self: DataSourceProvider { +extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, accessibilityActivate: Void) { Task { diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift index d2d2d588..48ca1936 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -9,7 +9,7 @@ import UIKit import TwidereUI -extension UITableViewDelegate where Self: DataSourceProvider { +extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvider { func aspectTableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): indexPath: \(indexPath.debugDescription)") @@ -57,7 +57,7 @@ extension UITableViewDelegate where Self: DataSourceProvider { } -extension UITableViewDelegate where Self: DataSourceProvider & MediaPreviewableViewController { +extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvider & MediaPreviewableViewController { func aspectTableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") diff --git a/TwidereX/Scene/Compose/ComposeViewController.swift b/TwidereX/Scene/Compose/ComposeViewController.swift index 3cc379de..c3f7f651 100644 --- a/TwidereX/Scene/Compose/ComposeViewController.swift +++ b/TwidereX/Scene/Compose/ComposeViewController.swift @@ -73,9 +73,6 @@ extension ComposeViewController { .assign(to: \.isEnabled, on: sendBarButtonItem) .store(in: &disposeBag) - // bind author - viewModel.$author.assign(to: &composeContentViewModel.$author) - composeContentViewController.delegate = self } @@ -112,6 +109,8 @@ extension ComposeViewController: ComposeContentViewControllerDelegate { ) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + guard let authContext = viewController.viewModel.authContext else { return } + let _item: MediaPreviewViewModel.Item? switch attachmentViewModel.output { case .image(let data, _): @@ -134,6 +133,7 @@ extension ComposeViewController: ComposeContentViewControllerDelegate { let mediaPreviewViewModel = MediaPreviewViewModel( context: context, + authContext: authContext, item: item, transitionItem: { let item = MediaPreviewTransitionItem( diff --git a/TwidereX/Scene/Compose/ComposeViewModel.swift b/TwidereX/Scene/Compose/ComposeViewModel.swift index 54bdc464..51838c6e 100644 --- a/TwidereX/Scene/Compose/ComposeViewModel.swift +++ b/TwidereX/Scene/Compose/ComposeViewModel.swift @@ -18,20 +18,11 @@ final class ComposeViewModel { let context: AppContext // output - @Published var author: UserObject? @Published var title = L10n.Scene.Compose.Title.compose init(context: AppContext) { self.context = context // end init - - context.authenticationService.activeAuthenticationIndex - .receive(on: DispatchQueue.main) - .sink { [weak self] authenticationIndex in - guard let self = self else { return } - self.author = authenticationIndex?.user - } - .store(in: &disposeBag) } } diff --git a/TwidereX/Scene/History/HistoryViewController.swift b/TwidereX/Scene/History/HistoryViewController.swift index a4c71b54..5818286b 100644 --- a/TwidereX/Scene/History/HistoryViewController.swift +++ b/TwidereX/Scene/History/HistoryViewController.swift @@ -121,3 +121,8 @@ extension HistoryViewController { } } + +// MARK: - AuthContextProvider +extension HistoryViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} diff --git a/TwidereX/Scene/History/Status/StatusHistoryViewController.swift b/TwidereX/Scene/History/Status/StatusHistoryViewController.swift index b86e34fc..61be74eb 100644 --- a/TwidereX/Scene/History/Status/StatusHistoryViewController.swift +++ b/TwidereX/Scene/History/Status/StatusHistoryViewController.swift @@ -123,5 +123,10 @@ extension StatusHistoryViewController: UITableViewDelegate, AutoGenerateTableVie } +// MARK: - AuthContextProvider +extension StatusHistoryViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - StatusViewTableViewCellDelegate extension StatusHistoryViewController: StatusViewTableViewCellDelegate { } diff --git a/TwidereX/Scene/History/User/UserHistoryViewController.swift b/TwidereX/Scene/History/User/UserHistoryViewController.swift index f3293c6d..ba6ce514 100644 --- a/TwidereX/Scene/History/User/UserHistoryViewController.swift +++ b/TwidereX/Scene/History/User/UserHistoryViewController.swift @@ -66,6 +66,11 @@ extension UserHistoryViewController { } +// MARK: - AuthContextProvider +extension UserHistoryViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - UITableViewDelegate extension UserHistoryViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { // sourcery:inline:UserHistoryViewController.AutoGenerateTableViewDelegate diff --git a/TwidereX/Scene/List/AddListMember/AddListMemberViewController.swift b/TwidereX/Scene/List/AddListMember/AddListMemberViewController.swift index ff0d1986..48c42d06 100644 --- a/TwidereX/Scene/List/AddListMember/AddListMemberViewController.swift +++ b/TwidereX/Scene/List/AddListMember/AddListMemberViewController.swift @@ -51,6 +51,7 @@ extension AddListMemberViewController { searchUserViewController.coordinator = coordinator searchUserViewController.viewModel = SearchUserViewModel( context: context, + authContext: viewModel.authContext, kind: .listMember(list: viewModel.list) ) viewModel.$userIdentifier.assign(to: &searchUserViewController.viewModel.$userIdentifier) diff --git a/TwidereX/Scene/List/AddListMember/AddListMemberViewModel.swift b/TwidereX/Scene/List/AddListMember/AddListMemberViewModel.swift index 01cf99c9..85fd920d 100644 --- a/TwidereX/Scene/List/AddListMember/AddListMemberViewModel.swift +++ b/TwidereX/Scene/List/AddListMember/AddListMemberViewModel.swift @@ -13,6 +13,7 @@ final class AddListMemberViewModel { // input let context: AppContext + let authContext: AuthContext let list: ListRecord weak var listMembershipViewModelDelegate: ListMembershipViewModelDelegate? @@ -21,15 +22,15 @@ final class AddListMemberViewModel { init( context: AppContext, + authContext: AuthContext, list: ListRecord ) { self.context = context + self.authContext = authContext self.list = list // end init - context.authenticationService.$activeAuthenticationContext - .map { $0?.userIdentifier } - .assign(to: &$userIdentifier) + userIdentifier = authContext.authenticationContext.userIdentifier } } diff --git a/TwidereX/Scene/List/CompositeList/CompositeListViewController.swift b/TwidereX/Scene/List/CompositeList/CompositeListViewController.swift index d28f950e..3d0ef212 100644 --- a/TwidereX/Scene/List/CompositeList/CompositeListViewController.swift +++ b/TwidereX/Scene/List/CompositeList/CompositeListViewController.swift @@ -159,7 +159,7 @@ extension CompositeListViewController: UITableViewDelegate { Task { switch item { case .list(let record, _): - let listStatusViewModel = ListStatusTimelineViewModel(context: context, list: record) + let listStatusViewModel = ListStatusTimelineViewModel(context: context, authContext: viewModel.authContext, list: record) coordinator.present( scene: .listStatus(viewModel: listStatusViewModel), from: self, diff --git a/TwidereX/Scene/List/CompositeList/CompositeListViewModel.swift b/TwidereX/Scene/List/CompositeList/CompositeListViewModel.swift index 779c4687..59082b3b 100644 --- a/TwidereX/Scene/List/CompositeList/CompositeListViewModel.swift +++ b/TwidereX/Scene/List/CompositeList/CompositeListViewModel.swift @@ -17,6 +17,7 @@ class CompositeListViewModel { // input let context: AppContext + let authContext: AuthContext let kind: Kind let listBatchFetchViewModel = ListBatchFetchViewModel() @@ -29,19 +30,21 @@ class CompositeListViewModel { init( context: AppContext, + authContext: AuthContext, kind: Kind ) { self.context = context + self.authContext = authContext self.kind = kind switch kind { case .lists: - self.ownedListViewModel = ListViewModel(context: context, kind: .owned(user: kind.user)) - self.subscribedListViewModel = ListViewModel(context: context, kind: .subscribed(user: kind.user)) - self.listedListViewModel = ListViewModel(context: context, kind: .none) + self.ownedListViewModel = ListViewModel(context: context, authContext: authContext, kind: .owned(user: kind.user)) + self.subscribedListViewModel = ListViewModel(context: context, authContext: authContext, kind: .subscribed(user: kind.user)) + self.listedListViewModel = ListViewModel(context: context, authContext: authContext, kind: .none) case .listed: - self.ownedListViewModel = ListViewModel(context: context, kind: .none) - self.subscribedListViewModel = ListViewModel(context: context, kind: .none) - self.listedListViewModel = ListViewModel(context: context, kind: .listed(user: kind.user)) + self.ownedListViewModel = ListViewModel(context: context, authContext: authContext, kind: .none) + self.subscribedListViewModel = ListViewModel(context: context, authContext: authContext, kind: .none) + self.listedListViewModel = ListViewModel(context: context, authContext: authContext, kind: .listed(user: kind.user)) } // end init } diff --git a/TwidereX/Scene/List/List/ListViewController.swift b/TwidereX/Scene/List/List/ListViewController.swift index 68d0a152..36bc9495 100644 --- a/TwidereX/Scene/List/List/ListViewController.swift +++ b/TwidereX/Scene/List/List/ListViewController.swift @@ -91,7 +91,7 @@ extension ListViewController: UITableViewDelegate { guard let diffableDataSource = viewModel.diffableDataSource else { return } guard case let .list(record, _) = diffableDataSource.itemIdentifier(for: indexPath) else { return } - let listStatusViewModel = ListStatusTimelineViewModel(context: context, list: record) + let listStatusViewModel = ListStatusTimelineViewModel(context: context, authContext: viewModel.authContext, list: record) coordinator.present( scene: .listStatus(viewModel: listStatusViewModel), from: self, diff --git a/TwidereX/Scene/List/List/ListViewModel.swift b/TwidereX/Scene/List/List/ListViewModel.swift index 107d87fb..dbd2ef3f 100644 --- a/TwidereX/Scene/List/List/ListViewModel.swift +++ b/TwidereX/Scene/List/List/ListViewModel.swift @@ -18,6 +18,7 @@ class ListViewModel { // input let context: AppContext + let authContext: AuthContext let kind: Kind let fetchedResultController: ListRecordFetchedResultController let listBatchFetchViewModel = ListBatchFetchViewModel() @@ -41,9 +42,11 @@ class ListViewModel { init( context: AppContext, + authContext: AuthContext, kind: Kind ) { self.context = context + self.authContext = authContext self.kind = kind self.fetchedResultController = ListRecordFetchedResultController(managedObjectContext: context.managedObjectContext) // end init diff --git a/TwidereX/Scene/List/ListUser/ListUserViewController.swift b/TwidereX/Scene/List/ListUser/ListUserViewController.swift index fecd8689..1261bbf6 100644 --- a/TwidereX/Scene/List/ListUser/ListUserViewController.swift +++ b/TwidereX/Scene/List/ListUser/ListUserViewController.swift @@ -115,7 +115,7 @@ extension ListUserViewController { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") let list = viewModel.kind.list - let addListMemberViewModel = AddListMemberViewModel(context: context, list: list) + let addListMemberViewModel = AddListMemberViewModel(context: context, authContext: authContext, list: list) addListMemberViewModel.listMembershipViewModelDelegate = self coordinator.present( @@ -216,3 +216,8 @@ extension ListUserViewController: ListMembershipViewModelDelegate { } // end Task } } + +// MARK: - AuthContextProvider +extension ListUserViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} diff --git a/TwidereX/Scene/List/ListUser/ListUserViewModel.swift b/TwidereX/Scene/List/ListUser/ListUserViewModel.swift index 70db12c6..3e05a99b 100644 --- a/TwidereX/Scene/List/ListUser/ListUserViewModel.swift +++ b/TwidereX/Scene/List/ListUser/ListUserViewModel.swift @@ -20,6 +20,7 @@ final class ListUserViewModel { // input let context: AppContext + let authContext: AuthContext let kind: Kind let fetchedResultController: UserRecordFetchedResultController let listMembershipViewModel: ListMembershipViewModel @@ -44,9 +45,11 @@ final class ListUserViewModel { init( context: AppContext, + authContext: AuthContext, kind: Kind ) { self.context = context + self.authContext = authContext self.kind = kind self.fetchedResultController = UserRecordFetchedResultController(managedObjectContext: context.managedObjectContext) self.listMembershipViewModel = ListMembershipViewModel(api: context.apiService, list: kind.list) diff --git a/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift b/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift index 4f1bf6d3..2f0fcd3f 100644 --- a/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift +++ b/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift @@ -335,6 +335,11 @@ extension MediaPreviewViewController: MediaPreviewImageViewControllerDelegate { } +// MARK: - AuthContextProvider +extension MediaPreviewViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - MediaInfoDescriptionViewDelegate extension MediaPreviewViewController: MediaInfoDescriptionViewDelegate { } diff --git a/TwidereX/Scene/MediaPreview/MediaPreviewViewModel.swift b/TwidereX/Scene/MediaPreview/MediaPreviewViewModel.swift index b0bda317..38b769d6 100644 --- a/TwidereX/Scene/MediaPreview/MediaPreviewViewModel.swift +++ b/TwidereX/Scene/MediaPreview/MediaPreviewViewModel.swift @@ -22,6 +22,7 @@ final class MediaPreviewViewModel: NSObject { // input let context: AppContext + let authContext: AuthContext let item: Item let transitionItem: MediaPreviewTransitionItem @@ -33,10 +34,12 @@ final class MediaPreviewViewModel: NSObject { init( context: AppContext, + authContext: AuthContext, item: Item, transitionItem: MediaPreviewTransitionItem ) { self.context = context + self.authContext = authContext self.item = item self.currentPage = { switch item { diff --git a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift index 0a3d61d0..3679d18b 100644 --- a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift +++ b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift @@ -174,6 +174,11 @@ extension NotificationTimelineViewController: UITableViewDelegate, AutoGenerateT } +// MARK: - AuthContextProvider +extension NotificationTimelineViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - StatusViewTableViewCellDelegate extension NotificationTimelineViewController: StatusViewTableViewCellDelegate { } diff --git a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift index fec95e2e..50de0d1f 100644 --- a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift @@ -134,7 +134,7 @@ extension NotificationTimelineViewModel { // load lastest func loadLatest() async { do { - switch (scope, authenticationContext) { + switch (scope, authContext.authenticationContext) { case (.twitter, .twitter(let authenticationContext)): _ = try await context.apiService.twitterMentionTimeline( query: Twitter.API.Statuses.Timeline.TimelineQuery( diff --git a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift index b902f781..1e7e4789 100644 --- a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift +++ b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift @@ -23,8 +23,8 @@ final class NotificationTimelineViewModel { // input let context: AppContext + let authContext: AuthContext let scope: Scope - let authenticationContext: AuthenticationContext let fetchedResultsController: FeedFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() @@ -50,18 +50,18 @@ final class NotificationTimelineViewModel { init( context: AppContext, - scope: Scope, - authenticationContext: AuthenticationContext + authContext: AuthContext, + scope: Scope ) { self.context = context + self.authContext = authContext self.scope = scope - self.authenticationContext = authenticationContext self.fetchedResultsController = FeedFetchedResultsController(managedObjectContext: context.managedObjectContext) // end init let predicate = NotificationTimelineViewModel.feedPredicate( scope: scope, - authenticationContext: authenticationContext + authenticationContext: authContext.authenticationContext ) self.fetchedResultsController.predicate = predicate } diff --git a/TwidereX/Scene/Notification/NotificationViewController.swift b/TwidereX/Scene/Notification/NotificationViewController.swift index c8ea1eae..47f80844 100644 --- a/TwidereX/Scene/Notification/NotificationViewController.swift +++ b/TwidereX/Scene/Notification/NotificationViewController.swift @@ -21,7 +21,7 @@ final class NotificationViewController: TabmanViewController, NeedsDependency, D weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } var disposeBag = Set() - private(set) lazy var viewModel = NotificationViewModel(context: context, coordinator: coordinator) + var viewModel: NotificationViewModel! private(set) var drawerSidebarTransitionController: DrawerSidebarTransitionController! let avatarBarButtonItem = AvatarBarButtonItem() @@ -135,7 +135,7 @@ extension NotificationViewController { @objc private func avatarButtonPressed(_ sender: UIButton) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - let drawerSidebarViewModel = DrawerSidebarViewModel(context: context) + let drawerSidebarViewModel = DrawerSidebarViewModel(context: context, authContext: viewModel.authContext) coordinator.present(scene: .drawerSidebar(viewModel: drawerSidebarViewModel), from: self, transition: .custom(transitioningDelegate: drawerSidebarTransitionController)) } @@ -200,3 +200,7 @@ extension NotificationViewController { // MARK: - AvatarBarButtonItemDelegate extension NotificationViewController: AvatarBarButtonItemDelegate { } +// MARK: - AuthContextProvider +extension NotificationViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} diff --git a/TwidereX/Scene/Notification/NotificationViewModel.swift b/TwidereX/Scene/Notification/NotificationViewModel.swift index 6e3e414d..ff5cd0cc 100644 --- a/TwidereX/Scene/Notification/NotificationViewModel.swift +++ b/TwidereX/Scene/Notification/NotificationViewModel.swift @@ -17,6 +17,7 @@ final class NotificationViewModel { // input let context: AppContext + let authContext: AuthContext let _coordinator: SceneCoordinator // only use for `setup` @Published var selectedScope: NotificationTimelineViewModel.Scope? = nil @@ -28,8 +29,13 @@ final class NotificationViewModel { @Published var currentPageIndex = 0 @Published var userIdentifier: UserIdentifier? - init(context: AppContext, coordinator: SceneCoordinator) { + init( + context: AppContext, + authContext: AuthContext, + coordinator: SceneCoordinator + ) { self.context = context + self.authContext = authContext self._coordinator = coordinator // end init @@ -70,8 +76,7 @@ extension NotificationViewModel { } let viewControllers = scopes.map { scope in createViewController( - scope: scope, - authenticationContext: authenticationContext + scope: scope ) } @@ -82,16 +87,15 @@ extension NotificationViewModel { } private func createViewController( - scope: NotificationTimelineViewModel.Scope, - authenticationContext: AuthenticationContext + scope: NotificationTimelineViewModel.Scope ) -> UIViewController { let viewController = NotificationTimelineViewController() viewController.context = context viewController.coordinator = _coordinator viewController.viewModel = NotificationTimelineViewModel( context: context, - scope: scope, - authenticationContext: authenticationContext + authContext: authContext, + scope: scope ) return viewController } diff --git a/TwidereX/Scene/Profile/FriendshipList/FollowerList/FollowerListViewController.swift b/TwidereX/Scene/Profile/FriendshipList/FollowerList/FollowerListViewController.swift index 5afc97b2..e027ba44 100644 --- a/TwidereX/Scene/Profile/FriendshipList/FollowerList/FollowerListViewController.swift +++ b/TwidereX/Scene/Profile/FriendshipList/FollowerList/FollowerListViewController.swift @@ -97,6 +97,11 @@ extension FollowerListViewController { } +// MARK: - AuthContextProvider +extension FollowerListViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - UITableViewDelegate extension FollowerListViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { // sourcery:inline:FollowerListViewController.AutoGenerateTableViewDelegate diff --git a/TwidereX/Scene/Profile/FriendshipList/FollowingList/FollowingListViewController.swift b/TwidereX/Scene/Profile/FriendshipList/FollowingList/FollowingListViewController.swift index 96025425..402ab604 100644 --- a/TwidereX/Scene/Profile/FriendshipList/FollowingList/FollowingListViewController.swift +++ b/TwidereX/Scene/Profile/FriendshipList/FollowingList/FollowingListViewController.swift @@ -100,6 +100,11 @@ extension FollowingListViewController { } +// MARK: - AuthContextProvider +extension FollowingListViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - UITableViewDelegate extension FollowingListViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { // sourcery:inline:FollowingListViewController.AutoGenerateTableViewDelegate diff --git a/TwidereX/Scene/Profile/FriendshipList/FriendshipListViewModel.swift b/TwidereX/Scene/Profile/FriendshipList/FriendshipListViewModel.swift index 531940cd..0e0fe4de 100644 --- a/TwidereX/Scene/Profile/FriendshipList/FriendshipListViewModel.swift +++ b/TwidereX/Scene/Profile/FriendshipList/FriendshipListViewModel.swift @@ -22,6 +22,7 @@ final class FriendshipListViewModel: NSObject { // input let context: AppContext + let authContext: AuthContext let kind: Kind let userIdentifier: UserIdentifier let userRecordFetchedResultController: UserRecordFetchedResultController @@ -46,10 +47,12 @@ final class FriendshipListViewModel: NSObject { init( context: AppContext, + authContext: AuthContext, kind: Kind, userIdentifier: UserIdentifier // identifier for friend list owner user ) { self.context = context + self.authContext = authContext self.kind = kind self.userIdentifier = userIdentifier self.userRecordFetchedResultController = UserRecordFetchedResultController(managedObjectContext: context.managedObjectContext) @@ -61,15 +64,14 @@ final class FriendshipListViewModel: NSObject { // convenience init for current active user convenience init?( context: AppContext, + authContext: AuthContext, kind: Kind ) { - guard let authenticationContext = context.authenticationService.activeAuthenticationContext else { return nil } - let userIdentifier = authenticationContext.userIdentifier - self.init( context: context, + authContext: authContext, kind: kind, - userIdentifier: userIdentifier + userIdentifier: authContext.authenticationContext.userIdentifier ) } diff --git a/TwidereX/Scene/Profile/LocalProfileViewModel.swift b/TwidereX/Scene/Profile/LocalProfileViewModel.swift index 0a2bf55d..5d4bdd09 100644 --- a/TwidereX/Scene/Profile/LocalProfileViewModel.swift +++ b/TwidereX/Scene/Profile/LocalProfileViewModel.swift @@ -10,8 +10,15 @@ import Foundation final class LocalProfileViewModel: ProfileViewModel { - init(context: AppContext, userRecord: UserRecord) { - super.init(context: context) + init( + context: AppContext, + authContext: AuthContext, + userRecord: UserRecord + ) { + super.init( + context: context, + authContext: authContext + ) setup(user: userRecord) } diff --git a/TwidereX/Scene/Profile/MeProfileViewModel.swift b/TwidereX/Scene/Profile/MeProfileViewModel.swift deleted file mode 100644 index 44a1cf59..00000000 --- a/TwidereX/Scene/Profile/MeProfileViewModel.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// MeProfileViewModel.swift -// TwidereX -// -// Created by Cirno MainasuK on 2020-9-28. -// - -import os.log -import UIKit -import Combine -import CoreData -import CoreDataStack -import TwitterSDK - -final class MeProfileViewModel: ProfileViewModel { - - override init(context: AppContext) { - super.init(context: context) - - context.authenticationService.$activeAuthenticationContext - .sink { [weak self] authenticationContext in - guard let self = self else { return } - Task { - await self.setup(authenticationContext: authenticationContext) - } - } - .store(in: &disposeBag) - } - - @MainActor - func setup(authenticationContext: AuthenticationContext?) async { - let managedObjectContext = context.managedObjectContext - self.user = await managedObjectContext.perform { - switch authenticationContext { - case .twitter(let authenticationContext): - let authentication = authenticationContext.authenticationRecord.object(in: managedObjectContext) - return authentication.flatMap { .twitter(object: $0.user) } - case .mastodon(let authenticationContext): - let authentication = authenticationContext.authenticationRecord.object(in: managedObjectContext) - return authentication.flatMap { .mastodon(object: $0.user) } - case nil: - return nil - } - } - } - -} diff --git a/TwidereX/Scene/Profile/ProfileViewController.swift b/TwidereX/Scene/Profile/ProfileViewController.swift index a173685a..9eb27631 100644 --- a/TwidereX/Scene/Profile/ProfileViewController.swift +++ b/TwidereX/Scene/Profile/ProfileViewController.swift @@ -57,6 +57,7 @@ final class ProfileViewController: UIViewController, NeedsDependency, DrawerSide let userTimelineViewModel = UserTimelineViewModel( context: context, + authContext: authContext, timelineContext: .init( timelineKind: .status, userIdentifier: viewModel.$userIdentifier @@ -66,6 +67,7 @@ final class ProfileViewController: UIViewController, NeedsDependency, DrawerSide let userMediaTimelineViewModel = UserMediaTimelineViewModel( context: context, + authContext: authContext, timelineContext: .init( timelineKind: .media, userIdentifier: viewModel.$userIdentifier @@ -75,6 +77,7 @@ final class ProfileViewController: UIViewController, NeedsDependency, DrawerSide let userLikeTimelineViewModel = UserTimelineViewModel( context: context, + authContext: authContext, timelineContext: .init( timelineKind: .like, userIdentifier: viewModel.$userIdentifier @@ -276,7 +279,7 @@ extension ProfileViewController { @objc private func avatarButtonPressed(_ sender: UIButton) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - let drawerSidebarViewModel = DrawerSidebarViewModel(context: context) + let drawerSidebarViewModel = DrawerSidebarViewModel(context: context, authContext: authContext) coordinator.present(scene: .drawerSidebar(viewModel: drawerSidebarViewModel), from: self, transition: .custom(transitioningDelegate: drawerSidebarTransitionController)) } @@ -301,6 +304,8 @@ extension ProfileViewController { let composeViewModel = ComposeViewModel(context: context) let composeContentViewModel = ComposeContentViewModel( + context: context, + authContext: authContext, kind: { if user == viewModel.me { return .post @@ -384,7 +389,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { assertionFailure() return } - let friendshipListViewModel = FriendshipListViewModel(context: context, kind: .following, userIdentifier: userIdentifier) + let friendshipListViewModel = FriendshipListViewModel(context: context, authContext: authContext, kind: .following, userIdentifier: userIdentifier) coordinator.present(scene: .friendshipList(viewModel: friendshipListViewModel), from: self, transition: .show) } @@ -393,7 +398,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { assertionFailure() return } - let friendshipListViewModel = FriendshipListViewModel(context: context, kind: .follower, userIdentifier: userIdentifier) + let friendshipListViewModel = FriendshipListViewModel(context: context, authContext: authContext, kind: .follower, userIdentifier: userIdentifier) coordinator.present(scene: .friendshipList(viewModel: friendshipListViewModel), from: self, transition: .show) } @@ -406,6 +411,7 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { let compositeListViewModel = CompositeListViewModel( context: context, + authContext: authContext, kind: .listed(user) ) coordinator.present( @@ -489,3 +495,8 @@ extension ProfileViewController: ScrollViewContainer { return tabBarPagerController.relayScrollView } } + +// MARK: - AuthContextProvider +extension ProfileViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} diff --git a/TwidereX/Scene/Profile/ProfileViewModel.swift b/TwidereX/Scene/Profile/ProfileViewModel.swift index d62f86de..adf4c95c 100644 --- a/TwidereX/Scene/Profile/ProfileViewModel.swift +++ b/TwidereX/Scene/Profile/ProfileViewModel.swift @@ -21,6 +21,7 @@ class ProfileViewModel: ObservableObject { // input let context: AppContext + let authContext: AuthContext @Published var me: UserObject? @Published var user: UserObject? let viewDidAppear = CurrentValueSubject(Void()) @@ -32,8 +33,12 @@ class ProfileViewModel: ObservableObject { // let suspended = CurrentValueSubject(false) - init(context: AppContext) { + init( + context: AppContext, + authContext: AuthContext + ) { self.context = context + self.authContext = authContext // end init // bind data after publisher setup diff --git a/TwidereX/Scene/Profile/RemoteProfileViewModel.swift b/TwidereX/Scene/Profile/RemoteProfileViewModel.swift index acc2a493..ce77b4ce 100644 --- a/TwidereX/Scene/Profile/RemoteProfileViewModel.swift +++ b/TwidereX/Scene/Profile/RemoteProfileViewModel.swift @@ -14,8 +14,12 @@ import MastodonSDK final class RemoteProfileViewModel: ProfileViewModel { - init(context: AppContext, profileContext: ProfileContext) { - super.init(context: context) + init( + context: AppContext, + authContext: AuthContext, + profileContext: ProfileContext + ) { + super.init(context: context, authContext: authContext) configure(profileContext: profileContext) } diff --git a/TwidereX/Scene/Root/ContentSplitViewController.swift b/TwidereX/Scene/Root/ContentSplitViewController.swift index 0dbacf27..d5ec060a 100644 --- a/TwidereX/Scene/Root/ContentSplitViewController.swift +++ b/TwidereX/Scene/Root/ContentSplitViewController.swift @@ -27,7 +27,7 @@ final class ContentSplitViewController: UIViewController, NeedsDependency { let sidebarViewController = SidebarViewController() sidebarViewController.context = context sidebarViewController.coordinator = coordinator - sidebarViewController.viewModel = SidebarViewModel(context: context) + sidebarViewController.viewModel = SidebarViewModel(context: context, authContext: authContext) sidebarViewController.viewModel.delegate = self return sidebarViewController }() @@ -248,7 +248,7 @@ extension ContentSplitViewController: SidebarViewModelDelegate { guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext else { return } let settingListViewModel = SettingListViewModel( context: context, - auth: .init(authenticationContext: authenticationContext) + authContext: .init(authenticationContext: authenticationContext) ) coordinator.present( scene: .setting(viewModel: settingListViewModel), diff --git a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift index 316881ef..d11e47b8 100644 --- a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift +++ b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift @@ -123,7 +123,10 @@ extension DrawerSidebarViewController: DrawerSidebarHeaderViewDelegate { _ headerView: DrawerSidebarHeaderView, avatarButtonDidPressed button: UIButton ) { - let profileViewModel = MeProfileViewModel(context: self.context) + let profileViewModel = ProfileViewModel( + context: context, + authContext: viewModel.authContext // me + ) // present from `presentingViewController` here to reduce transition delay coordinator.present(scene: .profile(viewModel: profileViewModel), from: presentingViewController, transition: .show) @@ -152,7 +155,7 @@ extension DrawerSidebarViewController: DrawerSidebarHeaderViewDelegate { profileDashboardView: ProfileDashboardView, followingMeterViewDidPressed meterView: ProfileDashboardMeterView ) { - guard let friendshipListViewModel = FriendshipListViewModel(context: context, kind: .following) else { + guard let friendshipListViewModel = FriendshipListViewModel(context: context, authContext: viewModel.authContext, kind: .following) else { assertionFailure() return } @@ -165,7 +168,7 @@ extension DrawerSidebarViewController: DrawerSidebarHeaderViewDelegate { profileDashboardView: ProfileDashboardView, followersMeterViewDidPressed meterView: ProfileDashboardMeterView ) { - guard let friendshipListViewModel = FriendshipListViewModel(context: context, kind: .follower) else { + guard let friendshipListViewModel = FriendshipListViewModel(context: context, authContext: viewModel.authContext, kind: .follower) else { assertionFailure() return } @@ -186,6 +189,7 @@ extension DrawerSidebarViewController: DrawerSidebarHeaderViewDelegate { let compositeListViewModel = CompositeListViewModel( context: context, + authContext: viewModel.authContext, kind: .listed(me) ) coordinator.present( @@ -207,14 +211,14 @@ extension DrawerSidebarViewController: UICollectionViewDelegate { guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } switch item { case .local: - let federatedTimelineViewModel = FederatedTimelineViewModel(context: context, isLocal: true) + let federatedTimelineViewModel = FederatedTimelineViewModel(context: context, authContext: viewModel.authContext, isLocal: true) coordinator.present(scene: .federatedTimeline(viewModel: federatedTimelineViewModel), from: presentingViewController, transition: .show) case .federated: - let federatedTimelineViewModel = FederatedTimelineViewModel(context: context, isLocal: false) + let federatedTimelineViewModel = FederatedTimelineViewModel(context: context, authContext: viewModel.authContext, isLocal: false) coordinator.present(scene: .federatedTimeline(viewModel: federatedTimelineViewModel), from: presentingViewController, transition: .show) case .likes: - let meLikeTimelineViewModel = MeLikeTimelineViewModel(context: context) - coordinator.present(scene: .userLikeTimeline(viewModel: meLikeTimelineViewModel), from: presentingViewController, transition: .show) + let userLikeTimelineViewModel = UserLikeTimelineViewModel(context: context, authContext: viewModel.authContext, timelineContext: .init(timelineKind: .like, userIdentifier: viewModel.authContext.authenticationContext.userIdentifier)) + coordinator.present(scene: .userLikeTimeline(viewModel: userLikeTimelineViewModel), from: presentingViewController, transition: .show) case .history: guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext else { return } let authContext = AuthContext(authenticationContext: authenticationContext) @@ -225,6 +229,7 @@ extension DrawerSidebarViewController: UICollectionViewDelegate { let compositeListViewModel = CompositeListViewModel( context: context, + authContext: viewModel.authContext, kind: .lists(me) ) coordinator.present( @@ -240,11 +245,10 @@ extension DrawerSidebarViewController: UICollectionViewDelegate { guard let diffableDataSource = viewModel.settingDiffableDataSource else { return } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } guard case .settings = item else { return } - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext else { return } dismiss(animated: true) { let settingListViewModel = SettingListViewModel( context: self.context, - auth: .init(authenticationContext: authenticationContext) + authContext: self.viewModel.authContext ) self.coordinator.present( scene: .setting(viewModel: settingListViewModel), diff --git a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewModel.swift b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewModel.swift index b3ee9cc3..15fbfd6a 100644 --- a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewModel.swift +++ b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewModel.swift @@ -17,13 +17,18 @@ final class DrawerSidebarViewModel { // input let context: AppContext + let authContext: AuthContext // output var sidebarDiffableDataSource: UICollectionViewDiffableDataSource? var settingDiffableDataSource: UICollectionViewDiffableDataSource? - init(context: AppContext) { + init( + context: AppContext, + authContext: AuthContext + ) { self.context = context + self.authContext = authContext // end init } diff --git a/TwidereX/Scene/Root/Sidebar/SidebarView.swift b/TwidereX/Scene/Root/Sidebar/SidebarView.swift index 48507de8..a1eade73 100644 --- a/TwidereX/Scene/Root/Sidebar/SidebarView.swift +++ b/TwidereX/Scene/Root/Sidebar/SidebarView.swift @@ -132,9 +132,10 @@ extension SidebarView { #if DEBUG struct SidebarView_Previews: PreviewProvider { static var previews: some View { - SidebarView(viewModel: SidebarViewModel(context: .shared)) - .previewLayout(.fixed(width: 80, height: 800)) - + if let authContext = AuthContext.mock(context: .shared) { + SidebarView(viewModel: SidebarViewModel(context: .shared, authContext: authContext)) + .previewLayout(.fixed(width: 80, height: 800)) + } } } #endif diff --git a/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift b/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift index 959d3779..badfd6a3 100644 --- a/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift @@ -27,6 +27,7 @@ final class SidebarViewModel: ObservableObject { // input let context: AppContext + let authContext: AuthContext @Published var activeTab: TabBarItem? // output @@ -36,8 +37,12 @@ final class SidebarViewModel: ObservableObject { @Published var hasUnreadPushNotification = false - init(context: AppContext) { + init( + context: AppContext, + authContext: AuthContext + ) { self.context = context + self.authContext = authContext Publishers.CombineLatest( context.authenticationService.$activeAuthenticationContext.removeDuplicates(), diff --git a/TwidereX/Scene/Search/SavedSearch/SavedSearchViewController.swift b/TwidereX/Scene/Search/SavedSearch/SavedSearchViewController.swift index a9bad016..90c43ba4 100644 --- a/TwidereX/Scene/Search/SavedSearch/SavedSearchViewController.swift +++ b/TwidereX/Scene/Search/SavedSearch/SavedSearchViewController.swift @@ -64,6 +64,11 @@ extension SavedSearchViewController { } +// MARK: - AuthContextProvider +extension SavedSearchViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - UITableViewDelegate extension SavedSearchViewController: UITableViewDelegate { diff --git a/TwidereX/Scene/Search/SavedSearch/SavedSearchViewModel.swift b/TwidereX/Scene/Search/SavedSearch/SavedSearchViewModel.swift index 696bf352..afa96c39 100644 --- a/TwidereX/Scene/Search/SavedSearch/SavedSearchViewModel.swift +++ b/TwidereX/Scene/Search/SavedSearch/SavedSearchViewModel.swift @@ -19,6 +19,7 @@ final class SavedSearchViewModel { // input let context: AppContext + let authContext: AuthContext let savedSearchService: SavedSearchService let savedSearchFetchedResultController: SavedSearchFetchedResultController @@ -26,15 +27,16 @@ final class SavedSearchViewModel { var diffableDataSource: UITableViewDiffableDataSource? @Published var isSavedSearchFetched = false - init(context: AppContext) { + init( + context: AppContext, + authContext: AuthContext + ) { self.context = context + self.authContext = authContext self.savedSearchService = SavedSearchService(apiService: context.apiService) self.savedSearchFetchedResultController = SavedSearchFetchedResultController(managedObjectContext: context.managedObjectContext) // end init - context.authenticationService.$activeAuthenticationContext - .map { $0?.userIdentifier } - .assign(to: \.userIdentifier, on: savedSearchFetchedResultController) - .store(in: &disposeBag) + savedSearchFetchedResultController.userIdentifier = authContext.authenticationContext.userIdentifier } } diff --git a/TwidereX/Scene/Search/Search/SearchViewController.swift b/TwidereX/Scene/Search/Search/SearchViewController.swift index 2a4165a5..898f91d7 100644 --- a/TwidereX/Scene/Search/Search/SearchViewController.swift +++ b/TwidereX/Scene/Search/Search/SearchViewController.swift @@ -23,7 +23,7 @@ final class SearchViewController: UIViewController, NeedsDependency, DrawerSideb var disposeBag = Set() var viewModel: SearchViewModel! - private(set) lazy var searchResultViewModel = SearchResultViewModel(context: context, coordinator: coordinator) + private(set) lazy var searchResultViewModel = SearchResultViewModel(context: context, authContext: authContext, coordinator: coordinator) private(set) lazy var searchResultViewController: SearchResultViewController = { let searchResultViewController = SearchResultViewController() searchResultViewController.context = context @@ -235,7 +235,7 @@ extension SearchViewController: UITableViewDelegate { case .twitter(let trend): self.searchText(trend.name) case .mastodon(let tag): - let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: tag.name) + let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, authContext: authContext, hashtag: tag.name) coordinator.present( scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: self, @@ -313,3 +313,8 @@ extension SearchViewController { } } + +// MARK: - AuthContextProvider +extension SearchViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} diff --git a/TwidereX/Scene/Search/Search/SearchViewModel.swift b/TwidereX/Scene/Search/Search/SearchViewModel.swift index 82f3f902..8f16a814 100644 --- a/TwidereX/Scene/Search/Search/SearchViewModel.swift +++ b/TwidereX/Scene/Search/Search/SearchViewModel.swift @@ -21,6 +21,7 @@ final class SearchViewModel { // input let context: AppContext + let authContext: AuthContext let savedSearchViewModel: SavedSearchViewModel let trendViewModel: TrendViewModel let viewDidAppear = PassthroughSubject() @@ -29,10 +30,14 @@ final class SearchViewModel { var diffableDataSource: UITableViewDiffableDataSource? @Published var savedSearchTexts = Set() - init(context: AppContext) { + init( + context: AppContext, + authContext: AuthContext + ) { self.context = context - self.savedSearchViewModel = SavedSearchViewModel(context: context) - self.trendViewModel = TrendViewModel(context: context) + self.authContext = authContext + self.savedSearchViewModel = SavedSearchViewModel(context: context, authContext: authContext) + self.trendViewModel = TrendViewModel(context: context, authContext: authContext) // end init viewDidAppear diff --git a/TwidereX/Scene/Search/SearchResult/Hashtag/SearchHashtagViewController.swift b/TwidereX/Scene/Search/SearchResult/Hashtag/SearchHashtagViewController.swift index f4fe0b21..6ced7f76 100644 --- a/TwidereX/Scene/Search/SearchResult/Hashtag/SearchHashtagViewController.swift +++ b/TwidereX/Scene/Search/SearchResult/Hashtag/SearchHashtagViewController.swift @@ -88,6 +88,11 @@ extension SearchHashtagViewController: DeselectRowTransitionCoordinator { } } +// MARK: - AuthContextProvider +extension SearchHashtagViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - UITableViewDelegate extension SearchHashtagViewController: UITableViewDelegate { @@ -97,7 +102,7 @@ extension SearchHashtagViewController: UITableViewDelegate { case .hashtag(let data): switch data { case .mastodon(let tag): - let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: tag.name) + let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, authContext: authContext, hashtag: tag.name) coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: self, transition: .show) } diff --git a/TwidereX/Scene/Search/SearchResult/Hashtag/SearchHashtagViewModel.swift b/TwidereX/Scene/Search/SearchResult/Hashtag/SearchHashtagViewModel.swift index fce02fc4..bbb78828 100644 --- a/TwidereX/Scene/Search/SearchResult/Hashtag/SearchHashtagViewModel.swift +++ b/TwidereX/Scene/Search/SearchResult/Hashtag/SearchHashtagViewModel.swift @@ -21,6 +21,7 @@ final class SearchHashtagViewModel { // input let context: AppContext + let authContext: AuthContext let listBatchFetchViewModel = ListBatchFetchViewModel() let viewDidAppear = PassthroughSubject() @Published var items: [HashtagItem] = [] @@ -41,8 +42,12 @@ final class SearchHashtagViewModel { return stateMachine }() - init(context: AppContext) { + init( + context: AppContext, + authContext: AuthContext + ) { self.context = context + self.authContext = authContext // end init $searchText diff --git a/TwidereX/Scene/Search/SearchResult/SearchResultViewModel.swift b/TwidereX/Scene/Search/SearchResult/SearchResultViewModel.swift index bd81c6b2..344d0f75 100644 --- a/TwidereX/Scene/Search/SearchResult/SearchResultViewModel.swift +++ b/TwidereX/Scene/Search/SearchResult/SearchResultViewModel.swift @@ -16,6 +16,7 @@ final class SearchResultViewModel { // input let context: AppContext + let authContext: AuthContext let _coordinator: SceneCoordinator // only use for `setup` var preferredScope: Scope? @Published var searchText: String = "" @@ -27,17 +28,16 @@ final class SearchResultViewModel { @Published var currentPageIndex = 0 @Published var userIdentifier: UserIdentifier? - init(context: AppContext, coordinator: SceneCoordinator) { + init( + context: AppContext, + authContext: AuthContext, + coordinator: SceneCoordinator + ) { self.context = context + self.authContext = authContext self._coordinator = coordinator - context.authenticationService.$activeAuthenticationContext - .receive(on: DispatchQueue.main) - .sink { [weak self] authenticationContext in - guard let self = self else { return } - self.setup(for: authenticationContext) - } - .store(in: &disposeBag) + self.setup(for: authContext.authenticationContext) } } @@ -102,28 +102,28 @@ extension SearchResultViewModel { switch scope { case .status: let _viewController = SearchTimelineViewController() - let _viewModel = SearchTimelineViewModel(context: context) + let _viewModel = SearchTimelineViewModel(context: context, authContext: authContext) _viewController.viewModel = _viewModel $searchText.assign(to: &_viewModel.$searchText) viewController = _viewController case .media: let _viewController = SearchMediaTimelineViewController() - let _viewModel = SearchMediaTimelineViewModel(context: context) + let _viewModel = SearchMediaTimelineViewModel(context: context, authContext: authContext) _viewController.viewModel = _viewModel $searchText.assign(to: &_viewModel.$searchText) viewController = _viewController case .user: let _viewController = SearchUserViewController() - _viewController.viewModel = SearchUserViewModel(context: context, kind: .friendship) + _viewController.viewModel = SearchUserViewModel(context: context, authContext: authContext, kind: .friendship) $searchText.assign(to: &_viewController.viewModel.$searchText) $userIdentifier.assign(to: &_viewController.viewModel.$userIdentifier) viewController = _viewController case .hashtag: let _viewController = SearchHashtagViewController() - _viewController.viewModel = SearchHashtagViewModel(context: context) + _viewController.viewModel = SearchHashtagViewModel(context: context, authContext: authContext) $searchText.assign(to: &_viewController.viewModel.$searchText) viewController = _viewController } // end switch diff --git a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewController.swift b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewController.swift index c8abfd94..01bf425f 100644 --- a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewController.swift +++ b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewController.swift @@ -101,6 +101,11 @@ extension SearchUserViewController: DeselectRowTransitionCoordinator { } } +// MARK: - AuthContextProvider +extension SearchUserViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - UITableViewDelegate extension SearchUserViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { // sourcery:inline:SearchUserViewController.AutoGenerateTableViewDelegate diff --git a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel.swift b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel.swift index 677e2601..687ad64c 100644 --- a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel.swift +++ b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel.swift @@ -24,6 +24,7 @@ final class SearchUserViewModel { // input let context: AppContext + let authContext: AuthContext let kind: Kind let userRecordFetchedResultController: UserRecordFetchedResultController let listMembershipViewModel: ListMembershipViewModel? @@ -49,9 +50,11 @@ final class SearchUserViewModel { init( context: AppContext, + authContext: AuthContext, kind: SearchUserViewModel.Kind ) { self.context = context + self.authContext = authContext self.kind = kind self.userRecordFetchedResultController = UserRecordFetchedResultController( managedObjectContext: context.managedObjectContext diff --git a/TwidereX/Scene/Search/Trend/TrendViewController.swift b/TwidereX/Scene/Search/Trend/TrendViewController.swift index 80b3d0e5..cc3b33a0 100644 --- a/TwidereX/Scene/Search/Trend/TrendViewController.swift +++ b/TwidereX/Scene/Search/Trend/TrendViewController.swift @@ -70,6 +70,11 @@ extension TrendViewController { } +// MARK: - AuthContextProvider +extension TrendViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - UITableViewDelegate extension TrendViewController: UITableViewDelegate { @@ -88,6 +93,7 @@ extension TrendViewController: UITableViewDelegate { case .mastodon(let tag): let hashtagTimelineViewModel = HashtagTimelineViewModel( context: context, + authContext: authContext, hashtag: tag.name ) coordinator.present( diff --git a/TwidereX/Scene/Search/Trend/TrendViewModel.swift b/TwidereX/Scene/Search/Trend/TrendViewModel.swift index cad1ce38..9904028f 100644 --- a/TwidereX/Scene/Search/Trend/TrendViewModel.swift +++ b/TwidereX/Scene/Search/Trend/TrendViewModel.swift @@ -20,6 +20,7 @@ final class TrendViewModel: ObservableObject { // input let context: AppContext + let authContext: AuthContext let trendService: TrendService @Published var trendGroupIndex: TrendService.TrendGroupIndex = .none @Published var searchText = "" @@ -32,8 +33,12 @@ final class TrendViewModel: ObservableObject { let activeTwitterTrendPlacePublisher = PassthroughSubject() - init(context: AppContext) { + init( + context: AppContext, + authContext: AuthContext + ) { self.context = context + self.authContext = authContext self.trendService = TrendService(apiService: context.apiService) // end init diff --git a/TwidereX/Scene/Setting/About/AboutView.swift b/TwidereX/Scene/Setting/About/AboutView.swift index 1f697b70..829f3355 100644 --- a/TwidereX/Scene/Setting/About/AboutView.swift +++ b/TwidereX/Scene/Setting/About/AboutView.swift @@ -116,18 +116,20 @@ struct AboutView: View { struct AboutView_Previews: PreviewProvider { static var previews: some View { Group { - AboutView(viewModel: AboutViewModel()) - AboutView(viewModel: AboutViewModel()) - .preferredColorScheme(.dark) - AboutView(viewModel: AboutViewModel()) - .previewDevice("iPhone SE") - AboutView(viewModel: AboutViewModel()) - .previewDevice("iPhone 13 mini") - AboutView(viewModel: AboutViewModel()) - .previewDevice("iPhone 8") - AboutView(viewModel: AboutViewModel()) - .previewDevice("iPad mini (6th generation)") - } + if let authContext = AuthContext.mock(context: AppContext.shared) { + AboutView(viewModel: AboutViewModel(authContext: authContext)) + AboutView(viewModel: AboutViewModel(authContext: authContext)) + .preferredColorScheme(.dark) + AboutView(viewModel: AboutViewModel(authContext: authContext)) + .previewDevice("iPhone SE") + AboutView(viewModel: AboutViewModel(authContext: authContext)) + .previewDevice("iPhone 13 mini") + AboutView(viewModel: AboutViewModel(authContext: authContext)) + .previewDevice("iPhone 8") + AboutView(viewModel: AboutViewModel(authContext: authContext)) + .previewDevice("iPad mini (6th generation)") + } + } // end Group } } diff --git a/TwidereX/Scene/Setting/About/AboutViewController.swift b/TwidereX/Scene/Setting/About/AboutViewController.swift index 38ef669b..4987ef73 100644 --- a/TwidereX/Scene/Setting/About/AboutViewController.swift +++ b/TwidereX/Scene/Setting/About/AboutViewController.swift @@ -17,7 +17,7 @@ final class AboutViewController: UIViewController, NeedsDependency { weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } var disposeBag = Set() - let viewModel = AboutViewModel() + var viewModel: AboutViewModel! } @@ -55,6 +55,7 @@ extension AboutViewController { case .twitter: let profileViewModel = RemoteProfileViewModel( context: self.context, + authContext: self.viewModel.authContext, profileContext: .twitter(.username("TwidereProject")) ) self.coordinator.present(scene: .profile(viewModel: profileViewModel), from: self, transition: .show) diff --git a/TwidereX/Scene/Setting/About/AboutViewModel.swift b/TwidereX/Scene/Setting/About/AboutViewModel.swift index 2e15c5ff..cebbf5c0 100644 --- a/TwidereX/Scene/Setting/About/AboutViewModel.swift +++ b/TwidereX/Scene/Setting/About/AboutViewModel.swift @@ -18,12 +18,13 @@ final class AboutViewModel: ObservableObject { var disposeBag = Set() // input + let authContext: AuthContext // output let entryPublisher = PassthroughSubject() - init() { - + init(authContext: AuthContext) { + self.authContext = authContext } } diff --git a/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceViewModel.swift b/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceViewModel.swift index c7c64fa4..3236dd4c 100644 --- a/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceViewModel.swift +++ b/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceViewModel.swift @@ -17,7 +17,7 @@ final class AccountPreferenceViewModel: ObservableObject { // input let context: AppContext - let auth: AuthContext + let authContext: AuthContext let user: UserObject // notification @@ -33,11 +33,11 @@ final class AccountPreferenceViewModel: ObservableObject { init( context: AppContext, - auth: AuthContext, + authContext: AuthContext, user: UserObject ) { self.context = context - self.auth = auth + self.authContext = authContext self.user = user // end init @@ -60,7 +60,7 @@ extension AccountPreferenceViewModel { mastodonNotificationSectionViewModel = user.mastodonAuthentication?.notificationSubscription.flatMap { return .init( context: context, - auth: auth, + authContext: authContext, notificationSubscription: $0 ) } diff --git a/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionViewModel.swift b/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionViewModel.swift index 0f1c3f7d..fe351dcc 100644 --- a/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionViewModel.swift +++ b/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionViewModel.swift @@ -15,7 +15,7 @@ final class MastodonNotificationSectionViewModel: ObservableObject { // input let context: AppContext - let auth: AuthContext + let authContext: AuthContext let notificationSubscription: MastodonNotificationSubscription // output @@ -31,11 +31,11 @@ final class MastodonNotificationSectionViewModel: ObservableObject { init( context: AppContext, - auth: AuthContext, + authContext: AuthContext, notificationSubscription: MastodonNotificationSubscription ) { self.context = context - self.auth = auth + self.authContext = authContext self.notificationSubscription = notificationSubscription self.isActive = notificationSubscription.isActive self.isNewFollowEnabled = notificationSubscription.follow @@ -89,7 +89,7 @@ extension MastodonNotificationSectionViewModel { action(object) } - await context.notificationService.notifySubscriber(authenticationContext: auth.authenticationContext) + await context.notificationService.notifySubscriber(authenticationContext: authContext.authenticationContext) } // end Task } diff --git a/TwidereX/Scene/Setting/List/SettingListView.swift b/TwidereX/Scene/Setting/List/SettingListView.swift index b3899106..3981f32f 100644 --- a/TwidereX/Scene/Setting/List/SettingListView.swift +++ b/TwidereX/Scene/Setting/List/SettingListView.swift @@ -214,15 +214,17 @@ struct SettingListView: View { struct SettingListView_Previews: PreviewProvider { static var previews: some View { Group { - SettingListView(viewModel: SettingListViewModel( - context: .shared, - auth: nil - )) - SettingListView(viewModel: SettingListViewModel( - context: .shared, - auth: nil - )) - .preferredColorScheme(.dark) + if let authContext = AuthContext.mock(context: .shared) { + SettingListView(viewModel: SettingListViewModel( + context: .shared, + authContext: authContext + )) + SettingListView(viewModel: SettingListViewModel( + context: .shared, + authContext: authContext + )) + .preferredColorScheme(.dark) + } } } } diff --git a/TwidereX/Scene/Setting/List/SettingListViewController.swift b/TwidereX/Scene/Setting/List/SettingListViewController.swift index cd67dc64..901d58dd 100644 --- a/TwidereX/Scene/Setting/List/SettingListViewController.swift +++ b/TwidereX/Scene/Setting/List/SettingListViewController.swift @@ -49,13 +49,11 @@ extension SettingListViewController { guard let self = self else { return } switch entry.type { case .account: - // FIXME: - guard let authenticationContext = self.context.authenticationService.activeAuthenticationContext else { return } guard let user = self.viewModel.user else { return } let accountPreferenceViewModel = AccountPreferenceViewModel( context: self.context, - auth: .init(authenticationContext: authenticationContext), + authContext: self.viewModel.authContext, user: user ) self.coordinator.present( @@ -79,7 +77,8 @@ extension SettingListViewController { case .appIcon: break case .about: - self.coordinator.present(scene: .about, from: self, transition: .show) + let aboutViewModel = AboutViewModel(authContext: self.viewModel.authContext) + self.coordinator.present(scene: .about(viewModel: aboutViewModel), from: self, transition: .show) #if DEBUG case .developer: self.coordinator.present(scene: .developer, from: self, transition: .show) diff --git a/TwidereX/Scene/Setting/List/SettingListViewModel.swift b/TwidereX/Scene/Setting/List/SettingListViewModel.swift index 30497af3..68aaafff 100644 --- a/TwidereX/Scene/Setting/List/SettingListViewModel.swift +++ b/TwidereX/Scene/Setting/List/SettingListViewModel.swift @@ -22,7 +22,7 @@ final class SettingListViewModel: ObservableObject { // input let context: AppContext - let auth: AuthContext? + let authContext: AuthContext // output let settingListEntryPublisher = PassthroughSubject() @@ -35,10 +35,10 @@ final class SettingListViewModel: ObservableObject { init( context: AppContext, - auth: AuthContext? + authContext: AuthContext ) { self.context = context - self.auth = auth + self.authContext = authContext // end init Task { @@ -55,6 +55,6 @@ final class SettingListViewModel: ObservableObject { extension SettingListViewModel { @MainActor func setupAccountSource() async { - user = auth?.authenticationContext.user(in: context.managedObjectContext) + user = authContext.authenticationContext.user(in: context.managedObjectContext) } } diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewController.swift b/TwidereX/Scene/StatusThread/StatusThreadViewController.swift index e6404163..4b5b407b 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewController.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewController.swift @@ -135,6 +135,10 @@ extension StatusThreadViewController: UITableViewDelegate, AutoGenerateTableView } +// MARK: - AuthContextProvider +extension StatusThreadViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} // MARK: - StatusViewTableViewCellDelegate extension StatusThreadViewController: StatusViewTableViewCellDelegate { } diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift index f5f0437d..cd36e0f8 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift @@ -24,6 +24,7 @@ final class StatusThreadViewModel { // input let context: AppContext + let authContext: AuthContext let twitterStatusThreadReplyViewModel: TwitterStatusThreadReplyViewModel let twitterStatusThreadLeafViewModel: TwitterStatusThreadLeafViewModel let mastodonStatusThreadViewModel: MastodonStatusThreadViewModel @@ -57,9 +58,11 @@ final class StatusThreadViewModel { private init( context: AppContext, + authContext: AuthContext, optionalRoot: StatusItem.Thread? ) { self.context = context + self.authContext = authContext self.twitterStatusThreadReplyViewModel = TwitterStatusThreadReplyViewModel(context: context) self.twitterStatusThreadLeafViewModel = TwitterStatusThreadLeafViewModel(context: context) self.mastodonStatusThreadViewModel = MastodonStatusThreadViewModel(context: context) @@ -116,10 +119,12 @@ final class StatusThreadViewModel { convenience init( context: AppContext, + authContext: AuthContext, root: StatusItem.Thread ) { self.init( context: context, + authContext: authContext, optionalRoot: root ) } diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift index fe924015..b62c76dd 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift @@ -186,7 +186,7 @@ extension TimelineViewController { @objc private func avatarButtonPressed(_ sender: UIButton) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - let drawerSidebarViewModel = DrawerSidebarViewModel(context: context) + let drawerSidebarViewModel = DrawerSidebarViewModel(context: context, authContext: authContext) coordinator.present(scene: .drawerSidebar(viewModel: drawerSidebarViewModel), from: self, transition: .custom(transitioningDelegate: drawerSidebarTransitionController)) } @@ -215,6 +215,8 @@ extension TimelineViewController { let composeViewModel = ComposeViewModel(context: context) let composeContentViewModel = ComposeContentViewModel( + context: context, + authContext: authContext, kind: { switch _viewModel.kind { case .home: @@ -260,5 +262,10 @@ extension TimelineViewController { } +// MARK: - AuthContextProvider +extension TimelineViewController: AuthContextProvider { + var authContext: AuthContext { _viewModel.authContext } +} + // MARK: - AvatarBarButtonItemDelegate extension TimelineViewController: AvatarBarButtonItemDelegate { } diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift index 007768d3..4f25b274 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift @@ -25,6 +25,7 @@ class TimelineViewModel: TimelineViewModelDriver { // input let context: AppContext + let authContext: AuthContext let kind: StatusFetchViewModel.Timeline.Kind let feedFetchedResultsController: FeedFetchedResultsController let statusRecordFetchedResultController: StatusRecordFetchedResultController @@ -68,9 +69,11 @@ class TimelineViewModel: TimelineViewModelDriver { init( context: AppContext, + authContext: AuthContext, kind: StatusFetchViewModel.Timeline.Kind ) { self.context = context + self.authContext = authContext self.kind = kind self.feedFetchedResultsController = FeedFetchedResultsController(managedObjectContext: context.managedObjectContext) self.statusRecordFetchedResultController = StatusRecordFetchedResultController(managedObjectContext: context.managedObjectContext) diff --git a/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel.swift b/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel.swift index dc43f1a3..11bb5aaa 100644 --- a/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel.swift @@ -14,10 +14,12 @@ final class FederatedTimelineViewModel: ListTimelineViewModel { init( context: AppContext, + authContext: AuthContext, isLocal: Bool ) { super.init( context: context, + authContext: authContext, kind: .public(isLocal: isLocal) ) diff --git a/TwidereX/Scene/Timeline/Hashtag/HashtagTimelineViewModel.swift b/TwidereX/Scene/Timeline/Hashtag/HashtagTimelineViewModel.swift index ea95d9b8..7abe1111 100644 --- a/TwidereX/Scene/Timeline/Hashtag/HashtagTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Hashtag/HashtagTimelineViewModel.swift @@ -14,16 +14,16 @@ final class HashtagTimelineViewModel: ListTimelineViewModel { init( context: AppContext, + authContext: AuthContext, hashtag: String ) { super.init( context: context, + authContext: authContext, kind: .hashtag(hashtag: hashtag) ) - - context.authenticationService.$activeAuthenticationContext - .map { $0?.userIdentifier } - .assign(to: &statusRecordFetchedResultController.$userIdentifier) + + statusRecordFetchedResultController.userIdentifier = authContext.authenticationContext.userIdentifier } deinit { diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift index 2c678063..f519cc23 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift @@ -205,6 +205,10 @@ extension HomeTimelineViewController { guard let self = self else { return } self.moveToFirst(action, category: .blockingAuthor) }), + UIAction(title: "First Duplicated Status", image: nil, attributes: [], handler: { [weak self] action in + guard let self = self else { return } + self.moveToFirst(action, category: .duplicated) + }), ] ) } @@ -268,7 +272,7 @@ extension HomeTimelineViewController { } @objc private func showStatusByID(_ id: String) { - Task { + Task { @MainActor in let authenticationContext = self.context.authenticationService.activeAuthenticationContext switch authenticationContext { case .twitter(let authenticationContext): @@ -285,9 +289,10 @@ extension HomeTimelineViewController { } let statusThreadViewModel = StatusThreadViewModel( context: self.context, + authContext: self.authContext, root: .root(context: .init(status: .twitter(record: .init(objectID: status.objectID)))) ) - await self.coordinator.present( + self.coordinator.present( scene: .statusThread(viewModel: statusThreadViewModel), from: self, transition: .show @@ -309,12 +314,12 @@ extension HomeTimelineViewController { } @objc private func showLocalTimelineAction(_ sender: UIAction) { - let federatedTimelineViewModel = FederatedTimelineViewModel(context: context, isLocal: true) + let federatedTimelineViewModel = FederatedTimelineViewModel(context: context, authContext: authContext, isLocal: true) coordinator.present(scene: .federatedTimeline(viewModel: federatedTimelineViewModel), from: self, transition: .show) } @objc private func showPublicTimelineAction(_ sender: UIAction) { - let federatedTimelineViewModel = FederatedTimelineViewModel(context: context, isLocal: false) + let federatedTimelineViewModel = FederatedTimelineViewModel(context: context, authContext: authContext, isLocal: false) coordinator.present(scene: .federatedTimeline(viewModel: federatedTimelineViewModel), from: self, transition: .show) } @@ -333,6 +338,7 @@ extension HomeTimelineViewController { case followsYouAuthor case blockingAuthor case status(id: String) + case duplicated func match(item: StatusItem) -> Bool { let authenticationContext = AppContext.shared.authenticationService.activeAuthenticationContext @@ -361,6 +367,8 @@ extension HomeTimelineViewController { return (status.repost ?? status).author.blockingBy.contains(me) case .status(let id): return status.id == id + case .duplicated: + return false default: return false } @@ -375,7 +383,49 @@ extension HomeTimelineViewController { } func firstMatch(in items: [StatusItem]) -> StatusItem? { - return items.first { item in self.match(item: item) } + switch self { + case .duplicated: + var index = 0 + while index < items.count - 2 { + defer { index += 1 } + + let this = items[index] + let next = items[index + 1] + + switch (this, next) { + case (.feed(let thisRecord), .feed(let nextRecord)): + guard let thisFeed = thisRecord.object(in: AppContext.shared.managedObjectContext) else { continue } + guard let nextFeed = nextRecord.object(in: AppContext.shared.managedObjectContext) else { continue } + + if let thisTwitterStatus = thisFeed.twitterStatus, + let nextTwitterStatus = nextFeed.twitterStatus + { + if thisTwitterStatus.id == nextTwitterStatus.id { + return this + } else { + continue + } + } else if let thisMastodonStatus = thisFeed.mastodonStatus, + let nextMastodonStatus = nextFeed.mastodonStatus + { + if thisMastodonStatus.id == nextMastodonStatus.id { + return this + } else { + continue + } + } else { + continue + } + default: + continue + } + } + let logger = Logger(subsystem: "HomeTimelineViewController", category: "DebugAction") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): not found duplicated in \(index) count items") + return nil + default: + return items.first { item in self.match(item: item) } + } } } diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel.swift index 77b47622..63d80cbe 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel.swift @@ -20,9 +20,17 @@ final class HomeTimelineViewModel: ListTimelineViewModel { @Published var unreadItemCount = 0 @Published var loadItemCount = 0 - init(context: AppContext) { - super.init(context: context, kind: .home) - + init( + context: AppContext, + authContext: AuthContext + ) { + super.init( + context: context, + authContext: authContext, + kind: .home + ) + // end init + enableAutoFetchLatest = true context.authenticationService.$activeAuthenticationContext diff --git a/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel.swift b/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel.swift index 164340b0..4f8a53b3 100644 --- a/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel.swift @@ -20,10 +20,12 @@ final class ListStatusTimelineViewModel: ListTimelineViewModel { init( context: AppContext, + authContext: AuthContext, list: ListRecord ) { super.init( context: context, + authContext: authContext, kind: .list(list: list) ) diff --git a/TwidereX/Scene/Timeline/Search/Media/SearchMediaTimelineViewModel.swift b/TwidereX/Scene/Timeline/Search/Media/SearchMediaTimelineViewModel.swift index e9414c50..d576e6bb 100644 --- a/TwidereX/Scene/Timeline/Search/Media/SearchMediaTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Search/Media/SearchMediaTimelineViewModel.swift @@ -18,7 +18,8 @@ final class SearchMediaTimelineViewModel: GridTimelineViewModel { // output init( - context: AppContext + context: AppContext, + authContext: AuthContext ) { let searchTimelineContext = StatusFetchViewModel.Timeline.Kind.SearchTimelineContext( timelineKind: .media, @@ -26,6 +27,7 @@ final class SearchMediaTimelineViewModel: GridTimelineViewModel { ) super.init( context: context, + authContext: authContext, kind: .search(searchTimelineContext: searchTimelineContext) ) diff --git a/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel.swift b/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel.swift index ed400487..7a449f4a 100644 --- a/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel.swift @@ -18,7 +18,8 @@ final class SearchTimelineViewModel: ListTimelineViewModel { // output init( - context: AppContext + context: AppContext, + authContext: AuthContext ) { let searchTimelineContext = StatusFetchViewModel.Timeline.Kind.SearchTimelineContext( timelineKind: .status, @@ -26,6 +27,7 @@ final class SearchTimelineViewModel: ListTimelineViewModel { ) super.init( context: context, + authContext: authContext, kind: .search(searchTimelineContext: searchTimelineContext) ) diff --git a/TwidereX/Scene/Timeline/User/Like/MeLikeTimelineViewModel.swift b/TwidereX/Scene/Timeline/User/Like/MeLikeTimelineViewModel.swift deleted file mode 100644 index 55acb5f4..00000000 --- a/TwidereX/Scene/Timeline/User/Like/MeLikeTimelineViewModel.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// MeLikeTimelineViewModel.swift -// TwidereX -// -// Created by MainasuK on 2022-6-15. -// Copyright © 2022 Twidere. All rights reserved. -// - -import UIKit -import TwidereCore - -final class MeLikeTimelineViewModel: UserLikeTimelineViewModel { - - init(context: AppContext) { - let timelineContext = StatusFetchViewModel.Timeline.Kind.UserTimelineContext( - timelineKind: .like, - userIdentifier: nil - ) - super.init( - context: context, - timelineContext: timelineContext - ) - - context.authenticationService.$activeAuthenticationContext - .map { $0?.userIdentifier } - .assign(to: &timelineContext.$userIdentifier) - } - -} diff --git a/TwidereX/Scene/Timeline/User/Media/UserMediaTimelineViewModel.swift b/TwidereX/Scene/Timeline/User/Media/UserMediaTimelineViewModel.swift index a57ba32b..ee707d20 100644 --- a/TwidereX/Scene/Timeline/User/Media/UserMediaTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/User/Media/UserMediaTimelineViewModel.swift @@ -12,8 +12,16 @@ import TwidereCore final class UserMediaTimelineViewModel: GridTimelineViewModel { - init(context: AppContext, timelineContext: StatusFetchViewModel.Timeline.Kind.UserTimelineContext) { - super.init(context: context, kind: .user(userTimelineContext: timelineContext)) + init( + context: AppContext, + authContext: AuthContext, + timelineContext: StatusFetchViewModel.Timeline.Kind.UserTimelineContext + ) { + super.init( + context: context, + authContext: authContext, + kind: .user(userTimelineContext: timelineContext) + ) timelineContext.$userIdentifier .assign(to: &statusRecordFetchedResultController.$userIdentifier) @@ -22,5 +30,5 @@ final class UserMediaTimelineViewModel: GridTimelineViewModel { deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) } - + } diff --git a/TwidereX/Scene/Timeline/User/Status/UserTimelineViewModel.swift b/TwidereX/Scene/Timeline/User/Status/UserTimelineViewModel.swift index acc73ee4..ae31bc75 100644 --- a/TwidereX/Scene/Timeline/User/Status/UserTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/User/Status/UserTimelineViewModel.swift @@ -14,8 +14,16 @@ class UserTimelineViewModel: ListTimelineViewModel { @Published var userIdentifier: UserIdentifier? - init(context: AppContext, timelineContext: StatusFetchViewModel.Timeline.Kind.UserTimelineContext) { - super.init(context: context, kind: .user(userTimelineContext: timelineContext)) + init( + context: AppContext, + authContext: AuthContext, + timelineContext: StatusFetchViewModel.Timeline.Kind.UserTimelineContext + ) { + super.init( + context: context, + authContext: authContext, + kind: .user(userTimelineContext: timelineContext) + ) timelineContext.$userIdentifier .assign(to: &$userIdentifier) diff --git a/TwidereX/Scene/Transition/DrawerSidebar/DrawerSidebarTransitionController.swift b/TwidereX/Scene/Transition/DrawerSidebar/DrawerSidebarTransitionController.swift index c27541a7..40fef328 100644 --- a/TwidereX/Scene/Transition/DrawerSidebar/DrawerSidebarTransitionController.swift +++ b/TwidereX/Scene/Transition/DrawerSidebar/DrawerSidebarTransitionController.swift @@ -9,7 +9,7 @@ import os.log import UIKit -protocol DrawerSidebarTransitionHostViewController: UIViewController & NeedsDependency { +protocol DrawerSidebarTransitionHostViewController: UIViewController & NeedsDependency & AuthContextProvider { var drawerSidebarTransitionController: DrawerSidebarTransitionController! { get } var avatarBarButtonItem: AvatarBarButtonItem { get } } @@ -145,8 +145,12 @@ extension DrawerSidebarTransitionController { switch transitionType { case .present: wantsInteractive = true + let drawerSidebarViewModel = DrawerSidebarViewModel( + context: hostViewController.context, + authContext: hostViewController.authContext + ) hostViewController.coordinator.present( - scene: .drawerSidebar(viewModel: DrawerSidebarViewModel(context: hostViewController.context)), + scene: .drawerSidebar(viewModel: drawerSidebarViewModel), from: hostViewController, transition: .custom(transitioningDelegate: self) ) diff --git a/TwidereX/Supporting Files/SceneDelegate.swift b/TwidereX/Supporting Files/SceneDelegate.swift index 77d8cf79..7f59dc6b 100644 --- a/TwidereX/Supporting Files/SceneDelegate.swift +++ b/TwidereX/Supporting Files/SceneDelegate.swift @@ -156,27 +156,30 @@ extension SceneDelegate { topMost.dismiss(animated: false) } let composeViewModel = ComposeViewModel(context: coordinator.context) - let composeContentViewModel = ComposeContentViewModel( - kind: .post, - configurationContext: .init( - apiService: coordinator.context.apiService, - authenticationService: coordinator.context.authenticationService, - mastodonEmojiService: coordinator.context.mastodonEmojiService, - statusViewConfigureContext: .init( - dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - authenticationContext: coordinator.context.authenticationService.$activeAuthenticationContext - ) - ) - ) - coordinator.present( - scene: .compose( - viewModel: composeViewModel, - contentViewModel: composeContentViewModel - ), - from: nil, - transition: .modal(animated: true) - ) + assertionFailure("TODO: check authContext and handle alert") +// let composeContentViewModel = ComposeContentViewModel( +// context: .shared, +// authContext: <#T##AuthContext#>, +// kind: .post, +// configurationContext: .init( +// apiService: coordinator.context.apiService, +// authenticationService: coordinator.context.authenticationService, +// mastodonEmojiService: coordinator.context.mastodonEmojiService, +// statusViewConfigureContext: .init( +// dateTimeProvider: DateTimeSwiftProvider(), +// twitterTextProvider: OfficialTwitterTextProvider(), +// authenticationContext: coordinator.context.authenticationService.$activeAuthenticationContext +// ) +// ) +// ) +// coordinator.present( +// scene: .compose( +// viewModel: composeViewModel, +// contentViewModel: composeContentViewModel +// ), +// from: nil, +// transition: .modal(animated: true) +// ) return true case "com.twidere.TwidereX.search": if let topMost = topMostViewController(), topMost.isModal { From 2283e5150ceb466404c1f08305741905ef31a1e2 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 2 Feb 2023 19:07:28 +0800 Subject: [PATCH 024/128] chore: deprecated AuthenticationService --- ShareExtension/ComposeViewController.swift | 4 +- .../Service/AuthenticationService.swift | 107 +++++----- .../Service/NotificationService.swift | 202 +++++++++--------- .../PollOptionView+Configuration.swift | 4 - .../Content/StatusView+Configuration.swift | 10 +- .../Content/UserView+Configuration.swift | 10 +- .../ComposeContentViewController.swift | 5 +- .../ComposeContentViewModel.swift | 15 +- .../MentionPick/MentionPickViewModel.swift | 34 ++- TwidereX.xcodeproj/project.pbxproj | 4 + TwidereX/Coordinator/SceneCoordinator.swift | 29 ++- .../Misc/History/HistorySection.swift | 4 +- .../Notification/NotificationSection.swift | 4 +- .../Diffable/Misc/TabBar/TabBarItem.swift | 5 +- TwidereX/Diffable/Status/StatusSection.swift | 2 +- TwidereX/Diffable/User/UserSection.swift | 5 +- ...pendency+AvatarBarButtonItemDelegate.swift | 4 +- .../Provider/DataSourceFacade+History.swift | 10 +- .../Provider/DataSourceFacade+LIst.swift | 3 +- .../Provider/DataSourceFacade+Media.swift | 2 +- .../Provider/DataSourceFacade+Poll.swift | 8 +- .../Provider/DataSourceFacade+Status.swift | 8 +- ...der+MediaInfoDescriptionViewDelegate.swift | 3 +- ...ider+StatusViewTableViewCellDelegate.swift | 57 +++-- ...ovider+UserViewTableViewCellDelegate.swift | 16 +- .../List/AccountListViewController.swift | 5 + .../List/AccountListViewModel+Diffable.swift | 15 +- .../Account/List/AccountListViewModel.swift | 49 ++++- .../StatusHistoryViewModel+Diffable.swift | 8 +- .../User/UserHistoryViewModel+Diffable.swift | 8 +- .../CompositeListViewController.swift | 1 + .../Scene/List/EditList/EditListView.swift | 14 +- .../List/EditList/EditListViewModel.swift | 15 +- .../Scene/List/List/ListViewModel+State.swift | 5 +- .../ListUser/ListUserViewController.swift | 37 ++-- .../ListUser/ListUserViewModel+Diffable.swift | 5 +- .../ListUser/ListUserViewModel+State.swift | 6 +- .../MediaPreviewViewController.swift | 4 +- .../MediaInfoDescriptionView+ViewModel.swift | 2 - ...tificationTimelineViewModel+Diffable.swift | 11 +- ...ionTimelineViewModel+LoadOldestState.swift | 7 +- .../NotificationViewController.swift | 19 +- .../Notification/NotificationViewModel.swift | 8 +- .../FollowingListViewModel+Diffable.swift | 5 +- .../FollowingListViewModel+State.swift | 8 +- .../Scene/Profile/MeProfileViewModel.swift | 27 +++ .../Scene/Profile/ProfileViewController.swift | 38 ++-- TwidereX/Scene/Profile/ProfileViewModel.swift | 48 ++--- .../Profile/RemoteProfileViewModel.swift | 4 +- .../Root/ContentSplitViewController.swift | 3 +- .../Drawer/DrawerSidebarViewController.swift | 22 +- .../DrawerSidebarViewModel+Diffable.swift | 50 +++-- .../Root/MainTab/MainTabBarController.swift | 9 +- .../Root/Sidebar/SidebarViewController.swift | 2 +- .../Scene/Root/Sidebar/SidebarViewModel.swift | 93 ++++---- .../SavedSearchViewController.swift | 5 +- .../Search/Search/SearchViewController.swift | 38 ++-- .../Search/SearchViewModel+Diffable.swift | 15 +- .../Scene/Search/Search/SearchViewModel.swift | 4 +- .../SearchHashtagViewModel+State.swift | 10 +- .../SearchResultViewController.swift | 3 +- .../User/SearchUserViewModel+Diffable.swift | 5 +- .../User/SearchUserViewModel+State.swift | 7 +- .../Scene/Search/Trend/TrendViewModel.swift | 28 +-- .../Setting/About/AboutViewController.swift | 4 +- .../Setting/Developer/DeveloperView.swift | 8 +- .../Developer/DeveloperViewController.swift | 16 +- .../Developer/DeveloperViewModel.swift | 8 +- .../DisplayPreferenceView.swift | 4 +- .../DisplayPreferenceViewController.swift | 2 +- .../DisplayPreferenceViewModel.swift | 8 +- .../List/SettingListViewController.swift | 6 +- .../PushNotificationScratchViewModel.swift | 48 ++++- .../StatusThreadViewModel+Diffable.swift | 4 +- ...tatusThreadViewModel+LoadThreadState.swift | 8 +- .../StatusThread/StatusThreadViewModel.swift | 2 +- ...tterStatusThreadReplyViewModel+State.swift | 4 +- .../TwitterStatusThreadReplyViewModel.swift | 7 +- .../Base/Common/TimelineViewController.swift | 23 +- .../TimelineViewModel+LoadOldestState.swift | 5 +- .../Base/Common/TimelineViewModel.swift | 3 +- .../Base/List/ListTimelineViewModel.swift | 3 +- .../FederatedTimelineViewModel+Diffable.swift | 4 +- .../FederatedTimelineViewModel.swift | 29 +-- .../HashtagTimelineViewModel+Diffable.swift | 4 +- ...meTimelineViewController+DebugAction.swift | 14 +- .../Home/HomeTimelineViewModel+Diffable.swift | 4 +- .../Timeline/Home/HomeTimelineViewModel.swift | 34 ++- .../ListStatusTimelineViewController.swift | 33 +-- ...ListStatusTimelineViewModel+Diffable.swift | 4 +- .../List/ListStatusTimelineViewModel.swift | 5 +- .../Media/SearchMediaTimelineViewModel.swift | 4 +- .../SearchTimelineViewModel+Diffable.swift | 4 +- .../Status/SearchTimelineViewModel.swift | 4 +- .../UserTimelineViewModel+Diffable.swift | 4 +- 95 files changed, 752 insertions(+), 756 deletions(-) create mode 100644 TwidereX/Scene/Profile/MeProfileViewModel.swift diff --git a/ShareExtension/ComposeViewController.swift b/ShareExtension/ComposeViewController.swift index 030677ef..f31d0813 100644 --- a/ShareExtension/ComposeViewController.swift +++ b/ShareExtension/ComposeViewController.swift @@ -109,9 +109,9 @@ extension ComposeViewController { authenticationService: context.authenticationService, mastodonEmojiService: context.mastodonEmojiService, statusViewConfigureContext: .init( + authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - authenticationContext: context.authenticationService.$activeAuthenticationContext + twitterTextProvider: OfficialTwitterTextProvider() ) ) ) diff --git a/TwidereSDK/Sources/TwidereCore/Service/AuthenticationService.swift b/TwidereSDK/Sources/TwidereCore/Service/AuthenticationService.swift index c0a15737..35938357 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/AuthenticationService.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/AuthenticationService.swift @@ -23,13 +23,12 @@ public class AuthenticationService: NSObject { let appSecret: AppSecret let managedObjectContext: NSManagedObjectContext // read-only let backgroundManagedObjectContext: NSManagedObjectContext - let authenticationIndexFetchedResultsController: NSFetchedResultsController + // let authenticationIndexFetchedResultsController: NSFetchedResultsController // output - @Published public var authenticationIndexes: [AuthenticationIndex] = [] - public let activeAuthenticationIndex = CurrentValueSubject(nil) - - @Published public var activeAuthenticationContext: AuthenticationContext? = nil + // @Published public var authenticationIndexes: [AuthenticationIndex] = [] + // public let activeAuthenticationIndex = CurrentValueSubject(nil) + // @Published public var activeAuthenticationContext: AuthenticationContext? = nil public init( managedObjectContext: NSManagedObjectContext, @@ -41,21 +40,21 @@ public class AuthenticationService: NSObject { self.backgroundManagedObjectContext = backgroundManagedObjectContext self.apiService = apiService self.appSecret = appSecret - self.authenticationIndexFetchedResultsController = { - let fetchRequest = AuthenticationIndex.sortedFetchRequest - fetchRequest.returnsObjectsAsFaults = false - fetchRequest.fetchBatchSize = 20 - let controller = NSFetchedResultsController( - fetchRequest: fetchRequest, - managedObjectContext: managedObjectContext, - sectionNameKeyPath: nil, - cacheName: nil - ) - return controller - }() + // self.authenticationIndexFetchedResultsController = { + // let fetchRequest = AuthenticationIndex.sortedFetchRequest + // fetchRequest.returnsObjectsAsFaults = false + // fetchRequest.fetchBatchSize = 20 + // let controller = NSFetchedResultsController( + // fetchRequest: fetchRequest, + // managedObjectContext: managedObjectContext, + // sectionNameKeyPath: nil, + // cacheName: nil + // ) + // return controller + // }() super.init() - authenticationIndexFetchedResultsController.delegate = self + //authenticationIndexFetchedResultsController.delegate = self // verify credentials for active authentication // FIXME: @@ -93,26 +92,26 @@ public class AuthenticationService: NSObject { // .store(in: &disposeBag) // bind activeAuthenticationIndex - $authenticationIndexes - .map { $0.sorted(by: { $0.activeAt > $1.activeAt }).first } - .assign(to: \.value, on: activeAuthenticationIndex) - .store(in: &disposeBag) - - // bind activeAuthenticationContext - activeAuthenticationIndex - .map { authenticationIndex -> AuthenticationContext? in - guard let authenticationIndex = authenticationIndex else { return nil } - guard let authenticationContext = AuthenticationContext(authenticationIndex: authenticationIndex, secret: appSecret.secret) else { return nil } - return authenticationContext - } - .assign(to: &$activeAuthenticationContext) - - do { - try authenticationIndexFetchedResultsController.performFetch() - authenticationIndexes = authenticationIndexFetchedResultsController.fetchedObjects ?? [] - } catch { - assertionFailure(error.localizedDescription) - } +// $authenticationIndexes +// .map { $0.sorted(by: { $0.activeAt > $1.activeAt }).first } +// .assign(to: \.value, on: activeAuthenticationIndex) +// .store(in: &disposeBag) +// +// // bind activeAuthenticationContext +// activeAuthenticationIndex +// .map { authenticationIndex -> AuthenticationContext? in +// guard let authenticationIndex = authenticationIndex else { return nil } +// guard let authenticationContext = AuthenticationContext(authenticationIndex: authenticationIndex, secret: appSecret.secret) else { return nil } +// return authenticationContext +// } +// .assign(to: &$activeAuthenticationContext) +// +// do { +// try authenticationIndexFetchedResultsController.performFetch() +// authenticationIndexes = authenticationIndexFetchedResultsController.fetchedObjects ?? [] +// } catch { +// assertionFailure(error.localizedDescription) +// } } } @@ -185,19 +184,19 @@ extension AuthenticationService { } // MARK: - NSFetchedResultsControllerDelegate -extension AuthenticationService: NSFetchedResultsControllerDelegate { - - public func controllerWillChangeContent(_ controller: NSFetchedResultsController) { - os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - } - - public func controllerDidChangeContent(_ controller: NSFetchedResultsController) { - switch controller { - case authenticationIndexFetchedResultsController: - authenticationIndexes = authenticationIndexFetchedResultsController.fetchedObjects ?? [] - default: - assertionFailure() - } - } - -} +//extension AuthenticationService: NSFetchedResultsControllerDelegate { +// +// public func controllerWillChangeContent(_ controller: NSFetchedResultsController) { +// os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) +// } +// +// public func controllerDidChangeContent(_ controller: NSFetchedResultsController) { +// switch controller { +// case authenticationIndexFetchedResultsController: +// authenticationIndexes = authenticationIndexFetchedResultsController.fetchedObjects ?? [] +// default: +// assertionFailure() +// } +// } +// +//} diff --git a/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift b/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift index 448c1b77..3db6be7f 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift @@ -54,65 +54,66 @@ final public actor NotificationService { // request notification permission if needs // register notification subscriber - authenticationService.$authenticationIndexes - .sink { [weak self] authenticationIndexes in - guard let self = self else { return } - - let managedObjectContext = authenticationService.managedObjectContext - // request permission when sign-in account - Task { - let isEmpty = await managedObjectContext.perform { - return authenticationIndexes.isEmpty - } - if !isEmpty { - await self.requestNotificationPermission() - } - } // end Task - - Task { - let authenticationContexts: [AuthenticationContext] = await managedObjectContext.perform { - let authenticationContexts = authenticationIndexes.compactMap { authenticationIndex in - AuthenticationContext(authenticationIndex: authenticationIndex, secret: self.appSecret.secret) - } - return authenticationContexts - } - await self.updateSubscribers(authenticationContexts) - } // end Task - } - .store(in: &disposeBag) // FIXME: how to use disposeBag in actor under Swift 6 ?? + // FIXME: add logic for refresh all accounts +// authenticationService.$authenticationIndexes +// .sink { [weak self] authenticationIndexes in +// guard let self = self else { return } +// +// let managedObjectContext = authenticationService.managedObjectContext +// // request permission when sign-in account +// Task { +// let isEmpty = await managedObjectContext.perform { +// return authenticationIndexes.isEmpty +// } +// if !isEmpty { +// await self.requestNotificationPermission() +// } +// } // end Task +// +// Task { +// let authenticationContexts: [AuthenticationContext] = await managedObjectContext.perform { +// let authenticationContexts = authenticationIndexes.compactMap { authenticationIndex in +// AuthenticationContext(authenticationIndex: authenticationIndex, secret: self.appSecret.secret) +// } +// return authenticationContexts +// } +// await self.updateSubscribers(authenticationContexts) +// } // end Task +// } +// .store(in: &disposeBag) // FIXME: how to use disposeBag in actor under Swift 6 ?? - Publishers.CombineLatest( - authenticationService.$authenticationIndexes, - applicationIconBadgeNeedsUpdate - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] authenticationIndexes, _ in - guard let self = self else { return } - - let authenticationContexts = authenticationIndexes.compactMap { authenticationIndex in - AuthenticationContext(authenticationIndex: authenticationIndex, secret: self.appSecret.secret) - } - - var count = 0 - for authenticationContext in authenticationContexts { - switch authenticationContext { - case .twitter: - continue - case .mastodon(let authenticationContext): - let accessToken = authenticationContext.authorization.accessToken - let _count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) - count += _count - } - } - - UserDefaults.shared.notificationBadgeCount = count - let _count = count - Task { - await self.updateApplicationIconBadge(count: _count) - } - self.unreadNotificationCountDidUpdate.send() - } - .store(in: &disposeBag) +// Publishers.CombineLatest( +// authenticationService.$authenticationIndexes, +// applicationIconBadgeNeedsUpdate +// ) +// .receive(on: DispatchQueue.main) +// .sink { [weak self] authenticationIndexes, _ in +// guard let self = self else { return } +// +// let authenticationContexts = authenticationIndexes.compactMap { authenticationIndex in +// AuthenticationContext(authenticationIndex: authenticationIndex, secret: self.appSecret.secret) +// } +// +// var count = 0 +// for authenticationContext in authenticationContexts { +// switch authenticationContext { +// case .twitter: +// continue +// case .mastodon(let authenticationContext): +// let accessToken = authenticationContext.authorization.accessToken +// let _count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) +// count += _count +// } +// } +// +// UserDefaults.shared.notificationBadgeCount = count +// let _count = count +// Task { +// await self.updateApplicationIconBadge(count: _count) +// } +// self.unreadNotificationCountDidUpdate.send() +// } +// .store(in: &disposeBag) } } @@ -120,37 +121,38 @@ final public actor NotificationService { extension NotificationService { public nonisolated func unreadApplicationShortcutItems() async -> [UIApplicationShortcutItem] { - guard let authenticationService = await self.authenticationService else { return [] } - let managedObjectContext = authenticationService.managedObjectContext - return await managedObjectContext.perform { - var items: [UIApplicationShortcutItem] = [] - for object in authenticationService.authenticationIndexes { - guard let authenticationIndex = managedObjectContext.object(with: object.objectID) as? AuthenticationIndex else { continue } - let _accessToken: String? = { - return authenticationIndex.mastodonAuthentication?.userAccessToken - }() - guard let accessToken = _accessToken else { continue} - - let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) - guard count > 0 else { continue } - - guard let user = authenticationIndex.user else { continue} - let title = "@\(user.username)" - let subtitle = L10n.Count.notification(count) - - let item = UIApplicationShortcutItem( - type: NotificationService.unreadShortcutItemIdentifier, - localizedTitle: title, - localizedSubtitle: subtitle, - icon: nil, - userInfo: [ - "accessToken": accessToken as NSSecureCoding - ] - ) - items.append(item) - } - return items - } + return [] +// guard let authenticationService = await self.authenticationService else { return [] } +// let managedObjectContext = authenticationService.managedObjectContext +// return await managedObjectContext.perform { +// var items: [UIApplicationShortcutItem] = [] +// for object in authenticationService.authenticationIndexes { +// guard let authenticationIndex = managedObjectContext.object(with: object.objectID) as? AuthenticationIndex else { continue } +// let _accessToken: String? = { +// return authenticationIndex.mastodonAuthentication?.userAccessToken +// }() +// guard let accessToken = _accessToken else { continue} +// +// let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) +// guard count > 0 else { continue } +// +// guard let user = authenticationIndex.user else { continue} +// let title = "@\(user.username)" +// let subtitle = L10n.Count.notification(count) +// +// let item = UIApplicationShortcutItem( +// type: NotificationService.unreadShortcutItemIdentifier, +// localizedTitle: title, +// localizedSubtitle: subtitle, +// icon: nil, +// userInfo: [ +// "accessToken": accessToken as NSSecureCoding +// ] +// ) +// items.append(item) +// } +// return items +// } } } @@ -158,17 +160,17 @@ extension NotificationService { extension NotificationService { public func clearNotificationCountForActiveUser() { - guard let authenticationService = self.authenticationService else { return } - guard let authenticationContext = authenticationService.activeAuthenticationContext else { return } - switch authenticationContext { - case .twitter: - return - case .mastodon(let authenticationContext): - let accessToken = authenticationContext.authorization.accessToken - UserDefaults.shared.setNotificationCountWithAccessToken(accessToken: accessToken, value: 0) - } - - applicationIconBadgeNeedsUpdate.send() +// guard let authenticationService = self.authenticationService else { return } +// guard let authenticationContext = authenticationService.activeAuthenticationContext else { return } +// switch authenticationContext { +// case .twitter: +// return +// case .mastodon(let authenticationContext): +// let accessToken = authenticationContext.authorization.accessToken +// UserDefaults.shared.setNotificationCountWithAccessToken(accessToken: accessToken, value: 0) +// } +// +// applicationIconBadgeNeedsUpdate.send() } public func updateToken(_ token: String?) { diff --git a/TwidereSDK/Sources/TwidereUI/Content/PollOptionView+Configuration.swift b/TwidereSDK/Sources/TwidereUI/Content/PollOptionView+Configuration.swift index a55fe24c..13662df5 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/PollOptionView+Configuration.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/PollOptionView+Configuration.swift @@ -24,10 +24,6 @@ extension PollOptionView { pollOption: PollOptionObject, configurationContext: ConfigurationContext ) { - configurationContext.authenticationContext - .assign(to: \.authenticationContext, on: viewModel) - .store(in: &disposeBag) - switch pollOption { case .twitter(let object): configure( diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+Configuration.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+Configuration.swift index ef0aced8..938ed2c7 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+Configuration.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+Configuration.swift @@ -19,18 +19,18 @@ import Meta extension StatusView { public struct ConfigurationContext { + public let authContext: AuthContext public let dateTimeProvider: DateTimeProvider public let twitterTextProvider: TwitterTextProvider - public let authenticationContext: Published.Publisher public init( + authContext: AuthContext, dateTimeProvider: DateTimeProvider, - twitterTextProvider: TwitterTextProvider, - authenticationContext: Published.Publisher + twitterTextProvider: TwitterTextProvider ) { + self.authContext = authContext self.dateTimeProvider = dateTimeProvider self.twitterTextProvider = twitterTextProvider - self.authenticationContext = authenticationContext } } } @@ -103,7 +103,6 @@ extension StatusView { viewModel.platform = .twitter viewModel.dateTimeProvider = configurationContext.dateTimeProvider viewModel.twitterTextProvider = configurationContext.twitterTextProvider - configurationContext.authenticationContext.assign(to: \.authenticationContext, on: viewModel).store(in: &disposeBag) configureHeader(status) configureAuthor(status) @@ -341,7 +340,6 @@ extension StatusView { viewModel.platform = .mastodon viewModel.dateTimeProvider = configurationContext.dateTimeProvider viewModel.twitterTextProvider = configurationContext.twitterTextProvider - configurationContext.authenticationContext.assign(to: \.authenticationContext, on: viewModel).store(in: &disposeBag) configureHeader(status, notification: notification) configureAuthor(status) diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView+Configuration.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView+Configuration.swift index 6fca1efa..e2d3a13a 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView+Configuration.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView+Configuration.swift @@ -17,15 +17,15 @@ import MastodonSDK extension UserView { public struct ConfigurationContext { + public let authContext: AuthContext public let listMembershipViewModel: ListMembershipViewModel? - public let authenticationContext: AuthenticationContext? public init( - listMembershipViewModel: ListMembershipViewModel?, - authenticationContext: AuthenticationContext? + authContext: AuthContext, + listMembershipViewModel: ListMembershipViewModel? ) { + self.authContext = authContext self.listMembershipViewModel = listMembershipViewModel - self.authenticationContext = authenticationContext } } } @@ -37,8 +37,6 @@ extension UserView { notification: NotificationObject?, configurationContext: ConfigurationContext ) { - viewModel.authenticationContext = configurationContext.authenticationContext - switch user { case .twitter(let user): configure(twitterUser: user) diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewController.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewController.swift index 7743b18d..a42af929 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewController.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -85,11 +85,12 @@ extension ComposeContentViewController { .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self = self else { return } + guard let authContext = self.viewModel.authContext else { return } guard let primaryItem = self.viewModel.primaryMentionPickItem else { return } let mentionPickViewModel = MentionPickViewModel( - apiService: self.viewModel.configurationContext.apiService, - authenticationService: self.viewModel.configurationContext.authenticationService, + context: self.viewModel.context, + authContext: authContext, primaryItem: primaryItem, secondaryItems: self.viewModel.secondaryMentionPickItems ) diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift index 944133f2..bb83ff8b 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -163,7 +163,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { self.authContext = authContext self.kind = kind self.configurationContext = configurationContext - self.platform = configurationContext.authenticationService.activeAuthenticationContext?.platform ?? .none + self.platform = authContext.authenticationContext.platform super.init() // end init @@ -226,7 +226,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { // set content text var mentionAccts: [String] = [] let _authorUserIdentifier: MastodonUserIdentifier? = { - switch configurationContext.authenticationService.activeAuthenticationContext?.userIdentifier { + switch authContext.authenticationContext.userIdentifier { case .mastodon(let userIdentifier): return userIdentifier default: return nil } @@ -466,17 +466,16 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { } .store(in: &disposeBag) - Publishers.CombineLatest( + Publishers.CombineLatest3( $isRequestLocation, - $currentLocation + $currentLocation, + $authContext ) - .asyncMap { [weak self] isRequestLocation, currentLocation -> Twitter.Entity.Place? in + .asyncMap { [weak self] isRequestLocation, currentLocation, authContext -> Twitter.Entity.Place? in guard let self = self else { return nil } guard isRequestLocation, let currentLocation = currentLocation else { return nil } - guard let authenticationContext = self.configurationContext.authenticationService.activeAuthenticationContext, - case let .twitter(twitterAuthenticationContext) = authenticationContext - else { return nil } + guard case let .twitter(twitterAuthenticationContext) = authContext?.authenticationContext else { return nil } do { let response = try await self.configurationContext.apiService.geoSearch( diff --git a/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewModel.swift b/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewModel.swift index fd4414b3..5248ff05 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewModel.swift @@ -18,8 +18,8 @@ public final class MentionPickViewModel { var disposeBag = Set() // input - let apiService: APIService - let authenticationService: AuthenticationService + let context: AppContext + let authContext: AuthContext let primaryItem: Item let secondaryItems: [Item] @@ -27,29 +27,25 @@ public final class MentionPickViewModel { var diffableDataSource: UITableViewDiffableDataSource? init( - apiService: APIService, - authenticationService: AuthenticationService, + context: AppContext, + authContext: AuthContext, primaryItem: Item, secondaryItems: [Item] ) { - self.apiService = apiService - self.authenticationService = authenticationService + self.context = context + self.authContext = authContext self.primaryItem = primaryItem self.secondaryItems = secondaryItems + // end init - authenticationService.$activeAuthenticationContext - .sink { [weak self] authenticationContext in - guard let self = self else { return } - switch authenticationContext { - case .twitter(let twitterAuthenticationContext): - Task { - try await self.resolveLoadingItems(twitterAuthenticationContext: twitterAuthenticationContext) - } - default: - break - } + switch authContext.authenticationContext { + case .twitter(let authenticationContext): + Task { + try await self.resolveLoadingItems(twitterAuthenticationContext: authenticationContext) } - .store(in: &disposeBag) + case .mastodon: + break + } } deinit { @@ -138,7 +134,7 @@ extension MentionPickViewModel { } } - let response = try await apiService.twitterUsers( + let response = try await context.apiService.twitterUsers( usernames: usernames, twitterAuthenticationContext: twitterAuthenticationContext ) diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 8872b3da..4e786734 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -151,6 +151,7 @@ DB580EB8288187BD00BC4A0F /* AccountPreferenceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB580EB3288187BD00BC4A0F /* AccountPreferenceViewController.swift */; }; DB580EB9288187BD00BC4A0F /* AccountPreferenceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB580EB4288187BD00BC4A0F /* AccountPreferenceViewModel.swift */; }; DB581A0127D89A3700C35B91 /* DataSourceFacade+LIst.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB581A0027D89A3700C35B91 /* DataSourceFacade+LIst.swift */; }; + DB58F1EC298BD07400836FBE /* MeProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58F1EB298BD07400836FBE /* MeProfileViewModel.swift */; }; DB5A2288255B9155006CA5B2 /* AccountListViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5A2287255B9155006CA5B2 /* AccountListViewModel+Diffable.swift */; }; DB5BF12327F5A549002A3EF5 /* PublishPostIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5BF12227F5A549002A3EF5 /* PublishPostIntentHandler.swift */; }; DB5BF12527F5A5C1002A3EF5 /* Account+Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5BF12427F5A5C1002A3EF5 /* Account+Fetch.swift */; }; @@ -668,6 +669,7 @@ DB580EB3288187BD00BC4A0F /* AccountPreferenceViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountPreferenceViewController.swift; sourceTree = ""; }; DB580EB4288187BD00BC4A0F /* AccountPreferenceViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountPreferenceViewModel.swift; sourceTree = ""; }; DB581A0027D89A3700C35B91 /* DataSourceFacade+LIst.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+LIst.swift"; sourceTree = ""; }; + DB58F1EB298BD07400836FBE /* MeProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeProfileViewModel.swift; sourceTree = ""; }; DB5918F6255E81FB00B20F6F /* MediaPreviewViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MediaPreviewViewController+DataSourceProvider.swift"; sourceTree = ""; }; DB5A2287255B9155006CA5B2 /* AccountListViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountListViewModel+Diffable.swift"; sourceTree = ""; }; DB5BF12227F5A549002A3EF5 /* PublishPostIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublishPostIntentHandler.swift; sourceTree = ""; }; @@ -1655,6 +1657,7 @@ DB6DF3DF252060AA00E8A273 /* ProfileViewModel.swift */, DB3B906826E8D1D80010F64C /* LocalProfileViewModel.swift */, DB76A67027609A8700A50673 /* RemoteProfileViewModel.swift */, + DB58F1EB298BD07400836FBE /* MeProfileViewModel.swift */, ); path = Profile; sourceTree = ""; @@ -3229,6 +3232,7 @@ DB25C4C8277994B800EC1435 /* CenterFootnoteLabelTableViewCell.swift in Sources */, DBA210352759D7B1000B7CB2 /* FollowingListViewModel+Diffable.swift in Sources */, DB2C8744274F4B7D00CE0398 /* SettingListViewController.swift in Sources */, + DB58F1EC298BD07400836FBE /* MeProfileViewModel.swift in Sources */, DBE6357F288555AE001C114B /* PushNotificationScratchView.swift in Sources */, DB47AB3E27CE1F9E00CD73C7 /* TimelineTopLoaderTableViewCell.swift in Sources */, DB0AD4E62858742D0002ABDB /* UserMediaTimelineViewModel+Diffable.swift in Sources */, diff --git a/TwidereX/Coordinator/SceneCoordinator.swift b/TwidereX/Coordinator/SceneCoordinator.swift index d848353e..b80797fe 100644 --- a/TwidereX/Coordinator/SceneCoordinator.swift +++ b/TwidereX/Coordinator/SceneCoordinator.swift @@ -100,11 +100,11 @@ extension SceneCoordinator { case setting(viewModel: SettingListViewModel) case accountPreference(viewModel: AccountPreferenceViewModel) case behaviorsPreference(viewModel: BehaviorsPreferenceViewModel) - case displayPreference + case displayPreference(viewModel: DisplayPreferenceViewModel) case about(viewModel: AboutViewModel) #if DEBUG - case developer + case developer(viewModel: DeveloperViewModel) case pushNotificationScratch #endif @@ -386,15 +386,19 @@ private extension SceneCoordinator { let _viewController = BehaviorsPreferenceViewController() _viewController.viewModel = viewModel viewController = _viewController - case .displayPreference: - viewController = DisplayPreferenceViewController() + case .displayPreference(let viewModel): + let _viewController = DisplayPreferenceViewController() + _viewController.viewModel = viewModel + viewController = _viewController case .about(let viewModel): let _viewController = AboutViewController() _viewController.viewModel = viewModel viewController = _viewController #if DEBUG - case .developer: - viewController = DeveloperViewController() + case .developer(let viewModel): + let _viewController = DeveloperViewController() + _viewController.viewModel = viewModel + viewController = _viewController case .pushNotificationScratch: viewController = PushNotificationScratchViewController() #endif @@ -461,12 +465,15 @@ extension SceneCoordinator { let authConext = AuthContext(authenticationContext: .mastodon(authenticationContext: mastodonAuthenticationContext)) // 1. active notification account - guard let currentAuthenticationContext = context.authenticationService.activeAuthenticationContext else { - // discard task if no available account - return - } + let authenticationIndexRequest = AuthenticationIndex.sortedFetchRequest + authenticationIndexRequest.fetchLimit = 1 + let _authenticationIndex = try context.managedObjectContext.fetch(authenticationIndexRequest).first + guard let authenticationIndex = _authenticationIndex, + let currentAuthenticationContext = AuthContext(authenticationIndex: authenticationIndex) + else { return } + let needsSwitchActiveAccount: Bool = { - switch currentAuthenticationContext { + switch currentAuthenticationContext.authenticationContext { case .mastodon(let authenticationContext): let result = authenticationContext.authorization.accessToken != pushNotification.accessToken return result diff --git a/TwidereX/Diffable/Misc/History/HistorySection.swift b/TwidereX/Diffable/Misc/History/HistorySection.swift index 29bba199..8ec81e4d 100644 --- a/TwidereX/Diffable/Misc/History/HistorySection.swift +++ b/TwidereX/Diffable/Misc/History/HistorySection.swift @@ -70,8 +70,8 @@ extension HistorySection { // user if let user = history.userObject { let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserRelationshipStyleTableViewCell.self), for: indexPath) as! UserRelationshipStyleTableViewCell - let authenticationContext = context.authenticationService.activeAuthenticationContext - let me = authenticationContext?.user(in: context.managedObjectContext) + let authenticationContext = configuration.userViewConfigurationContext.authContext.authenticationContext + let me = authenticationContext.user(in: context.managedObjectContext) let viewModel = UserTableViewCell.ViewModel( user: user, me: me, diff --git a/TwidereX/Diffable/Misc/Notification/NotificationSection.swift b/TwidereX/Diffable/Misc/Notification/NotificationSection.swift index b86b68c2..0552061c 100644 --- a/TwidereX/Diffable/Misc/Notification/NotificationSection.swift +++ b/TwidereX/Diffable/Misc/Notification/NotificationSection.swift @@ -61,8 +61,8 @@ extension NotificationSection { switch object { case .mastodon(let notification): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserNotificationStyleTableViewCell.self), for: indexPath) as! UserNotificationStyleTableViewCell - let authenticationContext = context.authenticationService.activeAuthenticationContext - let me = authenticationContext?.user(in: context.managedObjectContext) + let authenticationContext = configuration.statusViewConfigurationContext.authContext.authenticationContext + let me = authenticationContext.user(in: context.managedObjectContext) let user: UserObject = .mastodon(object: notification.account) configure( cell: cell, diff --git a/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift b/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift index 2f8b4e62..490e5a72 100644 --- a/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift +++ b/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift @@ -92,6 +92,7 @@ extension TabBarItem { viewController = _viewController case .notification: let _viewController = NotificationViewController() + _viewController.viewModel = NotificationViewModel(context: context, authContext: authContext, coordinator: coordinator) viewController = _viewController case .search: let _viewController = SearchViewController() @@ -99,7 +100,7 @@ extension TabBarItem { viewController = _viewController case .me: let _viewController = ProfileViewController() - let profileViewModel = ProfileViewModel(context: context, authContext: authContext) + let profileViewModel = MeProfileViewModel(context: context, authContext: authContext) _viewController.viewModel = profileViewModel viewController = _viewController case .local: @@ -125,7 +126,7 @@ extension TabBarItem { ) viewController = _viewController case .lists: - guard let me = context.authenticationService.activeAuthenticationContext?.user(in: context.managedObjectContext)?.asRecord else { + guard let me = authContext.authenticationContext.user(in: context.managedObjectContext)?.asRecord else { return AdaptiveStatusBarStyleNavigationController(rootViewController: UIViewController()) } let _viewController = CompositeListViewController() diff --git a/TwidereX/Diffable/Status/StatusSection.swift b/TwidereX/Diffable/Status/StatusSection.swift index ca5b8c92..b59a94be 100644 --- a/TwidereX/Diffable/Status/StatusSection.swift +++ b/TwidereX/Diffable/Status/StatusSection.swift @@ -223,7 +223,7 @@ extension StatusSection { // trigger update if needs // check is the first option in poll to trigger update poll only once if option.index == 0, option.poll.needsUpdate { - let authenticationContext = context.authenticationService.activeAuthenticationContext + let authenticationContext = configurationContext.authContext.authenticationContext switch (option, authenticationContext) { case (.twitter(let object), .twitter(let authenticationContext)): let status: ManagedObjectRecord = .init(objectID: object.poll.status.objectID) diff --git a/TwidereX/Diffable/User/UserSection.swift b/TwidereX/Diffable/User/UserSection.swift index 291d4c66..4ec55385 100644 --- a/TwidereX/Diffable/User/UserSection.swift +++ b/TwidereX/Diffable/User/UserSection.swift @@ -25,6 +25,7 @@ extension UserSection { static func diffableDataSource( tableView: UITableView, context: AppContext, + authContext: AuthContext, configuration: Configuration ) -> UITableViewDiffableDataSource { let cellTypes = [ @@ -69,8 +70,8 @@ extension UserSection { let cell = dequeueReusableCell(tableView: tableView, indexPath: indexPath, style: style) context.managedObjectContext.performAndWait { guard let user = record.object(in: context.managedObjectContext) else { return } - let authenticationContext = context.authenticationService.activeAuthenticationContext - let me = authenticationContext?.user(in: context.managedObjectContext) + let authenticationContext = authContext.authenticationContext + let me = authenticationContext.user(in: context.managedObjectContext) let viewModel = UserTableViewCell.ViewModel( user: user, me: me, diff --git a/TwidereX/Protocol/NeedsDependency+AvatarBarButtonItemDelegate.swift b/TwidereX/Protocol/NeedsDependency+AvatarBarButtonItemDelegate.swift index 0a3b4f14..de4a8a74 100644 --- a/TwidereX/Protocol/NeedsDependency+AvatarBarButtonItemDelegate.swift +++ b/TwidereX/Protocol/NeedsDependency+AvatarBarButtonItemDelegate.swift @@ -9,7 +9,7 @@ import UIKit // MARK: - AvatarBarButtonItemDelegate -extension NeedsDependency where Self: AvatarBarButtonItemDelegate { +extension NeedsDependency where Self: AvatarBarButtonItemDelegate & AuthContextProvider { func avatarBarButtonItem( _ barButtonItem: AvatarBarButtonItem, @@ -19,7 +19,7 @@ extension NeedsDependency where Self: AvatarBarButtonItemDelegate { let feedbackGenerator = UIImpactFeedbackGenerator(style: .light) feedbackGenerator.impactOccurred() - let accountListViewModel = AccountListViewModel(context: context) + let accountListViewModel = AccountListViewModel(context: context, authContext: authContext) coordinator.present( scene: .accountList(viewModel: accountListViewModel), from: nil, diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+History.swift b/TwidereX/Protocol/Provider/DataSourceFacade+History.swift index 15589a17..a19e8c6a 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+History.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+History.swift @@ -14,11 +14,11 @@ import TwidereCore extension DataSourceFacade { static func recordStatusHistory( - denpendency: NeedsDependency, + denpendency: NeedsDependency & AuthContextProvider, status: StatusRecord ) async { let now = Date() - guard let authenticationContext = denpendency.context.authenticationService.activeAuthenticationContext else { return } + let authenticationContext = denpendency.authContext.authenticationContext let acct = authenticationContext.acct let managedObjectContext = denpendency.context.backgroundManagedObjectContext @@ -54,12 +54,12 @@ extension DataSourceFacade { } // end func static func recordUserHistory( - denpendency: NeedsDependency, + denpendency: NeedsDependency & AuthContextProvider, user: UserRecord ) async { let now = Date() - guard let authenticationContext = denpendency.context.authenticationService.activeAuthenticationContext else { return } - + let authenticationContext = denpendency.authContext.authenticationContext + let acct = authenticationContext.acct let managedObjectContext = denpendency.context.backgroundManagedObjectContext let _history: ManagedObjectRecord? = await managedObjectContext.perform { diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+LIst.swift b/TwidereX/Protocol/Provider/DataSourceFacade+LIst.swift index 2e0759d0..ffd268d6 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+LIst.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+LIst.swift @@ -377,12 +377,13 @@ extension DataSourceFacade { @MainActor static func responseToListEditAction( - dependency: NeedsDependency & UIViewController, + dependency: NeedsDependency & AuthContextProvider & UIViewController, list: ListRecord, authenticationContext: AuthenticationContext ) async throws { let editListViewModel = EditListViewModel( context: dependency.context, + authContext: dependency.authContext, platform: { switch list { case .twitter: return .twitter diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift index acd47032..6ebf2598 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift @@ -95,7 +95,7 @@ extension DataSourceFacade { return status?.id } guard let statusID = _statusID, - case let .twitter(authenticationContext) = provider.context.authenticationService.activeAuthenticationContext + case let .twitter(authenticationContext) = provider.authContext.authenticationContext else { return } let _response = try? await provider.context.apiService.twitterStatusV1(statusIDs: [statusID], authenticationContext: authenticationContext) diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Poll.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Poll.swift index 2b2dfde2..3beba2d6 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Poll.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Poll.swift @@ -82,7 +82,7 @@ extension DataSourceFacade { extension DataSourceFacade { public static func responseToStatusPollOption( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, target: StatusTarget, status: StatusRecord, voteButtonDidPressed button: UIButton @@ -102,7 +102,7 @@ extension DataSourceFacade { } static func responseToStatusPollOption( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, status: StatusRecord, voteButtonDidPressed button: UIButton ) async { @@ -123,11 +123,11 @@ extension DataSourceFacade { } private static func responseToStatusPollOption( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, status: ManagedObjectRecord, voteButtonDidPressed button: UIButton ) async throws { - guard case let .mastodon(authenticationContext) = provider.context.authenticationService.activeAuthenticationContext else { return } + guard case let .mastodon(authenticationContext) = provider.authContext.authenticationContext else { return } // should use same context on UI to make transient property trigger update let managedObjectContext = provider.context.managedObjectContext diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift index 48767fd3..192bd17d 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift @@ -51,9 +51,9 @@ extension DataSourceFacade { authenticationService: provider.context.authenticationService, mastodonEmojiService: provider.context.mastodonEmojiService, statusViewConfigureContext: .init( + authContext: provider.authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - authenticationContext: provider.context.authenticationService.$activeAuthenticationContext + twitterTextProvider: OfficialTwitterTextProvider() ) ) ) @@ -108,7 +108,7 @@ extension DataSourceFacade { extension DataSourceFacade { static func responseToExpandContentAction( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, target: StatusTarget, status: StatusRecord ) async throws { @@ -154,7 +154,7 @@ extension DataSourceFacade { extension DataSourceFacade { static func responseToToggleMediaSensitiveAction( - provider: DataSourceProvider, + provider: DataSourceProvider & AuthContextProvider, target: StatusTarget, status: StatusRecord ) async throws { diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift index 4162c8ac..1e0c12a8 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift @@ -75,7 +75,6 @@ extension MediaInfoDescriptionViewDelegate where Self: DataSourceProvider & Auth func mediaInfoDescriptionView(_ mediaInfoDescriptionView: MediaInfoDescriptionView, statusToolbar: StatusToolbar, actionDidPressed action: StatusToolbar.Action, button: UIButton) { - guard let authenticationContext = context.authenticationService.activeAuthenticationContext else { return } Task { let source = DataSourceItem.Source(tableViewCell: nil, indexPath: nil) guard let item = await item(from: source) else { @@ -92,7 +91,7 @@ extension MediaInfoDescriptionViewDelegate where Self: DataSourceProvider & Auth status: status, action: action, sender: button, - authenticationContext: authenticationContext + authenticationContext: authContext.authenticationContext ) } // end Task } // end func diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift index a40d415a..af44c82b 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift @@ -25,7 +25,8 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC assertionFailure() return } - guard let status = await item.status(in: self.context.managedObjectContext) else { assertionFailure("only works for status data provider") + guard let status = await item.status(in: self.context.managedObjectContext) else { + assertionFailure("only works for status data provider") return } await DataSourceFacade.coordinateToProfileScene( @@ -52,7 +53,8 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC assertionFailure() return } - guard let status = await item.status(in: self.context.managedObjectContext) else { assertionFailure("only works for status data provider") + guard let status = await item.status(in: self.context.managedObjectContext) else { + assertionFailure("only works for status data provider") return } await DataSourceFacade.coordinateToProfileScene( @@ -75,7 +77,8 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC assertionFailure() return } - guard let status = await item.status(in: self.context.managedObjectContext) else { assertionFailure("only works for status data provider") + guard let status = await item.status(in: self.context.managedObjectContext) else { + assertionFailure("only works for status data provider") return } await DataSourceFacade.coordinateToProfileScene( @@ -89,7 +92,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC } // MARK: - spoiler -extension StatusViewTableViewCellDelegate where Self: DataSourceProvider { +extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, expandContentButtonDidPressed button: UIButton) { Task { let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) @@ -97,7 +100,8 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider { assertionFailure() return } - guard let status = await item.status(in: self.context.managedObjectContext) else { assertionFailure("only works for status data provider") + guard let status = await item.status(in: self.context.managedObjectContext) else { + assertionFailure("only works for status data provider") return } @@ -119,7 +123,8 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC assertionFailure() return } - guard let status = await item.status(in: self.context.managedObjectContext) else { assertionFailure("only works for status data provider") + guard let status = await item.status(in: self.context.managedObjectContext) else { + assertionFailure("only works for status data provider") return } @@ -140,7 +145,8 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC assertionFailure() return } - guard let status = await item.status(in: self.context.managedObjectContext) else { assertionFailure("only works for status data provider") + guard let status = await item.status(in: self.context.managedObjectContext) else { + assertionFailure("only works for status data provider") return } @@ -171,7 +177,8 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC assertionFailure() return } - guard let status = await item.status(in: self.context.managedObjectContext) else { assertionFailure("only works for status data provider") + guard let status = await item.status(in: self.context.managedObjectContext) else { + assertionFailure("only works for status data provider") return } // end switch @@ -202,7 +209,8 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC assertionFailure() return } - guard let status = await item.status(in: self.context.managedObjectContext) else { assertionFailure("only works for status data provider") + guard let status = await item.status(in: self.context.managedObjectContext) else { + assertionFailure("only works for status data provider") return } await DataSourceFacade.coordinateToMediaPreviewScene( @@ -225,7 +233,8 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC assertionFailure() return } - guard let status = await item.status(in: self.context.managedObjectContext) else { assertionFailure("only works for status data provider") + guard let status = await item.status(in: self.context.managedObjectContext) else { + assertionFailure("only works for status data provider") return } try await DataSourceFacade.responseToToggleMediaSensitiveAction( @@ -237,8 +246,8 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC } } -// poll -extension StatusViewTableViewCellDelegate where Self: DataSourceProvider { +// MARK: - poll +extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, pollTableView tableView: UITableView, didSelectRowAt indexPath: IndexPath) { Task { let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) @@ -246,7 +255,8 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider { assertionFailure() return } - guard let status = await item.status(in: self.context.managedObjectContext) else { assertionFailure("only works for status data provider") + guard let status = await item.status(in: self.context.managedObjectContext) else { + assertionFailure("only works for status data provider") return } await DataSourceFacade.responseToStatusPollOption( @@ -265,7 +275,8 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider { assertionFailure() return } - guard let status = await item.status(in: self.context.managedObjectContext) else { assertionFailure("only works for status data provider") + guard let status = await item.status(in: self.context.managedObjectContext) else { + assertionFailure("only works for status data provider") return } await DataSourceFacade.responseToStatusPollOption( @@ -286,7 +297,8 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC guard let item = await item(from: source) else { return } - guard let status = await item.status(in: self.context.managedObjectContext) else { assertionFailure("only works for status data provider") + guard let status = await item.status(in: self.context.managedObjectContext) else { + assertionFailure("only works for status data provider") return } @@ -309,14 +321,14 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC actionDidPressed action: StatusToolbar.Action, button: UIButton ) { - guard let authenticationContext = context.authenticationService.activeAuthenticationContext else { return } Task { let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) guard let item = await item(from: source) else { assertionFailure() return } - guard let status = await item.status(in: self.context.managedObjectContext) else { assertionFailure("only works for status data provider") + guard let status = await item.status(in: self.context.managedObjectContext) else { + assertionFailure("only works for status data provider") return } @@ -325,7 +337,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC status: status, action: action, sender: button, - authenticationContext: authenticationContext + authenticationContext: self.authContext.authenticationContext ) } // end Task } // end func @@ -343,7 +355,8 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC assertionFailure() return } - guard let status = await item.status(in: self.context.managedObjectContext) else { assertionFailure("only works for status data provider") + guard let status = await item.status(in: self.context.managedObjectContext) else { + assertionFailure("only works for status data provider") return } @@ -381,12 +394,11 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC button: button ) case .remove: - guard let authenticationContext = context.authenticationService.activeAuthenticationContext else { return } try await DataSourceFacade.responseToRemoveStatusAction( provider: self, target: .status, status: status, - authenticationContext: authenticationContext + authenticationContext: self.authContext.authenticationContext ) #if DEBUG case .copyID: @@ -425,7 +437,8 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider { guard let item = await item(from: source) else { return } - guard let status = await item.status(in: self.context.managedObjectContext) else { assertionFailure("only works for status data provider") + guard let status = await item.status(in: self.context.managedObjectContext) else { + assertionFailure("only works for status data provider") return } diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift index 48802925..c7f26789 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift @@ -58,7 +58,7 @@ extension UserViewTableViewCellDelegate where Self: DataSourceProvider { } // MARK: - membership -extension UserViewTableViewCellDelegate where Self: DataSourceProvider { +extension UserViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell( _ cell: UITableViewCell, @@ -70,9 +70,7 @@ extension UserViewTableViewCellDelegate where Self: DataSourceProvider { } Task { @MainActor in - guard let authenticationContext = context.authenticationService.activeAuthenticationContext else { - return - } + let authenticationContext = self.authContext.authenticationContext let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) guard let item = await item(from: source) else { @@ -111,7 +109,7 @@ extension UserViewTableViewCellDelegate where Self: DataSourceProvider { } // MARK: - follow request -extension UserViewTableViewCellDelegate where Self: DataSourceProvider { +extension UserViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell( _ cell: UITableViewCell, @@ -119,9 +117,7 @@ extension UserViewTableViewCellDelegate where Self: DataSourceProvider { acceptFollowReqeustButtonDidPressed button: UIButton ) { Task { - guard let authenticationContext = context.authenticationService.activeAuthenticationContext else { - return - } + let authenticationContext = self.authContext.authenticationContext let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) guard let item = await item(from: source) else { @@ -148,9 +144,7 @@ extension UserViewTableViewCellDelegate where Self: DataSourceProvider { rejectFollowReqeustButtonDidPressed button: UIButton ) { Task { - guard let authenticationContext = context.authenticationService.activeAuthenticationContext else { - return - } + let authenticationContext = self.authContext.authenticationContext let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) guard let item = await item(from: source) else { diff --git a/TwidereX/Scene/Account/List/AccountListViewController.swift b/TwidereX/Scene/Account/List/AccountListViewController.swift index 9c60f84a..076ff230 100644 --- a/TwidereX/Scene/Account/List/AccountListViewController.swift +++ b/TwidereX/Scene/Account/List/AccountListViewController.swift @@ -127,6 +127,11 @@ extension AccountListViewController: UITableViewDelegate { } } +// MARK: - AuthContextProvider +extension AccountListViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + // MARK: - UserViewTableViewCellDelegate extension AccountListViewController: UserViewTableViewCellDelegate { } diff --git a/TwidereX/Scene/Account/List/AccountListViewModel+Diffable.swift b/TwidereX/Scene/Account/List/AccountListViewModel+Diffable.swift index b3b30e6d..77029388 100644 --- a/TwidereX/Scene/Account/List/AccountListViewModel+Diffable.swift +++ b/TwidereX/Scene/Account/List/AccountListViewModel+Diffable.swift @@ -20,11 +20,12 @@ extension AccountListViewModel { diffableDataSource = UserSection.diffableDataSource( tableView: tableView, context: context, + authContext: authContext, configuration: UserSection.Configuration( userViewTableViewCellDelegate: userViewTableViewCellDelegate, userViewConfigurationContext: .init( - listMembershipViewModel: nil, - authenticationContext: context.authenticationService.activeAuthenticationContext + authContext: authContext, + listMembershipViewModel: nil ) ) ) @@ -33,18 +34,14 @@ extension AccountListViewModel { snapshot.appendSections([.main]) diffableDataSource?.apply(snapshot) - context.authenticationService.$authenticationIndexes + $items .receive(on: DispatchQueue.main) - .sink { [weak self] authenticationIndexes in + .sink { [weak self] items in guard let self = self else { return } guard let diffableDataSource = self.diffableDataSource else { return } - + var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) - let items = authenticationIndexes.map { authenticationIndex -> UserItem in - let record = ManagedObjectRecord(objectID: authenticationIndex.objectID) - return UserItem.authenticationIndex(record: record) - } snapshot.appendItems(items, toSection: .main) diffableDataSource.apply(snapshot) } diff --git a/TwidereX/Scene/Account/List/AccountListViewModel.swift b/TwidereX/Scene/Account/List/AccountListViewModel.swift index 6dacbccb..f18b6352 100644 --- a/TwidereX/Scene/Account/List/AccountListViewModel.swift +++ b/TwidereX/Scene/Account/List/AccountListViewModel.swift @@ -9,6 +9,7 @@ import os.log import UIKit import Combine +import CoreData import CoreDataStack final class AccountListViewModel: NSObject { @@ -17,14 +18,35 @@ final class AccountListViewModel: NSObject { // input let context: AppContext - + let authContext: AuthContext + let authenticationIndexFetchedResultsController: NSFetchedResultsController + // output var diffableDataSource: UITableViewDiffableDataSource! - var items = CurrentValueSubject<[UserItem], Never>([]) + @Published var items: [UserItem] = [] - init(context: AppContext) { + init( + context: AppContext, + authContext: AuthContext + ) { self.context = context + self.authContext = authContext + self.authenticationIndexFetchedResultsController = { + let fetchRequest = AuthenticationIndex.sortedFetchRequest + fetchRequest.returnsObjectsAsFaults = false + fetchRequest.fetchBatchSize = 20 + let controller = NSFetchedResultsController( + fetchRequest: fetchRequest, + managedObjectContext: context.managedObjectContext, + sectionNameKeyPath: nil, + cacheName: nil + ) + return controller + }() super.init() + + authenticationIndexFetchedResultsController.delegate = self + try? authenticationIndexFetchedResultsController.performFetch() } deinit { @@ -32,3 +54,24 @@ final class AccountListViewModel: NSObject { } } + +// MARK: - NSFetchedResultsControllerDelegate +extension AccountListViewModel: NSFetchedResultsControllerDelegate { + + public func controllerWillChangeContent(_ controller: NSFetchedResultsController) { + os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + + public func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + switch controller { + case authenticationIndexFetchedResultsController: + let authenticationIndexes = authenticationIndexFetchedResultsController.fetchedObjects ?? [] + items = authenticationIndexes.map { authenticationIndex in + UserItem.authenticationIndex(record: authenticationIndex.asRecrod) + } + default: + assertionFailure() + } + } + +} diff --git a/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift b/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift index d5b8a627..95d3b22b 100644 --- a/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift +++ b/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift @@ -22,14 +22,14 @@ extension StatusHistoryViewModel { configuration: .init( statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, statusViewConfigurationContext: .init( + authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - authenticationContext: context.authenticationService.$activeAuthenticationContext + twitterTextProvider: OfficialTwitterTextProvider() ), userViewTableViewCellDelegate: nil, userViewConfigurationContext: .init( - listMembershipViewModel: nil, - authenticationContext: context.authenticationService.activeAuthenticationContext + authContext: authContext, + listMembershipViewModel: nil ) ) ) diff --git a/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift b/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift index 655d0e22..5ae491cd 100644 --- a/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift +++ b/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift @@ -22,14 +22,14 @@ extension UserHistoryViewModel { configuration: .init( statusViewTableViewCellDelegate: nil, statusViewConfigurationContext: .init( + authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - authenticationContext: context.authenticationService.$activeAuthenticationContext + twitterTextProvider: OfficialTwitterTextProvider() ), userViewTableViewCellDelegate: userViewTableViewCellDelegate, userViewConfigurationContext: .init( - listMembershipViewModel: nil, - authenticationContext: context.authenticationService.activeAuthenticationContext + authContext: authContext, + listMembershipViewModel: nil ) ) ) diff --git a/TwidereX/Scene/List/CompositeList/CompositeListViewController.swift b/TwidereX/Scene/List/CompositeList/CompositeListViewController.swift index 3d0ef212..ea99fdbd 100644 --- a/TwidereX/Scene/List/CompositeList/CompositeListViewController.swift +++ b/TwidereX/Scene/List/CompositeList/CompositeListViewController.swift @@ -99,6 +99,7 @@ extension CompositeListViewController { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") let editListViewModel = EditListViewModel( context: context, + authContext: viewModel.authContext, platform: { switch viewModel.kind.user { case .twitter: return .twitter diff --git a/TwidereX/Scene/List/EditList/EditListView.swift b/TwidereX/Scene/List/EditList/EditListView.swift index 6683f89d..42941b52 100644 --- a/TwidereX/Scene/List/EditList/EditListView.swift +++ b/TwidereX/Scene/List/EditList/EditListView.swift @@ -40,12 +40,14 @@ struct EditListView: View { struct CreateListView_Previews: PreviewProvider { static var previews: some View { Group { - EditListView(viewModel: EditListViewModel(context: .shared, platform: .twitter, kind: .create)) - EditListView(viewModel: EditListViewModel(context: .shared, platform: .twitter, kind: .create)) - .preferredColorScheme(.dark) - EditListView(viewModel: EditListViewModel(context: .shared, platform: .mastodon, kind: .create)) - EditListView(viewModel: EditListViewModel(context: .shared, platform: .mastodon, kind: .create)) - .preferredColorScheme(.dark) + if let authContext = AuthContext.mock(context: .shared) { + EditListView(viewModel: EditListViewModel(context: .shared, authContext: authContext, platform: .twitter, kind: .create)) + EditListView(viewModel: EditListViewModel(context: .shared, authContext: authContext, platform: .twitter, kind: .create)) + .preferredColorScheme(.dark) + EditListView(viewModel: EditListViewModel(context: .shared, authContext: authContext, platform: .mastodon, kind: .create)) + EditListView(viewModel: EditListViewModel(context: .shared, authContext: authContext, platform: .mastodon, kind: .create)) + .preferredColorScheme(.dark) + } } } } diff --git a/TwidereX/Scene/List/EditList/EditListViewModel.swift b/TwidereX/Scene/List/EditList/EditListViewModel.swift index d0dee900..d122c18c 100644 --- a/TwidereX/Scene/List/EditList/EditListViewModel.swift +++ b/TwidereX/Scene/List/EditList/EditListViewModel.swift @@ -21,6 +21,7 @@ final class EditListViewModel: ObservableObject { // input let context: AppContext + let authContext: AuthContext let platform: Platform let kind: Kind @@ -34,10 +35,12 @@ final class EditListViewModel: ObservableObject { init( context: AppContext, + authContext: AuthContext, platform: Platform, kind: Kind ) { self.context = context + self.authContext = authContext self.platform = platform self.kind = kind // end init @@ -92,9 +95,7 @@ extension EditListViewModel { )) } }() - guard let query = _query, - let authenticationContext = context.authenticationService.activeAuthenticationContext - else { + guard let query = _query else { throw AppError.implicit(.badRequest) } @@ -107,7 +108,7 @@ extension EditListViewModel { do { let response = try await context.apiService.create( query: query, - authenticationContext: authenticationContext + authenticationContext: authContext.authenticationContext ) logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): create list success") @@ -184,9 +185,7 @@ extension EditListViewModel { )) } }() - guard let query = _query, - let authenticationContext = context.authenticationService.activeAuthenticationContext - else { + guard let query = _query else { throw AppError.implicit(.badRequest) } @@ -200,7 +199,7 @@ extension EditListViewModel { let response = try await context.apiService.update( list: list, query: query, - authenticationContext: authenticationContext + authenticationContext: authContext.authenticationContext ) logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): update list success") diff --git a/TwidereX/Scene/List/List/ListViewModel+State.swift b/TwidereX/Scene/List/List/ListViewModel+State.swift index 1f9cc796..1c90a67f 100644 --- a/TwidereX/Scene/List/List/ListViewModel+State.swift +++ b/TwidereX/Scene/List/List/ListViewModel+State.swift @@ -105,12 +105,11 @@ extension ListViewModel.State { break } - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext, - let user = viewModel.kind.user - else { + guard let user = viewModel.kind.user else { stateMachine.enter(Fail.self) return } + let authenticationContext = viewModel.authContext.authenticationContext if nextInput == nil { nextInput = { diff --git a/TwidereX/Scene/List/ListUser/ListUserViewController.swift b/TwidereX/Scene/List/ListUser/ListUserViewController.swift index 1261bbf6..9a8fe5e2 100644 --- a/TwidereX/Scene/List/ListUser/ListUserViewController.swift +++ b/TwidereX/Scene/List/ListUser/ListUserViewController.swift @@ -49,29 +49,23 @@ extension ListUserViewController { title = viewModel.kind.title view.backgroundColor = .systemBackground - context.authenticationService.$activeAuthenticationContext - .asyncMap { [weak self] authenticationContext -> UIBarButtonItem? in - guard let self = self else { return nil } - guard let authenticationContext = authenticationContext else { return nil } - // only setup bar button for `members` kind list - switch self.viewModel.kind { - case .members: break - case .subscribers: return nil - } + + let rightBarButtonItem: UIBarButtonItem? = { + // only setup bar button for `members` kind list + switch self.viewModel.kind { + case .members: // only setup bar button for myList - let managedObjectContext = self.context.managedObjectContext - let isMyList: Bool = await managedObjectContext.perform { + let isMyList: Bool = { + let managedObjectContext = self.context.managedObjectContext guard let list = self.viewModel.kind.list.object(in: managedObjectContext) else { return false } - return list.owner.userIdentifer == authenticationContext.userIdentifier - } + return list.owner.userIdentifer == viewModel.authContext.authenticationContext.userIdentifier + }() return isMyList ? self.addBarButtonItem : nil + case .subscribers: + return nil } - .receive(on: DispatchQueue.main) - .sink { [weak self] barButtonItem in - guard let self = self else { return } - self.navigationItem.rightBarButtonItem = barButtonItem - } - .store(in: &disposeBag) + }() + self.navigationItem.rightBarButtonItem = rightBarButtonItem tableView.translatesAutoresizingMaskIntoConstraints = false tableView.frame = view.bounds @@ -160,10 +154,7 @@ extension ListUserViewController: UserViewTableViewCellDelegate { return } - guard let authenticationContext = self.context.authenticationService.activeAuthenticationContext else { - assertionFailure() - return - } + let authenticationContext = self.viewModel.authContext.authenticationContext do { let list = self.viewModel.kind.list diff --git a/TwidereX/Scene/List/ListUser/ListUserViewModel+Diffable.swift b/TwidereX/Scene/List/ListUser/ListUserViewModel+Diffable.swift index aeb36a5c..1c4be627 100644 --- a/TwidereX/Scene/List/ListUser/ListUserViewModel+Diffable.swift +++ b/TwidereX/Scene/List/ListUser/ListUserViewModel+Diffable.swift @@ -19,11 +19,12 @@ extension ListUserViewModel { diffableDataSource = UserSection.diffableDataSource( tableView: tableView, context: context, + authContext: authContext, configuration: UserSection.Configuration( userViewTableViewCellDelegate: userViewTableViewCellDelegate, userViewConfigurationContext: .init( - listMembershipViewModel: listMembershipViewModel, - authenticationContext: context.authenticationService.activeAuthenticationContext + authContext: authContext, + listMembershipViewModel: listMembershipViewModel ) ) ) diff --git a/TwidereX/Scene/List/ListUser/ListUserViewModel+State.swift b/TwidereX/Scene/List/ListUser/ListUserViewModel+State.swift index 24e74e64..5816c2fb 100644 --- a/TwidereX/Scene/List/ListUser/ListUserViewModel+State.swift +++ b/TwidereX/Scene/List/ListUser/ListUserViewModel+State.swift @@ -105,11 +105,7 @@ extension ListUserViewModel.State { break } - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext else { - stateMachine.enter(Fail.self) - return - } - + let authenticationContext = viewModel.authContext.authenticationContext let list = viewModel.kind.list if nextInput == nil { diff --git a/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift b/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift index 2f0fcd3f..0b79cb65 100644 --- a/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift +++ b/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift @@ -139,9 +139,9 @@ extension MediaPreviewViewController { mediaInfoDescriptionView.configure( statusObject: status, configurationContext: .init( + authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - authenticationContext: context.authenticationService.$activeAuthenticationContext + twitterTextProvider: OfficialTwitterTextProvider() ) ) } else { diff --git a/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView+ViewModel.swift b/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView+ViewModel.swift index 3559651b..883c8809 100644 --- a/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView+ViewModel.swift +++ b/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView+ViewModel.swift @@ -192,7 +192,6 @@ extension MediaInfoDescriptionView { viewModel.platform = .twitter viewModel.dateTimeProvider = configurationContext.dateTimeProvider viewModel.twitterTextProvider = configurationContext.twitterTextProvider - configurationContext.authenticationContext.assign(to: \.authenticationContext, on: viewModel).store(in: &disposeBag) configureAuthor(twitterStatus: status) configureContent(twitterStatus: status) @@ -278,7 +277,6 @@ extension MediaInfoDescriptionView { viewModel.platform = .mastodon viewModel.dateTimeProvider = configurationContext.dateTimeProvider viewModel.twitterTextProvider = configurationContext.twitterTextProvider - configurationContext.authenticationContext.assign(to: \.authenticationContext, on: viewModel).store(in: &disposeBag) // configureHeader(mastodonStatus: status, mastodonNotification: notification) configureAuthor(mastodonStatus: status) diff --git a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift index 50de0d1f..1bd791fc 100644 --- a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift @@ -25,13 +25,13 @@ extension NotificationTimelineViewModel { statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, userViewTableViewCellDelegate: userViewTableViewCellDelegate, statusViewConfigurationContext: .init( + authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - authenticationContext: context.authenticationService.$activeAuthenticationContext + twitterTextProvider: OfficialTwitterTextProvider() ), userViewConfigurationContext: .init( - listMembershipViewModel: nil, - authenticationContext: context.authenticationService.activeAuthenticationContext + authContext: authContext, + listMembershipViewModel: nil ) ) diffableDataSource = NotificationSection.diffableDataSource( @@ -166,7 +166,8 @@ extension NotificationTimelineViewModel { // load timeline gap func loadMore(item: NotificationItem) async { guard case let .feedLoader(record) = item else { return } - guard let authenticationContext = context.authenticationService.activeAuthenticationContext else { return } + + let authenticationContext = authContext.authenticationContext let managedObjectContext = context.managedObjectContext let key = "LoadMore@\(record.objectID)" diff --git a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift index ec41c79d..a1e5a476 100644 --- a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift +++ b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift @@ -54,11 +54,8 @@ extension NotificationTimelineViewModel.LoadOldestState { super.didEnter(from: previousState) guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext - else { - stateMachine.enter(Fail.self) - return - } + + let authenticationContext = viewModel.authContext.authenticationContext guard let lsatFeedRecord = viewModel.fetchedResultsController.records.last else { stateMachine.enter(Fail.self) diff --git a/TwidereX/Scene/Notification/NotificationViewController.swift b/TwidereX/Scene/Notification/NotificationViewController.swift index 47f80844..01a21f0e 100644 --- a/TwidereX/Scene/Notification/NotificationViewController.swift +++ b/TwidereX/Scene/Notification/NotificationViewController.swift @@ -72,17 +72,14 @@ extension NotificationViewController { avatarBarButtonItem.avatarButton.addTarget(self, action: #selector(NotificationViewController.avatarButtonPressed(_:)), for: .touchUpInside) avatarBarButtonItem.delegate = self - Publishers.CombineLatest( - context.authenticationService.$activeAuthenticationContext, - viewModel.viewDidAppear - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] authenticationContext, _ in - guard let self = self else { return } - let user = authenticationContext?.user(in: self.context.managedObjectContext) - self.avatarBarButtonItem.configure(user: user) - } - .store(in: &disposeBag) + viewModel.viewDidAppear + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + let user = self.viewModel.authContext.authenticationContext.user(in: self.context.managedObjectContext) + self.avatarBarButtonItem.configure(user: user) + } + .store(in: &disposeBag) dataSource = viewModel viewModel.$viewControllers diff --git a/TwidereX/Scene/Notification/NotificationViewModel.swift b/TwidereX/Scene/Notification/NotificationViewModel.swift index ff5cd0cc..477cf598 100644 --- a/TwidereX/Scene/Notification/NotificationViewModel.swift +++ b/TwidereX/Scene/Notification/NotificationViewModel.swift @@ -39,13 +39,7 @@ final class NotificationViewModel { self._coordinator = coordinator // end init - context.authenticationService.$activeAuthenticationContext - .receive(on: DispatchQueue.main) - .sink { [weak self] authenticationContext in - guard let self = self else { return } - self.setup(for: authenticationContext) - } - .store(in: &disposeBag) + setup(for: authContext.authenticationContext) } } diff --git a/TwidereX/Scene/Profile/FriendshipList/FollowingListViewModel+Diffable.swift b/TwidereX/Scene/Profile/FriendshipList/FollowingListViewModel+Diffable.swift index c9a880b5..b7f80056 100644 --- a/TwidereX/Scene/Profile/FriendshipList/FollowingListViewModel+Diffable.swift +++ b/TwidereX/Scene/Profile/FriendshipList/FollowingListViewModel+Diffable.swift @@ -20,14 +20,15 @@ extension FriendshipListViewModel { let configuration = UserSection.Configuration( userViewTableViewCellDelegate: nil, userViewConfigurationContext: .init( - listMembershipViewModel: nil, - authenticationContext: context.authenticationService.activeAuthenticationContext + authContext: authContext, + listMembershipViewModel: nil ) ) diffableDataSource = UserSection.diffableDataSource( tableView: tableView, context: context, + authContext: authContext, configuration: configuration ) diff --git a/TwidereX/Scene/Profile/FriendshipList/FollowingListViewModel+State.swift b/TwidereX/Scene/Profile/FriendshipList/FollowingListViewModel+State.swift index 4d00081c..f781b657 100644 --- a/TwidereX/Scene/Profile/FriendshipList/FollowingListViewModel+State.swift +++ b/TwidereX/Scene/Profile/FriendshipList/FollowingListViewModel+State.swift @@ -55,12 +55,8 @@ extension FriendshipListViewModel.State { super.didEnter(from: previousState) guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext - else { - stateMachine.enter(Fail.self) - return - } - + + let authenticationContext = viewModel.authContext.authenticationContext if nextInput == nil { nextInput = { switch (viewModel.userIdentifier, authenticationContext) { diff --git a/TwidereX/Scene/Profile/MeProfileViewModel.swift b/TwidereX/Scene/Profile/MeProfileViewModel.swift new file mode 100644 index 00000000..5e2ceb9f --- /dev/null +++ b/TwidereX/Scene/Profile/MeProfileViewModel.swift @@ -0,0 +1,27 @@ +// +// MeProfileViewModel.swift +// TwidereX +// +// Created by Cirno MainasuK on 2020-9-28. +// + +import os.log +import UIKit +import Combine +import CoreData +import CoreDataStack +import TwitterSDK + +final class MeProfileViewModel: ProfileViewModel { + + override init( + context: AppContext, + authContext: AuthContext + ) { + super.init(context: context,authContext: authContext) + // end init + + self.user = authContext.authenticationContext.user(in: context.managedObjectContext) + } + +} diff --git a/TwidereX/Scene/Profile/ProfileViewController.swift b/TwidereX/Scene/Profile/ProfileViewController.swift index 9eb27631..bc865ba2 100644 --- a/TwidereX/Scene/Profile/ProfileViewController.swift +++ b/TwidereX/Scene/Profile/ProfileViewController.swift @@ -149,17 +149,15 @@ extension ProfileViewController { avatarBarButtonItem.avatarButton.addTarget(self, action: #selector(ProfileViewController.avatarButtonPressed(_:)), for: .touchUpInside) avatarBarButtonItem.delegate = self - Publishers.CombineLatest( - context.authenticationService.$activeAuthenticationContext, - viewModel.viewDidAppear - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] authenticationContext, _ in - guard let self = self else { return } - let user = authenticationContext?.user(in: self.context.managedObjectContext) - self.avatarBarButtonItem.configure(user: user) - } - .store(in: &disposeBag) + viewModel.viewDidAppear + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + let user = self.viewModel.authContext.authenticationContext.user(in: self.context.managedObjectContext) + self.avatarBarButtonItem.configure(user: user) + } + .store(in: &disposeBag) + } addChild(tabBarPagerController) @@ -204,21 +202,19 @@ extension ProfileViewController { } .store(in: &disposeBag) - Publishers.CombineLatest3( + Publishers.CombineLatest( viewModel.relationshipViewModel.$optionSet, // update trigger - viewModel.$userRecord, - context.authenticationService.$activeAuthenticationContext + viewModel.$userRecord ) .receive(on: DispatchQueue.main) - .sink { [weak self] optionSet, userRecord, authenticationContext in + .sink { [weak self] optionSet, userRecord in guard let self = self else { return } - guard let userRecord = userRecord, - let authenticationContext = authenticationContext - else { + guard let userRecord = userRecord else { self.moreMenuBarButtonItem.menu = nil self.navigationItem.rightBarButtonItems = [] return } + let authenticationContext = self.viewModel.authContext.authenticationContext Task { do { let menu = try await DataSourceFacade.createMenuForUser( @@ -318,9 +314,9 @@ extension ProfileViewController { authenticationService: context.authenticationService, mastodonEmojiService: context.mastodonEmojiService, statusViewConfigureContext: .init( + authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - authenticationContext: context.authenticationService.$activeAuthenticationContext + twitterTextProvider: OfficialTwitterTextProvider() ) ) ) @@ -337,9 +333,9 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { func headerViewController(_ viewController: ProfileHeaderViewController, profileHeaderView: ProfileHeaderView, friendshipButtonDidPressed button: UIButton) { guard let user = viewModel.user else { return } - guard let authenticationContext = context.authenticationService.activeAuthenticationContext else { return } guard let relationshipOptionSet = viewModel.relationshipViewModel.optionSet else { return } let record = UserRecord(object: user) + let authenticationContext = viewModel.authContext.authenticationContext Task { if relationshipOptionSet.contains(.blocking) { diff --git a/TwidereX/Scene/Profile/ProfileViewModel.swift b/TwidereX/Scene/Profile/ProfileViewModel.swift index adf4c95c..ba300f30 100644 --- a/TwidereX/Scene/Profile/ProfileViewModel.swift +++ b/TwidereX/Scene/Profile/ProfileViewModel.swift @@ -66,40 +66,28 @@ class ProfileViewModel: ObservableObject { .assign(to: &$userIdentifier) // bind active authentication - context.authenticationService.$activeAuthenticationContext - .sink { [weak self] authenticationContext in - guard let self = self else { return } - Task { - let managedObjectContext = self.context.managedObjectContext - self.me = await managedObjectContext.perform { - switch authenticationContext { - case .twitter(let authenticationContext): - let authentication = authenticationContext.authenticationRecord.object(in: managedObjectContext) - return authentication.flatMap { .twitter(object: $0.user) } - case .mastodon(let authenticationContext): - let authentication = authenticationContext.authenticationRecord.object(in: managedObjectContext) - return authentication.flatMap { .mastodon(object: $0.user) } - case nil: - return nil - } - } + Task { + let managedObjectContext = self.context.managedObjectContext + self.me = await managedObjectContext.perform { + switch authContext.authenticationContext { + case .twitter(let authenticationContext): + let authentication = authenticationContext.authenticationRecord.object(in: managedObjectContext) + return authentication.flatMap { .twitter(object: $0.user) } + case .mastodon(let authenticationContext): + let authentication = authenticationContext.authenticationRecord.object(in: managedObjectContext) + return authentication.flatMap { .mastodon(object: $0.user) } } } - .store(in: &disposeBag) + } // end Task // observe friendship - Publishers.CombineLatest( - $userRecord, - context.authenticationService.$activeAuthenticationContext - ) - .sink { [weak self] userRecord, authenticationContext in - guard let self = self else { return } - guard let userRecord = userRecord, - let authenticationContext = authenticationContext - else { return } - self.dispatchUpdateRelationshipTask(user: userRecord, authenticationContext: authenticationContext) - } - .store(in: &disposeBag) + $userRecord + .sink { [weak self] userRecord in + guard let self = self else { return } + guard let userRecord = userRecord else { return } + self.dispatchUpdateRelationshipTask(user: userRecord, authenticationContext: self.authContext.authenticationContext) + } + .store(in: &disposeBag) } deinit { diff --git a/TwidereX/Scene/Profile/RemoteProfileViewModel.swift b/TwidereX/Scene/Profile/RemoteProfileViewModel.swift index ce77b4ce..02e46fc2 100644 --- a/TwidereX/Scene/Profile/RemoteProfileViewModel.swift +++ b/TwidereX/Scene/Profile/RemoteProfileViewModel.swift @@ -30,7 +30,7 @@ final class RemoteProfileViewModel: ProfileViewModel { setup(user: record) case .twitter(let twitterContext): Task { - guard case let .twitter(authenticationContext) = context.authenticationService.activeAuthenticationContext else { return } + guard case let .twitter(authenticationContext) = self.authContext.authenticationContext else { return } do { let _record = try await fetchTwitterUser( twitterContext: twitterContext, @@ -44,7 +44,7 @@ final class RemoteProfileViewModel: ProfileViewModel { } // end Task case .mastodon(let mastodonContext): Task { - guard case let .mastodon(authenticationContext) = context.authenticationService.activeAuthenticationContext else { return } + guard case let .mastodon(authenticationContext) = self.authContext.authenticationContext else { return } do { let _record = try await fetchMastodonUser( mastodonContext: mastodonContext, diff --git a/TwidereX/Scene/Root/ContentSplitViewController.swift b/TwidereX/Scene/Root/ContentSplitViewController.swift index d5ec060a..8f37139d 100644 --- a/TwidereX/Scene/Root/ContentSplitViewController.swift +++ b/TwidereX/Scene/Root/ContentSplitViewController.swift @@ -245,10 +245,9 @@ extension ContentSplitViewController: SidebarViewModelDelegate { switch tab { case .settings: - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext else { return } let settingListViewModel = SettingListViewModel( context: context, - authContext: .init(authenticationContext: authenticationContext) + authContext: viewModel.authContext ) coordinator.present( scene: .setting(viewModel: settingListViewModel), diff --git a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift index d11e47b8..eaa1a294 100644 --- a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift +++ b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift @@ -102,15 +102,9 @@ extension DrawerSidebarViewController { settingCollectionView: settingCollectionView ) - context.authenticationService.$activeAuthenticationContext - .sink { [weak self] authenticationContext in - guard let self = self else { return } - let user = authenticationContext?.user(in: self.context.managedObjectContext) - self.headerView.configure(user: user) - } - .store(in: &disposeBag) - headerView.delegate = self + let user = viewModel.authContext.authenticationContext.user(in: self.context.managedObjectContext) + headerView.configure(user: user) } } @@ -123,7 +117,7 @@ extension DrawerSidebarViewController: DrawerSidebarHeaderViewDelegate { _ headerView: DrawerSidebarHeaderView, avatarButtonDidPressed button: UIButton ) { - let profileViewModel = ProfileViewModel( + let profileViewModel = MeProfileViewModel( context: context, authContext: viewModel.authContext // me ) @@ -138,7 +132,7 @@ extension DrawerSidebarViewController: DrawerSidebarHeaderViewDelegate { menuButtonDidPressed button: UIButton ) { dismiss(animated: true) { - let accountListViewModel = AccountListViewModel(context: self.context) + let accountListViewModel = AccountListViewModel(context: self.context, authContext: self.viewModel.authContext) self.coordinator.present(scene: .accountList(viewModel: accountListViewModel), from: nil, transition: .modal(animated: true, completion: nil)) } } @@ -181,7 +175,7 @@ extension DrawerSidebarViewController: DrawerSidebarHeaderViewDelegate { profileDashboardView: ProfileDashboardView, listedMeterViewDidPressed meterView: ProfileDashboardMeterView ) { - guard let me = context.authenticationService.activeAuthenticationContext?.user(in: context.managedObjectContext)?.asRecord else { return } + guard let me = viewModel.authContext.authenticationContext.user(in: context.managedObjectContext)?.asRecord else { return } switch me { case .twitter: break case .mastodon: return @@ -220,12 +214,10 @@ extension DrawerSidebarViewController: UICollectionViewDelegate { let userLikeTimelineViewModel = UserLikeTimelineViewModel(context: context, authContext: viewModel.authContext, timelineContext: .init(timelineKind: .like, userIdentifier: viewModel.authContext.authenticationContext.userIdentifier)) coordinator.present(scene: .userLikeTimeline(viewModel: userLikeTimelineViewModel), from: presentingViewController, transition: .show) case .history: - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext else { return } - let authContext = AuthContext(authenticationContext: authenticationContext) - let historyViewModel = HistoryViewModel(context: context, coordinator: coordinator, authContext: authContext) + let historyViewModel = HistoryViewModel(context: context, coordinator: coordinator, authContext: viewModel.authContext) coordinator.present(scene: .history(viewModel: historyViewModel), from: presentingViewController, transition: .show) case .lists: - guard let me = context.authenticationService.activeAuthenticationContext?.user(in: context.managedObjectContext)?.asRecord else { return } + guard let me = viewModel.authContext.authenticationContext.user(in: context.managedObjectContext)?.asRecord else { return } let compositeListViewModel = CompositeListViewModel( context: context, diff --git a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewModel+Diffable.swift b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewModel+Diffable.swift index ef901786..2735ec1a 100644 --- a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewModel+Diffable.swift +++ b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewModel+Diffable.swift @@ -21,34 +21,32 @@ extension DrawerSidebarViewModel { sidebarSnapshot.appendSections([.main]) sidebarDiffableDataSource?.applySnapshotUsingReloadData(sidebarSnapshot) - Publishers.CombineLatest( - context.authenticationService.$activeAuthenticationContext.removeDuplicates(), - UserDefaults.shared.publisher(for: \.preferredEnableHistory).removeDuplicates() - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] authenticationContext, preferredEnableHistory in - guard let self = self else { return } - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.main]) - switch authenticationContext { - case .twitter: - snapshot.appendItems([.likes], toSection: .main) - if preferredEnableHistory { - snapshot.appendItems([.history], toSection: .main) - } - snapshot.appendItems([.lists], toSection: .main) - case .mastodon: - snapshot.appendItems([.local, .federated, .likes], toSection: .main) - if preferredEnableHistory { - snapshot.appendItems([.history], toSection: .main) + UserDefaults.shared.publisher(for: \.preferredEnableHistory).removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak self] preferredEnableHistory in + guard let self = self else { return } + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + + let authenticationContext = self.authContext.authenticationContext + switch authenticationContext { + case .twitter: + snapshot.appendItems([.likes], toSection: .main) + if preferredEnableHistory { + snapshot.appendItems([.history], toSection: .main) + } + snapshot.appendItems([.lists], toSection: .main) + case .mastodon: + snapshot.appendItems([.local, .federated, .likes], toSection: .main) + if preferredEnableHistory { + snapshot.appendItems([.history], toSection: .main) + } + snapshot.appendItems([.lists], toSection: .main) } - snapshot.appendItems([.lists], toSection: .main) - case .none: - break + self.sidebarDiffableDataSource?.applySnapshotUsingReloadData(snapshot) } - self.sidebarDiffableDataSource?.applySnapshotUsingReloadData(snapshot) - } - .store(in: &disposeBag) + .store(in: &disposeBag) // setting settingDiffableDataSource = setupDiffableDataSource(collectionView: settingCollectionView) diff --git a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift index 29fce6d2..484b915b 100644 --- a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift +++ b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift @@ -241,17 +241,16 @@ extension MainTabBarController { @MainActor private func setupNotificationTabIconUpdater() async { // notification tab bar icon updater - await Publishers.CombineLatest3( - context.authenticationService.$activeAuthenticationContext, + await Publishers.CombineLatest( context.notificationService.unreadNotificationCountDidUpdate, // <-- actor property $currentTab ) .receive(on: DispatchQueue.main) - .sink { [weak self] authenticationContext, _, currentTab in + .sink { [weak self] _, currentTab in guard let self = self else { return } - guard let authenticationContext = authenticationContext else { return } guard let notificationViewController = self.notificationViewController else { return } + let authenticationContext = self.authContext.authenticationContext let hasUnreadPushNotification: Bool = { switch authenticationContext { case .twitter: @@ -397,7 +396,7 @@ extension MainTabBarController { case .me: let feedbackGenerator = UIImpactFeedbackGenerator(style: .light) feedbackGenerator.impactOccurred() - let accountListViewModel = AccountListViewModel(context: context) + let accountListViewModel = AccountListViewModel(context: context, authContext: authContext) coordinator.present(scene: .accountList(viewModel: accountListViewModel), from: self, transition: .modal(animated: true, completion: nil)) default: break diff --git a/TwidereX/Scene/Root/Sidebar/SidebarViewController.swift b/TwidereX/Scene/Root/Sidebar/SidebarViewController.swift index 642aaa35..f7e7cc4a 100644 --- a/TwidereX/Scene/Root/Sidebar/SidebarViewController.swift +++ b/TwidereX/Scene/Root/Sidebar/SidebarViewController.swift @@ -81,7 +81,7 @@ extension SidebarViewController { let impactFeedbackGenerator = UIImpactFeedbackGenerator() impactFeedbackGenerator.impactOccurred() - let accountListViewModel = AccountListViewModel(context: context) + let accountListViewModel = AccountListViewModel(context: context, authContext: viewModel.authContext) coordinator.present( scene: .accountList(viewModel: accountListViewModel), from: nil, diff --git a/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift b/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift index badfd6a3..e00cd753 100644 --- a/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift @@ -43,60 +43,51 @@ final class SidebarViewModel: ObservableObject { ) { self.context = context self.authContext = authContext + // end init - Publishers.CombineLatest( - context.authenticationService.$activeAuthenticationContext.removeDuplicates(), - UserDefaults.shared.publisher(for: \.preferredEnableHistory).removeDuplicates() - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] authenticationContext, preferredEnableHistory in - guard let self = self else { return } - - var items: [TabBarItem] = [] - switch authenticationContext { - case .twitter: - items.append(contentsOf: [.likes]) - if preferredEnableHistory { - items.append(contentsOf: [.history]) - } - items.append(contentsOf: [.lists]) - case .mastodon: - items.append(contentsOf: [.local, .federated, .likes]) - if preferredEnableHistory { - items.append(contentsOf: [.history]) - } - items.append(contentsOf: [.lists]) - case .none: - break - } - self.secondaryTabBarItems = items - } - .store(in: &disposeBag) - - context.authenticationService.$activeAuthenticationContext - .sink { [weak self] authenticationContext in + UserDefaults.shared.publisher(for: \.preferredEnableHistory) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak self] preferredEnableHistory in guard let self = self else { return } - let user = authenticationContext?.user(in: context.managedObjectContext) - switch user { - case .twitter(let object): - self.avatarURLSubscription = object.publisher(for: \.profileImageURL) - .sink { [weak self] _ in - guard let self = self else { return } - self.avatarURL = object.avatarImageURL() - } - case .mastodon(let object): - self.avatarURLSubscription = object.publisher(for: \.avatar) - .sink { [weak self] _ in - guard let self = self else { return } - self.avatarURL = object.avatar.flatMap { URL(string: $0) } - } - case .none: - self.avatarURL = nil + var items: [TabBarItem] = [] + switch self.authContext.authenticationContext { + case .twitter: + items.append(contentsOf: [.likes]) + if preferredEnableHistory { + items.append(contentsOf: [.history]) + } + items.append(contentsOf: [.lists]) + case .mastodon: + items.append(contentsOf: [.local, .federated, .likes]) + if preferredEnableHistory { + items.append(contentsOf: [.history]) + } + items.append(contentsOf: [.lists]) } + self.secondaryTabBarItems = items } .store(in: &disposeBag) + let user = authContext.authenticationContext.user(in: context.managedObjectContext) + switch user { + case .twitter(let object): + self.avatarURLSubscription = object.publisher(for: \.profileImageURL) + .sink { [weak self] _ in + guard let self = self else { return } + self.avatarURL = object.avatarImageURL() + } + case .mastodon(let object): + self.avatarURLSubscription = object.publisher(for: \.avatar) + .sink { [weak self] _ in + guard let self = self else { return } + self.avatarURL = object.avatar.flatMap { URL(string: $0) } + } + case .none: + self.avatarURL = nil + } + Task { await setupNotificationTabIconUpdater() } // end Task @@ -121,16 +112,14 @@ extension SidebarViewModel { @MainActor private func setupNotificationTabIconUpdater() async { // notification tab bar icon updater - await Publishers.CombineLatest3( - context.authenticationService.$activeAuthenticationContext, + await Publishers.CombineLatest( context.notificationService.unreadNotificationCountDidUpdate, // <-- actor property $activeTab ) .receive(on: DispatchQueue.main) - .sink { [weak self] authenticationContext, _, activeTab in + .sink { [weak self] _, activeTab in guard let self = self else { return } - guard let authenticationContext = authenticationContext else { return } - + let authenticationContext = self.authContext.authenticationContext let hasUnreadPushNotification: Bool = { switch authenticationContext { case .twitter: diff --git a/TwidereX/Scene/Search/SavedSearch/SavedSearchViewController.swift b/TwidereX/Scene/Search/SavedSearch/SavedSearchViewController.swift index 90c43ba4..92af6493 100644 --- a/TwidereX/Scene/Search/SavedSearch/SavedSearchViewController.swift +++ b/TwidereX/Scene/Search/SavedSearch/SavedSearchViewController.swift @@ -88,10 +88,11 @@ extension SavedSearchViewController: UITableViewDelegate { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") guard let diffableDataSource = self.viewModel.diffableDataSource, - case let .history(record) = diffableDataSource.itemIdentifier(for: indexPath), - let authenticationContext = self.viewModel.context.authenticationService.activeAuthenticationContext + case let .history(record) = diffableDataSource.itemIdentifier(for: indexPath) else { return nil } + let authenticationContext = authContext.authenticationContext + let deleteAction = UIContextualAction( style: .destructive, title: L10n.Common.Controls.Actions.delete, diff --git a/TwidereX/Scene/Search/Search/SearchViewController.swift b/TwidereX/Scene/Search/Search/SearchViewController.swift index 898f91d7..bdf64e3d 100644 --- a/TwidereX/Scene/Search/Search/SearchViewController.swift +++ b/TwidereX/Scene/Search/Search/SearchViewController.swift @@ -126,27 +126,20 @@ extension SearchViewController { .store(in: &disposeBag) // bind twitter trend place entry - viewModel.context.authenticationService.$activeAuthenticationContext - .receive(on: DispatchQueue.main) - .sink { [weak self] authenticationContext in - guard let self = self else { return } - switch authenticationContext { - case .twitter: - self.trendSectionHeaderView.button.isHidden = false - default: - self.trendSectionHeaderView.button.isHidden = true - } - } - .store(in: &disposeBag) + switch viewModel.authContext.authenticationContext { + case .twitter: + self.trendSectionHeaderView.button.isHidden = false + default: + self.trendSectionHeaderView.button.isHidden = true + } // bind searchBar bookmark - Publishers.CombineLatest3( + Publishers.CombineLatest( viewModel.$savedSearchTexts, - searchResultViewModel.$searchText, - context.authenticationService.$activeAuthenticationContext + searchResultViewModel.$searchText ) .receive(on: DispatchQueue.main) - .sink { [weak self] texts, searchText, activeAuthenticationContext in + .sink { [weak self] texts, searchText in guard let self = self else { return } let text = searchText.trimmingCharacters(in: .whitespacesAndNewlines) guard !text.isEmpty, @@ -155,12 +148,8 @@ extension SearchViewController { self.searchController.searchBar.showsBookmarkButton = false return } - switch activeAuthenticationContext { - case .twitter, .mastodon: - self.searchController.searchBar.showsBookmarkButton = true - case nil: - self.searchController.searchBar.showsBookmarkButton = false - } + + self.searchController.searchBar.showsBookmarkButton = true } .store(in: &disposeBag) } @@ -268,10 +257,11 @@ extension SearchViewController: UITableViewDelegate { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") guard let diffableDataSource = self.viewModel.diffableDataSource, - case let .history(record) = diffableDataSource.itemIdentifier(for: indexPath), - let authenticationContext = self.viewModel.context.authenticationService.activeAuthenticationContext + case let .history(record) = diffableDataSource.itemIdentifier(for: indexPath) else { return nil } + let authenticationContext = viewModel.authContext.authenticationContext + let deleteAction = UIContextualAction( style: .destructive, title: L10n.Common.Controls.Actions.delete, diff --git a/TwidereX/Scene/Search/Search/SearchViewModel+Diffable.swift b/TwidereX/Scene/Search/Search/SearchViewModel+Diffable.swift index 54996b05..55602440 100644 --- a/TwidereX/Scene/Search/Search/SearchViewModel+Diffable.swift +++ b/TwidereX/Scene/Search/Search/SearchViewModel+Diffable.swift @@ -79,22 +79,15 @@ extension SearchViewModel { } .eraseToAnyPublisher() - Publishers.CombineLatest3( + Publishers.CombineLatest( historyItems, - trendItems, - context.authenticationService.$activeAuthenticationContext + trendItems ) .throttle(for: 0.3, scheduler: DispatchQueue.main, latest: true) - .sink { [weak self] historyItems, trendItems, authenticationContext in + .sink { [weak self] historyItems, trendItems in guard let self = self else { return } guard let diffableDataSource = self.diffableDataSource else { return } - - guard let authenticationContext = authenticationContext else { - let snapshot = NSDiffableDataSourceSnapshot() - diffableDataSource.applySnapshotUsingReloadData(snapshot) - return - } - + var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.history, .trend]) diff --git a/TwidereX/Scene/Search/Search/SearchViewModel.swift b/TwidereX/Scene/Search/Search/SearchViewModel.swift index 8f16a814..9a89fcd0 100644 --- a/TwidereX/Scene/Search/Search/SearchViewModel.swift +++ b/TwidereX/Scene/Search/Search/SearchViewModel.swift @@ -43,7 +43,7 @@ final class SearchViewModel { viewDidAppear .sink { [weak self] _ in guard let self = self else { return } - guard let authenticationContext = self.context.authenticationService.activeAuthenticationContext else { return } + let authenticationContext = self.authContext.authenticationContext Task { do { @@ -62,7 +62,7 @@ final class SearchViewModel { ) .sink { [weak self] trendGroupIndex, _ in guard let self = self else { return } - guard let authenticationContext = self.context.authenticationService.activeAuthenticationContext else { return } + let authenticationContext = self.authContext.authenticationContext Task { @MainActor in do { diff --git a/TwidereX/Scene/Search/SearchResult/Hashtag/SearchHashtagViewModel+State.swift b/TwidereX/Scene/Search/SearchResult/Hashtag/SearchHashtagViewModel+State.swift index 7a84bf99..b7e839f5 100644 --- a/TwidereX/Scene/Search/SearchResult/Hashtag/SearchHashtagViewModel+State.swift +++ b/TwidereX/Scene/Search/SearchResult/Hashtag/SearchHashtagViewModel+State.swift @@ -71,18 +71,14 @@ extension SearchHashtagViewModel.State { } guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext - else { - stateMachine.enter(Fail.self) - return - } let searchText = viewModel.searchText - + let authenticationContext = viewModel.authContext.authenticationContext + if nextInput == nil { nextInput = { switch authenticationContext { - case .twitter(let authenticationContext): + case .twitter: assertionFailure() return nil case .mastodon(let authenticationContext): diff --git a/TwidereX/Scene/Search/SearchResult/SearchResultViewController.swift b/TwidereX/Scene/Search/SearchResult/SearchResultViewController.swift index 641a910f..1391bd7e 100644 --- a/TwidereX/Scene/Search/SearchResult/SearchResultViewController.swift +++ b/TwidereX/Scene/Search/SearchResult/SearchResultViewController.swift @@ -126,13 +126,12 @@ extension SearchResultViewController: UISearchBarDelegate { func searchBarBookmarkButtonClicked(_ searchBar: UISearchBar) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") guard let searchText = searchBar.text?.trimmingCharacters(in: .whitespacesAndNewlines), !searchText.isEmpty else { return } - guard let authenticationContext = context.authenticationService.activeAuthenticationContext else { return } Task { try await DataSourceFacade.responseToCreateSavedSearch( dependency: self, searchText: searchText, - authenticationContext: authenticationContext + authenticationContext: viewModel.authContext.authenticationContext ) } } diff --git a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+Diffable.swift b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+Diffable.swift index 28ec4d84..737f403e 100644 --- a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+Diffable.swift +++ b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+Diffable.swift @@ -22,11 +22,12 @@ extension SearchUserViewModel { diffableDataSource = UserSection.diffableDataSource( tableView: tableView, context: context, + authContext: authContext, configuration: UserSection.Configuration( userViewTableViewCellDelegate: userViewTableViewCellDelegate, userViewConfigurationContext: .init( - listMembershipViewModel: listMembershipViewModel, - authenticationContext: context.authenticationService.activeAuthenticationContext + authContext: authContext, + listMembershipViewModel: listMembershipViewModel ) ) ) diff --git a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+State.swift b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+State.swift index faf504fa..9ce3259c 100644 --- a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+State.swift +++ b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+State.swift @@ -72,13 +72,10 @@ extension SearchUserViewModel.State { } guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext - else { - stateMachine.enter(Fail.self) - return - } + let authenticationContext = viewModel.authContext.authenticationContext let searchText = viewModel.searchText + if nextInput == nil { nextInput = { switch authenticationContext { diff --git a/TwidereX/Scene/Search/Trend/TrendViewModel.swift b/TwidereX/Scene/Search/Trend/TrendViewModel.swift index 9904028f..ad08cfb1 100644 --- a/TwidereX/Scene/Search/Trend/TrendViewModel.swift +++ b/TwidereX/Scene/Search/Trend/TrendViewModel.swift @@ -42,23 +42,15 @@ final class TrendViewModel: ObservableObject { self.trendService = TrendService(apiService: context.apiService) // end init - context.authenticationService.$activeAuthenticationContext - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [weak self] authenticationContext in - guard let self = self else { return } - - switch authenticationContext { - case .twitter: - let placeID = TrendViewModel.defaultTwitterTrendPlace?.woeid ?? 1 // fallback to world-wide "1" - self.trendGroupIndex = .twitter(placeID: placeID) - case .mastodon(let authenticationContext): - self.trendGroupIndex = .mastodon(domain: authenticationContext.domain) - case nil: - self.trendGroupIndex = .none - } - } - .store(in: &disposeBag) + switch authContext.authenticationContext { + case .twitter: + let placeID = TrendViewModel.defaultTwitterTrendPlace?.woeid ?? 1 // fallback to world-wide "1" + self.trendGroupIndex = .twitter(placeID: placeID) + case .mastodon(let authenticationContext): + self.trendGroupIndex = .mastodon(domain: authenticationContext.domain) + case nil: + self.trendGroupIndex = .none + } Publishers.CombineLatest( $trendGroupIndex, @@ -86,7 +78,7 @@ final class TrendViewModel: ObservableObject { extension TrendViewModel { func fetchTrendPlaces() async throws { guard twitterTrendPlaces.isEmpty else { return } - guard case let .twitter(authenticationContext) = context.authenticationService.activeAuthenticationContext else { return } + guard case let .twitter(authenticationContext) = authContext.authenticationContext else { return } let response = try await context.apiService.twitterTrendPlaces(authenticationContext: authenticationContext) twitterTrendPlaces = response.value .filter { $0.parentID == 1 } diff --git a/TwidereX/Scene/Setting/About/AboutViewController.swift b/TwidereX/Scene/Setting/About/AboutViewController.swift index 4987ef73..2ba9fdc3 100644 --- a/TwidereX/Scene/Setting/About/AboutViewController.swift +++ b/TwidereX/Scene/Setting/About/AboutViewController.swift @@ -51,7 +51,7 @@ extension AboutViewController { let url = URL(string: "https://github.com/TwidereProject/TwidereX-iOS")! self.coordinator.present(scene: .safari(url: url.absoluteString), from: nil, transition: .safariPresent(animated: true, completion: nil)) case .twitter: - switch self.context.authenticationService.activeAuthenticationContext { + switch self.viewModel.authContext.authenticationContext { case .twitter: let profileViewModel = RemoteProfileViewModel( context: self.context, @@ -60,8 +60,6 @@ extension AboutViewController { ) self.coordinator.present(scene: .profile(viewModel: profileViewModel), from: self, transition: .show) case .mastodon: - fallthrough - case .none: let url = URL(string: "https://twitter.com/twidereproject")! self.coordinator.present(scene: .safari(url: url.absoluteString), from: nil, transition: .safariPresent(animated: true, completion: nil)) } diff --git a/TwidereX/Scene/Setting/Developer/DeveloperView.swift b/TwidereX/Scene/Setting/Developer/DeveloperView.swift index 39807056..106dc8a3 100644 --- a/TwidereX/Scene/Setting/Developer/DeveloperView.swift +++ b/TwidereX/Scene/Setting/Developer/DeveloperView.swift @@ -71,9 +71,11 @@ struct DeveloperView: View { struct DeveloperView_Previews: PreviewProvider { static var previews: some View { Group { - DeveloperView(viewModel: DeveloperViewModel()) - DeveloperView(viewModel: DeveloperViewModel()) - .preferredColorScheme(.dark) + if let authContext = AuthContext.mock(context: .shared) { + DeveloperView(viewModel: DeveloperViewModel(authContext: authContext)) + DeveloperView(viewModel: DeveloperViewModel(authContext: authContext)) + .preferredColorScheme(.dark) + } } } } diff --git a/TwidereX/Scene/Setting/Developer/DeveloperViewController.swift b/TwidereX/Scene/Setting/Developer/DeveloperViewController.swift index aba23dd6..59e05e6c 100644 --- a/TwidereX/Scene/Setting/Developer/DeveloperViewController.swift +++ b/TwidereX/Scene/Setting/Developer/DeveloperViewController.swift @@ -20,7 +20,7 @@ final class DeveloperViewController: UIViewController, NeedsDependency { weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } - let viewModel = DeveloperViewModel() + var viewModel: DeveloperViewModel! private(set) lazy var developerView = DeveloperView(viewModel: viewModel) } @@ -43,12 +43,16 @@ extension DeveloperViewController { hostingViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - if case let .twitter(authenticationContext) = context.authenticationService.activeAuthenticationContext { + if case let .twitter(authenticationContext) = viewModel.authContext.authenticationContext { Task { @MainActor in - viewModel.fetching = true - defer { viewModel.fetching = false } - let response = try await self.context.apiService.rateLimitStatus(authorization: authenticationContext.authorization) - viewModel.rateLimitStatusResources.value = response.value.resources + do { + viewModel.fetching = true + let response = try await self.context.apiService.rateLimitStatus(authorization: authenticationContext.authorization) + viewModel.rateLimitStatusResources.value = response.value.resources + } catch { + // do nothing + } + self.viewModel.fetching = false } // end Task } } diff --git a/TwidereX/Scene/Setting/Developer/DeveloperViewModel.swift b/TwidereX/Scene/Setting/Developer/DeveloperViewModel.swift index 4b6e0a31..cea3579a 100644 --- a/TwidereX/Scene/Setting/Developer/DeveloperViewModel.swift +++ b/TwidereX/Scene/Setting/Developer/DeveloperViewModel.swift @@ -18,6 +18,7 @@ final class DeveloperViewModel: ObservableObject { var disposeBag = Set() // input + let authContext: AuthContext let rateLimitStatusResources = CurrentValueSubject(nil) @Published var resourceFilterOption: DeveloperViewModel.ResourceFilterOption = .used @@ -25,7 +26,12 @@ final class DeveloperViewModel: ObservableObject { @Published var fetching = false @Published var sections: [Section] = [] - init() { + init( + authContext: AuthContext + ) { + self.authContext = authContext + // end init + Publishers.CombineLatest( $resourceFilterOption.eraseToAnyPublisher(), rateLimitStatusResources diff --git a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceView.swift b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceView.swift index 6b5058ba..da89acb5 100644 --- a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceView.swift +++ b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceView.swift @@ -27,9 +27,9 @@ struct DisplayPreferenceView: View { PrototypeStatusViewRepresentable( style: .timeline, configurationContext: StatusView.ConfigurationContext( + authContext: viewModel.authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - authenticationContext: viewModel.$authenticationContext + twitterTextProvider: OfficialTwitterTextProvider() ), height: $timelineStatusViewHeight ) diff --git a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewController.swift b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewController.swift index e380db83..cdf3903c 100644 --- a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewController.swift +++ b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewController.swift @@ -16,7 +16,7 @@ final class DisplayPreferenceViewController: UIViewController, NeedsDependency { weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } var disposeBag = Set() - let viewModel = DisplayPreferenceViewModel() + var viewModel: DisplayPreferenceViewModel! private(set) lazy var displayPreferenceView = DisplayPreferenceView(viewModel: viewModel) private(set) lazy var tableView: UITableView = { diff --git a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewModel.swift b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewModel.swift index cfc24428..05a3de4d 100644 --- a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewModel.swift +++ b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewModel.swift @@ -23,7 +23,8 @@ final class DisplayPreferenceViewModel: ObservableObject { @Published var viewSize: CGSize = .zero // input - + let authContext: AuthContext + // avatar @Published var avatarStyle = UserDefaults.shared.avatarStyle @@ -34,7 +35,10 @@ final class DisplayPreferenceViewModel: ObservableObject { // output @Published var authenticationContext: AuthenticationContext? - init() { + init(authContext: AuthContext) { + self.authContext = authContext + // end init + // avatar style UserDefaults.shared.publisher(for: \.avatarStyle) .removeDuplicates() diff --git a/TwidereX/Scene/Setting/List/SettingListViewController.swift b/TwidereX/Scene/Setting/List/SettingListViewController.swift index 901d58dd..dab54619 100644 --- a/TwidereX/Scene/Setting/List/SettingListViewController.swift +++ b/TwidereX/Scene/Setting/List/SettingListViewController.swift @@ -69,7 +69,8 @@ extension SettingListViewController { transition: .show ) case .display: - self.coordinator.present(scene: .displayPreference, from: self, transition: .show) + let displayPreferenceViewModel = DisplayPreferenceViewModel(authContext: self.viewModel.authContext) + self.coordinator.present(scene: .displayPreference(viewModel: displayPreferenceViewModel), from: self, transition: .show) case .layout: break case .webBrowser: @@ -81,7 +82,8 @@ extension SettingListViewController { self.coordinator.present(scene: .about(viewModel: aboutViewModel), from: self, transition: .show) #if DEBUG case .developer: - self.coordinator.present(scene: .developer, from: self, transition: .show) + let developerViewModel = DeveloperViewModel(authContext: self.viewModel.authContext) + self.coordinator.present(scene: .developer(viewModel: developerViewModel), from: self, transition: .show) #endif } } diff --git a/TwidereX/Scene/Setting/PushNotificationScratch/PushNotificationScratchViewModel.swift b/TwidereX/Scene/Setting/PushNotificationScratch/PushNotificationScratchViewModel.swift index 86891121..21e26965 100644 --- a/TwidereX/Scene/Setting/PushNotificationScratch/PushNotificationScratchViewModel.swift +++ b/TwidereX/Scene/Setting/PushNotificationScratch/PushNotificationScratchViewModel.swift @@ -6,29 +6,65 @@ // Copyright © 2022 Twidere. All rights reserved. // +import os.log import Foundation import CoreData import CoreDataStack import TwidereCore -final class PushNotificationScratchViewModel: ObservableObject { +final class PushNotificationScratchViewModel: NSObject, ObservableObject { // input let context: AppContext + let authenticationIndexFetchedResultsController: NSFetchedResultsController @Published var isRandomNotification = true @Published var notificationID = "" - @Published var accounts: [UserObject] - @Published var activeAccountIndex: Int = 0 - // output - + @Published var accounts: [UserObject] = [] + @Published var activeAccountIndex: Int = 0 + init(context: AppContext) { self.context = context + self.authenticationIndexFetchedResultsController = { + let fetchRequest = AuthenticationIndex.sortedFetchRequest + fetchRequest.returnsObjectsAsFaults = false + fetchRequest.fetchBatchSize = 20 + let controller = NSFetchedResultsController( + fetchRequest: fetchRequest, + managedObjectContext: context.managedObjectContext, + sectionNameKeyPath: nil, + cacheName: nil + ) + return controller + }() + super.init() // end init - accounts = context.authenticationService.authenticationIndexes.compactMap { $0.user } + authenticationIndexFetchedResultsController.delegate = self + try? authenticationIndexFetchedResultsController.performFetch() } } + +// MARK: - NSFetchedResultsControllerDelegate +extension PushNotificationScratchViewModel: NSFetchedResultsControllerDelegate { + + public func controllerWillChangeContent(_ controller: NSFetchedResultsController) { + os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + + public func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + switch controller { + case authenticationIndexFetchedResultsController: + let authenticationIndexes = authenticationIndexFetchedResultsController.fetchedObjects ?? [] + accounts = authenticationIndexes.compactMap { authenticationIndex in + authenticationIndex.user + } + default: + assertionFailure() + } + } + +} diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift index abafba71..c47e9784 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift @@ -23,9 +23,9 @@ extension StatusThreadViewModel { statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, statusViewConfigurationContext: .init( + authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - authenticationContext: context.authenticationService.$activeAuthenticationContext + twitterTextProvider: OfficialTwitterTextProvider() ) ) diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel+LoadThreadState.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel+LoadThreadState.swift index 740b6d2b..e63fe69f 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel+LoadThreadState.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel+LoadThreadState.swift @@ -96,7 +96,7 @@ extension StatusThreadViewModel.LoadThreadState { if twitterConversation.conversationID == nil { // fetch conversationID if not exist - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext?.twitterAuthenticationContext else { + guard case let .twitter(authenticationContext) = viewModel.authContext.authenticationContext else { await enter(state: PrepareFail.self) return } @@ -255,7 +255,7 @@ extension StatusThreadViewModel.LoadThreadState { twitterConversation: StatusThreadViewModel.ThreadContext.TwitterConversation ) async -> [TwitterStatusThreadLeafViewModel.Node] { guard let viewModel = viewModel, let stateMachine = stateMachine else { return [] } - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext?.twitterAuthenticationContext, + guard case let .twitter(authenticationContext) = viewModel.authContext.authenticationContext, let conversationID = twitterConversation.conversationID else { await enter(state: Fail.self) @@ -316,7 +316,7 @@ extension StatusThreadViewModel.LoadThreadState { twitterConversation: StatusThreadViewModel.ThreadContext.TwitterConversation ) async -> [TwitterStatusThreadLeafViewModel.Node] { guard let viewModel = viewModel, let stateMachine = stateMachine else { return [] } - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext?.twitterAuthenticationContext, + guard case let .twitter(authenticationContext) = viewModel.authContext.authenticationContext, let _ = twitterConversation.conversationID else { await enter(state: Fail.self) @@ -401,7 +401,7 @@ extension StatusThreadViewModel.LoadThreadState { descendantNodes: [] ) } - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext?.mastodonAuthenticationContext + guard case let .mastodon(authenticationContext) = viewModel.authContext.authenticationContext else { await enter(state: Fail.self) return MastodonContextResponse( diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift index cd36e0f8..05fad970 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift @@ -63,7 +63,7 @@ final class StatusThreadViewModel { ) { self.context = context self.authContext = authContext - self.twitterStatusThreadReplyViewModel = TwitterStatusThreadReplyViewModel(context: context) + self.twitterStatusThreadReplyViewModel = TwitterStatusThreadReplyViewModel(context: context, authContext: authContext) self.twitterStatusThreadLeafViewModel = TwitterStatusThreadLeafViewModel(context: context) self.mastodonStatusThreadViewModel = MastodonStatusThreadViewModel(context: context) self.root = CurrentValueSubject(optionalRoot) diff --git a/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadReplyViewModel+State.swift b/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadReplyViewModel+State.swift index ce523049..ab684479 100644 --- a/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadReplyViewModel+State.swift +++ b/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadReplyViewModel+State.swift @@ -220,9 +220,7 @@ extension TwitterStatusThreadReplyViewModel.State { super.didEnter(from: previousState) guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext, - case let .twitter(twitterAuthenticationContext) = authenticationContext - else { + guard case let .twitter(twitterAuthenticationContext) = viewModel.authContext.authenticationContext else { return } diff --git a/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadReplyViewModel.swift b/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadReplyViewModel.swift index 63edc846..3e5e56a7 100644 --- a/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadReplyViewModel.swift +++ b/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadReplyViewModel.swift @@ -21,6 +21,7 @@ final class TwitterStatusThreadReplyViewModel { // input let context: AppContext + let authContext: AuthContext @Published var root: ManagedObjectRecord? @Published var nodes: [TwitterStatusReplyNode] = [] @Published private(set) var deletedObjectIDs: Set = Set() @@ -44,8 +45,12 @@ final class TwitterStatusThreadReplyViewModel { return stateMachine }() - init(context: AppContext) { + init( + context: AppContext, + authContext: AuthContext + ) { self.context = context + self.authContext = authContext // end init Publishers.CombineLatest( diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift index b62c76dd..1ae91860 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift @@ -98,17 +98,14 @@ extension TimelineViewController { avatarBarButtonItem.delegate = self // bind avatarBarButtonItem data - Publishers.CombineLatest( - context.authenticationService.$activeAuthenticationContext, - _viewModel.viewDidAppear.eraseToAnyPublisher() - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] authenticationContext, _ in - guard let self = self else { return } - let user = authenticationContext?.user(in: self.context.managedObjectContext) - self.avatarBarButtonItem.configure(user: user) - } - .store(in: &disposeBag) + _viewModel.viewDidAppear + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + let user = self._viewModel.authContext.authenticationContext.user(in: self.context.managedObjectContext) + self.avatarBarButtonItem.configure(user: user) + } + .store(in: &disposeBag) // layout publish progress publishProgressView.translatesAutoresizingMaskIntoConstraints = false @@ -251,9 +248,9 @@ extension TimelineViewController { authenticationService: context.authenticationService, mastodonEmojiService: context.mastodonEmojiService, statusViewConfigureContext: .init( + authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - authenticationContext: context.authenticationService.$activeAuthenticationContext + twitterTextProvider: OfficialTwitterTextProvider() ) ) ) diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift index 0ba53692..86e630ae 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift @@ -121,10 +121,7 @@ extension TimelineViewModel.LoadOldestState { private func fetch(anchor record: StatusRecord?) async { guard let viewModel = viewModel, let _ = stateMachine else { return } - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext else { - enter(state: Fail.self) - return - } + let authenticationContext = viewModel.authContext.authenticationContext if nextInput == nil { let managedObjectContext = viewModel.context.managedObjectContext diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift index 4f25b274..966cdf02 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift @@ -136,10 +136,9 @@ extension TimelineViewModel { isLoadingLatest = false } - guard let authenticationContext = context.authenticationService.activeAuthenticationContext else { return } let fetchContext = StatusFetchViewModel.Timeline.FetchContext( managedObjectContext: context.managedObjectContext, - authenticationContext: authenticationContext, + authenticationContext: authContext.authenticationContext, kind: kind, position: { switch kind { diff --git a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift index 6ca31a44..b2dd60b0 100644 --- a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift @@ -46,10 +46,11 @@ extension ListTimelineViewModel { @MainActor func loadMore(item: StatusItem) async { guard case let .feedLoader(record) = item else { return } - guard let authenticationContext = context.authenticationService.activeAuthenticationContext else { return } guard let diffableDataSource = diffableDataSource else { return } var snapshot = diffableDataSource.snapshot() + let authenticationContext = authContext.authenticationContext + let managedObjectContext = context.managedObjectContext let key = "LoadMore@\(record.objectID)#\(UUID().uuidString)" diff --git a/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel+Diffable.swift index bd52ee95..5fe26670 100644 --- a/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel+Diffable.swift @@ -25,9 +25,9 @@ extension FederatedTimelineViewModel { statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, statusViewConfigurationContext: StatusView.ConfigurationContext( + authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - authenticationContext: context.authenticationService.$activeAuthenticationContext + twitterTextProvider: OfficialTwitterTextProvider() ) ) diffableDataSource = StatusSection.diffableDataSource( diff --git a/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel.swift b/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel.swift index 11bb5aaa..a833c8c8 100644 --- a/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel.swift @@ -24,26 +24,15 @@ final class FederatedTimelineViewModel: ListTimelineViewModel { ) enableAutoFetchLatest = true - - context.authenticationService.$activeAuthenticationContext - .sink { [weak self] authenticationContext in - guard let self = self else { return } - guard let authenticationContext = authenticationContext else { - self.statusRecordFetchedResultController.userIdentifier = nil - return - } - - switch authenticationContext { - case .twitter: - self.statusRecordFetchedResultController.userIdentifier = nil - case .mastodon(let authenticationContext): - self.statusRecordFetchedResultController.userIdentifier = .mastodon(.init( - domain: authenticationContext.domain, - id: authenticationContext.userID - )) - } - } - .store(in: &disposeBag) + switch authContext.authenticationContext { + case .twitter: + self.statusRecordFetchedResultController.userIdentifier = nil + case .mastodon(let authenticationContext): + self.statusRecordFetchedResultController.userIdentifier = .mastodon(.init( + domain: authenticationContext.domain, + id: authenticationContext.userID + )) + } } deinit { diff --git a/TwidereX/Scene/Timeline/Hashtag/HashtagTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/Hashtag/HashtagTimelineViewModel+Diffable.swift index 5c375573..81b76032 100644 --- a/TwidereX/Scene/Timeline/Hashtag/HashtagTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/Hashtag/HashtagTimelineViewModel+Diffable.swift @@ -25,9 +25,9 @@ extension HashtagTimelineViewModel { statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, statusViewConfigurationContext: StatusView.ConfigurationContext( + authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - authenticationContext: context.authenticationService.$activeAuthenticationContext + twitterTextProvider: OfficialTwitterTextProvider() ) ) diffableDataSource = StatusSection.diffableDataSource( diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift index f519cc23..cced6953 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift @@ -273,7 +273,7 @@ extension HomeTimelineViewController { @objc private func showStatusByID(_ id: String) { Task { @MainActor in - let authenticationContext = self.context.authenticationService.activeAuthenticationContext + let authenticationContext = self.viewModel.authContext.authenticationContext switch authenticationContext { case .twitter(let authenticationContext): _ = try await self.context.apiService.twitterStatus( @@ -324,7 +324,7 @@ extension HomeTimelineViewController { } @objc private func showAccountListAction(_ sender: UIAction) { - let accountListViewModel = AccountListViewModel(context: context) + let accountListViewModel = AccountListViewModel(context: context, authContext: authContext) coordinator.present(scene: .accountList(viewModel: accountListViewModel), from: self, transition: .modal(animated: true, completion: nil)) } @@ -340,8 +340,8 @@ extension HomeTimelineViewController { case status(id: String) case duplicated - func match(item: StatusItem) -> Bool { - let authenticationContext = AppContext.shared.authenticationService.activeAuthenticationContext + func match(item: StatusItem, authContext: AuthContext) -> Bool { + let authenticationContext = authContext.authenticationContext switch item { case .feed(let record): guard let feed = record.object(in: AppContext.shared.managedObjectContext) else { return false } @@ -382,7 +382,7 @@ extension HomeTimelineViewController { } } - func firstMatch(in items: [StatusItem]) -> StatusItem? { + func firstMatch(in items: [StatusItem], authContext: AuthContext) -> StatusItem? { switch self { case .duplicated: var index = 0 @@ -424,7 +424,7 @@ extension HomeTimelineViewController { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): not found duplicated in \(index) count items") return nil default: - return items.first { item in self.match(item: item) } + return items.first { item in self.match(item: item, authContext: authContext) } } } } @@ -433,7 +433,7 @@ extension HomeTimelineViewController { guard let diffableDataSource = viewModel.diffableDataSource else { return } let snapshot = diffableDataSource.snapshot() let items = snapshot.itemIdentifiers - guard let targetItem = category.firstMatch(in: items), + guard let targetItem = category.firstMatch(in: items, authContext: authContext), let index = snapshot.indexOfItem(targetItem) else { return } let indexPath = IndexPath(row: index, section: 0) diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel+Diffable.swift index e350ef22..669ada00 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel+Diffable.swift @@ -25,9 +25,9 @@ extension HomeTimelineViewModel { statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate, statusViewConfigurationContext: .init( + authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - authenticationContext: context.authenticationService.$activeAuthenticationContext + twitterTextProvider: OfficialTwitterTextProvider() ) ) diffableDataSource = StatusSection.diffableDataSource( diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel.swift index 63d80cbe..3364438d 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel.swift @@ -33,28 +33,20 @@ final class HomeTimelineViewModel: ListTimelineViewModel { enableAutoFetchLatest = true - context.authenticationService.$activeAuthenticationContext - .sink { [weak self] authenticationContext in - guard let self = self else { return } - let emptyFeedPredicate = Feed.nonePredicate() - guard let authenticationContext = authenticationContext else { - self.feedFetchedResultsController.predicate = emptyFeedPredicate - return - } - - let predicate: NSPredicate - switch authenticationContext { - case .twitter(let authenticationContext): - let userID = authenticationContext.userID - predicate = Feed.predicate(kind: .home, acct: Feed.Acct.twitter(userID: userID)) - case .mastodon(let authenticationContext): - let domain = authenticationContext.domain - let userID = authenticationContext.userID - predicate = Feed.predicate(kind: .home, acct: Feed.Acct.mastodon(domain: domain, userID: userID)) - } - self.feedFetchedResultsController.predicate = predicate + feedFetchedResultsController.predicate = { + let predicate: NSPredicate + let authenticationContext = authContext.authenticationContext + switch authenticationContext { + case .twitter(let authenticationContext): + let userID = authenticationContext.userID + predicate = Feed.predicate(kind: .home, acct: Feed.Acct.twitter(userID: userID)) + case .mastodon(let authenticationContext): + let domain = authenticationContext.domain + let userID = authenticationContext.userID + predicate = Feed.predicate(kind: .home, acct: Feed.Acct.mastodon(domain: domain, userID: userID)) } - .store(in: &disposeBag) + return predicate + }() } deinit { diff --git a/TwidereX/Scene/Timeline/List/ListStatusTimelineViewController.swift b/TwidereX/Scene/Timeline/List/ListStatusTimelineViewController.swift index 260fbc62..1921db6c 100644 --- a/TwidereX/Scene/Timeline/List/ListStatusTimelineViewController.swift +++ b/TwidereX/Scene/Timeline/List/ListStatusTimelineViewController.swift @@ -49,30 +49,19 @@ extension ListStatusTimelineViewController { } navigationItem.rightBarButtonItem = menuBarButtonItem - context.authenticationService.$activeAuthenticationContext - .asyncMap { [weak self] authenticationContext -> UIMenu? in - guard let self = self else { return nil } - guard case let .list(list) = self.viewModel.kind else { return nil } - guard let authenticationContext = authenticationContext else { return nil } - do { - let menu = try await DataSourceFacade.createMenuForList( - dependency: self, - list: list, - authenticationContext: authenticationContext - ) - return menu - } catch { - assertionFailure(error.localizedDescription) - return nil - } - } - .receive(on: DispatchQueue.main) - .sink { [weak self] menu in - guard let self = self else { return } - guard let menu = menu else { return } + Task { + guard case let .list(list) = self.viewModel.kind else { return } + do { + let menu = try await DataSourceFacade.createMenuForList( + dependency: self, + list: list, + authenticationContext: self.viewModel.authContext.authenticationContext + ) self.menuBarButtonItem.menu = menu + } catch { + assertionFailure(error.localizedDescription) } - .store(in: &disposeBag) + } // end Task viewModel.$isDeleted .receive(on: DispatchQueue.main) diff --git a/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel+Diffable.swift index 6af0c953..ec2c64fc 100644 --- a/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel+Diffable.swift @@ -21,9 +21,9 @@ extension ListStatusTimelineViewModel { statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, statusViewConfigurationContext: StatusView.ConfigurationContext( + authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - authenticationContext: context.authenticationService.$activeAuthenticationContext + twitterTextProvider: OfficialTwitterTextProvider() ) ) diffableDataSource = StatusSection.diffableDataSource( diff --git a/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel.swift b/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel.swift index 4f8a53b3..4a4aa562 100644 --- a/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel.swift @@ -30,10 +30,7 @@ final class ListStatusTimelineViewModel: ListTimelineViewModel { ) isFloatyButtonDisplay = false - - context.authenticationService.$activeAuthenticationContext - .map { $0?.userIdentifier } - .assign(to: &statusRecordFetchedResultController.$userIdentifier) + statusRecordFetchedResultController.userIdentifier = authContext.authenticationContext.userIdentifier // bind titile if let object = list.object(in: context.managedObjectContext) { diff --git a/TwidereX/Scene/Timeline/Search/Media/SearchMediaTimelineViewModel.swift b/TwidereX/Scene/Timeline/Search/Media/SearchMediaTimelineViewModel.swift index d576e6bb..4293d082 100644 --- a/TwidereX/Scene/Timeline/Search/Media/SearchMediaTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Search/Media/SearchMediaTimelineViewModel.swift @@ -34,9 +34,7 @@ final class SearchMediaTimelineViewModel: GridTimelineViewModel { isRefreshControlEnabled = false isFloatyButtonDisplay = false - context.authenticationService.$activeAuthenticationContext - .map { $0?.userIdentifier } - .assign(to: &statusRecordFetchedResultController.$userIdentifier) + statusRecordFetchedResultController.userIdentifier = authContext.authenticationContext.userIdentifier // bind searchText $searchText.assign(to: &searchTimelineContext.$searchText) diff --git a/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel+Diffable.swift index b89aa145..77205e67 100644 --- a/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel+Diffable.swift @@ -21,9 +21,9 @@ extension SearchTimelineViewModel { statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, statusViewConfigurationContext: StatusView.ConfigurationContext( + authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - authenticationContext: context.authenticationService.$activeAuthenticationContext + twitterTextProvider: OfficialTwitterTextProvider() ) ) diffableDataSource = StatusSection.diffableDataSource( diff --git a/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel.swift b/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel.swift index 7a449f4a..7d12ff63 100644 --- a/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel.swift @@ -34,9 +34,7 @@ final class SearchTimelineViewModel: ListTimelineViewModel { isRefreshControlEnabled = false isFloatyButtonDisplay = false - context.authenticationService.$activeAuthenticationContext - .map { $0?.userIdentifier } - .assign(to: &statusRecordFetchedResultController.$userIdentifier) + statusRecordFetchedResultController.userIdentifier = authContext.authenticationContext.userIdentifier // bind searchText $searchText.assign(to: &searchTimelineContext.$searchText) diff --git a/TwidereX/Scene/Timeline/User/Status/UserTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/User/Status/UserTimelineViewModel+Diffable.swift index efbb3c29..6e972e37 100644 --- a/TwidereX/Scene/Timeline/User/Status/UserTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/User/Status/UserTimelineViewModel+Diffable.swift @@ -25,9 +25,9 @@ extension UserTimelineViewModel { statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, statusViewConfigurationContext: StatusView.ConfigurationContext( + authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - authenticationContext: context.authenticationService.$activeAuthenticationContext + twitterTextProvider: OfficialTwitterTextProvider() ) ) diffableDataSource = StatusSection.diffableDataSource( From 31dd6f8231917c22242889afba41067699d2bde1 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 3 Feb 2023 14:21:22 +0800 Subject: [PATCH 025/128] chore: restore StatusView auth injection --- .../Sources/TwidereUI/Content/StatusView+Configuration.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+Configuration.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+Configuration.swift index 938ed2c7..72fbc2ca 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+Configuration.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+Configuration.swift @@ -97,6 +97,7 @@ extension StatusView { ) { viewModel.prepareForReuse() + viewModel.authenticationContext = configurationContext.authContext.authenticationContext viewModel.managedObjectContext = status.managedObjectContext viewModel.objects.insert(status) @@ -334,6 +335,7 @@ extension StatusView { ) { viewModel.prepareForReuse() + viewModel.authenticationContext = configurationContext.authContext.authenticationContext viewModel.managedObjectContext = status.managedObjectContext viewModel.objects.insert(status) From 4f8250323c90f351ee32acd19646471f3af8199c Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 3 Feb 2023 17:59:03 +0800 Subject: [PATCH 026/128] feat: rebuild the StatusView with SwiftUI --- ShareExtension/ComposeViewController.swift | 3 +- ShareExtension/ComposeViewModel.swift | 2 + TwidereSDK/Package.swift | 2 + .../Vendor/SwiftTwitterTextProvider.swift | 40 + .../Content/PrototypeStatusView.swift | 162 +- .../TwidereUI/Content/StatusHeaderView.swift | 30 + .../Content/StatusView+Configuration.swift | 1185 ++++----- .../Content/StatusView+ViewModel.swift | 1718 ++++++------ .../TwidereUI/Content/StatusView.swift | 2305 +++++++++-------- .../ComposeContent/ComposeContentView.swift | 17 +- .../Reply/ReplyStatusView.swift | 118 +- .../PrototypeStatusViewRepresentable.swift | 184 +- .../ReplyStatusViewRepresentable.swift | 56 +- .../TwidereUI/Utility/ViewLayoutFrame.swift | 57 + TwidereX.xcodeproj/project.pbxproj | 3 + .../xcshareddata/swiftpm/Package.resolved | 18 + .../Misc/History/HistorySection.swift | 130 +- .../Notification/NotificationSection.swift | 118 +- TwidereX/Diffable/Status/StatusSection.swift | 415 +-- .../Provider/DataSourceFacade+Status.swift | 3 +- ...ider+StatusViewTableViewCellDelegate.swift | 154 +- ...taSourceProvider+UITableViewDelegate.swift | 423 +-- .../Account/List/AccountListViewModel.swift | 10 +- TwidereX/Scene/Compose/ComposeViewModel.swift | 2 + .../StatusHistoryViewModel+Diffable.swift | 3 +- .../Status/StatusHistoryViewModel.swift | 2 + .../User/UserHistoryViewModel+Diffable.swift | 3 +- .../History/User/UserHistoryViewModel.swift | 4 +- .../MediaPreviewViewController.swift | 3 +- .../MediaPreview/MediaPreviewViewModel.swift | 2 + .../MediaInfoDescriptionView+ViewModel.swift | 116 +- ...tificationTimelineViewModel+Diffable.swift | 3 +- .../NotificationTimelineViewModel.swift | 2 + .../Scene/Profile/ProfileViewController.swift | 3 +- .../DisplayPreferenceView.swift | 20 +- .../StatusCollectionViewCell.swift | 20 +- .../StatusTableViewCell+ViewModel.swift | 156 +- .../TableViewCell/StatusTableViewCell.swift | 120 +- ...tusThreadRootTableViewCell+ViewModel.swift | 94 +- .../StatusThreadRootTableViewCell.swift | 158 +- .../StatusViewTableViewCellDelegate.swift | 79 - .../StatusThreadViewModel+Diffable.swift | 3 +- .../StatusThread/StatusThreadViewModel.swift | 3 + .../Base/Common/TimelineViewController.swift | 3 +- .../Base/Common/TimelineViewModel.swift | 2 + .../Base/List/ListTimelineViewModel.swift | 1 + .../FederatedTimelineViewModel+Diffable.swift | 3 +- .../HashtagTimelineViewModel+Diffable.swift | 3 +- .../Home/HomeTimelineViewModel+Diffable.swift | 3 +- ...ListStatusTimelineViewModel+Diffable.swift | 3 +- .../SearchTimelineViewModel+Diffable.swift | 3 +- .../Status/SearchTimelineViewModel.swift | 3 +- .../UserTimelineViewModel+Diffable.swift | 3 +- 53 files changed, 4158 insertions(+), 3818 deletions(-) create mode 100644 TwidereSDK/Sources/TwidereCore/Vendor/SwiftTwitterTextProvider.swift create mode 100644 TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift create mode 100644 TwidereSDK/Sources/TwidereUI/Utility/ViewLayoutFrame.swift diff --git a/ShareExtension/ComposeViewController.swift b/ShareExtension/ComposeViewController.swift index f31d0813..91ed6426 100644 --- a/ShareExtension/ComposeViewController.swift +++ b/ShareExtension/ComposeViewController.swift @@ -111,7 +111,8 @@ extension ComposeViewController { statusViewConfigureContext: .init( authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider() + twitterTextProvider: OfficialTwitterTextProvider(), + viewLayoutFramePublisher: viewModel.$viewLayoutFrame ) ) ) diff --git a/ShareExtension/ComposeViewModel.swift b/ShareExtension/ComposeViewModel.swift index b53e4a8f..bd17b518 100644 --- a/ShareExtension/ComposeViewModel.swift +++ b/ShareExtension/ComposeViewModel.swift @@ -9,6 +9,7 @@ import Foundation import Combine import TwidereCore +import TwidereUI final class ComposeViewModel { @@ -18,6 +19,7 @@ final class ComposeViewModel { // input let context: AppContext + @Published public var viewLayoutFrame = ViewLayoutFrame() @Published var authContext: AuthContext? @Published var isBusy = false diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index 7a47ac7e..dffa3a58 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -44,6 +44,7 @@ let package = Package( .package(url: "https://github.com/aheze/Popovers.git", from: "1.3.2"), .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", from: "4.2.2"), .package(url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.4"), + .package(url: "https://github.com/TwidereProject/twitter-text.git", exact: "0.0.3"), .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"), ], targets: [ @@ -112,6 +113,7 @@ let package = Package( .product(name: "AlamofireImage", package: "AlamofireImage"), .product(name: "AlamofireNetworkActivityIndicator", package: "AlamofireNetworkActivityIndicator"), .product(name: "MetaTextKit", package: "MetaTextKit"), + .product(name: "TwitterText", package: "twitter-text"), ] ), .target( diff --git a/TwidereSDK/Sources/TwidereCore/Vendor/SwiftTwitterTextProvider.swift b/TwidereSDK/Sources/TwidereCore/Vendor/SwiftTwitterTextProvider.swift new file mode 100644 index 00000000..1bb9ab29 --- /dev/null +++ b/TwidereSDK/Sources/TwidereCore/Vendor/SwiftTwitterTextProvider.swift @@ -0,0 +1,40 @@ +// +// SwiftTwitterTextProvider.swift +// +// +// Created by MainasuK on 2023/2/3. +// + +import Foundation +import TwitterText +import TwitterMeta + +public class SwiftTwitterTextProvider: TwitterTextProvider { + + public func parse(text: String) -> TwitterMeta.ParseResult { + let result = Parser.defaultParser.parseTweet(text: text) + return .init( + isValid: result.isValid, + weightedLength: result.weightedLength, + maxWeightedLength: Parser.defaultParser.maxWeightedTweetLength(), + entities: self.entities(in: text) + ) + } + + public func entities(in text: String) -> [TwitterMeta.TwitterTextProviderEntity] { + return TwitterText.entities(in: text).compactMap { entity in + switch entity.type { + case .url: return .url(range: entity.range) + case .screenName: return .screenName(range: entity.range) + case .hashtag: return .hashtag(range: entity.range) + case .listname: return .listName(range: entity.range) + case .symbol: return .symbol(range: entity.range) + case .tweetChar: return .tweetChar(range: entity.range) + case .tweetEmojiChar: return .tweetEmojiChar(range: entity.range) + } + } + } + + public init() { } + +} diff --git a/TwidereSDK/Sources/TwidereUI/Content/PrototypeStatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/PrototypeStatusView.swift index 3eff8fc8..17bb1a0c 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/PrototypeStatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/PrototypeStatusView.swift @@ -8,84 +8,84 @@ import UIKit import Combine -public protocol PrototypeStatusViewDelegate: AnyObject { - func layoutDidUpdate(_ view: PrototypeStatusView) -} - -final public class PrototypeStatusView: UIView { - - public var disposeBag = Set() - private var observations = Set() - - weak var delegate: PrototypeStatusViewDelegate? - - public let statusView = StatusView() - public private(set)var widthLayoutConstraint: NSLayoutConstraint! - - public override var intrinsicContentSize: CGSize { - let size = statusView.frame.size - defer { - self.delegate?.layoutDidUpdate(self) - } - return CGSize(width: UIView.noIntrinsicMetric, height: size.height) - } - - public override var frame: CGRect { - didSet { - guard frame != oldValue else { return } - layoutIfNeeded() - } - } - - public override func layoutSubviews() { - super.layoutSubviews() - - if frame.width != .zero { - statusView.frame.size.width = frame.width - widthLayoutConstraint.constant = frame.width - widthLayoutConstraint.isActive = true - } - - let targetSize = CGSize( - width: frame.width, - height: UIView.layoutFittingCompressedSize.height - ) - - statusView.frame.size.height = statusView.systemLayoutSizeFitting( - targetSize, - withHorizontalFittingPriority: .required, - verticalFittingPriority: .fittingSizeLevel - ).height - - invalidateIntrinsicContentSize() - } - - - public override init(frame: CGRect) { - super.init(frame: frame) - _init() - } - - public required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension PrototypeStatusView { - private func _init() { - widthLayoutConstraint = widthAnchor.constraint(equalToConstant: frame.width) - - addSubview(statusView) - - // trigger UIViewRepresentable size update - statusView - .observe(\.bounds, options: [.initial, .new]) { [weak self] statusView, _ in - guard let self = self else { return } - print(statusView.frame) - self.invalidateIntrinsicContentSize() - } - .store(in: &observations) - } -} +//public protocol PrototypeStatusViewDelegate: AnyObject { +// func layoutDidUpdate(_ view: PrototypeStatusView) +//} +// +//final public class PrototypeStatusView: UIView { +// +// public var disposeBag = Set() +// private var observations = Set() +// +// weak var delegate: PrototypeStatusViewDelegate? +// +// public let statusView = StatusView() +// public private(set)var widthLayoutConstraint: NSLayoutConstraint! +// +// public override var intrinsicContentSize: CGSize { +// let size = statusView.frame.size +// defer { +// self.delegate?.layoutDidUpdate(self) +// } +// return CGSize(width: UIView.noIntrinsicMetric, height: size.height) +// } +// +// public override var frame: CGRect { +// didSet { +// guard frame != oldValue else { return } +// layoutIfNeeded() +// } +// } +// +// public override func layoutSubviews() { +// super.layoutSubviews() +// +// if frame.width != .zero { +// statusView.frame.size.width = frame.width +// widthLayoutConstraint.constant = frame.width +// widthLayoutConstraint.isActive = true +// } +// +// let targetSize = CGSize( +// width: frame.width, +// height: UIView.layoutFittingCompressedSize.height +// ) +// +// statusView.frame.size.height = statusView.systemLayoutSizeFitting( +// targetSize, +// withHorizontalFittingPriority: .required, +// verticalFittingPriority: .fittingSizeLevel +// ).height +// +// invalidateIntrinsicContentSize() +// } +// +// +// public override init(frame: CGRect) { +// super.init(frame: frame) +// _init() +// } +// +// public required init?(coder: NSCoder) { +// super.init(coder: coder) +// _init() +// } +// +//} +// +//extension PrototypeStatusView { +// private func _init() { +// widthLayoutConstraint = widthAnchor.constraint(equalToConstant: frame.width) +// +// addSubview(statusView) +// +// // trigger UIViewRepresentable size update +// statusView +// .observe(\.bounds, options: [.initial, .new]) { [weak self] statusView, _ in +// guard let self = self else { return } +// print(statusView.frame) +// self.invalidateIntrinsicContentSize() +// } +// .store(in: &observations) +// } +//} diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift new file mode 100644 index 00000000..0abc4801 --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift @@ -0,0 +1,30 @@ +// +// StatusHeaderView.swift +// +// +// Created by MainasuK on 2023/2/3. +// + +import SwiftUI +import TwidereAsset +import TwidereLocalization + +public struct StatusHeaderView: View { + + @ObservedObject public var viewModel: ViewModel + + public var body: some View { + Text("Repost") + } + +} + +extension StatusHeaderView { + public class ViewModel: ObservableObject { + @Published public var label: AttributedString + + public init(label: AttributedString) { + self.label = label + } + } +} diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+Configuration.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+Configuration.swift index 72fbc2ca..8bae324b 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+Configuration.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+Configuration.swift @@ -1,6 +1,6 @@ // // StatusView+Configuration.swift -// +// // // Created by MainasuK on 2022-6-10. // @@ -22,605 +22,608 @@ extension StatusView { public let authContext: AuthContext public let dateTimeProvider: DateTimeProvider public let twitterTextProvider: TwitterTextProvider + public let viewLayoutFramePublisher: Published.Publisher? public init( authContext: AuthContext, dateTimeProvider: DateTimeProvider, - twitterTextProvider: TwitterTextProvider + twitterTextProvider: TwitterTextProvider, + viewLayoutFramePublisher: Published.Publisher? ) { self.authContext = authContext self.dateTimeProvider = dateTimeProvider self.twitterTextProvider = twitterTextProvider + self.viewLayoutFramePublisher = viewLayoutFramePublisher } } } -extension StatusView { - public func configure( - feed: Feed, - configurationContext: ConfigurationContext - ) { - switch feed.content { - case .none: - logger.log(level: .info, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Warning] feed content missing") - case .twitter(let status): - configure( - status: status, - configurationContext: configurationContext - ) - case .mastodon(let status): - configure( - status: status, - notification: nil, - configurationContext: configurationContext - ) - case .mastodonNotification(let notification): - guard let status = notification.status else { - assertionFailure() - return - } - configure( - status: status, - notification: notification, - configurationContext: configurationContext - ) - } - } - - public func configure( - statusObject object: StatusObject, - configurationContext: ConfigurationContext - ) { - switch object { - case .twitter(let status): - configure( - status: status, - configurationContext: configurationContext - ) - case .mastodon(let status): - configure( - status: status, - notification: nil, - configurationContext: configurationContext - ) - } - } - -} - -// MARK: - Twitter - -extension StatusView { - public func configure( - status: TwitterStatus, - configurationContext: ConfigurationContext - ) { - viewModel.prepareForReuse() - - viewModel.authenticationContext = configurationContext.authContext.authenticationContext - viewModel.managedObjectContext = status.managedObjectContext - viewModel.objects.insert(status) - - viewModel.platform = .twitter - viewModel.dateTimeProvider = configurationContext.dateTimeProvider - viewModel.twitterTextProvider = configurationContext.twitterTextProvider - - configureHeader(status) - configureAuthor(status) - configureContent(status) - configureMedia(status) - configurePoll(status) - configureLocation(status) - configureToolbar(status) - configureReplySettings(status) - - if let quote = status.quote ?? status.repost?.quote { - quoteStatusView?.configure( - status: quote, - configurationContext: configurationContext - ) - setQuoteDisplay() - } - } - - private func configureHeader(_ status: TwitterStatus) { - if let _ = status.repost { - status.author.publisher(for: \.name) - .map { name -> StatusView.ViewModel.Header in - let userRepostText = L10n.Common.Controls.Status.userRetweeted(name) - let metaContent = PlaintextMetaContent(string: userRepostText) - let info = ViewModel.Header.RepostInfo(authorNameMetaContent: metaContent) - return .repost(info: info) - } - .assign(to: \.header, on: viewModel) - .store(in: &disposeBag) - } else { - viewModel.header = .none - } - } - - private func configureAuthor(_ status: TwitterStatus) { - guard let dateTimeProvider = viewModel.dateTimeProvider else { - assertionFailure() - return - } - - let author = (status.repost ?? status).author - - viewModel.userIdentifier = .twitter(.init(id: author.id)) - - // author avatar - author.publisher(for: \.profileImageURL) - .map { _ in author.avatarImageURL() } - .assign(to: \.authorAvatarImageURL, on: viewModel) - .store(in: &disposeBag) - // lock - author.publisher(for: \.protected) - .assign(to: \.protected, on: viewModel) - .store(in: &disposeBag) - // author name - author.publisher(for: \.name) - .map { PlaintextMetaContent(string: $0) } - .assign(to: \.authorName, on: viewModel) - .store(in: &disposeBag) - // author username - author.publisher(for: \.username) - .map { $0 as String? } - .assign(to: \.authorUsername, on: viewModel) - .store(in: &disposeBag) - // timestamp - viewModel.dateTimeProvider = dateTimeProvider - (status.repost ?? status).publisher(for: \.createdAt) - .map { $0 as Date? } - .assign(to: \.timestamp, on: viewModel) - .store(in: &disposeBag) - } - - private func configureContent(_ status: TwitterStatus) { - guard let twitterTextProvider = viewModel.twitterTextProvider else { - assertionFailure() - return - } - - let status = status.repost ?? status - let content = TwitterContent(content: status.displayText) - let metaContent = TwitterMetaContent.convert( - content: content, - urlMaximumLength: 20, - twitterTextProvider: twitterTextProvider - ) - viewModel.spoilerContent = nil - viewModel.isContentReveal = true - viewModel.isContentSensitive = false - viewModel.isContentSensitiveToggled = false - viewModel.content = metaContent - viewModel.sharePlaintextContent = status.displayText - viewModel.language = status.language - viewModel.source = status.source - } - - private func configureMedia(_ status: TwitterStatus) { - let status = status.repost ?? status - - mediaGridContainerView.viewModel.resetContentWarningOverlay() - viewModel.isMediaSensitive = false - viewModel.isMediaSensitiveToggled = false - viewModel.isMediaSensitiveSwitchable = false - viewModel.mediaViewConfigurations = MediaView.configuration(twitterStatus: status) - } - - private func configurePoll(_ status: TwitterStatus) { - let status = status.repost ?? status - - // pollItems - status.publisher(for: \.poll) - .sink { [weak self] poll in - guard let self = self else { return } - guard let poll = poll else { - self.viewModel.pollItems = [] - return - } - - let options = poll.options.sorted(by: { $0.position < $1.position }) - let items: [PollItem] = options.map { .option(record: .twitter(record: .init(objectID: $0.objectID))) } - self.viewModel.pollItems = items - } - .store(in: &disposeBag) - // isVoteButtonEnabled - viewModel.isVoteButtonEnabled = false - // isVotable - viewModel.isVotable = false - // votesCount - if let poll = status.poll { - poll.publisher(for: \.updatedAt) - .map { _ in poll.options.map { Int($0.votes) }.reduce(0, +) } - .assign(to: \.voteCount, on: viewModel) - .store(in: &disposeBag) - } - // voterCount - // none - // expireAt - viewModel.expireAt = status.poll?.endDatetime - // expired - viewModel.expired = status.poll?.votingStatus == .closed - // isVoting - viewModel.isVoting = false - } - - private func configureLocation(_ status: TwitterStatus) { - let status = status.repost ?? status - status.publisher(for: \.location) - .map { $0?.fullName } - .assign(to: \.location, on: viewModel) - .store(in: &disposeBag) - } - - private func configureToolbar(_ status: TwitterStatus) { - let status = status.repost ?? status - - status.publisher(for: \.replyCount) - .map(Int.init) - .assign(to: \.replyCount, on: viewModel) - .store(in: &disposeBag) - status.publisher(for: \.repostCount) - .map(Int.init) - .assign(to: \.repostCount, on: viewModel) - .store(in: &disposeBag) - status.publisher(for: \.quoteCount) - .map(Int.init) - .assign(to: \.quoteCount, on: viewModel) - .store(in: &disposeBag) - status.publisher(for: \.likeCount) - .map(Int.init) - .assign(to: \.likeCount, on: viewModel) - .store(in: &disposeBag) - viewModel.shareStatusURL = status.statusURL.absoluteString - - // relationship - Publishers.CombineLatest( - viewModel.$authenticationContext, - status.publisher(for: \.repostBy) - ) - .map { authenticationContext, repostBy in - guard let authenticationContext = authenticationContext?.twitterAuthenticationContext else { - return false - } - let userID = authenticationContext.userID - return repostBy.contains(where: { $0.id == userID }) - } - .assign(to: \.isRepost, on: viewModel) - .store(in: &disposeBag) - - Publishers.CombineLatest( - viewModel.$authenticationContext, - status.publisher(for: \.likeBy) - ) - .map { authenticationContext, likeBy in - guard let authenticationContext = authenticationContext?.twitterAuthenticationContext else { - return false - } - let userID = authenticationContext.userID - return likeBy.contains(where: { $0.id == userID }) - } - .assign(to: \.isLike, on: viewModel) - .store(in: &disposeBag) - - let authorUserID = status.author.id - viewModel.$authenticationContext - .map { authenticationContext in - guard let authenticationContext = authenticationContext?.twitterAuthenticationContext else { - return false - } - return authenticationContext.userID == authorUserID - } - .assign(to: \.isDeletable, on: viewModel) - .store(in: &disposeBag) - } - - func configureReplySettings(_ status: TwitterStatus) { - let status = status.repost ?? status - - viewModel.replySettings = status.replySettings?.typed - } - -} - -// MARK: - Mastodon - -extension StatusView { - public func configure( - status: MastodonStatus, - notification: MastodonNotification?, - configurationContext: ConfigurationContext - ) { - viewModel.prepareForReuse() - - viewModel.authenticationContext = configurationContext.authContext.authenticationContext - viewModel.managedObjectContext = status.managedObjectContext - viewModel.objects.insert(status) - - viewModel.platform = .mastodon - viewModel.dateTimeProvider = configurationContext.dateTimeProvider - viewModel.twitterTextProvider = configurationContext.twitterTextProvider - - configureHeader(status, notification: notification) - configureAuthor(status) - configureContent(status) - configureMedia(status) - configurePoll(status) - configureToolbar(status) - } - - private func configureHeader( - _ status: MastodonStatus, - notification: MastodonNotification? - ) { - if let notification = notification { - let user = notification.account - let type = notification.notificationType - Publishers.CombineLatest( - user.publisher(for: \.displayName), - user.publisher(for: \.emojis) - ) - .map { _ in - guard let info = NotificationHeaderInfo(type: type, user: user) else { return .none } - return ViewModel.Header.notification(info: info) - } - .assign(to: \.header, on: viewModel) - .store(in: &disposeBag) - } else if let _ = status.repost { - Publishers.CombineLatest( - status.author.publisher(for: \.displayName), - status.author.publisher(for: \.emojis) - ) - .map { _, emojis -> StatusView.ViewModel.Header in - let name = status.author.name - let userRepostText = L10n.Common.Controls.Status.userBoosted(name) - let content = MastodonContent(content: userRepostText, emojis: emojis.asDictionary) - do { - let metaContent = try MastodonMetaContent.convert(document: content) - let info = ViewModel.Header.RepostInfo(authorNameMetaContent: metaContent) - return .repost(info: info) - } catch { - assertionFailure(error.localizedDescription) - let metaContent = PlaintextMetaContent(string: userRepostText) - let info = ViewModel.Header.RepostInfo(authorNameMetaContent: metaContent) - return .repost(info: info) - } - } - .assign(to: \.header, on: viewModel) - .store(in: &disposeBag) - } else { - viewModel.header = .none - } - } - - private func configureAuthor(_ status: MastodonStatus) { - let author = (status.repost ?? status).author - - viewModel.userIdentifier = .mastodon(.init(domain: author.domain, id: author.id)) - - // author avatar - author.publisher(for: \.avatar) - .map { url in url.flatMap { URL(string: $0) } } - .assign(to: \.authorAvatarImageURL, on: viewModel) - .store(in: &disposeBag) - // author name - Publishers.CombineLatest( - author.publisher(for: \.displayName), - author.publisher(for: \.emojis) - ) - .map { _, emojis in - do { - let content = MastodonContent(content: author.name, emojis: emojis.asDictionary) - let metaContent = try MastodonMetaContent.convert(document: content) - return metaContent - } catch { - assertionFailure(error.localizedDescription) - return PlaintextMetaContent(string: author.name) - } - } - .assign(to: \.authorName, on: viewModel) - .store(in: &disposeBag) - // author username - author.publisher(for: \.acct) - .map { $0 as String? } - .assign(to: \.authorUsername, on: viewModel) - .store(in: &disposeBag) - // protected - author.publisher(for: \.locked) - .assign(to: \.protected, on: viewModel) - .store(in: &disposeBag) - // visibility - viewModel.visibility = status.visibility.asStatusVisibility - // timestamp - (status.repost ?? status).publisher(for: \.createdAt) - .map { $0 as Date? } - .assign(to: \.timestamp, on: viewModel) - .store(in: &disposeBag) - } - - private func configureContent(_ status: MastodonStatus) { - let status = status.repost ?? status - do { - let content = MastodonContent(content: status.content, emojis: status.emojis.asDictionary) - let metaContent = try MastodonMetaContent.convert(document: content) - viewModel.content = metaContent - viewModel.sharePlaintextContent = metaContent.original - } catch { - assertionFailure(error.localizedDescription) - viewModel.content = PlaintextMetaContent(string: "") - } - - if let spoilerText = status.spoilerText, !spoilerText.isEmpty { - do { - let content = MastodonContent(content: spoilerText, emojis: status.emojis.asDictionary) - let metaContent = try MastodonMetaContent.convert(document: content) - viewModel.spoilerContent = metaContent - } catch { - assertionFailure() - viewModel.spoilerContent = nil - } - } else { - viewModel.spoilerContent = nil - } - - viewModel.isContentSensitiveToggled = status.isContentSensitiveToggled - status.publisher(for: \.isContentSensitiveToggled) - .assign(to: \.isContentSensitiveToggled, on: viewModel) - .store(in: &disposeBag) - - viewModel.language = status.language - viewModel.source = status.source - } - - private func configureMedia(_ status: MastodonStatus) { - let status = status.repost ?? status - - mediaGridContainerView.viewModel.resetContentWarningOverlay() - viewModel.isMediaSensitiveSwitchable = true - viewModel.mediaViewConfigurations = MediaView.configuration(mastodonStatus: status) - - // set directly without delay - viewModel.isMediaSensitiveToggled = status.isMediaSensitiveToggled - viewModel.isMediaSensitive = status.isMediaSensitive - mediaGridContainerView.configureOverlayDisplay( - isDisplay: status.isMediaSensitiveToggled ? !status.isMediaSensitive : !status.isMediaSensitive, - animated: false - ) - - status.publisher(for: \.isMediaSensitiveToggled) - .receive(on: DispatchQueue.main) - .assign(to: \.isMediaSensitiveToggled, on: viewModel) - .store(in: &disposeBag) - } - - private func configurePoll(_ status: MastodonStatus) { - let status = status.repost ?? status - - // pollItems - status.publisher(for: \.poll) - .sink { [weak self] poll in - guard let self = self else { return } - guard let poll = poll else { - self.viewModel.pollItems = [] - return - } - - let options = poll.options.sorted(by: { $0.index < $1.index }) - let items: [PollItem] = options.map { .option(record: .mastodon(record: .init(objectID: $0.objectID))) } - self.viewModel.pollItems = items - } - .store(in: &disposeBag) - // isVoteButtonEnabled - status.poll?.publisher(for: \.updatedAt) - .sink { [weak self] _ in - guard let self = self else { return } - guard let poll = status.poll else { return } - let options = poll.options - let hasSelectedOption = options.contains(where: { $0.isSelected }) - self.viewModel.isVoteButtonEnabled = hasSelectedOption - } - .store(in: &disposeBag) - // isVotable - if let poll = status.poll { - Publishers.CombineLatest3( - poll.publisher(for: \.voteBy), - poll.publisher(for: \.expired), - viewModel.$authenticationContext - ) - .map { voteBy, expired, authenticationContext in - guard case let .mastodon(authenticationContext) = authenticationContext else { return false } - let domain = authenticationContext.domain - let userID = authenticationContext.userID - let isVoted = voteBy.contains(where: { $0.domain == domain && $0.id == userID }) - return !isVoted && !expired - } - .assign(to: &viewModel.$isVotable) - } - // votesCount - status.poll?.publisher(for: \.votesCount) - .map { Int($0) } - .assign(to: \.voteCount, on: viewModel) - .store(in: &disposeBag) - // voterCount - status.poll?.publisher(for: \.votersCount) - .map { Int($0) } - .assign(to: \.voterCount, on: viewModel) - .store(in: &disposeBag) - // expireAt - status.poll?.publisher(for: \.expiresAt) - .assign(to: \.expireAt, on: viewModel) - .store(in: &disposeBag) - // expired - status.poll?.publisher(for: \.expired) - .assign(to: \.expired, on: viewModel) - .store(in: &disposeBag) - // isVoting - status.poll?.publisher(for: \.isVoting) - .assign(to: \.isVoting, on: viewModel) - .store(in: &disposeBag) - } - - private func configureToolbar(_ status: MastodonStatus) { - let status = status.repost ?? status - - viewModel.quoteCount = 0 - status.publisher(for: \.replyCount) - .map(Int.init) - .assign(to: \.replyCount, on: viewModel) - .store(in: &disposeBag) - status.publisher(for: \.repostCount) - .map(Int.init) - .assign(to: \.repostCount, on: viewModel) - .store(in: &disposeBag) - status.publisher(for: \.likeCount) - .map(Int.init) - .assign(to: \.likeCount, on: viewModel) - .store(in: &disposeBag) - viewModel.shareStatusURL = status.url ?? status.uri - - // relationship - Publishers.CombineLatest( - viewModel.$authenticationContext, - status.publisher(for: \.repostBy) - ) - .map { authenticationContext, repostBy in - guard let authenticationContext = authenticationContext?.mastodonAuthenticationContext else { - return false - } - let domain = authenticationContext.domain - let userID = authenticationContext.userID - return repostBy.contains(where: { $0.id == userID && $0.domain == domain }) - } - .assign(to: \.isRepost, on: viewModel) - .store(in: &disposeBag) - - Publishers.CombineLatest( - viewModel.$authenticationContext, - status.publisher(for: \.likeBy) - ) - .map { authenticationContext, likeBy in - guard let authenticationContext = authenticationContext?.mastodonAuthenticationContext else { - return false - } - let domain = authenticationContext.domain - let userID = authenticationContext.userID - return likeBy.contains(where: { $0.id == userID && $0.domain == domain }) - } - .assign(to: \.isLike, on: viewModel) - .store(in: &disposeBag) - - let authorUserID = status.author.id - viewModel.$authenticationContext - .map { authenticationContext in - guard let authenticationContext = authenticationContext?.mastodonAuthenticationContext else { - return false - } - return authenticationContext.userID == authorUserID - } - .assign(to: \.isDeletable, on: viewModel) - .store(in: &disposeBag) - } - -} +//extension StatusView { +// public func configure( +// feed: Feed, +// configurationContext: ConfigurationContext +// ) { +// switch feed.content { +// case .none: +// logger.log(level: .info, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Warning] feed content missing") +// case .twitter(let status): +// configure( +// status: status, +// configurationContext: configurationContext +// ) +// case .mastodon(let status): +// configure( +// status: status, +// notification: nil, +// configurationContext: configurationContext +// ) +// case .mastodonNotification(let notification): +// guard let status = notification.status else { +// assertionFailure() +// return +// } +// configure( +// status: status, +// notification: notification, +// configurationContext: configurationContext +// ) +// } +// } +// +// public func configure( +// statusObject object: StatusObject, +// configurationContext: ConfigurationContext +// ) { +// switch object { +// case .twitter(let status): +// configure( +// status: status, +// configurationContext: configurationContext +// ) +// case .mastodon(let status): +// configure( +// status: status, +// notification: nil, +// configurationContext: configurationContext +// ) +// } +// } +// +//} +// +//// MARK: - Twitter +// +//extension StatusView { +// public func configure( +// status: TwitterStatus, +// configurationContext: ConfigurationContext +// ) { +// viewModel.prepareForReuse() +// +// viewModel.authenticationContext = configurationContext.authContext.authenticationContext +// viewModel.managedObjectContext = status.managedObjectContext +// viewModel.objects.insert(status) +// +// viewModel.platform = .twitter +// viewModel.dateTimeProvider = configurationContext.dateTimeProvider +// viewModel.twitterTextProvider = configurationContext.twitterTextProvider +// +// configureHeader(status) +// configureAuthor(status) +// configureContent(status) +// configureMedia(status) +// configurePoll(status) +// configureLocation(status) +// configureToolbar(status) +// configureReplySettings(status) +// +// if let quote = status.quote ?? status.repost?.quote { +// quoteStatusView?.configure( +// status: quote, +// configurationContext: configurationContext +// ) +// setQuoteDisplay() +// } +// } +// +// private func configureHeader(_ status: TwitterStatus) { +// if let _ = status.repost { +// status.author.publisher(for: \.name) +// .map { name -> StatusView.ViewModel.Header in +// let userRepostText = L10n.Common.Controls.Status.userRetweeted(name) +// let metaContent = PlaintextMetaContent(string: userRepostText) +// let info = ViewModel.Header.RepostInfo(authorNameMetaContent: metaContent) +// return .repost(info: info) +// } +// .assign(to: \.header, on: viewModel) +// .store(in: &disposeBag) +// } else { +// viewModel.header = .none +// } +// } +// +// private func configureAuthor(_ status: TwitterStatus) { +// guard let dateTimeProvider = viewModel.dateTimeProvider else { +// assertionFailure() +// return +// } +// +// let author = (status.repost ?? status).author +// +// viewModel.userIdentifier = .twitter(.init(id: author.id)) +// +// // author avatar +// author.publisher(for: \.profileImageURL) +// .map { _ in author.avatarImageURL() } +// .assign(to: \.authorAvatarImageURL, on: viewModel) +// .store(in: &disposeBag) +// // lock +// author.publisher(for: \.protected) +// .assign(to: \.protected, on: viewModel) +// .store(in: &disposeBag) +// // author name +// author.publisher(for: \.name) +// .map { PlaintextMetaContent(string: $0) } +// .assign(to: \.authorName, on: viewModel) +// .store(in: &disposeBag) +// // author username +// author.publisher(for: \.username) +// .map { $0 as String? } +// .assign(to: \.authorUsername, on: viewModel) +// .store(in: &disposeBag) +// // timestamp +// viewModel.dateTimeProvider = dateTimeProvider +// (status.repost ?? status).publisher(for: \.createdAt) +// .map { $0 as Date? } +// .assign(to: \.timestamp, on: viewModel) +// .store(in: &disposeBag) +// } +// +// private func configureContent(_ status: TwitterStatus) { +// guard let twitterTextProvider = viewModel.twitterTextProvider else { +// assertionFailure() +// return +// } +// +// let status = status.repost ?? status +// let content = TwitterContent(content: status.displayText) +// let metaContent = TwitterMetaContent.convert( +// content: content, +// urlMaximumLength: 20, +// twitterTextProvider: twitterTextProvider +// ) +// viewModel.spoilerContent = nil +// viewModel.isContentReveal = true +// viewModel.isContentSensitive = false +// viewModel.isContentSensitiveToggled = false +// viewModel.content = metaContent +// viewModel.sharePlaintextContent = status.displayText +// viewModel.language = status.language +// viewModel.source = status.source +// } +// +// private func configureMedia(_ status: TwitterStatus) { +// let status = status.repost ?? status +// +// mediaGridContainerView.viewModel.resetContentWarningOverlay() +// viewModel.isMediaSensitive = false +// viewModel.isMediaSensitiveToggled = false +// viewModel.isMediaSensitiveSwitchable = false +// viewModel.mediaViewConfigurations = MediaView.configuration(twitterStatus: status) +// } +// +// private func configurePoll(_ status: TwitterStatus) { +// let status = status.repost ?? status +// +// // pollItems +// status.publisher(for: \.poll) +// .sink { [weak self] poll in +// guard let self = self else { return } +// guard let poll = poll else { +// self.viewModel.pollItems = [] +// return +// } +// +// let options = poll.options.sorted(by: { $0.position < $1.position }) +// let items: [PollItem] = options.map { .option(record: .twitter(record: .init(objectID: $0.objectID))) } +// self.viewModel.pollItems = items +// } +// .store(in: &disposeBag) +// // isVoteButtonEnabled +// viewModel.isVoteButtonEnabled = false +// // isVotable +// viewModel.isVotable = false +// // votesCount +// if let poll = status.poll { +// poll.publisher(for: \.updatedAt) +// .map { _ in poll.options.map { Int($0.votes) }.reduce(0, +) } +// .assign(to: \.voteCount, on: viewModel) +// .store(in: &disposeBag) +// } +// // voterCount +// // none +// // expireAt +// viewModel.expireAt = status.poll?.endDatetime +// // expired +// viewModel.expired = status.poll?.votingStatus == .closed +// // isVoting +// viewModel.isVoting = false +// } +// +// private func configureLocation(_ status: TwitterStatus) { +// let status = status.repost ?? status +// status.publisher(for: \.location) +// .map { $0?.fullName } +// .assign(to: \.location, on: viewModel) +// .store(in: &disposeBag) +// } +// +// private func configureToolbar(_ status: TwitterStatus) { +// let status = status.repost ?? status +// +// status.publisher(for: \.replyCount) +// .map(Int.init) +// .assign(to: \.replyCount, on: viewModel) +// .store(in: &disposeBag) +// status.publisher(for: \.repostCount) +// .map(Int.init) +// .assign(to: \.repostCount, on: viewModel) +// .store(in: &disposeBag) +// status.publisher(for: \.quoteCount) +// .map(Int.init) +// .assign(to: \.quoteCount, on: viewModel) +// .store(in: &disposeBag) +// status.publisher(for: \.likeCount) +// .map(Int.init) +// .assign(to: \.likeCount, on: viewModel) +// .store(in: &disposeBag) +// viewModel.shareStatusURL = status.statusURL.absoluteString +// +// // relationship +// Publishers.CombineLatest( +// viewModel.$authenticationContext, +// status.publisher(for: \.repostBy) +// ) +// .map { authenticationContext, repostBy in +// guard let authenticationContext = authenticationContext?.twitterAuthenticationContext else { +// return false +// } +// let userID = authenticationContext.userID +// return repostBy.contains(where: { $0.id == userID }) +// } +// .assign(to: \.isRepost, on: viewModel) +// .store(in: &disposeBag) +// +// Publishers.CombineLatest( +// viewModel.$authenticationContext, +// status.publisher(for: \.likeBy) +// ) +// .map { authenticationContext, likeBy in +// guard let authenticationContext = authenticationContext?.twitterAuthenticationContext else { +// return false +// } +// let userID = authenticationContext.userID +// return likeBy.contains(where: { $0.id == userID }) +// } +// .assign(to: \.isLike, on: viewModel) +// .store(in: &disposeBag) +// +// let authorUserID = status.author.id +// viewModel.$authenticationContext +// .map { authenticationContext in +// guard let authenticationContext = authenticationContext?.twitterAuthenticationContext else { +// return false +// } +// return authenticationContext.userID == authorUserID +// } +// .assign(to: \.isDeletable, on: viewModel) +// .store(in: &disposeBag) +// } +// +// func configureReplySettings(_ status: TwitterStatus) { +// let status = status.repost ?? status +// +// viewModel.replySettings = status.replySettings?.typed +// } +// +//} +// +//// MARK: - Mastodon +// +//extension StatusView { +// public func configure( +// status: MastodonStatus, +// notification: MastodonNotification?, +// configurationContext: ConfigurationContext +// ) { +// viewModel.prepareForReuse() +// +// viewModel.authenticationContext = configurationContext.authContext.authenticationContext +// viewModel.managedObjectContext = status.managedObjectContext +// viewModel.objects.insert(status) +// +// viewModel.platform = .mastodon +// viewModel.dateTimeProvider = configurationContext.dateTimeProvider +// viewModel.twitterTextProvider = configurationContext.twitterTextProvider +// +// configureHeader(status, notification: notification) +// configureAuthor(status) +// configureContent(status) +// configureMedia(status) +// configurePoll(status) +// configureToolbar(status) +// } +// +// private func configureHeader( +// _ status: MastodonStatus, +// notification: MastodonNotification? +// ) { +// if let notification = notification { +// let user = notification.account +// let type = notification.notificationType +// Publishers.CombineLatest( +// user.publisher(for: \.displayName), +// user.publisher(for: \.emojis) +// ) +// .map { _ in +// guard let info = NotificationHeaderInfo(type: type, user: user) else { return .none } +// return ViewModel.Header.notification(info: info) +// } +// .assign(to: \.header, on: viewModel) +// .store(in: &disposeBag) +// } else if let _ = status.repost { +// Publishers.CombineLatest( +// status.author.publisher(for: \.displayName), +// status.author.publisher(for: \.emojis) +// ) +// .map { _, emojis -> StatusView.ViewModel.Header in +// let name = status.author.name +// let userRepostText = L10n.Common.Controls.Status.userBoosted(name) +// let content = MastodonContent(content: userRepostText, emojis: emojis.asDictionary) +// do { +// let metaContent = try MastodonMetaContent.convert(document: content) +// let info = ViewModel.Header.RepostInfo(authorNameMetaContent: metaContent) +// return .repost(info: info) +// } catch { +// assertionFailure(error.localizedDescription) +// let metaContent = PlaintextMetaContent(string: userRepostText) +// let info = ViewModel.Header.RepostInfo(authorNameMetaContent: metaContent) +// return .repost(info: info) +// } +// } +// .assign(to: \.header, on: viewModel) +// .store(in: &disposeBag) +// } else { +// viewModel.header = .none +// } +// } +// +// private func configureAuthor(_ status: MastodonStatus) { +// let author = (status.repost ?? status).author +// +// viewModel.userIdentifier = .mastodon(.init(domain: author.domain, id: author.id)) +// +// // author avatar +// author.publisher(for: \.avatar) +// .map { url in url.flatMap { URL(string: $0) } } +// .assign(to: \.authorAvatarImageURL, on: viewModel) +// .store(in: &disposeBag) +// // author name +// Publishers.CombineLatest( +// author.publisher(for: \.displayName), +// author.publisher(for: \.emojis) +// ) +// .map { _, emojis in +// do { +// let content = MastodonContent(content: author.name, emojis: emojis.asDictionary) +// let metaContent = try MastodonMetaContent.convert(document: content) +// return metaContent +// } catch { +// assertionFailure(error.localizedDescription) +// return PlaintextMetaContent(string: author.name) +// } +// } +// .assign(to: \.authorName, on: viewModel) +// .store(in: &disposeBag) +// // author username +// author.publisher(for: \.acct) +// .map { $0 as String? } +// .assign(to: \.authorUsername, on: viewModel) +// .store(in: &disposeBag) +// // protected +// author.publisher(for: \.locked) +// .assign(to: \.protected, on: viewModel) +// .store(in: &disposeBag) +// // visibility +// viewModel.visibility = status.visibility.asStatusVisibility +// // timestamp +// (status.repost ?? status).publisher(for: \.createdAt) +// .map { $0 as Date? } +// .assign(to: \.timestamp, on: viewModel) +// .store(in: &disposeBag) +// } +// +// private func configureContent(_ status: MastodonStatus) { +// let status = status.repost ?? status +// do { +// let content = MastodonContent(content: status.content, emojis: status.emojis.asDictionary) +// let metaContent = try MastodonMetaContent.convert(document: content) +// viewModel.content = metaContent +// viewModel.sharePlaintextContent = metaContent.original +// } catch { +// assertionFailure(error.localizedDescription) +// viewModel.content = PlaintextMetaContent(string: "") +// } +// +// if let spoilerText = status.spoilerText, !spoilerText.isEmpty { +// do { +// let content = MastodonContent(content: spoilerText, emojis: status.emojis.asDictionary) +// let metaContent = try MastodonMetaContent.convert(document: content) +// viewModel.spoilerContent = metaContent +// } catch { +// assertionFailure() +// viewModel.spoilerContent = nil +// } +// } else { +// viewModel.spoilerContent = nil +// } +// +// viewModel.isContentSensitiveToggled = status.isContentSensitiveToggled +// status.publisher(for: \.isContentSensitiveToggled) +// .assign(to: \.isContentSensitiveToggled, on: viewModel) +// .store(in: &disposeBag) +// +// viewModel.language = status.language +// viewModel.source = status.source +// } +// +// private func configureMedia(_ status: MastodonStatus) { +// let status = status.repost ?? status +// +// mediaGridContainerView.viewModel.resetContentWarningOverlay() +// viewModel.isMediaSensitiveSwitchable = true +// viewModel.mediaViewConfigurations = MediaView.configuration(mastodonStatus: status) +// +// // set directly without delay +// viewModel.isMediaSensitiveToggled = status.isMediaSensitiveToggled +// viewModel.isMediaSensitive = status.isMediaSensitive +// mediaGridContainerView.configureOverlayDisplay( +// isDisplay: status.isMediaSensitiveToggled ? !status.isMediaSensitive : !status.isMediaSensitive, +// animated: false +// ) +// +// status.publisher(for: \.isMediaSensitiveToggled) +// .receive(on: DispatchQueue.main) +// .assign(to: \.isMediaSensitiveToggled, on: viewModel) +// .store(in: &disposeBag) +// } +// +// private func configurePoll(_ status: MastodonStatus) { +// let status = status.repost ?? status +// +// // pollItems +// status.publisher(for: \.poll) +// .sink { [weak self] poll in +// guard let self = self else { return } +// guard let poll = poll else { +// self.viewModel.pollItems = [] +// return +// } +// +// let options = poll.options.sorted(by: { $0.index < $1.index }) +// let items: [PollItem] = options.map { .option(record: .mastodon(record: .init(objectID: $0.objectID))) } +// self.viewModel.pollItems = items +// } +// .store(in: &disposeBag) +// // isVoteButtonEnabled +// status.poll?.publisher(for: \.updatedAt) +// .sink { [weak self] _ in +// guard let self = self else { return } +// guard let poll = status.poll else { return } +// let options = poll.options +// let hasSelectedOption = options.contains(where: { $0.isSelected }) +// self.viewModel.isVoteButtonEnabled = hasSelectedOption +// } +// .store(in: &disposeBag) +// // isVotable +// if let poll = status.poll { +// Publishers.CombineLatest3( +// poll.publisher(for: \.voteBy), +// poll.publisher(for: \.expired), +// viewModel.$authenticationContext +// ) +// .map { voteBy, expired, authenticationContext in +// guard case let .mastodon(authenticationContext) = authenticationContext else { return false } +// let domain = authenticationContext.domain +// let userID = authenticationContext.userID +// let isVoted = voteBy.contains(where: { $0.domain == domain && $0.id == userID }) +// return !isVoted && !expired +// } +// .assign(to: &viewModel.$isVotable) +// } +// // votesCount +// status.poll?.publisher(for: \.votesCount) +// .map { Int($0) } +// .assign(to: \.voteCount, on: viewModel) +// .store(in: &disposeBag) +// // voterCount +// status.poll?.publisher(for: \.votersCount) +// .map { Int($0) } +// .assign(to: \.voterCount, on: viewModel) +// .store(in: &disposeBag) +// // expireAt +// status.poll?.publisher(for: \.expiresAt) +// .assign(to: \.expireAt, on: viewModel) +// .store(in: &disposeBag) +// // expired +// status.poll?.publisher(for: \.expired) +// .assign(to: \.expired, on: viewModel) +// .store(in: &disposeBag) +// // isVoting +// status.poll?.publisher(for: \.isVoting) +// .assign(to: \.isVoting, on: viewModel) +// .store(in: &disposeBag) +// } +// +// private func configureToolbar(_ status: MastodonStatus) { +// let status = status.repost ?? status +// +// viewModel.quoteCount = 0 +// status.publisher(for: \.replyCount) +// .map(Int.init) +// .assign(to: \.replyCount, on: viewModel) +// .store(in: &disposeBag) +// status.publisher(for: \.repostCount) +// .map(Int.init) +// .assign(to: \.repostCount, on: viewModel) +// .store(in: &disposeBag) +// status.publisher(for: \.likeCount) +// .map(Int.init) +// .assign(to: \.likeCount, on: viewModel) +// .store(in: &disposeBag) +// viewModel.shareStatusURL = status.url ?? status.uri +// +// // relationship +// Publishers.CombineLatest( +// viewModel.$authenticationContext, +// status.publisher(for: \.repostBy) +// ) +// .map { authenticationContext, repostBy in +// guard let authenticationContext = authenticationContext?.mastodonAuthenticationContext else { +// return false +// } +// let domain = authenticationContext.domain +// let userID = authenticationContext.userID +// return repostBy.contains(where: { $0.id == userID && $0.domain == domain }) +// } +// .assign(to: \.isRepost, on: viewModel) +// .store(in: &disposeBag) +// +// Publishers.CombineLatest( +// viewModel.$authenticationContext, +// status.publisher(for: \.likeBy) +// ) +// .map { authenticationContext, likeBy in +// guard let authenticationContext = authenticationContext?.mastodonAuthenticationContext else { +// return false +// } +// let domain = authenticationContext.domain +// let userID = authenticationContext.userID +// return likeBy.contains(where: { $0.id == userID && $0.domain == domain }) +// } +// .assign(to: \.isLike, on: viewModel) +// .store(in: &disposeBag) +// +// let authorUserID = status.author.id +// viewModel.$authenticationContext +// .map { authenticationContext in +// guard let authenticationContext = authenticationContext?.mastodonAuthenticationContext else { +// return false +// } +// return authenticationContext.userID == authorUserID +// } +// .assign(to: \.isDeletable, on: viewModel) +// .store(in: &disposeBag) +// } +// +//} diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index 425f5645..e43e9a3d 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -24,104 +24,120 @@ import MastodonSDK extension StatusView { public final class ViewModel: ObservableObject { - static let pollOptionOrdinalNumberFormatter: NumberFormatter = { - let formatter = NumberFormatter() - formatter.numberStyle = .ordinal - return formatter - }() - - var disposeBag = Set() - var observations = Set() - var objects = Set() let logger = Logger(subsystem: "StatusView", category: "ViewModel") - @Published public var platform: Platform = .none - @Published public var authenticationContext: AuthenticationContext? // me - @Published public var managedObjectContext: NSManagedObjectContext? - - @Published public var header: Header = .none + @Published public var viewLayoutFrame = ViewLayoutFrame() + + @Published public var repostViewModel: StatusView.ViewModel? + @Published public var quoteViewModel: StatusView.ViewModel? - @Published public var userIdentifier: UserIdentifier? - @Published public var authorAvatarImage: UIImage? - @Published public var authorAvatarImageURL: URL? - @Published public var authorName: MetaContent? - @Published public var authorUsername: String? + // input + public let kind: Kind + public weak var delegate: StatusViewDelegate? - @Published public var protected: Bool = false + // output - @Published public var isMyself = false + // author + @Published public var avatarURL: URL? + @Published public var authorName: MetaContent = PlaintextMetaContent(string: "") + @Published public var authorUsernme = "" + +// static let pollOptionOrdinalNumberFormatter: NumberFormatter = { +// let formatter = NumberFormatter() +// formatter.numberStyle = .ordinal +// return formatter +// }() +// +// var disposeBag = Set() +// var observations = Set() +// var objects = Set() - @Published public var spoilerContent: MetaContent? - - @Published public var content: MetaContent? - @Published public var twitterTextProvider: TwitterTextProvider? - - @Published public var language: String? - @Published public var isTranslateButtonDisplay = false - - @Published public var mediaViewConfigurations: [MediaView.Configuration] = [] - - @Published public var isContentSensitive: Bool = false - @Published public var isContentSensitiveToggled: Bool = false - - @Published public var isContentReveal: Bool = false - - @Published public var isMediaSensitive: Bool = false - @Published public var isMediaSensitiveToggled: Bool = false - - @Published public var isMediaSensitiveSwitchable = false - @Published public var isMediaReveal: Bool = false - - // poll input - @Published public var pollItems: [PollItem] = [] - @Published public var isVotable: Bool = false - @Published public var isVoting: Bool = false - @Published public var isVoteButtonEnabled: Bool = false - @Published public var voterCount: Int? - @Published public var voteCount = 0 - @Published public var expireAt: Date? - @Published public var expired: Bool = false - - // poll output - @Published public var pollVoteDescription = "" - @Published public var pollCountdownDescription: String? - - @Published public var location: String? - @Published public var source: String? - - @Published public var isRepost = false - @Published public var isRepostEnabled = true - - @Published public var isLike = false - - @Published public var replyCount: Int = 0 - @Published public var repostCount: Int = 0 - @Published public var quoteCount: Int = 0 - @Published public var likeCount: Int = 0 - - @Published public var visibility: StatusVisibility? - @Published public var replySettings: Twitter.Entity.V2.Tweet.ReplySettings? - - @Published public var dateTimeProvider: DateTimeProvider? - @Published public var timestamp: Date? - @Published public var timeAgoStyleTimestamp: String? - @Published public var formattedStyleTimestamp: String? - - @Published public var sharePlaintextContent: String? - @Published public var shareStatusURL: String? - - @Published public var isDeletable = false - - @Published public var groupedAccessibilityLabel = "" - - let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common) - .autoconnect() - .share() - .eraseToAnyPublisher() - - // public let contentRevealChangePublisher = PassthroughSubject() - +// @Published public var platform: Platform = .none +// @Published public var authenticationContext: AuthenticationContext? // me +// @Published public var managedObjectContext: NSManagedObjectContext? +// +// @Published public var header: Header = .none +// +// @Published public var userIdentifier: UserIdentifier? +// @Published public var authorAvatarImage: UIImage? +// @Published public var authorAvatarImageURL: URL? +// @Published public var authorUsername: String? +// +// @Published public var protected: Bool = false +// +// @Published public var isMyself = false +// + @Published public var spoilerContent: MetaContent = PlaintextMetaContent(string: "") + @Published public var content: MetaContent = PlaintextMetaContent(string: "") + +// @Published public var twitterTextProvider: TwitterTextProvider? +// +// @Published public var language: String? +// @Published public var isTranslateButtonDisplay = false +// +// @Published public var mediaViewConfigurations: [MediaView.Configuration] = [] +// +// @Published public var isContentSensitive: Bool = false +// @Published public var isContentSensitiveToggled: Bool = false +// +// @Published public var isContentReveal: Bool = false +// +// @Published public var isMediaSensitive: Bool = false +// @Published public var isMediaSensitiveToggled: Bool = false +// +// @Published public var isMediaSensitiveSwitchable = false +// @Published public var isMediaReveal: Bool = false +// +// // poll input +// @Published public var pollItems: [PollItem] = [] +// @Published public var isVotable: Bool = false +// @Published public var isVoting: Bool = false +// @Published public var isVoteButtonEnabled: Bool = false +// @Published public var voterCount: Int? +// @Published public var voteCount = 0 +// @Published public var expireAt: Date? +// @Published public var expired: Bool = false +// +// // poll output +// @Published public var pollVoteDescription = "" +// @Published public var pollCountdownDescription: String? +// +// @Published public var location: String? +// @Published public var source: String? +// +// @Published public var isRepost = false +// @Published public var isRepostEnabled = true +// +// @Published public var isLike = false +// +// @Published public var replyCount: Int = 0 +// @Published public var repostCount: Int = 0 +// @Published public var quoteCount: Int = 0 +// @Published public var likeCount: Int = 0 +// +// @Published public var visibility: StatusVisibility? +// @Published public var replySettings: Twitter.Entity.V2.Tweet.ReplySettings? +// +// @Published public var dateTimeProvider: DateTimeProvider? +// @Published public var timestamp: Date? +// @Published public var timeAgoStyleTimestamp: String? +// @Published public var formattedStyleTimestamp: String? +// +// @Published public var sharePlaintextContent: String? +// @Published public var shareStatusURL: String? +// +// @Published public var isDeletable = false +// +// @Published public var groupedAccessibilityLabel = "" +// +// let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common) +// .autoconnect() +// .share() +// .eraseToAnyPublisher() +// +// // public let contentRevealChangePublisher = PassthroughSubject() +// public enum Header { case none case repost(info: RepostInfo) @@ -133,713 +149,859 @@ extension StatusView { } } - public func prepareForReuse() { - replySettings = nil - } - - init() { - // isMyself - Publishers.CombineLatest( - $authenticationContext, - $userIdentifier - ) - .map { authenticationContext, userIdentifier in - guard let authenticationContext = authenticationContext, - let userIdentifier = userIdentifier - else { return false } - return authenticationContext.userIdentifier == userIdentifier - } - .assign(to: &$isMyself) - // isContentSensitive - Publishers.CombineLatest( - $platform, - $spoilerContent - ) - .map { platform, spoilerContent in - switch platform { - case .none: return false - case .twitter: return false - case .mastodon: return spoilerContent != nil - } - } - .assign(to: &$isContentSensitive) - // isContentReveal - Publishers.CombineLatest( - $isContentSensitive, - $isContentSensitiveToggled - ) - .map { $0 ? $1 : !$1 } - .assign(to: &$isContentReveal) - // isMediaReveal - Publishers.CombineLatest( - $isMediaSensitive, - $isMediaSensitiveToggled - ) - .map { $0 ? $1 : !$1 } - .assign(to: &$isMediaReveal) - // isRepostEnabled - Publishers.CombineLatest4( - $platform, - $visibility, - $protected, - $isMyself - ) - .map { platform, visibility, protected, isMyself in - switch platform { - case .none: - return true - case .twitter: - return isMyself ? true : !protected - case .mastodon: - if isMyself { - return true - } - switch visibility { - case .none: - return true - case .mastodon(let visibility): - switch visibility { - case .public, .unlisted: - return true - case .private, .direct, ._other: - return false - } - } - } - } - .assign(to: &$isRepostEnabled) +// public func prepareForReuse() { +// replySettings = nil +// } +// + init( + kind: Kind, + delegate: StatusViewDelegate?, + viewLayoutFramePublisher: Published.Publisher? + ) { + self.kind = kind + self.delegate = delegate + // end init - Publishers.CombineLatest( - UserDefaults.shared.publisher(for: \.translateButtonPreference), - $language - ) - .map { preference, language -> Bool in - switch preference { - case .auto: - guard let language = language, !language.isEmpty else { - // default hidden - return false - } - let contentLocale = Locale(identifier: language) - guard let currentLanguageCode = Locale.current.languageCode, - let contentLanguageCode = contentLocale.languageCode - else { return true } - return currentLanguageCode != contentLanguageCode - case .always: return true - case .off: return false - } - } - .assign(to: &$isTranslateButtonDisplay) + viewLayoutFramePublisher?.assign(to: &$viewLayoutFrame) + +// // isMyself +// Publishers.CombineLatest( +// $authenticationContext, +// $userIdentifier +// ) +// .map { authenticationContext, userIdentifier in +// guard let authenticationContext = authenticationContext, +// let userIdentifier = userIdentifier +// else { return false } +// return authenticationContext.userIdentifier == userIdentifier +// } +// .assign(to: &$isMyself) +// // isContentSensitive +// Publishers.CombineLatest( +// $platform, +// $spoilerContent +// ) +// .map { platform, spoilerContent in +// switch platform { +// case .none: return false +// case .twitter: return false +// case .mastodon: return spoilerContent != nil +// } +// } +// .assign(to: &$isContentSensitive) +// // isContentReveal +// Publishers.CombineLatest( +// $isContentSensitive, +// $isContentSensitiveToggled +// ) +// .map { $0 ? $1 : !$1 } +// .assign(to: &$isContentReveal) +// // isMediaReveal +// Publishers.CombineLatest( +// $isMediaSensitive, +// $isMediaSensitiveToggled +// ) +// .map { $0 ? $1 : !$1 } +// .assign(to: &$isMediaReveal) +// // isRepostEnabled +// Publishers.CombineLatest4( +// $platform, +// $visibility, +// $protected, +// $isMyself +// ) +// .map { platform, visibility, protected, isMyself in +// switch platform { +// case .none: +// return true +// case .twitter: +// return isMyself ? true : !protected +// case .mastodon: +// if isMyself { +// return true +// } +// switch visibility { +// case .none: +// return true +// case .mastodon(let visibility): +// switch visibility { +// case .public, .unlisted: +// return true +// case .private, .direct, ._other: +// return false +// } +// } +// } +// } +// .assign(to: &$isRepostEnabled) +// +// Publishers.CombineLatest( +// UserDefaults.shared.publisher(for: \.translateButtonPreference), +// $language +// ) +// .map { preference, language -> Bool in +// switch preference { +// case .auto: +// guard let language = language, !language.isEmpty else { +// // default hidden +// return false +// } +// let contentLocale = Locale(identifier: language) +// guard let currentLanguageCode = Locale.current.languageCode, +// let contentLanguageCode = contentLocale.languageCode +// else { return true } +// return currentLanguageCode != contentLanguageCode +// case .always: return true +// case .off: return false +// } +// } +// .assign(to: &$isTranslateButtonDisplay) } } } +//extension StatusView.ViewModel { +// func bind(statusView: StatusView) { +// bindHeader(statusView: statusView) +// bindAuthor(statusView: statusView) +// bindContent(statusView: statusView) +// bindMedia(statusView: statusView) +// bindPoll(statusView: statusView) +// bindLocation(statusView: statusView) +// bindToolbar(statusView: statusView) +// bindReplySettings(statusView: statusView) +// bindAccessibility(statusView: statusView) +// } +// +// private func bindHeader(statusView: StatusView) { +// $header +// .sink { header in +// switch header { +// case .none: +// return +// case .repost(let info): +// statusView.headerIconImageView.image = Asset.Media.repeat.image +// statusView.headerIconImageView.tintColor = Asset.Colors.Theme.daylight.color +// statusView.headerTextLabel.setupAttributes(style: StatusView.headerTextLabelStyle) +// statusView.headerTextLabel.configure(content: info.authorNameMetaContent) +// statusView.setHeaderDisplay() +// case .notification(let info): +// statusView.headerIconImageView.image = info.iconImage +// statusView.headerIconImageView.tintColor = info.iconImageTintColor +// statusView.headerTextLabel.setupAttributes(style: StatusView.headerTextLabelStyle) +// statusView.headerTextLabel.configure(content: info.textMetaContent) +// statusView.setHeaderDisplay() +// } +// } +// .store(in: &disposeBag) +// } +// +// private func bindAuthor(statusView: StatusView) { +// // avatar +// Publishers.CombineLatest( +// $authorAvatarImage, +// $authorAvatarImageURL +// ) +// .sink { image, url in +// let configuration: AvatarImageView.Configuration = { +// if let image = image { +// return AvatarImageView.Configuration(image: image) +// } else { +// return AvatarImageView.Configuration(url: url) +// } +// }() +// statusView.authorAvatarButton.avatarImageView.configure(configuration: configuration) +// } +// .store(in: &disposeBag) +// UserDefaults.shared +// .observe(\.avatarStyle, options: [.initial, .new]) { defaults, _ in +// +// let avatarStyle = defaults.avatarStyle +// let animator = UIViewPropertyAnimator(duration: 0.3, timingParameters: UISpringTimingParameters()) +// animator.addAnimations { +// switch avatarStyle { +// case .circle: +// statusView.authorAvatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .circle)) +// case .roundedSquare: +// statusView.authorAvatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .scale(ratio: 4))) +// } +// } +// animator.startAnimation() +// } +// .store(in: &observations) +// // lock +// $protected +// .sink { protected in +// statusView.lockImageView.isHidden = !protected +// } +// .store(in: &disposeBag) +// // name +// $authorName +// .sink { metaContent in +// let metaContent = metaContent ?? PlaintextMetaContent(string: "") +// statusView.authorNameLabel.setupAttributes(style: StatusView.authorNameLabelStyle) +// statusView.authorNameLabel.configure(content: metaContent) +// } +// .store(in: &disposeBag) +// // username +// $authorUsername +// .map { text in +// guard let text = text else { return "" } +// return "@\(text)" +// } +// .assign(to: \.text, on: statusView.authorUsernameLabel) +// .store(in: &disposeBag) +// // visibility +// $visibility +// .sink { visibility in +// guard let visibility = visibility, +// let image = visibility.inlineImage +// else { return } +// +// statusView.visibilityImageView.image = image +// statusView.visibilityImageView.accessibilityLabel = visibility.accessibilityLabel +// statusView.visibilityImageView.accessibilityTraits = .staticText +// statusView.visibilityImageView.isAccessibilityElement = true +// statusView.setVisibilityDisplay() +// } +// .store(in: &disposeBag) +// // timestamp +// Publishers.CombineLatest3( +// $timestamp, +// $dateTimeProvider, +// timestampUpdatePublisher.prepend(Date()).eraseToAnyPublisher() +// ) +// .sink { [weak self] timestamp, dateTimeProvider, _ in +// guard let self = self else { return } +// self.timeAgoStyleTimestamp = dateTimeProvider?.shortTimeAgoSinceNow(to: timestamp) +// self.formattedStyleTimestamp = { +// let formatter = DateFormatter() +// formatter.dateStyle = .medium +// formatter.timeStyle = .medium +// let text = timestamp.flatMap { formatter.string(from: $0) } +// return text +// }() +// } +// .store(in: &disposeBag) +// $timeAgoStyleTimestamp +// .sink { timestamp in +// statusView.timestampLabel.text = timestamp +// } +// .store(in: &disposeBag) +// $formattedStyleTimestamp +// .sink { timestamp in +// statusView.metricsDashboardView.timestampLabel.text = timestamp +// } +// .store(in: &disposeBag) +// } +// +// private func bindContent(statusView: StatusView) { +// $content +// .sink { metaContent in +// guard let content = metaContent else { +// statusView.contentTextView.reset() +// return +// } +// statusView.contentTextView.configure(content: content) +// } +// .store(in: &disposeBag) +// $spoilerContent +// .sink { metaContent in +// guard let metaContent = metaContent else { +// statusView.spoilerContentTextView.reset() +// return +// } +// statusView.spoilerContentTextView.configure(content: metaContent) +// statusView.setSpoilerDisplay() +// } +// .store(in: &disposeBag) +// $isContentReveal +// .sink { isContentReveal in +// statusView.contentTextView.isHidden = !isContentReveal +// +// let label = isContentReveal ? L10n.Accessibility.Common.Status.Actions.hideContent : L10n.Accessibility.Common.Status.Actions.revealContent +// statusView.expandContentButton.accessibilityLabel = label +// } +// .store(in: &disposeBag) +// $isTranslateButtonDisplay +// .sink { isTranslateButtonDisplay in +// if isTranslateButtonDisplay { +// statusView.setTranslateButtonDisplay() +// } +// } +// .store(in: &disposeBag) +// $source +// .sink { source in +// statusView.metricsDashboardView.sourceLabel.text = source ?? "" +// } +// .store(in: &disposeBag) +// // dashboard +// $platform +// .assign(to: \.platform, on: statusView.metricsDashboardView.viewModel) +// .store(in: &disposeBag) +// Publishers.CombineLatest4( +// $replyCount, +// $repostCount, +// $quoteCount, +// $likeCount +// ) +// .sink { replyCount, repostCount, quoteCount, likeCount in +// switch statusView.style { +// case .plain: +// statusView.metricsDashboardView.viewModel.replyCount = replyCount +// statusView.metricsDashboardView.viewModel.repostCount = repostCount +// statusView.metricsDashboardView.viewModel.quoteCount = quoteCount +// statusView.metricsDashboardView.viewModel.likeCount = likeCount +// default: +// break +// } +// } +// .store(in: &disposeBag) +// } +// +// private func bindMedia(statusView: StatusView) { +// $mediaViewConfigurations +// .sink { [weak self] configurations in +// guard let self = self else { return } +// // self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): configure media") +// +// let maxSize = CGSize( +// width: statusView.contentMaxLayoutWidth, +// height: statusView.contentMaxLayoutWidth +// ) +// var needsDisplay = true +// switch configurations.count { +// case 0: +// needsDisplay = false +// case 1: +// let configuration = configurations[0] +// let adaptiveLayout = MediaGridContainerView.AdaptiveLayout( +// aspectRatio: configuration.aspectRadio, +// maxSize: maxSize +// ) +// let mediaView = statusView.mediaGridContainerView.dequeueMediaView(adaptiveLayout: adaptiveLayout) +// mediaView.setup(configuration: configuration) +// default: +// let gridLayout = MediaGridContainerView.GridLayout( +// count: configurations.count, +// maxSize: maxSize +// ) +// let mediaViews = statusView.mediaGridContainerView.dequeueMediaView(gridLayout: gridLayout) +// for (i, (configuration, mediaView)) in zip(configurations, mediaViews).enumerated() { +// guard i < MediaGridContainerView.maxCount else { break } +// mediaView.setup(configuration: configuration) +// } +// } +// if needsDisplay { +// statusView.setMediaDisplay() +// } +// } +// .store(in: &disposeBag) +// $isMediaReveal +// .sink { isMediaReveal in +// statusView.mediaGridContainerView.viewModel.isContentWarningOverlayDisplay = !isMediaReveal +// } +// .store(in: &disposeBag) +// $isMediaSensitiveSwitchable +// .sink { isMediaSensitiveSwitchable in +// statusView.mediaGridContainerView.viewModel.isSensitiveToggleButtonDisplay = isMediaSensitiveSwitchable +// } +// .store(in: &disposeBag) +// } +// +// private func bindPoll(statusView: StatusView) { +// $pollItems +// .sink { items in +// guard !items.isEmpty else { return } +// +// var snapshot = NSDiffableDataSourceSnapshot() +// snapshot.appendSections([.main]) +// snapshot.appendItems(items, toSection: .main) +// statusView.pollTableViewDiffableDataSource?.applySnapshotUsingReloadData(snapshot) +// +// statusView.pollTableViewHeightLayoutConstraint.constant = CGFloat(items.count) * PollOptionTableViewCell.height +// statusView.setPollDisplay() +// } +// .store(in: &disposeBag) +// $isVotable +// .sink { isVotable in +// statusView.pollTableView.allowsSelection = isVotable +// } +// .store(in: &disposeBag) +// // poll +// Publishers.CombineLatest( +// $voterCount, +// $voteCount +// ) +// .map { voterCount, voteCount -> String in +// var description = "" +// if let voterCount = voterCount { +// description += L10n.Count.people(voterCount) +// } else { +// description += L10n.Count.vote(voteCount) +// } +// return description +// } +// .assign(to: &$pollVoteDescription) +// Publishers.CombineLatest3( +// $expireAt, +// $expired, +// timestampUpdatePublisher.prepend(Date()).eraseToAnyPublisher() +// ) +// .map { expireAt, expired, _ -> String? in +// guard !expired else { +// return L10n.Common.Controls.Status.Poll.expired +// } +// +// guard let expireAt = expireAt, +// let timeLeft = expireAt.localizedTimeLeft +// else { +// return nil +// } +// +// return timeLeft +// } +// .assign(to: &$pollCountdownDescription) +// Publishers.CombineLatest( +// $pollVoteDescription, +// $pollCountdownDescription +// ) +// .sink { pollVoteDescription, pollCountdownDescription in +// let description = [ +// pollVoteDescription, +// pollCountdownDescription +// ] +// .compactMap { $0 } +// +// statusView.pollVoteDescriptionLabel.text = description.joined(separator: " · ") +// statusView.pollVoteDescriptionLabel.accessibilityLabel = description.joined(separator: ", ") +// } +// .store(in: &disposeBag) +// Publishers.CombineLatest( +// $isVotable, +// $isVoting +// ) +// .sink { isVotable, isVoting in +// guard isVotable else { +// statusView.pollVoteButton.isHidden = true +// statusView.pollVoteActivityIndicatorView.isHidden = true +// return +// } +// +// statusView.pollVoteButton.isHidden = isVoting +// statusView.pollVoteActivityIndicatorView.isHidden = !isVoting +// statusView.pollVoteActivityIndicatorView.startAnimating() +// } +// .store(in: &disposeBag) +// $isVoteButtonEnabled +// .assign(to: \.isEnabled, on: statusView.pollVoteButton) +// .store(in: &disposeBag) +// } +// +// private func bindLocation(statusView: StatusView) { +// $location +// .sink { location in +// guard let location = location, !location.isEmpty else { +// statusView.locationLabel.isAccessibilityElement = false +// return +// } +// statusView.locationLabel.isAccessibilityElement = true +// +// if statusView.traitCollection.preferredContentSizeCategory > .extraLarge { +// statusView.locationMapPinImageView.image = Asset.ObjectTools.mappin.image +// } else { +// statusView.locationMapPinImageView.image = Asset.ObjectTools.mappinMini.image +// } +// statusView.locationLabel.text = location +// statusView.locationLabel.accessibilityLabel = location +// +// statusView.setLocationDisplay() +// } +// .store(in: &disposeBag) +// } +// +// private func bindToolbar(statusView: StatusView) { +// // platform +// $platform +// .assign(to: \.platform, on: statusView.toolbar.viewModel) +// .store(in: &disposeBag) +// // reply +// $replyCount +// .sink { count in +// statusView.toolbar.setupReply(count: count, isEnabled: true) // TODO: +// } +// .store(in: &disposeBag) +// // repost +// Publishers.CombineLatest3( +// $repostCount, +// $isRepost, +// $isRepostEnabled +// ) +// .sink { count, isRepost, isEnabled in +// statusView.toolbar.setupRepost(count: count, isEnabled: isEnabled, isHighlighted: isRepost) +// } +// .store(in: &disposeBag) +// // like +// Publishers.CombineLatest( +// $likeCount, +// $isLike +// ) +// .sink { count, isLike in +// statusView.toolbar.setupLike(count: count, isHighlighted: isLike) +// } +// .store(in: &disposeBag) +// // menu +// Publishers.CombineLatest4( +// $sharePlaintextContent, +// $shareStatusURL, +// $mediaViewConfigurations, +// $isDeletable +// ) +// .sink { sharePlaintextContent, shareStatusURL, mediaViewConfigurations, isDeletable in +// statusView.toolbar.setupMenu(menuContext: .init( +// shareText: sharePlaintextContent, +// shareLink: shareStatusURL, +// displaySaveMediaAction: !mediaViewConfigurations.isEmpty, +// displayDeleteAction: isDeletable +// )) +// } +// .store(in: &disposeBag) +// } +// +// private func bindReplySettings(statusView: StatusView) { +// Publishers.CombineLatest( +// $replySettings, +// $authorUsername +// ) +// .sink { replySettings, authorUsername in +// guard let replySettings = replySettings else { return } +// guard let authorUsername = authorUsername else { return } +// switch replySettings { +// case .everyone: +// return +// case .following: +// statusView.replySettingBannerView.imageView.image = Asset.Communication.at.image.withRenderingMode(.alwaysTemplate) +// statusView.replySettingBannerView.label.text = L10n.Common.Controls.Status.ReplySettings.peopleUserFollowsOrMentionedCanReply("@\(authorUsername)") +// case .mentionedUsers: +// statusView.replySettingBannerView.imageView.image = Asset.Human.personCheckMini.image.withRenderingMode(.alwaysTemplate) +// statusView.replySettingBannerView.label.text = L10n.Common.Controls.Status.ReplySettings.peopleUserMentionedCanReply("@\(authorUsername)") +// } +// statusView.setReplySettingsDisplay() +// } +// .store(in: &disposeBag) +// } +// +// private func bindAccessibility(statusView: StatusView) { +// let authorAccessibilityLabel = Publishers.CombineLatest( +// $header, +// $authorName +// ) +// .map { header, authorName -> String? in +// var strings: [String?] = [] +// +// switch header { +// case .none: +// break +// case .notification(let info): +// strings.append(info.textMetaContent.string) +// case .repost(let info): +// strings.append(info.authorNameMetaContent.string) +// } +// +// strings.append(authorName?.string) +// +// return strings.compactMap { $0 }.joined(separator: ", ") +// } +// +// let metaAccessibilityLabel = Publishers.CombineLatest( +// $timeAgoStyleTimestamp, +// $visibility +// ) +// .map { timestamp, visibility -> String? in +// var strings: [String?] = [] +// +// strings.append(visibility?.accessibilityLabel) +// strings.append(timestamp) +// +// return strings.compactMap { $0 }.joined(separator: ", ") +// } +// +// let contentAccessibilityLabel = Publishers.CombineLatest4( +// $platform, +// $isContentReveal, +// $spoilerContent, +// $content +// ) +// .map { platform, isContentReveal, spoilerContent, content -> String? in +// var strings: [String?] = [] +// switch platform { +// case .none: +// break +// case .twitter: +// strings.append(content?.string) +// case .mastodon: +// if let spoilerContent = spoilerContent?.string { +// strings.append(L10n.Accessibility.Common.Status.contentWarning) +// strings.append(spoilerContent) +// } +// if isContentReveal { +// strings.append(content?.string) +// } +// } +// +// return strings.compactMap { $0 }.joined(separator: ", ") +// } +// +// let mediaAccessibilityLabel = $mediaViewConfigurations +// .map { configurations -> String? in +// let count = configurations.count +// return count > 0 ? L10n.Count.media(count) : nil +// } +// +// let toolbarAccessibilityLabel = Publishers.CombineLatest3( +// $platform, +// $isRepost, +// $isLike +// ) +// .map { platform, isRepost, isLike -> String? in +// var strings: [String?] = [] +// +// switch platform { +// case .none: +// break +// case .twitter: +// if isRepost { +// strings.append(L10n.Accessibility.Common.Status.retweeted) +// } +// case .mastodon: +// if isRepost { +// strings.append(L10n.Accessibility.Common.Status.boosted) +// } +// } +// +// if isLike { +// strings.append(L10n.Accessibility.Common.Status.liked) +// } +// +// return strings.compactMap { $0 }.joined(separator: ", ") +// } +// +// let pollAccessibilityLabel = Publishers.CombineLatest3( +// $pollItems, +// $pollVoteDescription, +// $pollCountdownDescription +// ) +// .map { items, pollVoteDescription, pollCountdownDescription -> String? in +// guard !items.isEmpty else { return nil } +// guard let managedObjectContext = self.managedObjectContext else { return nil } +// +// var strings: [String?] = [] +// +// let ordinalPrefix = L10n.Accessibility.Common.Status.pollOptionOrdinalPrefix +// +// for (i, item) in items.enumerated() { +// switch item { +// case .option(let record): +// guard let option = record.object(in: managedObjectContext) else { continue } +// let number = NSNumber(value: i + 1) +// guard let ordinal = StatusView.ViewModel.pollOptionOrdinalNumberFormatter.string(from: number) else { break } +// strings.append("\(ordinalPrefix), \(ordinal), \(option.title)") +// +// if option.isSelected { +// strings.append(L10n.Accessibility.VoiceOver.selected) +// } +// } +// } +// +// strings.append(pollVoteDescription) +// pollCountdownDescription.flatMap { strings.append($0) } +// +// return strings.compactMap { $0 }.joined(separator: ", ") +// } +// +// let groupOne = Publishers.CombineLatest4( +// authorAccessibilityLabel, +// metaAccessibilityLabel, +// contentAccessibilityLabel, +// mediaAccessibilityLabel +// ) +// .map { a, b, c, d -> String? in +// return [a, b, c, d] +// .compactMap { $0 } +// .joined(separator: ", ") +// } +// +// let groupTwo = Publishers.CombineLatest3( +// pollAccessibilityLabel, +// $location, +// toolbarAccessibilityLabel +// ) +// .map { a, b, c -> String? in +// return [a, b, c] +// .compactMap { $0 } +// .joined(separator: ", ") +// } +// +// Publishers.CombineLatest( +// groupOne, +// groupTwo +// ) +// .map { a, b -> String in +// return [a, b] +// .compactMap { $0 } +// .joined(separator: ", ") +// } +// .assign(to: &$groupedAccessibilityLabel) +// +// $groupedAccessibilityLabel +// .sink { accessibilityLabel in +// statusView.accessibilityLabel = accessibilityLabel +// } +// .store(in: &disposeBag) +// +// // poll +// $pollItems +// .sink { items in +// statusView.pollVoteDescriptionLabel.isAccessibilityElement = !items.isEmpty +// statusView.pollVoteButton.isAccessibilityElement = !items.isEmpty +// } +// .store(in: &disposeBag) +// } +// +//} + extension StatusView.ViewModel { - func bind(statusView: StatusView) { - bindHeader(statusView: statusView) - bindAuthor(statusView: statusView) - bindContent(statusView: statusView) - bindMedia(statusView: statusView) - bindPoll(statusView: statusView) - bindLocation(statusView: statusView) - bindToolbar(statusView: statusView) - bindReplySettings(statusView: statusView) - bindAccessibility(statusView: statusView) - } - - private func bindHeader(statusView: StatusView) { - $header - .sink { header in - switch header { - case .none: - return - case .repost(let info): - statusView.headerIconImageView.image = Asset.Media.repeat.image - statusView.headerIconImageView.tintColor = Asset.Colors.Theme.daylight.color - statusView.headerTextLabel.setupAttributes(style: StatusView.headerTextLabelStyle) - statusView.headerTextLabel.configure(content: info.authorNameMetaContent) - statusView.setHeaderDisplay() - case .notification(let info): - statusView.headerIconImageView.image = info.iconImage - statusView.headerIconImageView.tintColor = info.iconImageTintColor - statusView.headerTextLabel.setupAttributes(style: StatusView.headerTextLabelStyle) - statusView.headerTextLabel.configure(content: info.textMetaContent) - statusView.setHeaderDisplay() - } - } - .store(in: &disposeBag) - } - - private func bindAuthor(statusView: StatusView) { - // avatar - Publishers.CombineLatest( - $authorAvatarImage, - $authorAvatarImageURL - ) - .sink { image, url in - let configuration: AvatarImageView.Configuration = { - if let image = image { - return AvatarImageView.Configuration(image: image) - } else { - return AvatarImageView.Configuration(url: url) - } - }() - statusView.authorAvatarButton.avatarImageView.configure(configuration: configuration) - } - .store(in: &disposeBag) - UserDefaults.shared - .observe(\.avatarStyle, options: [.initial, .new]) { defaults, _ in - - let avatarStyle = defaults.avatarStyle - let animator = UIViewPropertyAnimator(duration: 0.3, timingParameters: UISpringTimingParameters()) - animator.addAnimations { - switch avatarStyle { - case .circle: - statusView.authorAvatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .circle)) - case .roundedSquare: - statusView.authorAvatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .scale(ratio: 4))) - } - } - animator.startAnimation() - } - .store(in: &observations) - // lock - $protected - .sink { protected in - statusView.lockImageView.isHidden = !protected - } - .store(in: &disposeBag) - // name - $authorName - .sink { metaContent in - let metaContent = metaContent ?? PlaintextMetaContent(string: "") - statusView.authorNameLabel.setupAttributes(style: StatusView.authorNameLabelStyle) - statusView.authorNameLabel.configure(content: metaContent) - } - .store(in: &disposeBag) - // username - $authorUsername - .map { text in - guard let text = text else { return "" } - return "@\(text)" - } - .assign(to: \.text, on: statusView.authorUsernameLabel) - .store(in: &disposeBag) - // visibility - $visibility - .sink { visibility in - guard let visibility = visibility, - let image = visibility.inlineImage - else { return } - - statusView.visibilityImageView.image = image - statusView.visibilityImageView.accessibilityLabel = visibility.accessibilityLabel - statusView.visibilityImageView.accessibilityTraits = .staticText - statusView.visibilityImageView.isAccessibilityElement = true - statusView.setVisibilityDisplay() - } - .store(in: &disposeBag) - // timestamp - Publishers.CombineLatest3( - $timestamp, - $dateTimeProvider, - timestampUpdatePublisher.prepend(Date()).eraseToAnyPublisher() - ) - .sink { [weak self] timestamp, dateTimeProvider, _ in - guard let self = self else { return } - self.timeAgoStyleTimestamp = dateTimeProvider?.shortTimeAgoSinceNow(to: timestamp) - self.formattedStyleTimestamp = { - let formatter = DateFormatter() - formatter.dateStyle = .medium - formatter.timeStyle = .medium - let text = timestamp.flatMap { formatter.string(from: $0) } - return text - }() - } - .store(in: &disposeBag) - $timeAgoStyleTimestamp - .sink { timestamp in - statusView.timestampLabel.text = timestamp - } - .store(in: &disposeBag) - $formattedStyleTimestamp - .sink { timestamp in - statusView.metricsDashboardView.timestampLabel.text = timestamp - } - .store(in: &disposeBag) - } - - private func bindContent(statusView: StatusView) { - $content - .sink { metaContent in - guard let content = metaContent else { - statusView.contentTextView.reset() - return - } - statusView.contentTextView.configure(content: content) - } - .store(in: &disposeBag) - $spoilerContent - .sink { metaContent in - guard let metaContent = metaContent else { - statusView.spoilerContentTextView.reset() - return - } - statusView.spoilerContentTextView.configure(content: metaContent) - statusView.setSpoilerDisplay() - } - .store(in: &disposeBag) - $isContentReveal - .sink { isContentReveal in - statusView.contentTextView.isHidden = !isContentReveal - - let label = isContentReveal ? L10n.Accessibility.Common.Status.Actions.hideContent : L10n.Accessibility.Common.Status.Actions.revealContent - statusView.expandContentButton.accessibilityLabel = label - } - .store(in: &disposeBag) - $isTranslateButtonDisplay - .sink { isTranslateButtonDisplay in - if isTranslateButtonDisplay { - statusView.setTranslateButtonDisplay() - } - } - .store(in: &disposeBag) - $source - .sink { source in - statusView.metricsDashboardView.sourceLabel.text = source ?? "" - } - .store(in: &disposeBag) - // dashboard - $platform - .assign(to: \.platform, on: statusView.metricsDashboardView.viewModel) - .store(in: &disposeBag) - Publishers.CombineLatest4( - $replyCount, - $repostCount, - $quoteCount, - $likeCount - ) - .sink { replyCount, repostCount, quoteCount, likeCount in - switch statusView.style { - case .plain: - statusView.metricsDashboardView.viewModel.replyCount = replyCount - statusView.metricsDashboardView.viewModel.repostCount = repostCount - statusView.metricsDashboardView.viewModel.quoteCount = quoteCount - statusView.metricsDashboardView.viewModel.likeCount = likeCount - default: - break - } - } - .store(in: &disposeBag) - } - - private func bindMedia(statusView: StatusView) { - $mediaViewConfigurations - .sink { [weak self] configurations in - guard let self = self else { return } - // self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): configure media") - - let maxSize = CGSize( - width: statusView.contentMaxLayoutWidth, - height: statusView.contentMaxLayoutWidth - ) - var needsDisplay = true - switch configurations.count { - case 0: - needsDisplay = false - case 1: - let configuration = configurations[0] - let adaptiveLayout = MediaGridContainerView.AdaptiveLayout( - aspectRatio: configuration.aspectRadio, - maxSize: maxSize - ) - let mediaView = statusView.mediaGridContainerView.dequeueMediaView(adaptiveLayout: adaptiveLayout) - mediaView.setup(configuration: configuration) - default: - let gridLayout = MediaGridContainerView.GridLayout( - count: configurations.count, - maxSize: maxSize - ) - let mediaViews = statusView.mediaGridContainerView.dequeueMediaView(gridLayout: gridLayout) - for (i, (configuration, mediaView)) in zip(configurations, mediaViews).enumerated() { - guard i < MediaGridContainerView.maxCount else { break } - mediaView.setup(configuration: configuration) - } - } - if needsDisplay { - statusView.setMediaDisplay() - } - } - .store(in: &disposeBag) - $isMediaReveal - .sink { isMediaReveal in - statusView.mediaGridContainerView.viewModel.isContentWarningOverlayDisplay = !isMediaReveal - } - .store(in: &disposeBag) - $isMediaSensitiveSwitchable - .sink { isMediaSensitiveSwitchable in - statusView.mediaGridContainerView.viewModel.isSensitiveToggleButtonDisplay = isMediaSensitiveSwitchable - } - .store(in: &disposeBag) + public enum Kind { + case timeline + case repost + case quote + case reference + case conversationRoot + case conversationThread } - - private func bindPoll(statusView: StatusView) { - $pollItems - .sink { items in - guard !items.isEmpty else { return } - - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.main]) - snapshot.appendItems(items, toSection: .main) - statusView.pollTableViewDiffableDataSource?.applySnapshotUsingReloadData(snapshot) - - statusView.pollTableViewHeightLayoutConstraint.constant = CGFloat(items.count) * PollOptionTableViewCell.height - statusView.setPollDisplay() - } - .store(in: &disposeBag) - $isVotable - .sink { isVotable in - statusView.pollTableView.allowsSelection = isVotable - } - .store(in: &disposeBag) - // poll - Publishers.CombineLatest( - $voterCount, - $voteCount - ) - .map { voterCount, voteCount -> String in - var description = "" - if let voterCount = voterCount { - description += L10n.Count.people(voterCount) - } else { - description += L10n.Count.vote(voteCount) - } - return description - } - .assign(to: &$pollVoteDescription) - Publishers.CombineLatest3( - $expireAt, - $expired, - timestampUpdatePublisher.prepend(Date()).eraseToAnyPublisher() - ) - .map { expireAt, expired, _ -> String? in - guard !expired else { - return L10n.Common.Controls.Status.Poll.expired - } - - guard let expireAt = expireAt, - let timeLeft = expireAt.localizedTimeLeft - else { - return nil - } - - return timeLeft - } - .assign(to: &$pollCountdownDescription) - Publishers.CombineLatest( - $pollVoteDescription, - $pollCountdownDescription - ) - .sink { pollVoteDescription, pollCountdownDescription in - let description = [ - pollVoteDescription, - pollCountdownDescription - ] - .compactMap { $0 } - - statusView.pollVoteDescriptionLabel.text = description.joined(separator: " · ") - statusView.pollVoteDescriptionLabel.accessibilityLabel = description.joined(separator: ", ") - } - .store(in: &disposeBag) - Publishers.CombineLatest( - $isVotable, - $isVoting - ) - .sink { isVotable, isVoting in - guard isVotable else { - statusView.pollVoteButton.isHidden = true - statusView.pollVoteActivityIndicatorView.isHidden = true - return - } - - statusView.pollVoteButton.isHidden = isVoting - statusView.pollVoteActivityIndicatorView.isHidden = !isVoting - statusView.pollVoteActivityIndicatorView.startAnimating() - } - .store(in: &disposeBag) - $isVoteButtonEnabled - .assign(to: \.isEnabled, on: statusView.pollVoteButton) - .store(in: &disposeBag) - } - - private func bindLocation(statusView: StatusView) { - $location - .sink { location in - guard let location = location, !location.isEmpty else { - statusView.locationLabel.isAccessibilityElement = false - return - } - statusView.locationLabel.isAccessibilityElement = true - - if statusView.traitCollection.preferredContentSizeCategory > .extraLarge { - statusView.locationMapPinImageView.image = Asset.ObjectTools.mappin.image - } else { - statusView.locationMapPinImageView.image = Asset.ObjectTools.mappinMini.image - } - statusView.locationLabel.text = location - statusView.locationLabel.accessibilityLabel = location - - statusView.setLocationDisplay() - } - .store(in: &disposeBag) - } - - private func bindToolbar(statusView: StatusView) { - // platform - $platform - .assign(to: \.platform, on: statusView.toolbar.viewModel) - .store(in: &disposeBag) - // reply - $replyCount - .sink { count in - statusView.toolbar.setupReply(count: count, isEnabled: true) // TODO: - } - .store(in: &disposeBag) - // repost - Publishers.CombineLatest3( - $repostCount, - $isRepost, - $isRepostEnabled - ) - .sink { count, isRepost, isEnabled in - statusView.toolbar.setupRepost(count: count, isEnabled: isEnabled, isHighlighted: isRepost) - } - .store(in: &disposeBag) - // like - Publishers.CombineLatest( - $likeCount, - $isLike - ) - .sink { count, isLike in - statusView.toolbar.setupLike(count: count, isHighlighted: isLike) - } - .store(in: &disposeBag) - // menu - Publishers.CombineLatest4( - $sharePlaintextContent, - $shareStatusURL, - $mediaViewConfigurations, - $isDeletable - ) - .sink { sharePlaintextContent, shareStatusURL, mediaViewConfigurations, isDeletable in - statusView.toolbar.setupMenu(menuContext: .init( - shareText: sharePlaintextContent, - shareLink: shareStatusURL, - displaySaveMediaAction: !mediaViewConfigurations.isEmpty, - displayDeleteAction: isDeletable - )) - } - .store(in: &disposeBag) - } - - private func bindReplySettings(statusView: StatusView) { - Publishers.CombineLatest( - $replySettings, - $authorUsername - ) - .sink { replySettings, authorUsername in - guard let replySettings = replySettings else { return } - guard let authorUsername = authorUsername else { return } - switch replySettings { - case .everyone: - return - case .following: - statusView.replySettingBannerView.imageView.image = Asset.Communication.at.image.withRenderingMode(.alwaysTemplate) - statusView.replySettingBannerView.label.text = L10n.Common.Controls.Status.ReplySettings.peopleUserFollowsOrMentionedCanReply("@\(authorUsername)") - case .mentionedUsers: - statusView.replySettingBannerView.imageView.image = Asset.Human.personCheckMini.image.withRenderingMode(.alwaysTemplate) - statusView.replySettingBannerView.label.text = L10n.Common.Controls.Status.ReplySettings.peopleUserMentionedCanReply("@\(authorUsername)") - } - statusView.setReplySettingsDisplay() +} + +extension StatusView.ViewModel { + public convenience init?( + feed: Feed, + delegate: StatusViewDelegate?, + viewLayoutFramePublisher: Published.Publisher? + ) { + switch feed.content { + case .twitter(let status): + self.init( + status: status, + kind: .timeline, + delegate: delegate, + viewLayoutFramePublisher: viewLayoutFramePublisher + ) + case .mastodon(let status): + self.init( + status: status, + kind: .timeline, + delegate: delegate, + viewLayoutFramePublisher: viewLayoutFramePublisher + ) + case .mastodonNotification(let notification): + return nil + case .none: + return nil } - .store(in: &disposeBag) } - - private func bindAccessibility(statusView: StatusView) { - let authorAccessibilityLabel = Publishers.CombineLatest( - $header, - $authorName +} + +extension StatusView.ViewModel { + public convenience init( + status: TwitterStatus, + kind: Kind, + delegate: StatusViewDelegate?, + viewLayoutFramePublisher: Published.Publisher? + ) { + self.init( + kind: kind, + delegate: delegate, + viewLayoutFramePublisher: viewLayoutFramePublisher ) - .map { header, authorName -> String? in - var strings: [String?] = [] - - switch header { - case .none: - break - case .notification(let info): - strings.append(info.textMetaContent.string) - case .repost(let info): - strings.append(info.authorNameMetaContent.string) - } - - strings.append(authorName?.string) - - return strings.compactMap { $0 }.joined(separator: ", ") - } - let metaAccessibilityLabel = Publishers.CombineLatest( - $timeAgoStyleTimestamp, - $visibility - ) - .map { timestamp, visibility -> String? in - var strings: [String?] = [] - - strings.append(visibility?.accessibilityLabel) - strings.append(timestamp) - - return strings.compactMap { $0 }.joined(separator: ", ") + if let repost = status.repost { + repostViewModel = .init( + status: repost, + kind: .repost, + delegate: delegate, + viewLayoutFramePublisher: viewLayoutFramePublisher + ) } - - let contentAccessibilityLabel = Publishers.CombineLatest4( - $platform, - $isContentReveal, - $spoilerContent, - $content - ) - .map { platform, isContentReveal, spoilerContent, content -> String? in - var strings: [String?] = [] - switch platform { - case .none: - break - case .twitter: - strings.append(content?.string) - case .mastodon: - if let spoilerContent = spoilerContent?.string { - strings.append(L10n.Accessibility.Common.Status.contentWarning) - strings.append(spoilerContent) - } - if isContentReveal { - strings.append(content?.string) - } - } - - return strings.compactMap { $0 }.joined(separator: ", ") + if let quote = status.quote { + quoteViewModel = .init( + status: quote, + kind: .quote, + delegate: delegate, + viewLayoutFramePublisher: viewLayoutFramePublisher + ) } - let mediaAccessibilityLabel = $mediaViewConfigurations - .map { configurations -> String? in - let count = configurations.count - return count > 0 ? L10n.Count.media(count) : nil + status.author.publisher(for: \.profileImageURL) + .map { _ in status.author.avatarImageURL() } + .assign(to: &$avatarURL) + status.author.publisher(for: \.name) + .map { PlaintextMetaContent(string: $0) } + .assign(to: &$authorName) + status.author.publisher(for: \.username) + .assign(to: &$authorUsernme) + + status.publisher(for: \.text) + .map { _ in + let content = TwitterContent(content: status.displayText) + let metaContent = TwitterMetaContent.convert( + content: content, + urlMaximumLength: 20, + twitterTextProvider: SwiftTwitterTextProvider() + ) + return metaContent } - - let toolbarAccessibilityLabel = Publishers.CombineLatest3( - $platform, - $isRepost, - $isLike + .assign(to: &$content) + } +} + +extension StatusView.ViewModel { + public convenience init( + status: MastodonStatus, + kind: Kind, + delegate: StatusViewDelegate?, + viewLayoutFramePublisher: Published.Publisher? + ) { + self.init( + kind: kind, + delegate: delegate, + viewLayoutFramePublisher: viewLayoutFramePublisher ) - .map { platform, isRepost, isLike -> String? in - var strings: [String?] = [] - - switch platform { - case .none: - break - case .twitter: - if isRepost { - strings.append(L10n.Accessibility.Common.Status.retweeted) - } - case .mastodon: - if isRepost { - strings.append(L10n.Accessibility.Common.Status.boosted) - } - } - - if isLike { - strings.append(L10n.Accessibility.Common.Status.liked) - } - - return strings.compactMap { $0 }.joined(separator: ", ") - } - let pollAccessibilityLabel = Publishers.CombineLatest3( - $pollItems, - $pollVoteDescription, - $pollCountdownDescription + if let repost = status.repost { + repostViewModel = .init( + status: repost, + kind: .repost, + delegate: delegate, + viewLayoutFramePublisher: viewLayoutFramePublisher ) - .map { items, pollVoteDescription, pollCountdownDescription -> String? in - guard !items.isEmpty else { return nil } - guard let managedObjectContext = self.managedObjectContext else { return nil } - - var strings: [String?] = [] - - let ordinalPrefix = L10n.Accessibility.Common.Status.pollOptionOrdinalPrefix - - for (i, item) in items.enumerated() { - switch item { - case .option(let record): - guard let option = record.object(in: managedObjectContext) else { continue } - let number = NSNumber(value: i + 1) - guard let ordinal = StatusView.ViewModel.pollOptionOrdinalNumberFormatter.string(from: number) else { break } - strings.append("\(ordinalPrefix), \(ordinal), \(option.title)") - - if option.isSelected { - strings.append(L10n.Accessibility.VoiceOver.selected) - } - } - } - - strings.append(pollVoteDescription) - pollCountdownDescription.flatMap { strings.append($0) } - - return strings.compactMap { $0 }.joined(separator: ", ") - } - - let groupOne = Publishers.CombineLatest4( - authorAccessibilityLabel, - metaAccessibilityLabel, - contentAccessibilityLabel, - mediaAccessibilityLabel - ) - .map { a, b, c, d -> String? in - return [a, b, c, d] - .compactMap { $0 } - .joined(separator: ", ") } - let groupTwo = Publishers.CombineLatest3( - pollAccessibilityLabel, - $location, - toolbarAccessibilityLabel - ) - .map { a, b, c -> String? in - return [a, b, c] - .compactMap { $0 } - .joined(separator: ", ") - } - - Publishers.CombineLatest( - groupOne, - groupTwo - ) - .map { a, b -> String in - return [a, b] - .compactMap { $0 } - .joined(separator: ", ") + status.author.publisher(for: \.avatar) + .compactMap { $0.flatMap { URL(string: $0) } } + .assign(to: &$avatarURL) + status.author.publisher(for: \.displayName) + .compactMap { _ in status.author.nameMetaContent } + .assign(to: &$authorName) + status.author.publisher(for: \.username) + .assign(to: &$authorUsernme) + + do { + let content = MastodonContent(content: status.content, emojis: status.emojis.asDictionary) + let metaContent = try MastodonMetaContent.convert(document: content) + self.content = metaContent + // viewModel.sharePlaintextContent = metaContent.original + } catch { + assertionFailure(error.localizedDescription) + self.content = PlaintextMetaContent(string: "") } - .assign(to: &$groupedAccessibilityLabel) - - $groupedAccessibilityLabel - .sink { accessibilityLabel in - statusView.accessibilityLabel = accessibilityLabel - } - .store(in: &disposeBag) - - // poll - $pollItems - .sink { items in - statusView.pollVoteDescriptionLabel.isAccessibilityElement = !items.isEmpty - statusView.pollVoteButton.isAccessibilityElement = !items.isEmpty - } - .store(in: &disposeBag) } - } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index fd521bdc..18751253 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -9,6 +9,7 @@ import os.log import Combine import UIKit +import SwiftUI import MetaTextKit import MetaTextArea import MetaLabel @@ -16,1151 +17,1201 @@ import TwidereCommon import TwidereCore import NIOPosix -public protocol StatusViewDelegate: AnyObject { - func statusView(_ statusView: StatusView, headerDidPressed header: UIView) - - func statusView(_ statusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) - func statusView(_ statusView: StatusView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) - - func statusView(_ statusView: StatusView, expandContentButtonDidPressed button: UIButton) - - func statusView(_ statusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) - func statusView(_ statusView: StatusView, quoteStatusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) - - func statusView(_ statusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int) - func statusView(_ statusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, toggleContentWarningOverlayViewDisplay contentWarningOverlayView: ContentWarningOverlayView) - func statusView(_ statusView: StatusView, quoteStatusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int) - - func statusView(_ statusView: StatusView, pollTableView tableView: UITableView, didSelectRowAt indexPath: IndexPath) - func statusView(_ statusView: StatusView, pollVoteButtonDidPressed button: UIButton) - - func statusView(_ statusView: StatusView, quoteStatusViewDidPressed quoteStatusView: StatusView) - - func statusView(_ statusView: StatusView, statusToolbar: StatusToolbar, actionDidPressed action: StatusToolbar.Action, button: UIButton) - func statusView(_ statusView: StatusView, statusToolbar: StatusToolbar, menuActionDidPressed action: StatusToolbar.MenuAction, menuButton button: UIButton) - - func statusView(_ statusView: StatusView, translateButtonDidPressed button: UIButton) - - // a11y - func statusView(_ statusView: StatusView, accessibilityActivate: Void) -} - -public final class StatusView: UIView { - - private var _disposeBag = Set() // which lifetime same to view scope - public var disposeBag = Set() // clear when reuse - - public weak var delegate: StatusViewDelegate? - - public static let bodyContainerStackViewSpacing: CGFloat = 10 - public static let quoteStatusViewContainerLayoutMargin: CGFloat = 12 - - let logger = Logger(subsystem: "StatusView", category: "View") - - public private(set) var style: Style? - - public private(set) lazy var viewModel: ViewModel = { - let viewModel = ViewModel() - viewModel.bind(statusView: self) - return viewModel - }() - - // container - public let containerStackView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .vertical - stackView.spacing = 8 - return stackView - }() - - // header - public let headerContainerView = UIView() - public let headerIconImageView = UIImageView() - public static var headerTextLabelStyle: TextStyle { .statusHeader } - public let headerTextLabel: MetaLabel = { - let label = MetaLabel(style: .statusHeader) - label.isUserInteractionEnabled = false - return label - }() - - // avatar - public let authorAvatarButton = AvatarButton() - - // author - public static var authorNameLabelStyle: TextStyle { .statusAuthorName } - public let authorNameLabel: MetaLabel = { - let label = MetaLabel(style: StatusView.authorNameLabelStyle) - label.accessibilityTraits = .staticText - return label - }() - public let lockImageView: UIImageView = { - let imageView = UIImageView() - imageView.tintColor = .secondaryLabel - imageView.contentMode = .scaleAspectFill - imageView.image = Asset.ObjectTools.lockMiniInline.image.withRenderingMode(.alwaysTemplate) - return imageView - }() - public let authorUsernameLabel = PlainLabel(style: .statusAuthorUsername) - public let visibilityImageView: UIImageView = { - let imageView = UIImageView() - imageView.tintColor = .secondaryLabel - imageView.contentMode = .scaleAspectFill - imageView.image = Asset.ObjectTools.globeMiniInline.image.withRenderingMode(.alwaysTemplate) - return imageView - }() - public let timestampLabel = PlainLabel(style: .statusTimestamp) - - // spoiler - public let spoilerContentTextView: MetaTextAreaView = { - let textView = MetaTextAreaView() - textView.textAttributes = [ - .font: UIFont.preferredFont(forTextStyle: .body), - .foregroundColor: UIColor.label.withAlphaComponent(0.8), - ] - textView.linkAttributes = [ - .font: UIFont.preferredFont(forTextStyle: .body), - .foregroundColor: Asset.Colors.Theme.daylight.color - ] - return textView - }() - - public let expandContentButtonContainer: UIStackView = { - let stackView = UIStackView() - stackView.axis = .horizontal - stackView.alignment = .leading - return stackView - }() - public let expandContentButton: HitTestExpandedButton = { - let button = HitTestExpandedButton(type: .system) - button.setImage(Asset.Editing.ellipsisLarge.image.withRenderingMode(.alwaysTemplate), for: .normal) - button.layer.masksToBounds = true - button.layer.cornerRadius = 10 - button.layer.cornerCurve = .circular - button.backgroundColor = .tertiarySystemFill - return button - }() - - // content - public let contentTextView: MetaTextAreaView = { - let textView = MetaTextAreaView() - textView.textAttributes = [ - .font: UIFont.preferredFont(forTextStyle: .body), - .foregroundColor: UIColor.label.withAlphaComponent(0.8), - ] - textView.linkAttributes = [ - .font: UIFont.preferredFont(forTextStyle: .body), - .foregroundColor: Asset.Colors.Theme.daylight.color - ] - return textView - }() - - // translate - let translateButtonContainer = UIStackView() - public let translateButton: UIButton = { - let button = HitTestExpandedButton(type: .system) - button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 14, weight: .medium)) - button.setTitle(L10n.Common.Controls.Status.Actions.translate, for: .normal) - return button - }() - - // media - public let mediaGridContainerView = MediaGridContainerView() +public struct StatusView: View { - // poll - public let pollTableView: UITableView = { - let tableView = UITableView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) - tableView.register(PollOptionTableViewCell.self, forCellReuseIdentifier: String(describing: PollOptionTableViewCell.self)) - tableView.isScrollEnabled = false - tableView.estimatedRowHeight = 36 - tableView.tableFooterView = UIView() - tableView.backgroundColor = .clear - tableView.separatorStyle = .none - return tableView - }() - public var pollTableViewHeightLayoutConstraint: NSLayoutConstraint! - public var pollTableViewDiffableDataSource: UITableViewDiffableDataSource? + @ObservedObject public private(set) var viewModel: ViewModel - public let pollVoteInfoContainerView = UIStackView() - public let pollVoteDescriptionLabel = PlainLabel(style: .pollVoteDescription) - public let pollVoteButton: HitTestExpandedButton = { - let button = HitTestExpandedButton(type: .system) - button.setTitle(L10n.Common.Controls.Status.Actions.vote, for: .normal) - button.setTitleColor(Asset.Colors.hightLight.color, for: .normal) - button.setTitleColor(.secondaryLabel, for: .disabled) - return button - }() - public let pollVoteActivityIndicatorView: UIActivityIndicatorView = { - let activityIndicatorView = UIActivityIndicatorView(style: .medium) - activityIndicatorView.hidesWhenStopped = true - activityIndicatorView.tintColor = .secondaryLabel - return activityIndicatorView - }() - - // quote - public private(set) var quoteStatusView: StatusView? { - didSet { - if let quoteStatusView = quoteStatusView { - quoteStatusView.delegate = self - - let quoteTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer - quoteTapGestureRecognizer.delegate = self - quoteTapGestureRecognizer.addTarget(self, action: #selector(StatusView.quoteStatusViewDidPressed(_:))) - quoteStatusView.addGestureRecognizer(quoteTapGestureRecognizer) - } - } - } - - // location - public let locationContainer: UIStackView = { - let stackView = UIStackView() - stackView.axis = .horizontal - stackView.spacing = 6 - return stackView - }() - public let locationMapPinImageView: UIImageView = { - let imageView = UIImageView() - imageView.tintColor = .secondaryLabel - return imageView - }() - public let locationLabel = PlainLabel(style: .statusLocation) - - // metrics - public let metricsDashboardView = StatusMetricsDashboardView() - - // toolbar - public let toolbar = StatusToolbar() - - // reply settings - public let replySettingBannerView = ReplySettingBannerView() - - public override init(frame: CGRect) { - super.init(frame: frame) - _init() - } - - public required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - - public override func willMove(toWindow newWindow: UIWindow?) { - super.willMove(toWindow: newWindow) - assert(style != nil, "Needs setup style before use") + public init(viewModel: StatusView.ViewModel) { + self.viewModel = viewModel } - deinit { - viewModel.disposeBag.removeAll() - } - -} - -extension StatusView { - - public func prepareForReuse() { - disposeBag.removeAll() - viewModel.objects.removeAll() - viewModel.authorAvatarImageURL = nil - authorAvatarButton.avatarImageView.cancelTask() - quoteStatusView?.prepareForReuse() - mediaGridContainerView.prepareForReuse() - if var snapshot = pollTableViewDiffableDataSource?.snapshot() { - snapshot.deleteAllItems() - pollTableViewDiffableDataSource?.applySnapshotUsingReloadData(snapshot) - } - Style.prepareForReuse(statusView: self) - } - - private func _init() { - containerStackView.translatesAutoresizingMaskIntoConstraints = false - addSubview(containerStackView) - NSLayoutConstraint.activate([ - containerStackView.topAnchor.constraint(equalTo: topAnchor), - containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor), - containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor), - containerStackView.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - - // header - let headerTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer - headerTapGestureRecognizer.addTarget(self, action: #selector(StatusView.headerTapGestureRecognizerHandler(_:))) - headerContainerView.addGestureRecognizer(headerTapGestureRecognizer) - - // avatar button - authorAvatarButton.accessibilityLabel = L10n.Accessibility.Common.Status.authorAvatar - authorAvatarButton.accessibilityHint = L10n.Accessibility.VoiceOver.doubleTapToOpenProfile - authorAvatarButton.addTarget(self, action: #selector(StatusView.authorAvatarButtonDidPressed(_:)), for: .touchUpInside) - - // expand content - expandContentButton.addTarget(self, action: #selector(StatusView.expandContentButtonDidPressed(_:)), for: .touchUpInside) - - // content - contentTextView.delegate = self - - // translateButton - translateButton.addTarget(self, action: #selector(StatusView.translateButtonDidPressed(_:)), for: .touchUpInside) - - // media grid - mediaGridContainerView.delegate = self - - // poll - pollTableView.translatesAutoresizingMaskIntoConstraints = false - pollTableViewHeightLayoutConstraint = pollTableView.heightAnchor.constraint(equalToConstant: 36).priority(.required - 10) - NSLayoutConstraint.activate([ - pollTableViewHeightLayoutConstraint, - ]) - pollTableView.delegate = self - pollVoteButton.addTarget(self, action: #selector(StatusView.pollVoteButtonDidPressed(_:)), for: .touchUpInside) - - // toolbar - toolbar.delegate = self - - // theme - ThemeService.shared.theme - .sink { [weak self] theme in - guard let self = self else { return } - self.update(theme: theme) - } - .store(in: &disposeBag) - } - - public func setup(style: Style) { - guard self.style == nil else { - assertionFailure("Should only setup once") - return - } - self.style = style - style.layout(statusView: self) - Style.prepareForReuse(statusView: self) - } - -} - -extension StatusView { - public enum Style { - case inline // for timeline - case plain // for thread - case quote // for quote - case composeReply // for compose - - func layout(statusView: StatusView) { - switch self { - case .inline: layoutInline(statusView: statusView) - case .plain: layoutPlain(statusView: statusView) - case .quote: layoutQuote(statusView: statusView) - case .composeReply: layoutComposeReply(statusView: statusView) - } - } - - static func prepareForReuse(statusView: StatusView) { - statusView.headerContainerView.isHidden = true - statusView.lockImageView.isHidden = true - statusView.visibilityImageView.isHidden = true - statusView.spoilerContentTextView.isHidden = true - statusView.expandContentButtonContainer.isHidden = true - statusView.translateButtonContainer.isHidden = true - statusView.mediaGridContainerView.isHidden = true - statusView.pollTableView.isHidden = true - statusView.pollVoteInfoContainerView.isHidden = true - statusView.pollVoteActivityIndicatorView.isHidden = true - statusView.quoteStatusView?.isHidden = true - statusView.locationContainer.isHidden = true - statusView.replySettingBannerView.isHidden = true - } - } -} - -extension StatusView.Style { - - private func layoutInline(statusView: StatusView) { - // container: V - [ header container | body container ] - - // header container: H - [ icon | label ] - statusView.containerStackView.addArrangedSubview(statusView.headerContainerView) - statusView.headerIconImageView.translatesAutoresizingMaskIntoConstraints = false - statusView.headerTextLabel.translatesAutoresizingMaskIntoConstraints = false - statusView.headerContainerView.addSubview(statusView.headerIconImageView) - statusView.headerContainerView.addSubview(statusView.headerTextLabel) - NSLayoutConstraint.activate([ - statusView.headerTextLabel.topAnchor.constraint(equalTo: statusView.headerContainerView.topAnchor), - statusView.headerTextLabel.bottomAnchor.constraint(equalTo: statusView.headerContainerView.bottomAnchor), - statusView.headerTextLabel.trailingAnchor.constraint(equalTo: statusView.headerContainerView.trailingAnchor), - statusView.headerIconImageView.centerYAnchor.constraint(equalTo: statusView.headerTextLabel.centerYAnchor), - statusView.headerIconImageView.heightAnchor.constraint(equalTo: statusView.headerTextLabel.heightAnchor, multiplier: 1.0).priority(.required - 1), - statusView.headerIconImageView.widthAnchor.constraint(equalTo: statusView.headerIconImageView.heightAnchor, multiplier: 1.0).priority(.required - 1), - statusView.headerTextLabel.leadingAnchor.constraint(equalTo: statusView.headerIconImageView.trailingAnchor, constant: 4), - // align to author name below - ]) - statusView.headerTextLabel.setContentHuggingPriority(.required - 10, for: .vertical) - statusView.headerIconImageView.setContentHuggingPriority(.defaultLow, for: .vertical) - statusView.headerIconImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) - - // body container: H - [ authorAvatarButton | content container ] - let bodyContainerStackView = UIStackView() - bodyContainerStackView.axis = .horizontal - bodyContainerStackView.spacing = StatusView.bodyContainerStackViewSpacing - bodyContainerStackView.alignment = .top - statusView.containerStackView.addArrangedSubview(bodyContainerStackView) - - // authorAvatarButton - let authorAvatarButtonSize = CGSize(width: 44, height: 44) - statusView.authorAvatarButton.size = authorAvatarButtonSize - statusView.authorAvatarButton.avatarImageView.imageViewSize = authorAvatarButtonSize - statusView.authorAvatarButton.translatesAutoresizingMaskIntoConstraints = false - bodyContainerStackView.addArrangedSubview(statusView.authorAvatarButton) - NSLayoutConstraint.activate([ - statusView.authorAvatarButton.widthAnchor.constraint(equalToConstant: authorAvatarButtonSize.width).priority(.required - 1), - statusView.authorAvatarButton.heightAnchor.constraint(equalToConstant: authorAvatarButtonSize.height).priority(.required - 1), - ]) - - // content container: V - [ author content | contentTextView | mediaGridContainerView | quoteStatusView | … | location content | toolbar ] - let contentContainerView = UIStackView() - contentContainerView.axis = .vertical - contentContainerView.spacing = 10 - bodyContainerStackView.addArrangedSubview(contentContainerView) - - // author content: H - [ authorNameLabel | lockImageView | authorUsernameLabel | padding | visibilityImageView (for Mastodon) | timestampLabel ] - let authorContentStackView = UIStackView() - authorContentStackView.axis = .horizontal - authorContentStackView.spacing = 6 - contentContainerView.addArrangedSubview(authorContentStackView) - contentContainerView.setCustomSpacing(4, after: authorContentStackView) - UIContentSizeCategory.publisher - .sink { category in - authorContentStackView.axis = category > .accessibilityLarge ? .vertical : .horizontal - authorContentStackView.alignment = category > .accessibilityLarge ? .leading : .fill - } - .store(in: &statusView._disposeBag) - - // authorNameLabel - authorContentStackView.addArrangedSubview(statusView.authorNameLabel) - statusView.authorNameLabel.setContentHuggingPriority(.required - 10, for: .horizontal) - statusView.authorNameLabel.setContentCompressionResistancePriority(.required - 10, for: .horizontal) - // lockImageView - statusView.lockImageView.translatesAutoresizingMaskIntoConstraints = false - authorContentStackView.addArrangedSubview(statusView.lockImageView) - // authorUsernameLabel - authorContentStackView.addArrangedSubview(statusView.authorUsernameLabel) - NSLayoutConstraint.activate([ - statusView.lockImageView.heightAnchor.constraint(equalTo: statusView.authorUsernameLabel.heightAnchor).priority(.required - 10), - ]) - statusView.lockImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) - statusView.lockImageView.setContentCompressionResistancePriority(.required - 10, for: .horizontal) - statusView.authorUsernameLabel.setContentCompressionResistancePriority(.required - 11, for: .horizontal) - // padding - authorContentStackView.addArrangedSubview(UIView()) - // visibilityImageView - authorContentStackView.addArrangedSubview(statusView.visibilityImageView) - statusView.visibilityImageView.setContentHuggingPriority(.required - 9, for: .horizontal) - statusView.visibilityImageView.setContentCompressionResistancePriority(.required - 9, for: .horizontal) - // timestampLabel - authorContentStackView.addArrangedSubview(statusView.timestampLabel) - statusView.timestampLabel.setContentHuggingPriority(.required - 8, for: .horizontal) - statusView.timestampLabel.setContentCompressionResistancePriority(.required - 8, for: .horizontal) - - // set header label align to author name - NSLayoutConstraint.activate([ - statusView.headerTextLabel.leadingAnchor.constraint(equalTo: statusView.authorNameLabel.leadingAnchor), - ]) - - // spoilerContentTextView - contentContainerView.addArrangedSubview(statusView.spoilerContentTextView) - statusView.spoilerContentTextView.setContentHuggingPriority(.required - 9, for: .vertical) - statusView.spoilerContentTextView.setContentCompressionResistancePriority(.required - 10, for: .vertical) - - contentContainerView.addArrangedSubview(statusView.expandContentButtonContainer) - statusView.expandContentButton.translatesAutoresizingMaskIntoConstraints = false - statusView.expandContentButtonContainer.addArrangedSubview(statusView.expandContentButton) - statusView.expandContentButtonContainer.addArrangedSubview(UIView()) - - // contentTextView - // statusView.contentTextView.translatesAutoresizingMaskIntoConstraints = false - contentContainerView.addArrangedSubview(statusView.contentTextView) - statusView.contentTextView.setContentHuggingPriority(.required - 10, for: .vertical) - - // mediaGridContainerView - statusView.mediaGridContainerView.translatesAutoresizingMaskIntoConstraints = false - contentContainerView.addArrangedSubview(statusView.mediaGridContainerView) - - // pollTableView - contentContainerView.addArrangedSubview(statusView.pollTableView) - - statusView.pollVoteInfoContainerView.axis = .horizontal - statusView.pollVoteInfoContainerView.addArrangedSubview(statusView.pollVoteDescriptionLabel) - statusView.pollVoteInfoContainerView.addArrangedSubview(UIView()) // spacer - statusView.pollVoteInfoContainerView.addArrangedSubview(statusView.pollVoteButton) - statusView.pollVoteButton.setContentHuggingPriority(.required - 10, for: .horizontal) - statusView.pollVoteButton.setContentCompressionResistancePriority(.required - 9, for: .horizontal) - statusView.pollVoteInfoContainerView.addArrangedSubview(statusView.pollVoteActivityIndicatorView) - statusView.pollVoteInfoContainerView.setContentHuggingPriority(.required - 11, for: .horizontal) - statusView.pollVoteInfoContainerView.setContentCompressionResistancePriority(.required - 10, for: .horizontal) - contentContainerView.addArrangedSubview(statusView.pollVoteInfoContainerView) - - // quoteStatusView - let quoteStatusView = StatusView() - quoteStatusView.setup(style: .quote) - statusView.quoteStatusView = quoteStatusView - contentContainerView.addArrangedSubview(quoteStatusView) - - // location content: H - [ locationMapPinImageView | locationLabel ] - contentContainerView.addArrangedSubview(statusView.locationContainer) - - statusView.locationMapPinImageView.translatesAutoresizingMaskIntoConstraints = false - statusView.locationLabel.translatesAutoresizingMaskIntoConstraints = false - - // locationMapPinImageView - statusView.locationContainer.addArrangedSubview(statusView.locationMapPinImageView) - // locationLabel - statusView.locationContainer.addArrangedSubview(statusView.locationLabel) - - NSLayoutConstraint.activate([ - statusView.locationMapPinImageView.heightAnchor.constraint(equalTo: statusView.locationLabel.heightAnchor, multiplier: 1.0).priority(.required - 1), - statusView.locationMapPinImageView.widthAnchor.constraint(equalTo: statusView.locationMapPinImageView.heightAnchor, multiplier: 1.0).priority(.required - 1), - ]) - statusView.locationLabel.setContentHuggingPriority(.required - 10, for: .vertical) - statusView.locationMapPinImageView.setContentHuggingPriority(.defaultLow, for: .vertical) - statusView.locationMapPinImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) - - // toolbar - contentContainerView.addArrangedSubview(statusView.toolbar) - statusView.toolbar.setContentHuggingPriority(.required - 9, for: .vertical) - } - - private func layoutPlain(statusView: StatusView) { - // container: V - [ header container | author container | contentTextView | mediaGridContainerView | quoteStatusView | location content | toolbar ] - - // header container: H - [ icon | label ] - statusView.containerStackView.addArrangedSubview(statusView.headerContainerView) - statusView.headerIconImageView.translatesAutoresizingMaskIntoConstraints = false - statusView.headerTextLabel.translatesAutoresizingMaskIntoConstraints = false - statusView.headerContainerView.addSubview(statusView.headerIconImageView) - statusView.headerContainerView.addSubview(statusView.headerTextLabel) - NSLayoutConstraint.activate([ - statusView.headerTextLabel.topAnchor.constraint(equalTo: statusView.headerContainerView.topAnchor), - statusView.headerTextLabel.bottomAnchor.constraint(equalTo: statusView.headerContainerView.bottomAnchor), - statusView.headerTextLabel.trailingAnchor.constraint(equalTo: statusView.headerContainerView.trailingAnchor), - statusView.headerIconImageView.centerYAnchor.constraint(equalTo: statusView.headerTextLabel.centerYAnchor), - statusView.headerIconImageView.heightAnchor.constraint(equalTo: statusView.headerTextLabel.heightAnchor, multiplier: 1.0).priority(.required - 1), - statusView.headerIconImageView.widthAnchor.constraint(equalTo: statusView.headerIconImageView.heightAnchor, multiplier: 1.0).priority(.required - 1), - statusView.headerTextLabel.leadingAnchor.constraint(equalTo: statusView.headerIconImageView.trailingAnchor, constant: 4), - // align to author name below - ]) - statusView.headerTextLabel.setContentHuggingPriority(.required - 10, for: .vertical) - statusView.headerIconImageView.setContentHuggingPriority(.defaultLow, for: .vertical) - statusView.headerIconImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) - statusView.headerContainerView.isHidden = true - - // author content: H - [ authorAvatarButton | author info content ] - let authorContentStackView = UIStackView() - let authorContentStackViewSpacing: CGFloat = 10 - authorContentStackView.axis = .horizontal - authorContentStackView.spacing = authorContentStackViewSpacing - statusView.containerStackView.addArrangedSubview(authorContentStackView) - - // authorAvatarButton - let authorAvatarButtonSize = CGSize(width: 44, height: 44) - statusView.authorAvatarButton.size = authorAvatarButtonSize - statusView.authorAvatarButton.avatarImageView.imageViewSize = authorAvatarButtonSize - statusView.authorAvatarButton.translatesAutoresizingMaskIntoConstraints = false - authorContentStackView.addArrangedSubview(statusView.authorAvatarButton) - let authorAvatarButtonWidthFixLayoutConstraint = statusView.authorAvatarButton.widthAnchor.constraint(equalToConstant: authorAvatarButtonSize.width).priority(.required - 1) - NSLayoutConstraint.activate([ - authorAvatarButtonWidthFixLayoutConstraint, - statusView.authorAvatarButton.heightAnchor.constraint(equalTo: statusView.authorAvatarButton.widthAnchor, multiplier: 1.0).priority(.required - 1), - ]) - - // author info content: V - [ author info headline content | author info sub-headline content ] - let authorInfoContentStackView = UIStackView() - authorInfoContentStackView.axis = .vertical - authorContentStackView.addArrangedSubview(authorInfoContentStackView) - - // author info headline content: H - [ authorNameLabel | lockImageView | padding | visibilityImageView (for Mastodon) ] - let authorInfoHeadlineContentStackView = UIStackView() - authorInfoHeadlineContentStackView.axis = .horizontal - authorInfoHeadlineContentStackView.spacing = 2 - authorInfoContentStackView.addArrangedSubview(authorInfoHeadlineContentStackView) - - // authorNameLabel - authorInfoHeadlineContentStackView.addArrangedSubview(statusView.authorNameLabel) - statusView.authorNameLabel.setContentCompressionResistancePriority(.required - 10, for: .horizontal) - // lockImageView - statusView.lockImageView.translatesAutoresizingMaskIntoConstraints = false - authorInfoHeadlineContentStackView.addArrangedSubview(statusView.lockImageView) - NSLayoutConstraint.activate([ - statusView.lockImageView.heightAnchor.constraint(equalTo: statusView.authorNameLabel.heightAnchor).priority(.required - 10), - ]) - statusView.lockImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) - statusView.lockImageView.setContentCompressionResistancePriority(.required - 10, for: .horizontal) - // padding - authorInfoHeadlineContentStackView.addArrangedSubview(UIView()) - // visibilityImageView - authorInfoHeadlineContentStackView.addArrangedSubview(statusView.visibilityImageView) - statusView.visibilityImageView.setContentHuggingPriority(.required - 9, for: .horizontal) - statusView.visibilityImageView.setContentCompressionResistancePriority(.required - 9, for: .horizontal) - - // set header label align to author name - NSLayoutConstraint.activate([ - statusView.headerTextLabel.leadingAnchor.constraint(equalTo: statusView.authorAvatarButton.trailingAnchor, constant: authorContentStackViewSpacing), - ]) - - // author info sub-headline content: H - [ authorUsernameLabel ] - let authorInfoSubHeadlineContentStackView = UIStackView() - authorInfoSubHeadlineContentStackView.axis = .horizontal - authorInfoContentStackView.addArrangedSubview(authorInfoSubHeadlineContentStackView) - - UIContentSizeCategory.publisher - .sink { category in - if category >= .extraExtraLarge { - authorContentStackView.axis = .vertical - authorContentStackView.alignment = .leading // set leading - } else { - authorContentStackView.axis = .horizontal - authorContentStackView.alignment = .fill // restore default + public var body: some View { + VStack(spacing: .zero) { + if let repostViewModel = viewModel.repostViewModel { + // header + statusHeaderView + // post + StatusView(viewModel: repostViewModel) + } else { + // content + contentView + // quote + if let quoteViewModel = viewModel.quoteViewModel { + StatusView(viewModel: quoteViewModel) } } - .store(in: &statusView._disposeBag) - - // authorUsernameLabel - authorInfoSubHeadlineContentStackView.addArrangedSubview(statusView.authorUsernameLabel) - - // spoilerContentTextView - statusView.containerStackView.addArrangedSubview(statusView.spoilerContentTextView) - statusView.spoilerContentTextView.setContentCompressionResistancePriority(.required - 10, for: .vertical) - statusView.spoilerContentTextView.setContentHuggingPriority(.required - 9, for: .vertical) - - statusView.containerStackView.addArrangedSubview(statusView.expandContentButtonContainer) - statusView.expandContentButton.translatesAutoresizingMaskIntoConstraints = false - statusView.expandContentButtonContainer.addArrangedSubview(statusView.expandContentButton) - statusView.expandContentButtonContainer.addArrangedSubview(UIView()) - - // contentTextView - statusView.containerStackView.addArrangedSubview(statusView.contentTextView) - - // translateButtonContainer: H - [ translateButton | (spacer) ] - statusView.translateButtonContainer.axis = .horizontal - statusView.containerStackView.addArrangedSubview(statusView.translateButtonContainer) - - statusView.translateButtonContainer.addArrangedSubview(statusView.translateButton) - statusView.translateButtonContainer.addArrangedSubview(UIView()) - statusView.translateButton.setContentHuggingPriority(.required - 1, for: .vertical) - statusView.translateButton.setContentCompressionResistancePriority(.required - 1, for: .vertical) - - // mediaGridContainerView - statusView.containerStackView.addArrangedSubview(statusView.mediaGridContainerView) - - // pollTableView - statusView.containerStackView.addArrangedSubview(statusView.pollTableView) - - statusView.pollVoteInfoContainerView.axis = .horizontal - statusView.pollVoteInfoContainerView.addArrangedSubview(statusView.pollVoteDescriptionLabel) - statusView.pollVoteInfoContainerView.addArrangedSubview(UIView()) // spacer - statusView.pollVoteInfoContainerView.addArrangedSubview(statusView.pollVoteButton) - statusView.pollVoteButton.setContentHuggingPriority(.required - 10, for: .horizontal) - statusView.pollVoteButton.setContentCompressionResistancePriority(.required - 9, for: .horizontal) - statusView.pollVoteInfoContainerView.addArrangedSubview(statusView.pollVoteActivityIndicatorView) - statusView.pollVoteInfoContainerView.setContentHuggingPriority(.required - 11, for: .horizontal) - statusView.pollVoteInfoContainerView.setContentCompressionResistancePriority(.required - 10, for: .horizontal) - statusView.containerStackView.addArrangedSubview(statusView.pollVoteInfoContainerView) - - // quoteStatusView - let quoteStatusView = StatusView() - quoteStatusView.setup(style: .quote) - statusView.quoteStatusView = quoteStatusView - statusView.containerStackView.addArrangedSubview(quoteStatusView) - - // location content: H - [ padding | locationMapPinImageView | locationLabel | padding ] - statusView.containerStackView.addArrangedSubview(statusView.locationContainer) - - statusView.locationMapPinImageView.translatesAutoresizingMaskIntoConstraints = false - statusView.locationLabel.translatesAutoresizingMaskIntoConstraints = false - - // locationLeadingPadding - let locationLeadingPadding = UIView() - locationLeadingPadding.translatesAutoresizingMaskIntoConstraints = false - statusView.locationContainer.addArrangedSubview(locationLeadingPadding) - // locationMapPinImageView - statusView.locationContainer.addArrangedSubview(statusView.locationMapPinImageView) - // locationLabel - statusView.locationContainer.addArrangedSubview(statusView.locationLabel) - // locationTrailingPadding - let locationTrailingPadding = UIView() - locationTrailingPadding.translatesAutoresizingMaskIntoConstraints = false - statusView.locationContainer.addArrangedSubview(locationTrailingPadding) - - // center alignment - NSLayoutConstraint.activate([ - locationLeadingPadding.widthAnchor.constraint(equalTo: locationTrailingPadding.widthAnchor).priority(.defaultHigh), - ]) - - NSLayoutConstraint.activate([ - statusView.locationMapPinImageView.heightAnchor.constraint(equalTo: statusView.locationLabel.heightAnchor, multiplier: 1.0).priority(.required - 1), - statusView.locationMapPinImageView.widthAnchor.constraint(equalTo: statusView.locationMapPinImageView.heightAnchor, multiplier: 1.0).priority(.required - 1), - ]) - statusView.locationLabel.setContentHuggingPriority(.required - 10, for: .vertical) - statusView.locationMapPinImageView.setContentHuggingPriority(.defaultLow, for: .vertical) - statusView.locationMapPinImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) - - // metrics - statusView.containerStackView.addArrangedSubview(statusView.metricsDashboardView) - - UIContentSizeCategory.publisher - .sink { category in - if category >= .extraExtraLarge { - statusView.metricsDashboardView.metaContainer.axis = .vertical - } else { - statusView.metricsDashboardView.metaContainer.axis = .horizontal - } - } - .store(in: &statusView._disposeBag) - - // toolbar - statusView.containerStackView.addArrangedSubview(statusView.toolbar) - statusView.toolbar.setContentHuggingPriority(.required - 9, for: .vertical) - - // reply settings - statusView.containerStackView.addArrangedSubview(statusView.replySettingBannerView) - } - - private func layoutQuote(statusView: StatusView) { - // container: V - [ body container ] - // set `isLayoutMarginsRelativeArrangement` not works with AutoLayout (priority issue) - // add constraint to workaround - statusView.containerStackView.backgroundColor = .secondarySystemBackground - statusView.containerStackView.layer.masksToBounds = true - statusView.containerStackView.layer.cornerCurve = .continuous - statusView.containerStackView.layer.cornerRadius = 12 - - // body container: V - [ author content | content container | contentTextView | mediaGridContainerView ] - let bodyContainerStackView = UIStackView() - bodyContainerStackView.axis = .vertical - bodyContainerStackView.spacing = StatusView.bodyContainerStackViewSpacing - bodyContainerStackView.translatesAutoresizingMaskIntoConstraints = false - statusView.containerStackView.addSubview(bodyContainerStackView) - NSLayoutConstraint.activate([ - bodyContainerStackView.topAnchor.constraint(equalTo: statusView.containerStackView.topAnchor, constant: StatusView.quoteStatusViewContainerLayoutMargin).priority(.required - 1), - bodyContainerStackView.leadingAnchor.constraint(equalTo: statusView.containerStackView.leadingAnchor, constant: StatusView.quoteStatusViewContainerLayoutMargin).priority(.required - 1), - statusView.containerStackView.trailingAnchor.constraint(equalTo: bodyContainerStackView.trailingAnchor, constant: StatusView.quoteStatusViewContainerLayoutMargin).priority(.required - 1), - statusView.containerStackView.bottomAnchor.constraint(equalTo: bodyContainerStackView.bottomAnchor, constant: StatusView.quoteStatusViewContainerLayoutMargin).priority(.required - 1), - ]) - - // author content: H - [ authorAvatarButton | authorNameLabel | lockImageView | authorUsernameLabel | padding ] - let authorContentStackView = UIStackView() - authorContentStackView.axis = .horizontal - bodyContainerStackView.alignment = .top - authorContentStackView.spacing = 6 - bodyContainerStackView.addArrangedSubview(authorContentStackView) - bodyContainerStackView.setCustomSpacing(4, after: authorContentStackView) - - // authorAvatarButton - statusView.authorAvatarButton.translatesAutoresizingMaskIntoConstraints = false - authorContentStackView.addArrangedSubview(statusView.authorAvatarButton) - // authorNameLabel - authorContentStackView.addArrangedSubview(statusView.authorNameLabel) - statusView.authorNameLabel.setContentCompressionResistancePriority(.required - 10, for: .horizontal) - // lockImageView - statusView.lockImageView.translatesAutoresizingMaskIntoConstraints = false - authorContentStackView.addArrangedSubview(statusView.lockImageView) - // authorUsernameLabel - authorContentStackView.addArrangedSubview(statusView.authorUsernameLabel) - NSLayoutConstraint.activate([ - statusView.lockImageView.heightAnchor.constraint(equalTo: statusView.authorUsernameLabel.heightAnchor).priority(.required - 10), - ]) - statusView.lockImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) - statusView.lockImageView.setContentCompressionResistancePriority(.required - 10, for: .horizontal) - statusView.authorUsernameLabel.setContentCompressionResistancePriority(.required - 11, for: .horizontal) - // padding - authorContentStackView.addArrangedSubview(UIView()) - - NSLayoutConstraint.activate([ - statusView.authorAvatarButton.heightAnchor.constraint(equalTo: statusView.authorNameLabel.heightAnchor, multiplier: 1.0).priority(.required - 10), - statusView.authorAvatarButton.widthAnchor.constraint(equalTo: statusView.authorNameLabel.heightAnchor, multiplier: 1.0).priority(.required - 10), - ]) - // low priority for intrinsic size hugging - statusView.authorAvatarButton.setContentHuggingPriority(.defaultLow - 10, for: .vertical) - statusView.authorAvatarButton.setContentHuggingPriority(.defaultLow - 10, for: .horizontal) - // high priority but lower then layout constraint for size compression - statusView.authorAvatarButton.setContentCompressionResistancePriority(.required - 11, for: .vertical) - statusView.authorAvatarButton.setContentCompressionResistancePriority(.required - 11, for: .horizontal) - statusView.authorNameLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical) - statusView.authorNameLabel.setContentHuggingPriority(.required - 1, for: .vertical) - - // contentTextView - statusView.contentTextView.translatesAutoresizingMaskIntoConstraints = false - bodyContainerStackView.addArrangedSubview(statusView.contentTextView) - statusView.contentTextView.setContentHuggingPriority(.required - 10, for: .vertical) - statusView.contentTextView.textAttributes = [ - .font: UIFont.preferredFont(forTextStyle: .callout), - .foregroundColor: UIColor.secondaryLabel, - ] - statusView.contentTextView.linkAttributes = [ - .font: UIFont.preferredFont(forTextStyle: .callout), - .foregroundColor: Asset.Colors.Theme.daylight.color - ] - - // mediaGridContainerView - statusView.mediaGridContainerView.translatesAutoresizingMaskIntoConstraints = false - bodyContainerStackView.addArrangedSubview(statusView.mediaGridContainerView) - } - - func layoutComposeReply(statusView: StatusView) { - // container: V - [ body container ] - - // body container: H - [ authorAvatarButton | content container ] - let bodyContainerStackView = UIStackView() - bodyContainerStackView.axis = .horizontal - bodyContainerStackView.spacing = StatusView.bodyContainerStackViewSpacing - bodyContainerStackView.alignment = .top - statusView.containerStackView.addArrangedSubview(bodyContainerStackView) - - // authorAvatarButton - let authorAvatarButtonSize = CGSize(width: 44, height: 44) - statusView.authorAvatarButton.size = authorAvatarButtonSize - statusView.authorAvatarButton.avatarImageView.imageViewSize = authorAvatarButtonSize - statusView.authorAvatarButton.translatesAutoresizingMaskIntoConstraints = false - bodyContainerStackView.addArrangedSubview(statusView.authorAvatarButton) - NSLayoutConstraint.activate([ - statusView.authorAvatarButton.widthAnchor.constraint(equalToConstant: authorAvatarButtonSize.width).priority(.required - 1), - statusView.authorAvatarButton.heightAnchor.constraint(equalToConstant: authorAvatarButtonSize.height).priority(.required - 1), - ]) - - // content container: V - [ author content | contentTextView | mediaGridContainerView | quoteStatusView | … | location content | toolbar ] - let contentContainerView = UIStackView() - contentContainerView.axis = .vertical - contentContainerView.spacing = 10 - bodyContainerStackView.addArrangedSubview(contentContainerView) - - // author content: H - [ authorNameLabel | lockImageView | authorUsernameLabel | padding | visibilityImageView (for Mastodon) | timestampLabel ] - let authorContentStackView = UIStackView() - authorContentStackView.axis = .horizontal - authorContentStackView.spacing = 6 - contentContainerView.addArrangedSubview(authorContentStackView) - contentContainerView.setCustomSpacing(4, after: authorContentStackView) - UIContentSizeCategory.publisher - .sink { category in - authorContentStackView.axis = category > .accessibilityLarge ? .vertical : .horizontal - } - .store(in: &statusView._disposeBag) - - // authorNameLabel - authorContentStackView.addArrangedSubview(statusView.authorNameLabel) - statusView.authorNameLabel.setContentCompressionResistancePriority(.required - 10, for: .horizontal) - // lockImageView - statusView.lockImageView.translatesAutoresizingMaskIntoConstraints = false - authorContentStackView.addArrangedSubview(statusView.lockImageView) - // authorUsernameLabel - authorContentStackView.addArrangedSubview(statusView.authorUsernameLabel) - NSLayoutConstraint.activate([ - statusView.lockImageView.heightAnchor.constraint(equalTo: statusView.authorUsernameLabel.heightAnchor).priority(.required - 10), - ]) - statusView.lockImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) - statusView.lockImageView.setContentCompressionResistancePriority(.required - 10, for: .horizontal) - statusView.authorUsernameLabel.setContentCompressionResistancePriority(.required - 11, for: .horizontal) - // padding - authorContentStackView.addArrangedSubview(UIView()) - // visibilityImageView - authorContentStackView.addArrangedSubview(statusView.visibilityImageView) - statusView.visibilityImageView.setContentHuggingPriority(.required - 9, for: .horizontal) - statusView.visibilityImageView.setContentCompressionResistancePriority(.required - 9, for: .horizontal) - // timestampLabel - authorContentStackView.addArrangedSubview(statusView.timestampLabel) - statusView.timestampLabel.setContentHuggingPriority(.required - 9, for: .horizontal) - statusView.timestampLabel.setContentCompressionResistancePriority(.required - 9, for: .horizontal) - - // spoilerContentTextView - contentContainerView.addArrangedSubview(statusView.spoilerContentTextView) - statusView.spoilerContentTextView.setContentCompressionResistancePriority(.required - 10, for: .vertical) - statusView.spoilerContentTextView.setContentHuggingPriority(.required - 9, for: .vertical) - - contentContainerView.addArrangedSubview(statusView.expandContentButtonContainer) - statusView.expandContentButton.translatesAutoresizingMaskIntoConstraints = false - statusView.expandContentButtonContainer.addArrangedSubview(statusView.expandContentButton) - statusView.expandContentButtonContainer.addArrangedSubview(UIView()) - - // contentTextView - statusView.contentTextView.translatesAutoresizingMaskIntoConstraints = false - contentContainerView.addArrangedSubview(statusView.contentTextView) - statusView.contentTextView.setContentHuggingPriority(.required - 10, for: .vertical) - - // mediaGridContainerView - statusView.mediaGridContainerView.translatesAutoresizingMaskIntoConstraints = false - contentContainerView.addArrangedSubview(statusView.mediaGridContainerView) - - // quoteStatusView - let quoteStatusView = StatusView() - quoteStatusView.setup(style: .quote) - statusView.quoteStatusView = quoteStatusView - contentContainerView.addArrangedSubview(quoteStatusView) - - // location content: H - [ locationMapPinImageView | locationLabel ] - contentContainerView.addArrangedSubview(statusView.locationContainer) - - statusView.locationMapPinImageView.translatesAutoresizingMaskIntoConstraints = false - statusView.locationLabel.translatesAutoresizingMaskIntoConstraints = false - - // locationMapPinImageView - statusView.locationContainer.addArrangedSubview(statusView.locationMapPinImageView) - // locationLabel - statusView.locationContainer.addArrangedSubview(statusView.locationLabel) - - NSLayoutConstraint.activate([ - statusView.locationMapPinImageView.heightAnchor.constraint(equalTo: statusView.locationLabel.heightAnchor, multiplier: 1.0).priority(.required - 1), - statusView.locationMapPinImageView.widthAnchor.constraint(equalTo: statusView.locationMapPinImageView.heightAnchor, multiplier: 1.0).priority(.required - 1), - ]) - statusView.locationLabel.setContentHuggingPriority(.required - 10, for: .vertical) - statusView.locationMapPinImageView.setContentHuggingPriority(.defaultLow, for: .vertical) - statusView.locationMapPinImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) - } - -} - -extension StatusView { - - private func update(theme: Theme) { - headerIconImageView.tintColor = theme.accentColor - } - - public func setHeaderDisplay() { - headerContainerView.isHidden = false - } - - public func setLockDisplay() { - lockImageView.isHidden = false - } - - public func setVisibilityDisplay() { - visibilityImageView.isHidden = false - } - - public func setSpoilerDisplay() { - spoilerContentTextView.isHidden = false - expandContentButtonContainer.isHidden = false - } - - public func setTranslateButtonDisplay() { - translateButtonContainer.isHidden = false - } - - public func setMediaDisplay() { - mediaGridContainerView.isHidden = false - } - - public func setPollDisplay() { - pollTableView.isHidden = false - pollVoteInfoContainerView.isHidden = false - } - - public func setQuoteDisplay() { - quoteStatusView?.isHidden = false - } - - public func setLocationDisplay() { - locationContainer.isHidden = false - } - - public func setReplySettingsDisplay() { - replySettingBannerView.isHidden = false - } - - // content text Width - public var contentMaxLayoutWidth: CGFloat { - let inset = contentLayoutInset - return frame.width - inset.left - inset.right - } - - public var contentLayoutInset: UIEdgeInsets { - guard let style = style else { - assertionFailure("Needs setup style before use") - return .zero - } - - switch style { - case .inline, .composeReply: - let left = authorAvatarButton.size.width + StatusView.bodyContainerStackViewSpacing - return UIEdgeInsets(top: 0, left: left, bottom: 0, right: 0) - case .plain: - return .zero - case .quote: - let margin = StatusView.quoteStatusViewContainerLayoutMargin - return UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) } } } extension StatusView { - @objc private func headerTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - delegate?.statusView(self, headerDidPressed: headerContainerView) - } - - @objc private func authorAvatarButtonDidPressed(_ sender: UIButton) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - delegate?.statusView(self, authorAvatarButtonDidPressed: authorAvatarButton) - } - - @objc private func expandContentButtonDidPressed(_ sender: UIButton) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - delegate?.statusView(self, expandContentButtonDidPressed: sender) - } - - @objc private func pollVoteButtonDidPressed(_ sender: UIButton) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - delegate?.statusView(self, pollVoteButtonDidPressed: sender) - } - - @objc private func quoteStatusViewDidPressed(_ sender: UITapGestureRecognizer) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - guard let quoteStatusView = quoteStatusView else { return } - delegate?.statusView(self, quoteStatusViewDidPressed: quoteStatusView) - } - - @objc private func quoteAuthorAvatarButtonDidPressed(_ sender: UITapGestureRecognizer) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - guard let quoteStatusView = quoteStatusView else { return } - delegate?.statusView(self, quoteStatusView: quoteStatusView, authorAvatarButtonDidPressed: quoteStatusView.authorAvatarButton) - } - - @objc private func translateButtonDidPressed(_ sender: UIButton) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - delegate?.statusView(self, translateButtonDidPressed: sender) - } -} - -// MARK: - MetaTextAreaViewDelegate -extension StatusView: MetaTextAreaViewDelegate { - public func metaTextAreaView(_ metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public))") - delegate?.statusView(self, metaTextAreaView: metaTextAreaView, didSelectMeta: meta) - } -} - -// MARK: - MediaGridContainerViewDelegate -extension StatusView: MediaGridContainerViewDelegate { - public func mediaGridContainerView(_ container: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int) { - delegate?.statusView(self, mediaGridContainerView: container, didTapMediaView: mediaView, at: index) - } - - public func mediaGridContainerView(_ container: MediaGridContainerView, toggleContentWarningOverlayViewDisplay contentWarningOverlayView: ContentWarningOverlayView) { - delegate?.statusView(self, mediaGridContainerView: container, toggleContentWarningOverlayViewDisplay: contentWarningOverlayView) - } -} - -// MARK: - StatusToolbarDelegate -extension StatusView: StatusToolbarDelegate { - public func statusToolbar(_ statusToolbar: StatusToolbar, actionDidPressed action: StatusToolbar.Action, button: UIButton) { - delegate?.statusView(self, statusToolbar: statusToolbar, actionDidPressed: action, button: button) - } - - public func statusToolbar(_ statusToolbar: StatusToolbar, menuActionDidPressed action: StatusToolbar.MenuAction, menuButton button: UIButton) { - delegate?.statusView(self, statusToolbar: statusToolbar, menuActionDidPressed: action, menuButton: button) - } -} - -// MARK: - StatusViewDelegate -// relay for quoteStatsView -extension StatusView: StatusViewDelegate { - - public func statusView(_ statusView: StatusView, headerDidPressed header: UIView) { - assertionFailure() - } - - public func statusView(_ statusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) { - guard statusView === quoteStatusView else { - assertionFailure() - return + public var statusHeaderView: some View { + Button { + + } label: { + HStack { + Text("Header") + Spacer() + } } - - delegate?.statusView(self, quoteStatusView: statusView, authorAvatarButtonDidPressed: button) - } - - public func statusView(_ statusView: StatusView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) { - assertionFailure() } - public func statusView(_ statusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) { - guard statusView === quoteStatusView else { - assertionFailure() - return + public var contentView: some View { + HStack { + Text(viewModel.content.attributedString) + .lineLimit(nil) + .multilineTextAlignment(.leading) + Spacer() } - - delegate?.statusView(self, quoteStatusView: statusView, metaTextAreaView: metaTextAreaView, didSelectMeta: meta) - } - - public func statusView(_ statusView: StatusView, expandContentButtonDidPressed button: UIButton) { - assertionFailure() - } - - public func statusView(_ statusView: StatusView, quoteStatusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) { - assertionFailure() - } - - public func statusView(_ statusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int) { - guard statusView === quoteStatusView else { - assertionFailure() - return - } - - delegate?.statusView(self, quoteStatusView: statusView, mediaGridContainerView: containerView, didTapMediaView: mediaView, at: index) - } - - public func statusView(_ statusView: StatusView, pollTableView tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - assertionFailure() - } - - public func statusView(_ statusView: StatusView, pollVoteButtonDidPressed button: UIButton) { - assertionFailure() - } - - public func statusView(_ statusView: StatusView, quoteStatusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int) { - assertionFailure() - } - - public func statusView(_ statusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, toggleContentWarningOverlayViewDisplay contentWarningOverlayView: ContentWarningOverlayView) { - assertionFailure() - } - - public func statusView(_ statusView: StatusView, quoteStatusViewDidPressed quoteStatusView: StatusView) { - assertionFailure() - } - - public func statusView(_ statusView: StatusView, statusToolbar: StatusToolbar, actionDidPressed action: StatusToolbar.Action, button: UIButton) { - assertionFailure() - } - - public func statusView(_ statusView: StatusView, statusToolbar: StatusToolbar, menuActionDidPressed action: StatusToolbar.MenuAction, menuButton button: UIButton) { - assertionFailure() - } - - public func statusView(_ statusView: StatusView, translateButtonDidPressed button: UIButton) { - assertionFailure() } - - public func statusView(_ statusView: StatusView, accessibilityActivate: Void) { - assertionFailure() - } - } -// MARK: - UITableViewDelegate -extension StatusView: UITableViewDelegate { - public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): select \(indexPath.debugDescription)") - - switch tableView { - case pollTableView: - delegate?.statusView(self, pollTableView: tableView, didSelectRowAt: indexPath) - default: - assertionFailure() - } - } +public protocol StatusViewDelegate: AnyObject { +// func statusView(_ statusView: StatusView, headerDidPressed header: UIView) +// +// func statusView(_ statusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) +// func statusView(_ statusView: StatusView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) +// +// func statusView(_ statusView: StatusView, expandContentButtonDidPressed button: UIButton) +// +// func statusView(_ statusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) +// func statusView(_ statusView: StatusView, quoteStatusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) +// +// func statusView(_ statusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int) +// func statusView(_ statusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, toggleContentWarningOverlayViewDisplay contentWarningOverlayView: ContentWarningOverlayView) +// func statusView(_ statusView: StatusView, quoteStatusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int) +// +// func statusView(_ statusView: StatusView, pollTableView tableView: UITableView, didSelectRowAt indexPath: IndexPath) +// func statusView(_ statusView: StatusView, pollVoteButtonDidPressed button: UIButton) +// +// func statusView(_ statusView: StatusView, quoteStatusViewDidPressed quoteStatusView: StatusView) +// +// func statusView(_ statusView: StatusView, statusToolbar: StatusToolbar, actionDidPressed action: StatusToolbar.Action, button: UIButton) +// func statusView(_ statusView: StatusView, statusToolbar: StatusToolbar, menuActionDidPressed action: StatusToolbar.MenuAction, menuButton button: UIButton) +// +// func statusView(_ statusView: StatusView, translateButtonDidPressed button: UIButton) +// +// // a11y +// func statusView(_ statusView: StatusView, accessibilityActivate: Void) } -// MARK: - UIGestureRecognizerDelegate -extension StatusView: UIGestureRecognizerDelegate { - public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { - if let view = touch.view, view is AvatarButton { - return false - } - - return true - } -} +//public final class StatusView: UIView { +// +// private var _disposeBag = Set() // which lifetime same to view scope +// public var disposeBag = Set() // clear when reuse +// +// public weak var delegate: StatusViewDelegate? +// +// public static let bodyContainerStackViewSpacing: CGFloat = 10 +// public static let quoteStatusViewContainerLayoutMargin: CGFloat = 12 +// +// let logger = Logger(subsystem: "StatusView", category: "View") +// +// public private(set) var style: Style? +// +// public private(set) lazy var viewModel: ViewModel = { +// let viewModel = ViewModel() +// viewModel.bind(statusView: self) +// return viewModel +// }() +// +// // container +// public let containerStackView: UIStackView = { +// let stackView = UIStackView() +// stackView.axis = .vertical +// stackView.spacing = 8 +// return stackView +// }() +// +// // header +// public let headerContainerView = UIView() +// public let headerIconImageView = UIImageView() +// public static var headerTextLabelStyle: TextStyle { .statusHeader } +// public let headerTextLabel: MetaLabel = { +// let label = MetaLabel(style: .statusHeader) +// label.isUserInteractionEnabled = false +// return label +// }() +// +// // avatar +// public let authorAvatarButton = AvatarButton() +// +// // author +// public static var authorNameLabelStyle: TextStyle { .statusAuthorName } +// public let authorNameLabel: MetaLabel = { +// let label = MetaLabel(style: StatusView.authorNameLabelStyle) +// label.accessibilityTraits = .staticText +// return label +// }() +// public let lockImageView: UIImageView = { +// let imageView = UIImageView() +// imageView.tintColor = .secondaryLabel +// imageView.contentMode = .scaleAspectFill +// imageView.image = Asset.ObjectTools.lockMiniInline.image.withRenderingMode(.alwaysTemplate) +// return imageView +// }() +// public let authorUsernameLabel = PlainLabel(style: .statusAuthorUsername) +// public let visibilityImageView: UIImageView = { +// let imageView = UIImageView() +// imageView.tintColor = .secondaryLabel +// imageView.contentMode = .scaleAspectFill +// imageView.image = Asset.ObjectTools.globeMiniInline.image.withRenderingMode(.alwaysTemplate) +// return imageView +// }() +// public let timestampLabel = PlainLabel(style: .statusTimestamp) +// +// // spoiler +// public let spoilerContentTextView: MetaTextAreaView = { +// let textView = MetaTextAreaView() +// textView.textAttributes = [ +// .font: UIFont.preferredFont(forTextStyle: .body), +// .foregroundColor: UIColor.label.withAlphaComponent(0.8), +// ] +// textView.linkAttributes = [ +// .font: UIFont.preferredFont(forTextStyle: .body), +// .foregroundColor: Asset.Colors.Theme.daylight.color +// ] +// return textView +// }() +// +// public let expandContentButtonContainer: UIStackView = { +// let stackView = UIStackView() +// stackView.axis = .horizontal +// stackView.alignment = .leading +// return stackView +// }() +// public let expandContentButton: HitTestExpandedButton = { +// let button = HitTestExpandedButton(type: .system) +// button.setImage(Asset.Editing.ellipsisLarge.image.withRenderingMode(.alwaysTemplate), for: .normal) +// button.layer.masksToBounds = true +// button.layer.cornerRadius = 10 +// button.layer.cornerCurve = .circular +// button.backgroundColor = .tertiarySystemFill +// return button +// }() +// +// // content +// public let contentTextView: MetaTextAreaView = { +// let textView = MetaTextAreaView() +// textView.textAttributes = [ +// .font: UIFont.preferredFont(forTextStyle: .body), +// .foregroundColor: UIColor.label.withAlphaComponent(0.8), +// ] +// textView.linkAttributes = [ +// .font: UIFont.preferredFont(forTextStyle: .body), +// .foregroundColor: Asset.Colors.Theme.daylight.color +// ] +// return textView +// }() +// +// // translate +// let translateButtonContainer = UIStackView() +// public let translateButton: UIButton = { +// let button = HitTestExpandedButton(type: .system) +// button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 14, weight: .medium)) +// button.setTitle(L10n.Common.Controls.Status.Actions.translate, for: .normal) +// return button +// }() +// +// // media +// public let mediaGridContainerView = MediaGridContainerView() +// +// // poll +// public let pollTableView: UITableView = { +// let tableView = UITableView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) +// tableView.register(PollOptionTableViewCell.self, forCellReuseIdentifier: String(describing: PollOptionTableViewCell.self)) +// tableView.isScrollEnabled = false +// tableView.estimatedRowHeight = 36 +// tableView.tableFooterView = UIView() +// tableView.backgroundColor = .clear +// tableView.separatorStyle = .none +// return tableView +// }() +// public var pollTableViewHeightLayoutConstraint: NSLayoutConstraint! +// public var pollTableViewDiffableDataSource: UITableViewDiffableDataSource? +// +// public let pollVoteInfoContainerView = UIStackView() +// public let pollVoteDescriptionLabel = PlainLabel(style: .pollVoteDescription) +// public let pollVoteButton: HitTestExpandedButton = { +// let button = HitTestExpandedButton(type: .system) +// button.setTitle(L10n.Common.Controls.Status.Actions.vote, for: .normal) +// button.setTitleColor(Asset.Colors.hightLight.color, for: .normal) +// button.setTitleColor(.secondaryLabel, for: .disabled) +// return button +// }() +// public let pollVoteActivityIndicatorView: UIActivityIndicatorView = { +// let activityIndicatorView = UIActivityIndicatorView(style: .medium) +// activityIndicatorView.hidesWhenStopped = true +// activityIndicatorView.tintColor = .secondaryLabel +// return activityIndicatorView +// }() +// +// // quote +// public private(set) var quoteStatusView: StatusView? { +// didSet { +// if let quoteStatusView = quoteStatusView { +// quoteStatusView.delegate = self +// +// let quoteTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer +// quoteTapGestureRecognizer.delegate = self +// quoteTapGestureRecognizer.addTarget(self, action: #selector(StatusView.quoteStatusViewDidPressed(_:))) +// quoteStatusView.addGestureRecognizer(quoteTapGestureRecognizer) +// } +// } +// } +// +// // location +// public let locationContainer: UIStackView = { +// let stackView = UIStackView() +// stackView.axis = .horizontal +// stackView.spacing = 6 +// return stackView +// }() +// public let locationMapPinImageView: UIImageView = { +// let imageView = UIImageView() +// imageView.tintColor = .secondaryLabel +// return imageView +// }() +// public let locationLabel = PlainLabel(style: .statusLocation) +// +// // metrics +// public let metricsDashboardView = StatusMetricsDashboardView() +// +// // toolbar +// public let toolbar = StatusToolbar() +// +// // reply settings +// public let replySettingBannerView = ReplySettingBannerView() +// +// public override init(frame: CGRect) { +// super.init(frame: frame) +// _init() +// } +// +// public required init?(coder: NSCoder) { +// super.init(coder: coder) +// _init() +// } +// +// public override func willMove(toWindow newWindow: UIWindow?) { +// super.willMove(toWindow: newWindow) +// assert(style != nil, "Needs setup style before use") +// } +// +// deinit { +// viewModel.disposeBag.removeAll() +// } +// +//} +// +//extension StatusView { +// +// public func prepareForReuse() { +// disposeBag.removeAll() +// viewModel.objects.removeAll() +// viewModel.authorAvatarImageURL = nil +// authorAvatarButton.avatarImageView.cancelTask() +// quoteStatusView?.prepareForReuse() +// mediaGridContainerView.prepareForReuse() +// if var snapshot = pollTableViewDiffableDataSource?.snapshot() { +// snapshot.deleteAllItems() +// pollTableViewDiffableDataSource?.applySnapshotUsingReloadData(snapshot) +// } +// Style.prepareForReuse(statusView: self) +// } +// +// private func _init() { +// containerStackView.translatesAutoresizingMaskIntoConstraints = false +// addSubview(containerStackView) +// NSLayoutConstraint.activate([ +// containerStackView.topAnchor.constraint(equalTo: topAnchor), +// containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor), +// containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor), +// containerStackView.bottomAnchor.constraint(equalTo: bottomAnchor), +// ]) +// +// // header +// let headerTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer +// headerTapGestureRecognizer.addTarget(self, action: #selector(StatusView.headerTapGestureRecognizerHandler(_:))) +// headerContainerView.addGestureRecognizer(headerTapGestureRecognizer) +// +// // avatar button +// authorAvatarButton.accessibilityLabel = L10n.Accessibility.Common.Status.authorAvatar +// authorAvatarButton.accessibilityHint = L10n.Accessibility.VoiceOver.doubleTapToOpenProfile +// authorAvatarButton.addTarget(self, action: #selector(StatusView.authorAvatarButtonDidPressed(_:)), for: .touchUpInside) +// +// // expand content +// expandContentButton.addTarget(self, action: #selector(StatusView.expandContentButtonDidPressed(_:)), for: .touchUpInside) +// +// // content +// contentTextView.delegate = self +// +// // translateButton +// translateButton.addTarget(self, action: #selector(StatusView.translateButtonDidPressed(_:)), for: .touchUpInside) +// +// // media grid +// mediaGridContainerView.delegate = self +// +// // poll +// pollTableView.translatesAutoresizingMaskIntoConstraints = false +// pollTableViewHeightLayoutConstraint = pollTableView.heightAnchor.constraint(equalToConstant: 36).priority(.required - 10) +// NSLayoutConstraint.activate([ +// pollTableViewHeightLayoutConstraint, +// ]) +// pollTableView.delegate = self +// pollVoteButton.addTarget(self, action: #selector(StatusView.pollVoteButtonDidPressed(_:)), for: .touchUpInside) +// +// // toolbar +// toolbar.delegate = self +// +// // theme +// ThemeService.shared.theme +// .sink { [weak self] theme in +// guard let self = self else { return } +// self.update(theme: theme) +// } +// .store(in: &disposeBag) +// } +// +// public func setup(style: Style) { +// guard self.style == nil else { +// assertionFailure("Should only setup once") +// return +// } +// self.style = style +// style.layout(statusView: self) +// Style.prepareForReuse(statusView: self) +// } +// +//} +// +//extension StatusView { +// public enum Style { +// case inline // for timeline +// case plain // for thread +// case quote // for quote +// case composeReply // for compose +// +// func layout(statusView: StatusView) { +// switch self { +// case .inline: layoutInline(statusView: statusView) +// case .plain: layoutPlain(statusView: statusView) +// case .quote: layoutQuote(statusView: statusView) +// case .composeReply: layoutComposeReply(statusView: statusView) +// } +// } +// +// static func prepareForReuse(statusView: StatusView) { +// statusView.headerContainerView.isHidden = true +// statusView.lockImageView.isHidden = true +// statusView.visibilityImageView.isHidden = true +// statusView.spoilerContentTextView.isHidden = true +// statusView.expandContentButtonContainer.isHidden = true +// statusView.translateButtonContainer.isHidden = true +// statusView.mediaGridContainerView.isHidden = true +// statusView.pollTableView.isHidden = true +// statusView.pollVoteInfoContainerView.isHidden = true +// statusView.pollVoteActivityIndicatorView.isHidden = true +// statusView.quoteStatusView?.isHidden = true +// statusView.locationContainer.isHidden = true +// statusView.replySettingBannerView.isHidden = true +// } +// } +//} +// +//extension StatusView.Style { +// +// private func layoutInline(statusView: StatusView) { +// // container: V - [ header container | body container ] +// +// // header container: H - [ icon | label ] +// statusView.containerStackView.addArrangedSubview(statusView.headerContainerView) +// statusView.headerIconImageView.translatesAutoresizingMaskIntoConstraints = false +// statusView.headerTextLabel.translatesAutoresizingMaskIntoConstraints = false +// statusView.headerContainerView.addSubview(statusView.headerIconImageView) +// statusView.headerContainerView.addSubview(statusView.headerTextLabel) +// NSLayoutConstraint.activate([ +// statusView.headerTextLabel.topAnchor.constraint(equalTo: statusView.headerContainerView.topAnchor), +// statusView.headerTextLabel.bottomAnchor.constraint(equalTo: statusView.headerContainerView.bottomAnchor), +// statusView.headerTextLabel.trailingAnchor.constraint(equalTo: statusView.headerContainerView.trailingAnchor), +// statusView.headerIconImageView.centerYAnchor.constraint(equalTo: statusView.headerTextLabel.centerYAnchor), +// statusView.headerIconImageView.heightAnchor.constraint(equalTo: statusView.headerTextLabel.heightAnchor, multiplier: 1.0).priority(.required - 1), +// statusView.headerIconImageView.widthAnchor.constraint(equalTo: statusView.headerIconImageView.heightAnchor, multiplier: 1.0).priority(.required - 1), +// statusView.headerTextLabel.leadingAnchor.constraint(equalTo: statusView.headerIconImageView.trailingAnchor, constant: 4), +// // align to author name below +// ]) +// statusView.headerTextLabel.setContentHuggingPriority(.required - 10, for: .vertical) +// statusView.headerIconImageView.setContentHuggingPriority(.defaultLow, for: .vertical) +// statusView.headerIconImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) +// +// // body container: H - [ authorAvatarButton | content container ] +// let bodyContainerStackView = UIStackView() +// bodyContainerStackView.axis = .horizontal +// bodyContainerStackView.spacing = StatusView.bodyContainerStackViewSpacing +// bodyContainerStackView.alignment = .top +// statusView.containerStackView.addArrangedSubview(bodyContainerStackView) +// +// // authorAvatarButton +// let authorAvatarButtonSize = CGSize(width: 44, height: 44) +// statusView.authorAvatarButton.size = authorAvatarButtonSize +// statusView.authorAvatarButton.avatarImageView.imageViewSize = authorAvatarButtonSize +// statusView.authorAvatarButton.translatesAutoresizingMaskIntoConstraints = false +// bodyContainerStackView.addArrangedSubview(statusView.authorAvatarButton) +// NSLayoutConstraint.activate([ +// statusView.authorAvatarButton.widthAnchor.constraint(equalToConstant: authorAvatarButtonSize.width).priority(.required - 1), +// statusView.authorAvatarButton.heightAnchor.constraint(equalToConstant: authorAvatarButtonSize.height).priority(.required - 1), +// ]) +// +// // content container: V - [ author content | contentTextView | mediaGridContainerView | quoteStatusView | … | location content | toolbar ] +// let contentContainerView = UIStackView() +// contentContainerView.axis = .vertical +// contentContainerView.spacing = 10 +// bodyContainerStackView.addArrangedSubview(contentContainerView) +// +// // author content: H - [ authorNameLabel | lockImageView | authorUsernameLabel | padding | visibilityImageView (for Mastodon) | timestampLabel ] +// let authorContentStackView = UIStackView() +// authorContentStackView.axis = .horizontal +// authorContentStackView.spacing = 6 +// contentContainerView.addArrangedSubview(authorContentStackView) +// contentContainerView.setCustomSpacing(4, after: authorContentStackView) +// UIContentSizeCategory.publisher +// .sink { category in +// authorContentStackView.axis = category > .accessibilityLarge ? .vertical : .horizontal +// authorContentStackView.alignment = category > .accessibilityLarge ? .leading : .fill +// } +// .store(in: &statusView._disposeBag) +// +// // authorNameLabel +// authorContentStackView.addArrangedSubview(statusView.authorNameLabel) +// statusView.authorNameLabel.setContentHuggingPriority(.required - 10, for: .horizontal) +// statusView.authorNameLabel.setContentCompressionResistancePriority(.required - 10, for: .horizontal) +// // lockImageView +// statusView.lockImageView.translatesAutoresizingMaskIntoConstraints = false +// authorContentStackView.addArrangedSubview(statusView.lockImageView) +// // authorUsernameLabel +// authorContentStackView.addArrangedSubview(statusView.authorUsernameLabel) +// NSLayoutConstraint.activate([ +// statusView.lockImageView.heightAnchor.constraint(equalTo: statusView.authorUsernameLabel.heightAnchor).priority(.required - 10), +// ]) +// statusView.lockImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) +// statusView.lockImageView.setContentCompressionResistancePriority(.required - 10, for: .horizontal) +// statusView.authorUsernameLabel.setContentCompressionResistancePriority(.required - 11, for: .horizontal) +// // padding +// authorContentStackView.addArrangedSubview(UIView()) +// // visibilityImageView +// authorContentStackView.addArrangedSubview(statusView.visibilityImageView) +// statusView.visibilityImageView.setContentHuggingPriority(.required - 9, for: .horizontal) +// statusView.visibilityImageView.setContentCompressionResistancePriority(.required - 9, for: .horizontal) +// // timestampLabel +// authorContentStackView.addArrangedSubview(statusView.timestampLabel) +// statusView.timestampLabel.setContentHuggingPriority(.required - 8, for: .horizontal) +// statusView.timestampLabel.setContentCompressionResistancePriority(.required - 8, for: .horizontal) +// +// // set header label align to author name +// NSLayoutConstraint.activate([ +// statusView.headerTextLabel.leadingAnchor.constraint(equalTo: statusView.authorNameLabel.leadingAnchor), +// ]) +// +// // spoilerContentTextView +// contentContainerView.addArrangedSubview(statusView.spoilerContentTextView) +// statusView.spoilerContentTextView.setContentHuggingPriority(.required - 9, for: .vertical) +// statusView.spoilerContentTextView.setContentCompressionResistancePriority(.required - 10, for: .vertical) +// +// contentContainerView.addArrangedSubview(statusView.expandContentButtonContainer) +// statusView.expandContentButton.translatesAutoresizingMaskIntoConstraints = false +// statusView.expandContentButtonContainer.addArrangedSubview(statusView.expandContentButton) +// statusView.expandContentButtonContainer.addArrangedSubview(UIView()) +// +// // contentTextView +// // statusView.contentTextView.translatesAutoresizingMaskIntoConstraints = false +// contentContainerView.addArrangedSubview(statusView.contentTextView) +// statusView.contentTextView.setContentHuggingPriority(.required - 10, for: .vertical) +// +// // mediaGridContainerView +// statusView.mediaGridContainerView.translatesAutoresizingMaskIntoConstraints = false +// contentContainerView.addArrangedSubview(statusView.mediaGridContainerView) +// +// // pollTableView +// contentContainerView.addArrangedSubview(statusView.pollTableView) +// +// statusView.pollVoteInfoContainerView.axis = .horizontal +// statusView.pollVoteInfoContainerView.addArrangedSubview(statusView.pollVoteDescriptionLabel) +// statusView.pollVoteInfoContainerView.addArrangedSubview(UIView()) // spacer +// statusView.pollVoteInfoContainerView.addArrangedSubview(statusView.pollVoteButton) +// statusView.pollVoteButton.setContentHuggingPriority(.required - 10, for: .horizontal) +// statusView.pollVoteButton.setContentCompressionResistancePriority(.required - 9, for: .horizontal) +// statusView.pollVoteInfoContainerView.addArrangedSubview(statusView.pollVoteActivityIndicatorView) +// statusView.pollVoteInfoContainerView.setContentHuggingPriority(.required - 11, for: .horizontal) +// statusView.pollVoteInfoContainerView.setContentCompressionResistancePriority(.required - 10, for: .horizontal) +// contentContainerView.addArrangedSubview(statusView.pollVoteInfoContainerView) +// +// // quoteStatusView +// let quoteStatusView = StatusView() +// quoteStatusView.setup(style: .quote) +// statusView.quoteStatusView = quoteStatusView +// contentContainerView.addArrangedSubview(quoteStatusView) +// +// // location content: H - [ locationMapPinImageView | locationLabel ] +// contentContainerView.addArrangedSubview(statusView.locationContainer) +// +// statusView.locationMapPinImageView.translatesAutoresizingMaskIntoConstraints = false +// statusView.locationLabel.translatesAutoresizingMaskIntoConstraints = false +// +// // locationMapPinImageView +// statusView.locationContainer.addArrangedSubview(statusView.locationMapPinImageView) +// // locationLabel +// statusView.locationContainer.addArrangedSubview(statusView.locationLabel) +// +// NSLayoutConstraint.activate([ +// statusView.locationMapPinImageView.heightAnchor.constraint(equalTo: statusView.locationLabel.heightAnchor, multiplier: 1.0).priority(.required - 1), +// statusView.locationMapPinImageView.widthAnchor.constraint(equalTo: statusView.locationMapPinImageView.heightAnchor, multiplier: 1.0).priority(.required - 1), +// ]) +// statusView.locationLabel.setContentHuggingPriority(.required - 10, for: .vertical) +// statusView.locationMapPinImageView.setContentHuggingPriority(.defaultLow, for: .vertical) +// statusView.locationMapPinImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) +// +// // toolbar +// contentContainerView.addArrangedSubview(statusView.toolbar) +// statusView.toolbar.setContentHuggingPriority(.required - 9, for: .vertical) +// } +// +// private func layoutPlain(statusView: StatusView) { +// // container: V - [ header container | author container | contentTextView | mediaGridContainerView | quoteStatusView | location content | toolbar ] +// +// // header container: H - [ icon | label ] +// statusView.containerStackView.addArrangedSubview(statusView.headerContainerView) +// statusView.headerIconImageView.translatesAutoresizingMaskIntoConstraints = false +// statusView.headerTextLabel.translatesAutoresizingMaskIntoConstraints = false +// statusView.headerContainerView.addSubview(statusView.headerIconImageView) +// statusView.headerContainerView.addSubview(statusView.headerTextLabel) +// NSLayoutConstraint.activate([ +// statusView.headerTextLabel.topAnchor.constraint(equalTo: statusView.headerContainerView.topAnchor), +// statusView.headerTextLabel.bottomAnchor.constraint(equalTo: statusView.headerContainerView.bottomAnchor), +// statusView.headerTextLabel.trailingAnchor.constraint(equalTo: statusView.headerContainerView.trailingAnchor), +// statusView.headerIconImageView.centerYAnchor.constraint(equalTo: statusView.headerTextLabel.centerYAnchor), +// statusView.headerIconImageView.heightAnchor.constraint(equalTo: statusView.headerTextLabel.heightAnchor, multiplier: 1.0).priority(.required - 1), +// statusView.headerIconImageView.widthAnchor.constraint(equalTo: statusView.headerIconImageView.heightAnchor, multiplier: 1.0).priority(.required - 1), +// statusView.headerTextLabel.leadingAnchor.constraint(equalTo: statusView.headerIconImageView.trailingAnchor, constant: 4), +// // align to author name below +// ]) +// statusView.headerTextLabel.setContentHuggingPriority(.required - 10, for: .vertical) +// statusView.headerIconImageView.setContentHuggingPriority(.defaultLow, for: .vertical) +// statusView.headerIconImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) +// statusView.headerContainerView.isHidden = true +// +// // author content: H - [ authorAvatarButton | author info content ] +// let authorContentStackView = UIStackView() +// let authorContentStackViewSpacing: CGFloat = 10 +// authorContentStackView.axis = .horizontal +// authorContentStackView.spacing = authorContentStackViewSpacing +// statusView.containerStackView.addArrangedSubview(authorContentStackView) +// +// // authorAvatarButton +// let authorAvatarButtonSize = CGSize(width: 44, height: 44) +// statusView.authorAvatarButton.size = authorAvatarButtonSize +// statusView.authorAvatarButton.avatarImageView.imageViewSize = authorAvatarButtonSize +// statusView.authorAvatarButton.translatesAutoresizingMaskIntoConstraints = false +// authorContentStackView.addArrangedSubview(statusView.authorAvatarButton) +// let authorAvatarButtonWidthFixLayoutConstraint = statusView.authorAvatarButton.widthAnchor.constraint(equalToConstant: authorAvatarButtonSize.width).priority(.required - 1) +// NSLayoutConstraint.activate([ +// authorAvatarButtonWidthFixLayoutConstraint, +// statusView.authorAvatarButton.heightAnchor.constraint(equalTo: statusView.authorAvatarButton.widthAnchor, multiplier: 1.0).priority(.required - 1), +// ]) +// +// // author info content: V - [ author info headline content | author info sub-headline content ] +// let authorInfoContentStackView = UIStackView() +// authorInfoContentStackView.axis = .vertical +// authorContentStackView.addArrangedSubview(authorInfoContentStackView) +// +// // author info headline content: H - [ authorNameLabel | lockImageView | padding | visibilityImageView (for Mastodon) ] +// let authorInfoHeadlineContentStackView = UIStackView() +// authorInfoHeadlineContentStackView.axis = .horizontal +// authorInfoHeadlineContentStackView.spacing = 2 +// authorInfoContentStackView.addArrangedSubview(authorInfoHeadlineContentStackView) +// +// // authorNameLabel +// authorInfoHeadlineContentStackView.addArrangedSubview(statusView.authorNameLabel) +// statusView.authorNameLabel.setContentCompressionResistancePriority(.required - 10, for: .horizontal) +// // lockImageView +// statusView.lockImageView.translatesAutoresizingMaskIntoConstraints = false +// authorInfoHeadlineContentStackView.addArrangedSubview(statusView.lockImageView) +// NSLayoutConstraint.activate([ +// statusView.lockImageView.heightAnchor.constraint(equalTo: statusView.authorNameLabel.heightAnchor).priority(.required - 10), +// ]) +// statusView.lockImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) +// statusView.lockImageView.setContentCompressionResistancePriority(.required - 10, for: .horizontal) +// // padding +// authorInfoHeadlineContentStackView.addArrangedSubview(UIView()) +// // visibilityImageView +// authorInfoHeadlineContentStackView.addArrangedSubview(statusView.visibilityImageView) +// statusView.visibilityImageView.setContentHuggingPriority(.required - 9, for: .horizontal) +// statusView.visibilityImageView.setContentCompressionResistancePriority(.required - 9, for: .horizontal) +// +// // set header label align to author name +// NSLayoutConstraint.activate([ +// statusView.headerTextLabel.leadingAnchor.constraint(equalTo: statusView.authorAvatarButton.trailingAnchor, constant: authorContentStackViewSpacing), +// ]) +// +// // author info sub-headline content: H - [ authorUsernameLabel ] +// let authorInfoSubHeadlineContentStackView = UIStackView() +// authorInfoSubHeadlineContentStackView.axis = .horizontal +// authorInfoContentStackView.addArrangedSubview(authorInfoSubHeadlineContentStackView) +// +// UIContentSizeCategory.publisher +// .sink { category in +// if category >= .extraExtraLarge { +// authorContentStackView.axis = .vertical +// authorContentStackView.alignment = .leading // set leading +// } else { +// authorContentStackView.axis = .horizontal +// authorContentStackView.alignment = .fill // restore default +// } +// } +// .store(in: &statusView._disposeBag) +// +// // authorUsernameLabel +// authorInfoSubHeadlineContentStackView.addArrangedSubview(statusView.authorUsernameLabel) +// +// // spoilerContentTextView +// statusView.containerStackView.addArrangedSubview(statusView.spoilerContentTextView) +// statusView.spoilerContentTextView.setContentCompressionResistancePriority(.required - 10, for: .vertical) +// statusView.spoilerContentTextView.setContentHuggingPriority(.required - 9, for: .vertical) +// +// statusView.containerStackView.addArrangedSubview(statusView.expandContentButtonContainer) +// statusView.expandContentButton.translatesAutoresizingMaskIntoConstraints = false +// statusView.expandContentButtonContainer.addArrangedSubview(statusView.expandContentButton) +// statusView.expandContentButtonContainer.addArrangedSubview(UIView()) +// +// // contentTextView +// statusView.containerStackView.addArrangedSubview(statusView.contentTextView) +// +// // translateButtonContainer: H - [ translateButton | (spacer) ] +// statusView.translateButtonContainer.axis = .horizontal +// statusView.containerStackView.addArrangedSubview(statusView.translateButtonContainer) +// +// statusView.translateButtonContainer.addArrangedSubview(statusView.translateButton) +// statusView.translateButtonContainer.addArrangedSubview(UIView()) +// statusView.translateButton.setContentHuggingPriority(.required - 1, for: .vertical) +// statusView.translateButton.setContentCompressionResistancePriority(.required - 1, for: .vertical) +// +// // mediaGridContainerView +// statusView.containerStackView.addArrangedSubview(statusView.mediaGridContainerView) +// +// // pollTableView +// statusView.containerStackView.addArrangedSubview(statusView.pollTableView) +// +// statusView.pollVoteInfoContainerView.axis = .horizontal +// statusView.pollVoteInfoContainerView.addArrangedSubview(statusView.pollVoteDescriptionLabel) +// statusView.pollVoteInfoContainerView.addArrangedSubview(UIView()) // spacer +// statusView.pollVoteInfoContainerView.addArrangedSubview(statusView.pollVoteButton) +// statusView.pollVoteButton.setContentHuggingPriority(.required - 10, for: .horizontal) +// statusView.pollVoteButton.setContentCompressionResistancePriority(.required - 9, for: .horizontal) +// statusView.pollVoteInfoContainerView.addArrangedSubview(statusView.pollVoteActivityIndicatorView) +// statusView.pollVoteInfoContainerView.setContentHuggingPriority(.required - 11, for: .horizontal) +// statusView.pollVoteInfoContainerView.setContentCompressionResistancePriority(.required - 10, for: .horizontal) +// statusView.containerStackView.addArrangedSubview(statusView.pollVoteInfoContainerView) +// +// // quoteStatusView +// let quoteStatusView = StatusView() +// quoteStatusView.setup(style: .quote) +// statusView.quoteStatusView = quoteStatusView +// statusView.containerStackView.addArrangedSubview(quoteStatusView) +// +// // location content: H - [ padding | locationMapPinImageView | locationLabel | padding ] +// statusView.containerStackView.addArrangedSubview(statusView.locationContainer) +// +// statusView.locationMapPinImageView.translatesAutoresizingMaskIntoConstraints = false +// statusView.locationLabel.translatesAutoresizingMaskIntoConstraints = false +// +// // locationLeadingPadding +// let locationLeadingPadding = UIView() +// locationLeadingPadding.translatesAutoresizingMaskIntoConstraints = false +// statusView.locationContainer.addArrangedSubview(locationLeadingPadding) +// // locationMapPinImageView +// statusView.locationContainer.addArrangedSubview(statusView.locationMapPinImageView) +// // locationLabel +// statusView.locationContainer.addArrangedSubview(statusView.locationLabel) +// // locationTrailingPadding +// let locationTrailingPadding = UIView() +// locationTrailingPadding.translatesAutoresizingMaskIntoConstraints = false +// statusView.locationContainer.addArrangedSubview(locationTrailingPadding) +// +// // center alignment +// NSLayoutConstraint.activate([ +// locationLeadingPadding.widthAnchor.constraint(equalTo: locationTrailingPadding.widthAnchor).priority(.defaultHigh), +// ]) +// +// NSLayoutConstraint.activate([ +// statusView.locationMapPinImageView.heightAnchor.constraint(equalTo: statusView.locationLabel.heightAnchor, multiplier: 1.0).priority(.required - 1), +// statusView.locationMapPinImageView.widthAnchor.constraint(equalTo: statusView.locationMapPinImageView.heightAnchor, multiplier: 1.0).priority(.required - 1), +// ]) +// statusView.locationLabel.setContentHuggingPriority(.required - 10, for: .vertical) +// statusView.locationMapPinImageView.setContentHuggingPriority(.defaultLow, for: .vertical) +// statusView.locationMapPinImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) +// +// // metrics +// statusView.containerStackView.addArrangedSubview(statusView.metricsDashboardView) +// +// UIContentSizeCategory.publisher +// .sink { category in +// if category >= .extraExtraLarge { +// statusView.metricsDashboardView.metaContainer.axis = .vertical +// } else { +// statusView.metricsDashboardView.metaContainer.axis = .horizontal +// } +// } +// .store(in: &statusView._disposeBag) +// +// // toolbar +// statusView.containerStackView.addArrangedSubview(statusView.toolbar) +// statusView.toolbar.setContentHuggingPriority(.required - 9, for: .vertical) +// +// // reply settings +// statusView.containerStackView.addArrangedSubview(statusView.replySettingBannerView) +// } +// +// private func layoutQuote(statusView: StatusView) { +// // container: V - [ body container ] +// // set `isLayoutMarginsRelativeArrangement` not works with AutoLayout (priority issue) +// // add constraint to workaround +// statusView.containerStackView.backgroundColor = .secondarySystemBackground +// statusView.containerStackView.layer.masksToBounds = true +// statusView.containerStackView.layer.cornerCurve = .continuous +// statusView.containerStackView.layer.cornerRadius = 12 +// +// // body container: V - [ author content | content container | contentTextView | mediaGridContainerView ] +// let bodyContainerStackView = UIStackView() +// bodyContainerStackView.axis = .vertical +// bodyContainerStackView.spacing = StatusView.bodyContainerStackViewSpacing +// bodyContainerStackView.translatesAutoresizingMaskIntoConstraints = false +// statusView.containerStackView.addSubview(bodyContainerStackView) +// NSLayoutConstraint.activate([ +// bodyContainerStackView.topAnchor.constraint(equalTo: statusView.containerStackView.topAnchor, constant: StatusView.quoteStatusViewContainerLayoutMargin).priority(.required - 1), +// bodyContainerStackView.leadingAnchor.constraint(equalTo: statusView.containerStackView.leadingAnchor, constant: StatusView.quoteStatusViewContainerLayoutMargin).priority(.required - 1), +// statusView.containerStackView.trailingAnchor.constraint(equalTo: bodyContainerStackView.trailingAnchor, constant: StatusView.quoteStatusViewContainerLayoutMargin).priority(.required - 1), +// statusView.containerStackView.bottomAnchor.constraint(equalTo: bodyContainerStackView.bottomAnchor, constant: StatusView.quoteStatusViewContainerLayoutMargin).priority(.required - 1), +// ]) +// +// // author content: H - [ authorAvatarButton | authorNameLabel | lockImageView | authorUsernameLabel | padding ] +// let authorContentStackView = UIStackView() +// authorContentStackView.axis = .horizontal +// bodyContainerStackView.alignment = .top +// authorContentStackView.spacing = 6 +// bodyContainerStackView.addArrangedSubview(authorContentStackView) +// bodyContainerStackView.setCustomSpacing(4, after: authorContentStackView) +// +// // authorAvatarButton +// statusView.authorAvatarButton.translatesAutoresizingMaskIntoConstraints = false +// authorContentStackView.addArrangedSubview(statusView.authorAvatarButton) +// // authorNameLabel +// authorContentStackView.addArrangedSubview(statusView.authorNameLabel) +// statusView.authorNameLabel.setContentCompressionResistancePriority(.required - 10, for: .horizontal) +// // lockImageView +// statusView.lockImageView.translatesAutoresizingMaskIntoConstraints = false +// authorContentStackView.addArrangedSubview(statusView.lockImageView) +// // authorUsernameLabel +// authorContentStackView.addArrangedSubview(statusView.authorUsernameLabel) +// NSLayoutConstraint.activate([ +// statusView.lockImageView.heightAnchor.constraint(equalTo: statusView.authorUsernameLabel.heightAnchor).priority(.required - 10), +// ]) +// statusView.lockImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) +// statusView.lockImageView.setContentCompressionResistancePriority(.required - 10, for: .horizontal) +// statusView.authorUsernameLabel.setContentCompressionResistancePriority(.required - 11, for: .horizontal) +// // padding +// authorContentStackView.addArrangedSubview(UIView()) +// +// NSLayoutConstraint.activate([ +// statusView.authorAvatarButton.heightAnchor.constraint(equalTo: statusView.authorNameLabel.heightAnchor, multiplier: 1.0).priority(.required - 10), +// statusView.authorAvatarButton.widthAnchor.constraint(equalTo: statusView.authorNameLabel.heightAnchor, multiplier: 1.0).priority(.required - 10), +// ]) +// // low priority for intrinsic size hugging +// statusView.authorAvatarButton.setContentHuggingPriority(.defaultLow - 10, for: .vertical) +// statusView.authorAvatarButton.setContentHuggingPriority(.defaultLow - 10, for: .horizontal) +// // high priority but lower then layout constraint for size compression +// statusView.authorAvatarButton.setContentCompressionResistancePriority(.required - 11, for: .vertical) +// statusView.authorAvatarButton.setContentCompressionResistancePriority(.required - 11, for: .horizontal) +// statusView.authorNameLabel.setContentCompressionResistancePriority(.required - 1, for: .vertical) +// statusView.authorNameLabel.setContentHuggingPriority(.required - 1, for: .vertical) +// +// // contentTextView +// statusView.contentTextView.translatesAutoresizingMaskIntoConstraints = false +// bodyContainerStackView.addArrangedSubview(statusView.contentTextView) +// statusView.contentTextView.setContentHuggingPriority(.required - 10, for: .vertical) +// statusView.contentTextView.textAttributes = [ +// .font: UIFont.preferredFont(forTextStyle: .callout), +// .foregroundColor: UIColor.secondaryLabel, +// ] +// statusView.contentTextView.linkAttributes = [ +// .font: UIFont.preferredFont(forTextStyle: .callout), +// .foregroundColor: Asset.Colors.Theme.daylight.color +// ] +// +// // mediaGridContainerView +// statusView.mediaGridContainerView.translatesAutoresizingMaskIntoConstraints = false +// bodyContainerStackView.addArrangedSubview(statusView.mediaGridContainerView) +// } +// +// func layoutComposeReply(statusView: StatusView) { +// // container: V - [ body container ] +// +// // body container: H - [ authorAvatarButton | content container ] +// let bodyContainerStackView = UIStackView() +// bodyContainerStackView.axis = .horizontal +// bodyContainerStackView.spacing = StatusView.bodyContainerStackViewSpacing +// bodyContainerStackView.alignment = .top +// statusView.containerStackView.addArrangedSubview(bodyContainerStackView) +// +// // authorAvatarButton +// let authorAvatarButtonSize = CGSize(width: 44, height: 44) +// statusView.authorAvatarButton.size = authorAvatarButtonSize +// statusView.authorAvatarButton.avatarImageView.imageViewSize = authorAvatarButtonSize +// statusView.authorAvatarButton.translatesAutoresizingMaskIntoConstraints = false +// bodyContainerStackView.addArrangedSubview(statusView.authorAvatarButton) +// NSLayoutConstraint.activate([ +// statusView.authorAvatarButton.widthAnchor.constraint(equalToConstant: authorAvatarButtonSize.width).priority(.required - 1), +// statusView.authorAvatarButton.heightAnchor.constraint(equalToConstant: authorAvatarButtonSize.height).priority(.required - 1), +// ]) +// +// // content container: V - [ author content | contentTextView | mediaGridContainerView | quoteStatusView | … | location content | toolbar ] +// let contentContainerView = UIStackView() +// contentContainerView.axis = .vertical +// contentContainerView.spacing = 10 +// bodyContainerStackView.addArrangedSubview(contentContainerView) +// +// // author content: H - [ authorNameLabel | lockImageView | authorUsernameLabel | padding | visibilityImageView (for Mastodon) | timestampLabel ] +// let authorContentStackView = UIStackView() +// authorContentStackView.axis = .horizontal +// authorContentStackView.spacing = 6 +// contentContainerView.addArrangedSubview(authorContentStackView) +// contentContainerView.setCustomSpacing(4, after: authorContentStackView) +// UIContentSizeCategory.publisher +// .sink { category in +// authorContentStackView.axis = category > .accessibilityLarge ? .vertical : .horizontal +// } +// .store(in: &statusView._disposeBag) +// +// // authorNameLabel +// authorContentStackView.addArrangedSubview(statusView.authorNameLabel) +// statusView.authorNameLabel.setContentCompressionResistancePriority(.required - 10, for: .horizontal) +// // lockImageView +// statusView.lockImageView.translatesAutoresizingMaskIntoConstraints = false +// authorContentStackView.addArrangedSubview(statusView.lockImageView) +// // authorUsernameLabel +// authorContentStackView.addArrangedSubview(statusView.authorUsernameLabel) +// NSLayoutConstraint.activate([ +// statusView.lockImageView.heightAnchor.constraint(equalTo: statusView.authorUsernameLabel.heightAnchor).priority(.required - 10), +// ]) +// statusView.lockImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) +// statusView.lockImageView.setContentCompressionResistancePriority(.required - 10, for: .horizontal) +// statusView.authorUsernameLabel.setContentCompressionResistancePriority(.required - 11, for: .horizontal) +// // padding +// authorContentStackView.addArrangedSubview(UIView()) +// // visibilityImageView +// authorContentStackView.addArrangedSubview(statusView.visibilityImageView) +// statusView.visibilityImageView.setContentHuggingPriority(.required - 9, for: .horizontal) +// statusView.visibilityImageView.setContentCompressionResistancePriority(.required - 9, for: .horizontal) +// // timestampLabel +// authorContentStackView.addArrangedSubview(statusView.timestampLabel) +// statusView.timestampLabel.setContentHuggingPriority(.required - 9, for: .horizontal) +// statusView.timestampLabel.setContentCompressionResistancePriority(.required - 9, for: .horizontal) +// +// // spoilerContentTextView +// contentContainerView.addArrangedSubview(statusView.spoilerContentTextView) +// statusView.spoilerContentTextView.setContentCompressionResistancePriority(.required - 10, for: .vertical) +// statusView.spoilerContentTextView.setContentHuggingPriority(.required - 9, for: .vertical) +// +// contentContainerView.addArrangedSubview(statusView.expandContentButtonContainer) +// statusView.expandContentButton.translatesAutoresizingMaskIntoConstraints = false +// statusView.expandContentButtonContainer.addArrangedSubview(statusView.expandContentButton) +// statusView.expandContentButtonContainer.addArrangedSubview(UIView()) +// +// // contentTextView +// statusView.contentTextView.translatesAutoresizingMaskIntoConstraints = false +// contentContainerView.addArrangedSubview(statusView.contentTextView) +// statusView.contentTextView.setContentHuggingPriority(.required - 10, for: .vertical) +// +// // mediaGridContainerView +// statusView.mediaGridContainerView.translatesAutoresizingMaskIntoConstraints = false +// contentContainerView.addArrangedSubview(statusView.mediaGridContainerView) +// +// // quoteStatusView +// let quoteStatusView = StatusView() +// quoteStatusView.setup(style: .quote) +// statusView.quoteStatusView = quoteStatusView +// contentContainerView.addArrangedSubview(quoteStatusView) +// +// // location content: H - [ locationMapPinImageView | locationLabel ] +// contentContainerView.addArrangedSubview(statusView.locationContainer) +// +// statusView.locationMapPinImageView.translatesAutoresizingMaskIntoConstraints = false +// statusView.locationLabel.translatesAutoresizingMaskIntoConstraints = false +// +// // locationMapPinImageView +// statusView.locationContainer.addArrangedSubview(statusView.locationMapPinImageView) +// // locationLabel +// statusView.locationContainer.addArrangedSubview(statusView.locationLabel) +// +// NSLayoutConstraint.activate([ +// statusView.locationMapPinImageView.heightAnchor.constraint(equalTo: statusView.locationLabel.heightAnchor, multiplier: 1.0).priority(.required - 1), +// statusView.locationMapPinImageView.widthAnchor.constraint(equalTo: statusView.locationMapPinImageView.heightAnchor, multiplier: 1.0).priority(.required - 1), +// ]) +// statusView.locationLabel.setContentHuggingPriority(.required - 10, for: .vertical) +// statusView.locationMapPinImageView.setContentHuggingPriority(.defaultLow, for: .vertical) +// statusView.locationMapPinImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) +// } +// +//} +// +//extension StatusView { +// +// private func update(theme: Theme) { +// headerIconImageView.tintColor = theme.accentColor +// } +// +// public func setHeaderDisplay() { +// headerContainerView.isHidden = false +// } +// +// public func setLockDisplay() { +// lockImageView.isHidden = false +// } +// +// public func setVisibilityDisplay() { +// visibilityImageView.isHidden = false +// } +// +// public func setSpoilerDisplay() { +// spoilerContentTextView.isHidden = false +// expandContentButtonContainer.isHidden = false +// } +// +// public func setTranslateButtonDisplay() { +// translateButtonContainer.isHidden = false +// } +// +// public func setMediaDisplay() { +// mediaGridContainerView.isHidden = false +// } +// +// public func setPollDisplay() { +// pollTableView.isHidden = false +// pollVoteInfoContainerView.isHidden = false +// } +// +// public func setQuoteDisplay() { +// quoteStatusView?.isHidden = false +// } +// +// public func setLocationDisplay() { +// locationContainer.isHidden = false +// } +// +// public func setReplySettingsDisplay() { +// replySettingBannerView.isHidden = false +// } +// +// // content text Width +// public var contentMaxLayoutWidth: CGFloat { +// let inset = contentLayoutInset +// return frame.width - inset.left - inset.right +// } +// +// public var contentLayoutInset: UIEdgeInsets { +// guard let style = style else { +// assertionFailure("Needs setup style before use") +// return .zero +// } +// +// switch style { +// case .inline, .composeReply: +// let left = authorAvatarButton.size.width + StatusView.bodyContainerStackViewSpacing +// return UIEdgeInsets(top: 0, left: left, bottom: 0, right: 0) +// case .plain: +// return .zero +// case .quote: +// let margin = StatusView.quoteStatusViewContainerLayoutMargin +// return UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) +// } +// } +// +//} +// +//extension StatusView { +// @objc private func headerTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") +// delegate?.statusView(self, headerDidPressed: headerContainerView) +// } +// +// @objc private func authorAvatarButtonDidPressed(_ sender: UIButton) { +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") +// delegate?.statusView(self, authorAvatarButtonDidPressed: authorAvatarButton) +// } +// +// @objc private func expandContentButtonDidPressed(_ sender: UIButton) { +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") +// delegate?.statusView(self, expandContentButtonDidPressed: sender) +// } +// +// @objc private func pollVoteButtonDidPressed(_ sender: UIButton) { +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") +// delegate?.statusView(self, pollVoteButtonDidPressed: sender) +// } +// +// @objc private func quoteStatusViewDidPressed(_ sender: UITapGestureRecognizer) { +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") +// guard let quoteStatusView = quoteStatusView else { return } +// delegate?.statusView(self, quoteStatusViewDidPressed: quoteStatusView) +// } +// +// @objc private func quoteAuthorAvatarButtonDidPressed(_ sender: UITapGestureRecognizer) { +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") +// guard let quoteStatusView = quoteStatusView else { return } +// delegate?.statusView(self, quoteStatusView: quoteStatusView, authorAvatarButtonDidPressed: quoteStatusView.authorAvatarButton) +// } +// +// @objc private func translateButtonDidPressed(_ sender: UIButton) { +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") +// delegate?.statusView(self, translateButtonDidPressed: sender) +// } +//} +// +//// MARK: - MetaTextAreaViewDelegate +//extension StatusView: MetaTextAreaViewDelegate { +// public func metaTextAreaView(_ metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) { +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public))") +// delegate?.statusView(self, metaTextAreaView: metaTextAreaView, didSelectMeta: meta) +// } +//} +// +//// MARK: - MediaGridContainerViewDelegate +//extension StatusView: MediaGridContainerViewDelegate { +// public func mediaGridContainerView(_ container: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int) { +// delegate?.statusView(self, mediaGridContainerView: container, didTapMediaView: mediaView, at: index) +// } +// +// public func mediaGridContainerView(_ container: MediaGridContainerView, toggleContentWarningOverlayViewDisplay contentWarningOverlayView: ContentWarningOverlayView) { +// delegate?.statusView(self, mediaGridContainerView: container, toggleContentWarningOverlayViewDisplay: contentWarningOverlayView) +// } +//} +// +//// MARK: - StatusToolbarDelegate +//extension StatusView: StatusToolbarDelegate { +// public func statusToolbar(_ statusToolbar: StatusToolbar, actionDidPressed action: StatusToolbar.Action, button: UIButton) { +// delegate?.statusView(self, statusToolbar: statusToolbar, actionDidPressed: action, button: button) +// } +// +// public func statusToolbar(_ statusToolbar: StatusToolbar, menuActionDidPressed action: StatusToolbar.MenuAction, menuButton button: UIButton) { +// delegate?.statusView(self, statusToolbar: statusToolbar, menuActionDidPressed: action, menuButton: button) +// } +//} +// +//// MARK: - StatusViewDelegate +//// relay for quoteStatsView +//extension StatusView: StatusViewDelegate { +// +// public func statusView(_ statusView: StatusView, headerDidPressed header: UIView) { +// assertionFailure() +// } +// +// public func statusView(_ statusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) { +// guard statusView === quoteStatusView else { +// assertionFailure() +// return +// } +// +// delegate?.statusView(self, quoteStatusView: statusView, authorAvatarButtonDidPressed: button) +// } +// +// public func statusView(_ statusView: StatusView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) { +// assertionFailure() +// } +// +// public func statusView(_ statusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) { +// guard statusView === quoteStatusView else { +// assertionFailure() +// return +// } +// +// delegate?.statusView(self, quoteStatusView: statusView, metaTextAreaView: metaTextAreaView, didSelectMeta: meta) +// } +// +// public func statusView(_ statusView: StatusView, expandContentButtonDidPressed button: UIButton) { +// assertionFailure() +// } +// +// public func statusView(_ statusView: StatusView, quoteStatusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) { +// assertionFailure() +// } +// +// public func statusView(_ statusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int) { +// guard statusView === quoteStatusView else { +// assertionFailure() +// return +// } +// +// delegate?.statusView(self, quoteStatusView: statusView, mediaGridContainerView: containerView, didTapMediaView: mediaView, at: index) +// } +// +// public func statusView(_ statusView: StatusView, pollTableView tableView: UITableView, didSelectRowAt indexPath: IndexPath) { +// assertionFailure() +// } +// +// public func statusView(_ statusView: StatusView, pollVoteButtonDidPressed button: UIButton) { +// assertionFailure() +// } +// +// public func statusView(_ statusView: StatusView, quoteStatusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int) { +// assertionFailure() +// } +// +// public func statusView(_ statusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, toggleContentWarningOverlayViewDisplay contentWarningOverlayView: ContentWarningOverlayView) { +// assertionFailure() +// } +// +// public func statusView(_ statusView: StatusView, quoteStatusViewDidPressed quoteStatusView: StatusView) { +// assertionFailure() +// } +// +// public func statusView(_ statusView: StatusView, statusToolbar: StatusToolbar, actionDidPressed action: StatusToolbar.Action, button: UIButton) { +// assertionFailure() +// } +// +// public func statusView(_ statusView: StatusView, statusToolbar: StatusToolbar, menuActionDidPressed action: StatusToolbar.MenuAction, menuButton button: UIButton) { +// assertionFailure() +// } +// +// public func statusView(_ statusView: StatusView, translateButtonDidPressed button: UIButton) { +// assertionFailure() +// } +// +// public func statusView(_ statusView: StatusView, accessibilityActivate: Void) { +// assertionFailure() +// } +// +//} +// +//// MARK: - UITableViewDelegate +//extension StatusView: UITableViewDelegate { +// public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): select \(indexPath.debugDescription)") +// +// switch tableView { +// case pollTableView: +// delegate?.statusView(self, pollTableView: tableView, didSelectRowAt: indexPath) +// default: +// assertionFailure() +// } +// } +//} +// +//// MARK: - UIGestureRecognizerDelegate +//extension StatusView: UIGestureRecognizerDelegate { +// public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { +// if let view = touch.view, view is AvatarButton { +// return false +// } +// +// return true +// } +//} diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift index 6fdb032a..17c0744d 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift @@ -36,14 +36,15 @@ public struct ComposeContentView: View { // reply switch viewModel.kind { case .reply(let status): - ReplyStatusViewRepresentable( - statusObject: status, - configurationContext: viewModel.configurationContext.statusViewConfigureContext, - width: viewModel.viewSize.width - 2 * ComposeContentView.contentMargin - ) - .padding(.top, 8) - .padding(.horizontal, ComposeContentView.contentMargin) - .frame(width: viewModel.viewSize.width) + EmptyView() +// ReplyStatusViewRepresentable( +// statusObject: status, +// configurationContext: viewModel.configurationContext.statusViewConfigureContext, +// width: viewModel.viewSize.width - 2 * ComposeContentView.contentMargin +// ) +// .padding(.top, 8) +// .padding(.horizontal, ComposeContentView.contentMargin) +// .frame(width: viewModel.viewSize.width) default: EmptyView() } diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Reply/ReplyStatusView.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Reply/ReplyStatusView.swift index e58553d0..05aa3d73 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Reply/ReplyStatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Reply/ReplyStatusView.swift @@ -8,62 +8,62 @@ import UIKit import Combine -public final class ReplyStatusView: UIView { - - public var disposeBag = Set() - private var observations = Set() - - public let statusView = StatusView() - public private(set)var widthLayoutConstraint: NSLayoutConstraint! - - public let conversationLinkLineView = SeparatorLineView() - - public override var intrinsicContentSize: CGSize { - return statusView.frame.size - } - - public override init(frame: CGRect) { - super.init(frame: frame) - _init() - } - - public required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension ReplyStatusView { - private func _init() { - widthLayoutConstraint = widthAnchor.constraint(equalToConstant: frame.width) - - statusView.translatesAutoresizingMaskIntoConstraints = false - addSubview(statusView) - NSLayoutConstraint.activate([ - statusView.topAnchor.constraint(equalTo: topAnchor), - statusView.leadingAnchor.constraint(equalTo: leadingAnchor), - trailingAnchor.constraint(equalTo: statusView.trailingAnchor), - statusView.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - statusView.setup(style: .composeReply) - - conversationLinkLineView.translatesAutoresizingMaskIntoConstraints = false - addSubview(conversationLinkLineView) - NSLayoutConstraint.activate([ - conversationLinkLineView.topAnchor.constraint(equalTo: statusView.authorAvatarButton.bottomAnchor, constant: 2), - conversationLinkLineView.centerXAnchor.constraint(equalTo: statusView.authorAvatarButton.centerXAnchor), - bottomAnchor.constraint(equalTo: conversationLinkLineView.bottomAnchor), - conversationLinkLineView.widthAnchor.constraint(equalToConstant: 1), - ]) - - // trigger UIViewRepresentable size update - statusView - .observe(\.bounds, options: [.initial, .new]) { [weak self] statusView, _ in - guard let self = self else { return } - print(statusView.frame) - self.invalidateIntrinsicContentSize() - } - .store(in: &observations) - } -} +//public final class ReplyStatusView: UIView { +// +// public var disposeBag = Set() +// private var observations = Set() +// +// public let statusView = StatusView() +// public private(set)var widthLayoutConstraint: NSLayoutConstraint! +// +// public let conversationLinkLineView = SeparatorLineView() +// +// public override var intrinsicContentSize: CGSize { +// return statusView.frame.size +// } +// +// public override init(frame: CGRect) { +// super.init(frame: frame) +// _init() +// } +// +// public required init?(coder: NSCoder) { +// super.init(coder: coder) +// _init() +// } +// +//} +// +//extension ReplyStatusView { +// private func _init() { +// widthLayoutConstraint = widthAnchor.constraint(equalToConstant: frame.width) +// +// statusView.translatesAutoresizingMaskIntoConstraints = false +// addSubview(statusView) +// NSLayoutConstraint.activate([ +// statusView.topAnchor.constraint(equalTo: topAnchor), +// statusView.leadingAnchor.constraint(equalTo: leadingAnchor), +// trailingAnchor.constraint(equalTo: statusView.trailingAnchor), +// statusView.bottomAnchor.constraint(equalTo: bottomAnchor), +// ]) +// statusView.setup(style: .composeReply) +// +// conversationLinkLineView.translatesAutoresizingMaskIntoConstraints = false +// addSubview(conversationLinkLineView) +// NSLayoutConstraint.activate([ +// conversationLinkLineView.topAnchor.constraint(equalTo: statusView.authorAvatarButton.bottomAnchor, constant: 2), +// conversationLinkLineView.centerXAnchor.constraint(equalTo: statusView.authorAvatarButton.centerXAnchor), +// bottomAnchor.constraint(equalTo: conversationLinkLineView.bottomAnchor), +// conversationLinkLineView.widthAnchor.constraint(equalToConstant: 1), +// ]) +// +// // trigger UIViewRepresentable size update +// statusView +// .observe(\.bounds, options: [.initial, .new]) { [weak self] statusView, _ in +// guard let self = self else { return } +// print(statusView.frame) +// self.invalidateIntrinsicContentSize() +// } +// .store(in: &observations) +// } +//} diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/PrototypeStatusViewRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/PrototypeStatusViewRepresentable.swift index 78268ebf..55c68aee 100644 --- a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/PrototypeStatusViewRepresentable.swift +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/PrototypeStatusViewRepresentable.swift @@ -9,95 +9,95 @@ import SwiftUI import TwitterMeta import TwidereCore -public struct PrototypeStatusViewRepresentable: UIViewRepresentable { - - private let now = Date() - - let style: Style - let configurationContext: StatusView.ConfigurationContext - - @Binding var height: CGFloat - - public init( - style: Style, - configurationContext: StatusView.ConfigurationContext, - height: Binding - ) { - self.style = style - self.configurationContext = configurationContext - self._height = height - } - - public func makeUIView(context: Context) -> PrototypeStatusView { - let view = PrototypeStatusView() - switch style { - case .timeline: - view.statusView.setup(style: .inline) - view.statusView.toolbar.setup(style: .inline) - case .thread: - view.statusView.setup(style: .plain) - view.statusView.toolbar.setup(style: .plain) - } - view.delegate = context.coordinator - - view.translatesAutoresizingMaskIntoConstraints = false - view.setContentCompressionResistancePriority(.required, for: .vertical) - - view.statusView.prepareForReuse() - view.statusView.viewModel.timestamp = now - view.statusView.viewModel.dateTimeProvider = configurationContext.dateTimeProvider - - return view - } - - public func updateUIView(_ view: PrototypeStatusView, context: Context) { - let statusView = view.statusView - statusView.viewModel.authorAvatarImage = Asset.Scene.Preference.twidereAvatar.image - statusView.viewModel.authorName = PlaintextMetaContent(string: "Twidere") - statusView.viewModel.authorUsername = "TwidereProject" - - let content = TwitterContent(content: L10n.Scene.Settings.Display.Preview.thankForUsingTwidereX) - statusView.viewModel.content = TwitterMetaContent.convert( - content: content, - urlMaximumLength: 16, - twitterTextProvider: configurationContext.twitterTextProvider - ) - - view.setNeedsLayout() - view.layoutIfNeeded() - } - - public func makeCoordinator() -> Coordinator { - Coordinator(self) - } - -} - -extension PrototypeStatusViewRepresentable { - - public class Coordinator: PrototypeStatusViewDelegate { - - let representable: PrototypeStatusViewRepresentable - - init(_ representable: PrototypeStatusViewRepresentable) { - self.representable = representable - } - - public func layoutDidUpdate(_ view: PrototypeStatusView) { - DispatchQueue.main.async { - self.representable.height = view.statusView.frame.height - } - } - - } - -} - -extension PrototypeStatusViewRepresentable { - - public enum Style: Hashable, CaseIterable { - case timeline - case thread - } - -} +//public struct PrototypeStatusViewRepresentable: UIViewRepresentable { +// +// private let now = Date() +// +// let style: Style +// let configurationContext: StatusView.ConfigurationContext +// +// @Binding var height: CGFloat +// +// public init( +// style: Style, +// configurationContext: StatusView.ConfigurationContext, +// height: Binding +// ) { +// self.style = style +// self.configurationContext = configurationContext +// self._height = height +// } +// +// public func makeUIView(context: Context) -> PrototypeStatusView { +// let view = PrototypeStatusView() +// switch style { +// case .timeline: +// view.statusView.setup(style: .inline) +// view.statusView.toolbar.setup(style: .inline) +// case .thread: +// view.statusView.setup(style: .plain) +// view.statusView.toolbar.setup(style: .plain) +// } +// view.delegate = context.coordinator +// +// view.translatesAutoresizingMaskIntoConstraints = false +// view.setContentCompressionResistancePriority(.required, for: .vertical) +// +// view.statusView.prepareForReuse() +// view.statusView.viewModel.timestamp = now +// view.statusView.viewModel.dateTimeProvider = configurationContext.dateTimeProvider +// +// return view +// } +// +// public func updateUIView(_ view: PrototypeStatusView, context: Context) { +// let statusView = view.statusView +// statusView.viewModel.authorAvatarImage = Asset.Scene.Preference.twidereAvatar.image +// statusView.viewModel.authorName = PlaintextMetaContent(string: "Twidere") +// statusView.viewModel.authorUsername = "TwidereProject" +// +// let content = TwitterContent(content: L10n.Scene.Settings.Display.Preview.thankForUsingTwidereX) +// statusView.viewModel.content = TwitterMetaContent.convert( +// content: content, +// urlMaximumLength: 16, +// twitterTextProvider: configurationContext.twitterTextProvider +// ) +// +// view.setNeedsLayout() +// view.layoutIfNeeded() +// } +// +// public func makeCoordinator() -> Coordinator { +// Coordinator(self) +// } +// +//} +// +//extension PrototypeStatusViewRepresentable { +// +// public class Coordinator: PrototypeStatusViewDelegate { +// +// let representable: PrototypeStatusViewRepresentable +// +// init(_ representable: PrototypeStatusViewRepresentable) { +// self.representable = representable +// } +// +// public func layoutDidUpdate(_ view: PrototypeStatusView) { +// DispatchQueue.main.async { +// self.representable.height = view.statusView.frame.height +// } +// } +// +// } +// +//} +// +//extension PrototypeStatusViewRepresentable { +// +// public enum Style: Hashable, CaseIterable { +// case timeline +// case thread +// } +// +//} diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/ReplyStatusViewRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/ReplyStatusViewRepresentable.swift index a3a13c82..9acc3985 100644 --- a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/ReplyStatusViewRepresentable.swift +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/ReplyStatusViewRepresentable.swift @@ -9,31 +9,31 @@ import UIKit import SwiftUI import TwidereCore -public struct ReplyStatusViewRepresentable: UIViewRepresentable { - - let statusObject: StatusObject - let configurationContext: StatusView.ConfigurationContext - let width: CGFloat - - public func makeUIView(context: Context) -> ReplyStatusView { - let view = ReplyStatusView() - // using view `intrinsicContentSize` for layout - view.translatesAutoresizingMaskIntoConstraints = false - return view - } - - public func updateUIView(_ view: ReplyStatusView, context: Context) { - if width != .zero { - view.statusView.frame.size.width = width - view.widthLayoutConstraint.constant = width - view.widthLayoutConstraint.isActive = true - } - - view.statusView.prepareForReuse() - view.statusView.configure( - statusObject: statusObject, - configurationContext: configurationContext - ) - } - -} +//public struct ReplyStatusViewRepresentable: UIViewRepresentable { +// +// let statusObject: StatusObject +// let configurationContext: StatusView.ConfigurationContext +// let width: CGFloat +// +// public func makeUIView(context: Context) -> ReplyStatusView { +// let view = ReplyStatusView() +// // using view `intrinsicContentSize` for layout +// view.translatesAutoresizingMaskIntoConstraints = false +// return view +// } +// +// public func updateUIView(_ view: ReplyStatusView, context: Context) { +// if width != .zero { +// view.statusView.frame.size.width = width +// view.widthLayoutConstraint.constant = width +// view.widthLayoutConstraint.isActive = true +// } +// +// view.statusView.prepareForReuse() +// view.statusView.configure( +// statusObject: statusObject, +// configurationContext: configurationContext +// ) +// } +// +//} diff --git a/TwidereSDK/Sources/TwidereUI/Utility/ViewLayoutFrame.swift b/TwidereSDK/Sources/TwidereUI/Utility/ViewLayoutFrame.swift new file mode 100644 index 00000000..817651eb --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Utility/ViewLayoutFrame.swift @@ -0,0 +1,57 @@ +// +// ViewLayoutFrame.swift +// +// +// Created by MainasuK on 2023/2/3. +// + +import os.log +import UIKit +import CoreGraphics + +public struct ViewLayoutFrame { + let logger = Logger(subsystem: "ViewLayoutFrame", category: "ViewLayoutFrame") + + public var layoutFrame: CGRect + public var safeAreaLayoutFrame: CGRect + public var readableContentLayoutFrame: CGRect + + public init( + layoutFrame: CGRect = .zero, + safeAreaLayoutFrame: CGRect = .zero, + readableContentLayoutFrame: CGRect = .zero + ) { + self.layoutFrame = layoutFrame + self.safeAreaLayoutFrame = safeAreaLayoutFrame + self.readableContentLayoutFrame = readableContentLayoutFrame + } +} + +extension ViewLayoutFrame { + public mutating func update(view: UIView) { + guard view.window != nil else { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): layoutFrame update for a view without attached window. Skip this invalid update") + return + } + + let layoutFrame = view.frame + if self.layoutFrame != layoutFrame { + self.layoutFrame = layoutFrame + } + + let safeAreaLayoutFrame = view.safeAreaLayoutGuide.layoutFrame + if self.safeAreaLayoutFrame != safeAreaLayoutFrame { + self.safeAreaLayoutFrame = safeAreaLayoutFrame + } + + let readableContentLayoutFrame = view.readableContentGuide.layoutFrame + if self.readableContentLayoutFrame != readableContentLayoutFrame { + self.readableContentLayoutFrame = readableContentLayoutFrame + } + + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): layoutFrame: \(layoutFrame.debugDescription)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): safeAreaLayoutFrame: \(safeAreaLayoutFrame.debugDescription)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): readableContentLayoutFrame: \(readableContentLayoutFrame.debugDescription)") + + } +} diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 4e786734..1fe487ae 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3618,6 +3618,7 @@ DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4116,6 +4117,7 @@ DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4147,6 +4149,7 @@ DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index 164d87a9..90627cc0 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -217,6 +217,15 @@ "version": "2.6.1" } }, + { + "package": "twitter-text", + "repositoryURL": "https://github.com/TwidereProject/twitter-text.git", + "state": { + "branch": null, + "revision": "c88fa4ed8dd7441f827942b83d0564101bbdc508", + "version": "0.0.3" + } + }, { "package": "UITextView+Placeholder", "repositoryURL": "https://github.com/MainasuK/UITextView-Placeholder.git", @@ -225,6 +234,15 @@ "revision": "20f513ded04a040cdf5467f0891849b1763ede3b", "version": "1.4.1" } + }, + { + "package": "UnicodeURL", + "repositoryURL": "https://github.com/nysander/UnicodeURL.git", + "state": { + "branch": null, + "revision": "fe15c8b585c2e6d22dfe81da899183e6aa376a93", + "version": "0.1.0" + } } ] }, diff --git a/TwidereX/Diffable/Misc/History/HistorySection.swift b/TwidereX/Diffable/Misc/History/HistorySection.swift index 8ec81e4d..c43473dc 100644 --- a/TwidereX/Diffable/Misc/History/HistorySection.swift +++ b/TwidereX/Diffable/Misc/History/HistorySection.swift @@ -52,39 +52,39 @@ extension HistorySection { return UITableViewCell() } // status - if let status = history.statusObject { - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell - StatusSection.setupStatusPollDataSource( - context: context, - statusView: cell.statusView, - configurationContext: configuration.statusViewConfigurationContext - ) - configure( - tableView: tableView, - cell: cell, - viewModel: StatusTableViewCell.ViewModel(value: .statusObject(status)), - configuration: configuration - ) - return cell - } - // user - if let user = history.userObject { - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserRelationshipStyleTableViewCell.self), for: indexPath) as! UserRelationshipStyleTableViewCell - let authenticationContext = configuration.userViewConfigurationContext.authContext.authenticationContext - let me = authenticationContext.user(in: context.managedObjectContext) - let viewModel = UserTableViewCell.ViewModel( - user: user, - me: me, - notification: nil - ) - configure( - cell: cell, - viewModel: viewModel, - configuration: configuration - ) - return cell - } - +// if let status = history.statusObject { +// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell +// StatusSection.setupStatusPollDataSource( +// context: context, +// statusView: cell.statusView, +// configurationContext: configuration.statusViewConfigurationContext +// ) +// configure( +// tableView: tableView, +// cell: cell, +// viewModel: StatusTableViewCell.ViewModel(value: .statusObject(status)), +// configuration: configuration +// ) +// return cell +// } +// // user +// if let user = history.userObject { +// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserRelationshipStyleTableViewCell.self), for: indexPath) as! UserRelationshipStyleTableViewCell +// let authenticationContext = configuration.userViewConfigurationContext.authContext.authenticationContext +// let me = authenticationContext.user(in: context.managedObjectContext) +// let viewModel = UserTableViewCell.ViewModel( +// user: user, +// me: me, +// notification: nil +// ) +// configure( +// cell: cell, +// viewModel: viewModel, +// configuration: configuration +// ) +// return cell +// } + return UITableViewCell() } return cell @@ -98,37 +98,37 @@ extension HistorySection { extension HistorySection { - static func configure( - tableView: UITableView, - cell: StatusTableViewCell, - viewModel: StatusTableViewCell.ViewModel, - configuration: Configuration - ) { - StatusSection.configure( - tableView: tableView, - cell: cell, - viewModel: viewModel, - configuration: .init( - statusViewTableViewCellDelegate: configuration.statusViewTableViewCellDelegate, - timelineMiddleLoaderTableViewCellDelegate: nil, - statusViewConfigurationContext: configuration.statusViewConfigurationContext - ) - ) - } - - static func configure( - cell: UserTableViewCell, - viewModel: UserTableViewCell.ViewModel, - configuration: Configuration - ) { - UserSection.configure( - cell: cell, - viewModel: viewModel, - configuration: .init( - userViewTableViewCellDelegate: configuration.userViewTableViewCellDelegate, - userViewConfigurationContext: configuration.userViewConfigurationContext - ) - ) - } +// static func configure( +// tableView: UITableView, +// cell: StatusTableViewCell, +// viewModel: StatusTableViewCell.ViewModel, +// configuration: Configuration +// ) { +// StatusSection.configure( +// tableView: tableView, +// cell: cell, +// viewModel: viewModel, +// configuration: .init( +// statusViewTableViewCellDelegate: configuration.statusViewTableViewCellDelegate, +// timelineMiddleLoaderTableViewCellDelegate: nil, +// statusViewConfigurationContext: configuration.statusViewConfigurationContext +// ) +// ) +// } +// +// static func configure( +// cell: UserTableViewCell, +// viewModel: UserTableViewCell.ViewModel, +// configuration: Configuration +// ) { +// UserSection.configure( +// cell: cell, +// viewModel: viewModel, +// configuration: .init( +// userViewTableViewCellDelegate: configuration.userViewTableViewCellDelegate, +// userViewConfigurationContext: configuration.userViewConfigurationContext +// ) +// ) +// } } diff --git a/TwidereX/Diffable/Misc/Notification/NotificationSection.swift b/TwidereX/Diffable/Misc/Notification/NotificationSection.swift index 0552061c..106188c3 100644 --- a/TwidereX/Diffable/Misc/Notification/NotificationSection.swift +++ b/TwidereX/Diffable/Misc/Notification/NotificationSection.swift @@ -32,65 +32,67 @@ extension NotificationSection { return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in // data source should dispatch in main thread assert(Thread.isMainThread) - + + return UITableViewCell() + // configure cell with item - switch item { - case .feed(let record): - return context.managedObjectContext.performAndWait { - guard let feed = record.object(in: context.managedObjectContext) else { - assertionFailure() - return UITableViewCell() - } - - switch feed.objectContent { - case .status: - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell - StatusSection.setupStatusPollDataSource( - context: context, - statusView: cell.statusView, - configurationContext: configuration.statusViewConfigurationContext - ) - configure( - tableView: tableView, - cell: cell, - viewModel: StatusTableViewCell.ViewModel(value: .feed(feed)), - configuration: configuration - ) - return cell - case .notification(let object): - switch object { - case .mastodon(let notification): - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserNotificationStyleTableViewCell.self), for: indexPath) as! UserNotificationStyleTableViewCell - let authenticationContext = configuration.statusViewConfigurationContext.authContext.authenticationContext - let me = authenticationContext.user(in: context.managedObjectContext) - let user: UserObject = .mastodon(object: notification.account) - configure( - cell: cell, - viewModel: UserTableViewCell.ViewModel( - user: user, - me: me, - notification: .mastodon(object: notification) - ), - configuration: configuration - ) - return cell - } - case .none: - assertionFailure() - return UITableViewCell() - } - } // end return context.managedObjectContext.performAndWait - - case .feedLoader: - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell - cell.viewModel.isFetching = true - return cell - - case .bottomLoader: - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell - cell.activityIndicatorView.startAnimating() - return cell - } // end switch +// switch item { +// case .feed(let record): +// return context.managedObjectContext.performAndWait { +// guard let feed = record.object(in: context.managedObjectContext) else { +// assertionFailure() +// return UITableViewCell() +// } +// +// switch feed.objectContent { +// case .status: +// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell +// StatusSection.setupStatusPollDataSource( +// context: context, +// statusView: cell.statusView, +// configurationContext: configuration.statusViewConfigurationContext +// ) +// configure( +// tableView: tableView, +// cell: cell, +// viewModel: StatusTableViewCell.ViewModel(value: .feed(feed)), +// configuration: configuration +// ) +// return cell +// case .notification(let object): +// switch object { +// case .mastodon(let notification): +// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserNotificationStyleTableViewCell.self), for: indexPath) as! UserNotificationStyleTableViewCell +// let authenticationContext = configuration.statusViewConfigurationContext.authContext.authenticationContext +// let me = authenticationContext.user(in: context.managedObjectContext) +// let user: UserObject = .mastodon(object: notification.account) +// configure( +// cell: cell, +// viewModel: UserTableViewCell.ViewModel( +// user: user, +// me: me, +// notification: .mastodon(object: notification) +// ), +// configuration: configuration +// ) +// return cell +// } +// case .none: +// assertionFailure() +// return UITableViewCell() +// } +// } // end return context.managedObjectContext.performAndWait +// +// case .feedLoader: +// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell +// cell.viewModel.isFetching = true +// return cell +// +// case .bottomLoader: +// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell +// cell.activityIndicatorView.startAnimating() +// return cell +// } // end switch } } diff --git a/TwidereX/Diffable/Status/StatusSection.swift b/TwidereX/Diffable/Status/StatusSection.swift index b59a94be..fe29d3ce 100644 --- a/TwidereX/Diffable/Status/StatusSection.swift +++ b/TwidereX/Diffable/Status/StatusSection.swift @@ -9,6 +9,7 @@ import os.log import UIKit import Combine +import SwiftUI import CoreData import CoreDataStack import MetaTextKit @@ -48,72 +49,78 @@ extension StatusSection { switch item { case .feed(let record): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell - setupStatusPollDataSource( - context: context, - statusView: cell.statusView, - configurationContext: configuration.statusViewConfigurationContext - ) +// setupStatusPollDataSource( +// context: context, +// statusView: cell.statusView, +// configurationContext: configuration.statusViewConfigurationContext +// ) context.managedObjectContext.performAndWait { guard let feed = record.object(in: context.managedObjectContext) else { return } - configure( - tableView: tableView, - cell: cell, - viewModel: StatusTableViewCell.ViewModel(value: .feed(feed)), - configuration: configuration + let _viewModel = StatusView.ViewModel( + feed: feed, + delegate: cell, + viewLayoutFramePublisher: configuration.statusViewConfigurationContext.viewLayoutFramePublisher ) + guard let viewModel = _viewModel else { return } + cell.contentConfiguration = UIHostingConfiguration { + StatusView(viewModel: viewModel) + } } return cell case .feedLoader(let record): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell - context.managedObjectContext.performAndWait { - guard let feed = record.object(in: context.managedObjectContext) else { return } - configure( - cell: cell, - feed: feed, - configuration: configuration - ) - } +// context.managedObjectContext.performAndWait { +// guard let feed = record.object(in: context.managedObjectContext) else { return } +// configure( +// cell: cell, +// feed: feed, +// configuration: configuration +// ) +// } return cell case .status(let status): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell - setupStatusPollDataSource( - context: context, - statusView: cell.statusView, - configurationContext: configuration.statusViewConfigurationContext - ) - context.managedObjectContext.performAndWait { - switch status { - case .twitter(let record): - guard let status = record.object(in: context.managedObjectContext) else { return } - configure( - tableView: tableView, - cell: cell, - viewModel: StatusTableViewCell.ViewModel(value: .twitterStatus(status)), - configuration: configuration - ) - case .mastodon(let record): - guard let status = record.object(in: context.managedObjectContext) else { return } - configure( - tableView: tableView, - cell: cell, - viewModel: StatusTableViewCell.ViewModel(value: .mastodonStatus(status)), - configuration: configuration - ) - } // end switch - } +// setupStatusPollDataSource( +// context: context, +// statusView: cell.statusView, +// configurationContext: configuration.statusViewConfigurationContext +// ) +// context.managedObjectContext.performAndWait { +// switch status { +// case .twitter(let record): +// guard let status = record.object(in: context.managedObjectContext) else { return } +// configure( +// tableView: tableView, +// cell: cell, +// viewModel: StatusTableViewCell.ViewModel(value: .twitterStatus(status)), +// configuration: configuration +// ) +// case .mastodon(let record): +// guard let status = record.object(in: context.managedObjectContext) else { return } +// configure( +// tableView: tableView, +// cell: cell, +// viewModel: StatusTableViewCell.ViewModel(value: .mastodonStatus(status)), +// configuration: configuration +// ) +// } // end switch +// } return cell case .thread(let thread): - return StatusSection.dequeueConfiguredReusableCell( - context: context, - tableView: tableView, - indexPath: indexPath, - configuration: ThreadCellRegistrationConfiguration( - thread: thread, - configuration: configuration - ) - ) + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell + + return cell +// return StatusSection.dequeueConfiguredReusableCell( +// context: context, +// tableView: tableView, +// indexPath: indexPath, +// configuration: ThreadCellRegistrationConfiguration( +// thread: thread, +// configuration: configuration +// ) +// ) case .topLoader: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell @@ -131,163 +138,163 @@ extension StatusSection { extension StatusSection { - struct ThreadCellRegistrationConfiguration { - let thread: StatusItem.Thread - let configuration: Configuration - } - - static func dequeueConfiguredReusableCell( - context: AppContext, - tableView: UITableView, - indexPath: IndexPath, - configuration: ThreadCellRegistrationConfiguration - ) -> UITableViewCell { - let managedObjectContext = context.managedObjectContext - - let configurationContext = configuration.configuration.statusViewConfigurationContext - - switch configuration.thread { - case .root(let threadContext): - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusThreadRootTableViewCell.self), for: indexPath) as! StatusThreadRootTableViewCell - setupStatusPollDataSource( - context: context, - statusView: cell.statusView, - configurationContext: configurationContext - ) - managedObjectContext.performAndWait { - guard let status = threadContext.status.object(in: managedObjectContext) else { return } - cell.configure( - tableView: tableView, - viewModel: StatusThreadRootTableViewCell.ViewModel(value: .statusObject(status)), - configurationContext: configurationContext, - delegate: configuration.configuration.statusViewTableViewCellDelegate - ) - } - if threadContext.displayUpperConversationLink { - cell.setConversationLinkLineViewDisplay() - } - return cell - case .reply(let threadContext), - .leaf(let threadContext): - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell - setupStatusPollDataSource( - context: context, - statusView: cell.statusView, - configurationContext: configurationContext - ) - managedObjectContext.performAndWait { - guard let status = threadContext.status.object(in: managedObjectContext) else { return } - cell.configure( - tableView: tableView, - viewModel: StatusTableViewCell.ViewModel(value: .statusObject(status)), - configurationContext: configurationContext, - delegate: configuration.configuration.statusViewTableViewCellDelegate - ) - } - if threadContext.displayUpperConversationLink { - cell.setTopConversationLinkLineViewDisplay() - } - if threadContext.displayBottomConversationLink { - cell.setBottomConversationLinkLineViewDisplay() - } - return cell - } - } +// struct ThreadCellRegistrationConfiguration { +// let thread: StatusItem.Thread +// let configuration: Configuration +// } +// +// static func dequeueConfiguredReusableCell( +// context: AppContext, +// tableView: UITableView, +// indexPath: IndexPath, +// configuration: ThreadCellRegistrationConfiguration +// ) -> UITableViewCell { +// let managedObjectContext = context.managedObjectContext +// +// let configurationContext = configuration.configuration.statusViewConfigurationContext +// +// switch configuration.thread { +// case .root(let threadContext): +// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusThreadRootTableViewCell.self), for: indexPath) as! StatusThreadRootTableViewCell +// setupStatusPollDataSource( +// context: context, +// statusView: cell.statusView, +// configurationContext: configurationContext +// ) +// managedObjectContext.performAndWait { +// guard let status = threadContext.status.object(in: managedObjectContext) else { return } +// cell.configure( +// tableView: tableView, +// viewModel: StatusThreadRootTableViewCell.ViewModel(value: .statusObject(status)), +// configurationContext: configurationContext, +// delegate: configuration.configuration.statusViewTableViewCellDelegate +// ) +// } +// if threadContext.displayUpperConversationLink { +// cell.setConversationLinkLineViewDisplay() +// } +// return cell +// case .reply(let threadContext), +// .leaf(let threadContext): +// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell +// setupStatusPollDataSource( +// context: context, +// statusView: cell.statusView, +// configurationContext: configurationContext +// ) +// managedObjectContext.performAndWait { +// guard let status = threadContext.status.object(in: managedObjectContext) else { return } +// cell.configure( +// tableView: tableView, +// viewModel: StatusTableViewCell.ViewModel(value: .statusObject(status)), +// configurationContext: configurationContext, +// delegate: configuration.configuration.statusViewTableViewCellDelegate +// ) +// } +// if threadContext.displayUpperConversationLink { +// cell.setTopConversationLinkLineViewDisplay() +// } +// if threadContext.displayBottomConversationLink { +// cell.setBottomConversationLinkLineViewDisplay() +// } +// return cell +// } +// } - public static func setupStatusPollDataSource( - context: AppContext, - statusView: StatusView, - configurationContext: PollOptionView.ConfigurationContext - ) { - let managedObjectContext = context.managedObjectContext - statusView.pollTableViewDiffableDataSource = UITableViewDiffableDataSource(tableView: statusView.pollTableView) { tableView, indexPath, item in - switch item { - case .option(let record): - // Fix cell reuse animation issue - let cell: PollOptionTableViewCell = { - let _cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PollOptionTableViewCell.self) + "@\(indexPath.row)#\(indexPath.section)") as? PollOptionTableViewCell - _cell?.prepareForReuse() - return _cell ?? PollOptionTableViewCell() - }() - - managedObjectContext.performAndWait { - guard let option = record.object(in: managedObjectContext) else { - assertionFailure() - return - } - cell.optionView.configure( - pollOption: option, - configurationContext: configurationContext - ) - - // trigger update if needs - // check is the first option in poll to trigger update poll only once - if option.index == 0, option.poll.needsUpdate { - let authenticationContext = configurationContext.authContext.authenticationContext - switch (option, authenticationContext) { - case (.twitter(let object), .twitter(let authenticationContext)): - let status: ManagedObjectRecord = .init(objectID: object.poll.status.objectID) - Task { [weak context] in - guard let context = context else { return } - let statusIDs: [Twitter.Entity.V2.Tweet.ID] = await context.managedObjectContext.perform { - guard let _status = status.object(in: context.managedObjectContext) else { return [] } - let status = _status.repost ?? _status - return [status.id] - } - _ = try await context.apiService.twitterStatus( - statusIDs: statusIDs, - authenticationContext: authenticationContext - ) - } // end Task - case (.mastodon(let object), .mastodon(let authenticationContext)): - let status: ManagedObjectRecord = .init(objectID: object.poll.status.objectID) - Task { [weak context] in - guard let context = context else { return } - _ = try await context.apiService.viewMastodonStatusPoll( - status: status, - authenticationContext: authenticationContext - ) - } // end Task - default: - assertionFailure() - } - } - } // end managedObjectContext.performAndWait - return cell - } - } - var _snapshot = NSDiffableDataSourceSnapshot() - _snapshot.appendSections([.main]) - statusView.pollTableViewDiffableDataSource?.applySnapshotUsingReloadData(_snapshot) - } +// public static func setupStatusPollDataSource( +// context: AppContext, +// statusView: StatusView, +// configurationContext: PollOptionView.ConfigurationContext +// ) { +// let managedObjectContext = context.managedObjectContext +// statusView.pollTableViewDiffableDataSource = UITableViewDiffableDataSource(tableView: statusView.pollTableView) { tableView, indexPath, item in +// switch item { +// case .option(let record): +// // Fix cell reuse animation issue +// let cell: PollOptionTableViewCell = { +// let _cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PollOptionTableViewCell.self) + "@\(indexPath.row)#\(indexPath.section)") as? PollOptionTableViewCell +// _cell?.prepareForReuse() +// return _cell ?? PollOptionTableViewCell() +// }() +// +// managedObjectContext.performAndWait { +// guard let option = record.object(in: managedObjectContext) else { +// assertionFailure() +// return +// } +// cell.optionView.configure( +// pollOption: option, +// configurationContext: configurationContext +// ) +// +// // trigger update if needs +// // check is the first option in poll to trigger update poll only once +// if option.index == 0, option.poll.needsUpdate { +// let authenticationContext = configurationContext.authContext.authenticationContext +// switch (option, authenticationContext) { +// case (.twitter(let object), .twitter(let authenticationContext)): +// let status: ManagedObjectRecord = .init(objectID: object.poll.status.objectID) +// Task { [weak context] in +// guard let context = context else { return } +// let statusIDs: [Twitter.Entity.V2.Tweet.ID] = await context.managedObjectContext.perform { +// guard let _status = status.object(in: context.managedObjectContext) else { return [] } +// let status = _status.repost ?? _status +// return [status.id] +// } +// _ = try await context.apiService.twitterStatus( +// statusIDs: statusIDs, +// authenticationContext: authenticationContext +// ) +// } // end Task +// case (.mastodon(let object), .mastodon(let authenticationContext)): +// let status: ManagedObjectRecord = .init(objectID: object.poll.status.objectID) +// Task { [weak context] in +// guard let context = context else { return } +// _ = try await context.apiService.viewMastodonStatusPoll( +// status: status, +// authenticationContext: authenticationContext +// ) +// } // end Task +// default: +// assertionFailure() +// } +// } +// } // end managedObjectContext.performAndWait +// return cell +// } +// } +// var _snapshot = NSDiffableDataSourceSnapshot() +// _snapshot.appendSections([.main]) +// statusView.pollTableViewDiffableDataSource?.applySnapshotUsingReloadData(_snapshot) +// } } extension StatusSection { - static func configure( - tableView: UITableView, - cell: StatusTableViewCell, - viewModel: StatusTableViewCell.ViewModel, - configuration: Configuration - ) { - cell.configure( - tableView: tableView, - viewModel: viewModel, - configurationContext: configuration.statusViewConfigurationContext, - delegate: configuration.statusViewTableViewCellDelegate - ) - } - - static func configure( - cell: TimelineMiddleLoaderTableViewCell, - feed: Feed, - configuration: Configuration - ) { - cell.configure( - feed: feed, - delegate: configuration.timelineMiddleLoaderTableViewCellDelegate - ) - } +// static func configure( +// tableView: UITableView, +// cell: StatusTableViewCell, +// viewModel: StatusTableViewCell.ViewModel, +// configuration: Configuration +// ) { +// cell.configure( +// tableView: tableView, +// viewModel: viewModel, +// configurationContext: configuration.statusViewConfigurationContext, +// delegate: configuration.statusViewTableViewCellDelegate +// ) +// } +// +// static func configure( +// cell: TimelineMiddleLoaderTableViewCell, +// feed: Feed, +// configuration: Configuration +// ) { +// cell.configure( +// feed: feed, +// delegate: configuration.timelineMiddleLoaderTableViewCellDelegate +// ) +// } } diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift index 192bd17d..c17a7db3 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift @@ -53,7 +53,8 @@ extension DataSourceFacade { statusViewConfigureContext: .init( authContext: provider.authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider() + twitterTextProvider: OfficialTwitterTextProvider(), + viewLayoutFramePublisher: composeViewModel.$viewLayoutFrame ) ) ) diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift index af44c82b..1e7ac848 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift @@ -349,83 +349,83 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC menuActionDidPressed action: StatusToolbar.MenuAction, menuButton button: UIButton ) { - Task { - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { - assertionFailure() - return - } - guard let status = await item.status(in: self.context.managedObjectContext) else { - assertionFailure("only works for status data provider") - return - } - - switch action { - case .saveMedia: - let mediaViewConfigurations = await statusView.viewModel.mediaViewConfigurations - let impactFeedbackGenerator = await UIImpactFeedbackGenerator(style: .light) - let notificationFeedbackGenerator = await UINotificationFeedbackGenerator() - - do { - await impactFeedbackGenerator.impactOccurred() - for configuration in mediaViewConfigurations { - guard let url = configuration.downloadURL.flatMap({ URL(string: $0) }) else { continue } - try await context.photoLibraryService.save(source: .remote(url: url), resourceType: configuration.resourceType) - } - await context.photoLibraryService.presentSuccessNotification(title: L10n.Common.Alerts.PhotoSaved.title) - await notificationFeedbackGenerator.notificationOccurred(.success) - } catch { - await context.photoLibraryService.presentFailureNotification( - error: error, - title: L10n.Common.Alerts.PhotoSaveFail.title, - message: L10n.Common.Alerts.PhotoSaveFail.message - ) - await notificationFeedbackGenerator.notificationOccurred(.error) - } - case .translate: - try await DataSourceFacade.responseToStatusTranslate( - provider: self, - status: status - ) - case .share: - await DataSourceFacade.responseToStatusShareAction( - provider: self, - status: status, - button: button - ) - case .remove: - try await DataSourceFacade.responseToRemoveStatusAction( - provider: self, - target: .status, - status: status, - authenticationContext: self.authContext.authenticationContext - ) - #if DEBUG - case .copyID: - let _statusID: String? = await context.managedObjectContext.perform { - guard let status = status.object(in: self.context.managedObjectContext) else { return nil } - return status.id - } - if let statusID = _statusID { - UIPasteboard.general.string = statusID - } - #endif - case .appearEvent: - let _record = await DataSourceFacade.status( - managedObjectContext: context.managedObjectContext, - status: status, - target: .status - ) - guard let record = _record else { - return - } - - await DataSourceFacade.recordStatusHistory( - denpendency: self, - status: record - ) - } // end switch - } // end Task +// Task { +// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) +// guard let item = await item(from: source) else { +// assertionFailure() +// return +// } +// guard let status = await item.status(in: self.context.managedObjectContext) else { +// assertionFailure("only works for status data provider") +// return +// } +// +// switch action { +// case .saveMedia: +// let mediaViewConfigurations = await statusView.viewModel.mediaViewConfigurations +// let impactFeedbackGenerator = await UIImpactFeedbackGenerator(style: .light) +// let notificationFeedbackGenerator = await UINotificationFeedbackGenerator() +// +// do { +// await impactFeedbackGenerator.impactOccurred() +// for configuration in mediaViewConfigurations { +// guard let url = configuration.downloadURL.flatMap({ URL(string: $0) }) else { continue } +// try await context.photoLibraryService.save(source: .remote(url: url), resourceType: configuration.resourceType) +// } +// await context.photoLibraryService.presentSuccessNotification(title: L10n.Common.Alerts.PhotoSaved.title) +// await notificationFeedbackGenerator.notificationOccurred(.success) +// } catch { +// await context.photoLibraryService.presentFailureNotification( +// error: error, +// title: L10n.Common.Alerts.PhotoSaveFail.title, +// message: L10n.Common.Alerts.PhotoSaveFail.message +// ) +// await notificationFeedbackGenerator.notificationOccurred(.error) +// } +// case .translate: +// try await DataSourceFacade.responseToStatusTranslate( +// provider: self, +// status: status +// ) +// case .share: +// await DataSourceFacade.responseToStatusShareAction( +// provider: self, +// status: status, +// button: button +// ) +// case .remove: +// try await DataSourceFacade.responseToRemoveStatusAction( +// provider: self, +// target: .status, +// status: status, +// authenticationContext: self.authContext.authenticationContext +// ) +// #if DEBUG +// case .copyID: +// let _statusID: String? = await context.managedObjectContext.perform { +// guard let status = status.object(in: self.context.managedObjectContext) else { return nil } +// return status.id +// } +// if let statusID = _statusID { +// UIPasteboard.general.string = statusID +// } +// #endif +// case .appearEvent: +// let _record = await DataSourceFacade.status( +// managedObjectContext: context.managedObjectContext, +// status: status, +// target: .status +// ) +// guard let record = _record else { +// return +// } +// +// await DataSourceFacade.recordStatusHistory( +// denpendency: self, +// status: record +// ) +// } // end switch +// } // end Task } // end func } diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift index 48ca1936..2df8fcff 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -62,173 +62,173 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid func aspectTableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - guard let cell = tableView.cellForRow(at: indexPath) as? StatusViewContainerTableViewCell else { return nil } - - defer { - Task { - guard let item = await item(from: .init(tableViewCell: cell, indexPath: indexPath)) else { return } - guard let status = await item.status(in: context.managedObjectContext) else { return } - await DataSourceFacade.recordStatusHistory( - denpendency: self, - status: status - ) - } // end Task - } - - // TODO: - // this must call before check `isContentWarningOverlayDisplay`. otherwise, will get BadAccess exception - let mediaViews = cell.statusView.mediaGridContainerView.mediaViews - - if cell.statusView.mediaGridContainerView.viewModel.isContentWarningOverlayDisplay == true { - return nil - } - - for (i, mediaView) in mediaViews.enumerated() { - let pointInMediaView = mediaView.convert(point, from: tableView) - guard mediaView.point(inside: pointInMediaView, with: nil) else { - continue - } - guard let image = mediaView.thumbnail(), - let assetURLString = mediaView.configuration?.downloadURL, - let assetURL = URL(string: assetURLString), - let resourceType = mediaView.configuration?.resourceType - else { - // not provide preview unless thumbnail ready - return nil - } - - let contextMenuImagePreviewViewModel = ContextMenuImagePreviewViewModel(aspectRatio: image.size, thumbnail: image) - - let configuration = TimelineTableViewCellContextMenuConfiguration(identifier: nil) { () -> UIViewController? in - if UIDevice.current.userInterfaceIdiom == .pad && mediaViews.count == 1 { - return nil - } - let previewProvider = ContextMenuImagePreviewViewController() - previewProvider.viewModel = contextMenuImagePreviewViewModel - return previewProvider - - } actionProvider: { _ -> UIMenu? in - return UIMenu( - title: "", - image: nil, - identifier: nil, - options: [], - children: [ - UIAction( - title: L10n.Common.Controls.Actions.save, - image: UIImage(systemName: "square.and.arrow.down"), - attributes: [], - state: .off - ) { [weak self] _ in - guard let self = self else { return } - Task { @MainActor in - let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) - let notificationFeedbackGenerator = UINotificationFeedbackGenerator() - - do { - impactFeedbackGenerator.impactOccurred() - try await self.context.photoLibraryService.save( - source: .remote(url: assetURL), - resourceType: resourceType - ) - self.context.photoLibraryService.presentSuccessNotification(title: L10n.Common.Alerts.PhotoSaved.title) - notificationFeedbackGenerator.notificationOccurred(.success) - } catch { - self.context.photoLibraryService.presentFailureNotification( - error: error, - title: L10n.Common.Alerts.PhotoSaveFail.title, - message: L10n.Common.Alerts.PhotoSaveFail.message - ) - notificationFeedbackGenerator.notificationOccurred(.error) - } - } // end Task - }, - UIAction( - title: L10n.Common.Controls.Actions.copy, - image: UIImage(systemName: "doc.on.doc"), - attributes: [], - state: .off - ) { [weak self] _ in - guard let self = self else { return } - Task { @MainActor in - let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) - let notificationFeedbackGenerator = UINotificationFeedbackGenerator() - - do { - impactFeedbackGenerator.impactOccurred() - try await self.context.photoLibraryService.copy( - source: .remote(url: assetURL), - resourceType: resourceType - ) - self.context.photoLibraryService.presentSuccessNotification(title: L10n.Common.Alerts.PhotoCopied.title) - notificationFeedbackGenerator.notificationOccurred(.success) - } catch { - self.context.photoLibraryService.presentFailureNotification( - error: error, - title: L10n.Common.Alerts.PhotoCopied.title, - message: L10n.Common.Alerts.PhotoCopyFail.message - ) - notificationFeedbackGenerator.notificationOccurred(.error) - } - } // end Task - }, - UIMenu( - title: L10n.Common.Controls.Actions.share, - image: UIImage(systemName: "square.and.arrow.up"), - identifier: nil, - options: [], - children: [ - UIAction( - title: L10n.Common.Controls.Actions.ShareMediaMenu.link, - image: UIImage(systemName: "link"), - attributes: [], - state: .off - ) { [weak self] _ in - guard let self = self else { return } - Task { @MainActor in - let applicationActivities: [UIActivity] = [ - SafariActivity(sceneCoordinator: self.coordinator) - ] - let activityViewController = UIActivityViewController( - activityItems: [assetURL], - applicationActivities: applicationActivities - ) - activityViewController.popoverPresentationController?.sourceView = mediaView - self.present(activityViewController, animated: true, completion: nil) - } // end Task - }, - UIAction( - title: L10n.Common.Controls.Actions.ShareMediaMenu.media, - image: UIImage(systemName: "photo"), - attributes: [], - state: .off - ) { [weak self] _ in - guard let self = self else { return } - Task { @MainActor in - let applicationActivities: [UIActivity] = [ - SafariActivity(sceneCoordinator: self.coordinator) - ] - // FIXME: handle error - guard let url = try await self.context.photoLibraryService.file(from: .remote(url: assetURL)) else { - return - } - let activityViewController = UIActivityViewController( - activityItems: [url], - applicationActivities: applicationActivities - ) - activityViewController.popoverPresentationController?.sourceView = mediaView - self.present(activityViewController, animated: true, completion: nil) - } // end Task - }, - ] - ), - ] // end children - ) // end return UIMenu - } - configuration.indexPath = indexPath - configuration.index = i - return configuration - } // end for … in … +// guard let cell = tableView.cellForRow(at: indexPath) as? StatusViewContainerTableViewCell else { return nil } +// +// defer { +// Task { +// guard let item = await item(from: .init(tableViewCell: cell, indexPath: indexPath)) else { return } +// guard let status = await item.status(in: context.managedObjectContext) else { return } +// await DataSourceFacade.recordStatusHistory( +// denpendency: self, +// status: status +// ) +// } // end Task +// } +// +// // TODO: +// // this must call before check `isContentWarningOverlayDisplay`. otherwise, will get BadAccess exception +// let mediaViews = cell.statusView.mediaGridContainerView.mediaViews +// +// if cell.statusView.mediaGridContainerView.viewModel.isContentWarningOverlayDisplay == true { +// return nil +// } +// +// for (i, mediaView) in mediaViews.enumerated() { +// let pointInMediaView = mediaView.convert(point, from: tableView) +// guard mediaView.point(inside: pointInMediaView, with: nil) else { +// continue +// } +// guard let image = mediaView.thumbnail(), +// let assetURLString = mediaView.configuration?.downloadURL, +// let assetURL = URL(string: assetURLString), +// let resourceType = mediaView.configuration?.resourceType +// else { +// // not provide preview unless thumbnail ready +// return nil +// } +// +// let contextMenuImagePreviewViewModel = ContextMenuImagePreviewViewModel(aspectRatio: image.size, thumbnail: image) +// +// let configuration = TimelineTableViewCellContextMenuConfiguration(identifier: nil) { () -> UIViewController? in +// if UIDevice.current.userInterfaceIdiom == .pad && mediaViews.count == 1 { +// return nil +// } +// let previewProvider = ContextMenuImagePreviewViewController() +// previewProvider.viewModel = contextMenuImagePreviewViewModel +// return previewProvider +// +// } actionProvider: { _ -> UIMenu? in +// return UIMenu( +// title: "", +// image: nil, +// identifier: nil, +// options: [], +// children: [ +// UIAction( +// title: L10n.Common.Controls.Actions.save, +// image: UIImage(systemName: "square.and.arrow.down"), +// attributes: [], +// state: .off +// ) { [weak self] _ in +// guard let self = self else { return } +// Task { @MainActor in +// let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) +// let notificationFeedbackGenerator = UINotificationFeedbackGenerator() +// +// do { +// impactFeedbackGenerator.impactOccurred() +// try await self.context.photoLibraryService.save( +// source: .remote(url: assetURL), +// resourceType: resourceType +// ) +// self.context.photoLibraryService.presentSuccessNotification(title: L10n.Common.Alerts.PhotoSaved.title) +// notificationFeedbackGenerator.notificationOccurred(.success) +// } catch { +// self.context.photoLibraryService.presentFailureNotification( +// error: error, +// title: L10n.Common.Alerts.PhotoSaveFail.title, +// message: L10n.Common.Alerts.PhotoSaveFail.message +// ) +// notificationFeedbackGenerator.notificationOccurred(.error) +// } +// } // end Task +// }, +// UIAction( +// title: L10n.Common.Controls.Actions.copy, +// image: UIImage(systemName: "doc.on.doc"), +// attributes: [], +// state: .off +// ) { [weak self] _ in +// guard let self = self else { return } +// Task { @MainActor in +// let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) +// let notificationFeedbackGenerator = UINotificationFeedbackGenerator() +// +// do { +// impactFeedbackGenerator.impactOccurred() +// try await self.context.photoLibraryService.copy( +// source: .remote(url: assetURL), +// resourceType: resourceType +// ) +// self.context.photoLibraryService.presentSuccessNotification(title: L10n.Common.Alerts.PhotoCopied.title) +// notificationFeedbackGenerator.notificationOccurred(.success) +// } catch { +// self.context.photoLibraryService.presentFailureNotification( +// error: error, +// title: L10n.Common.Alerts.PhotoCopied.title, +// message: L10n.Common.Alerts.PhotoCopyFail.message +// ) +// notificationFeedbackGenerator.notificationOccurred(.error) +// } +// } // end Task +// }, +// UIMenu( +// title: L10n.Common.Controls.Actions.share, +// image: UIImage(systemName: "square.and.arrow.up"), +// identifier: nil, +// options: [], +// children: [ +// UIAction( +// title: L10n.Common.Controls.Actions.ShareMediaMenu.link, +// image: UIImage(systemName: "link"), +// attributes: [], +// state: .off +// ) { [weak self] _ in +// guard let self = self else { return } +// Task { @MainActor in +// let applicationActivities: [UIActivity] = [ +// SafariActivity(sceneCoordinator: self.coordinator) +// ] +// let activityViewController = UIActivityViewController( +// activityItems: [assetURL], +// applicationActivities: applicationActivities +// ) +// activityViewController.popoverPresentationController?.sourceView = mediaView +// self.present(activityViewController, animated: true, completion: nil) +// } // end Task +// }, +// UIAction( +// title: L10n.Common.Controls.Actions.ShareMediaMenu.media, +// image: UIImage(systemName: "photo"), +// attributes: [], +// state: .off +// ) { [weak self] _ in +// guard let self = self else { return } +// Task { @MainActor in +// let applicationActivities: [UIActivity] = [ +// SafariActivity(sceneCoordinator: self.coordinator) +// ] +// // FIXME: handle error +// guard let url = try await self.context.photoLibraryService.file(from: .remote(url: assetURL)) else { +// return +// } +// let activityViewController = UIActivityViewController( +// activityItems: [url], +// applicationActivities: applicationActivities +// ) +// activityViewController.popoverPresentationController?.sourceView = mediaView +// self.present(activityViewController, animated: true, completion: nil) +// } // end Task +// }, +// ] +// ), +// ] // end children +// ) // end return UIMenu +// } +// configuration.indexPath = indexPath +// configuration.index = i +// return configuration +// } // end for … in … return nil } @@ -254,19 +254,20 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid _ tableView: UITableView, configuration: UIContextMenuConfiguration ) -> UITargetedPreview? { - guard let configuration = configuration as? TimelineTableViewCellContextMenuConfiguration else { return nil } - guard let indexPath = configuration.indexPath, let index = configuration.index else { return nil } - if let cell = tableView.cellForRow(at: indexPath) as? StatusViewContainerTableViewCell { - let mediaViews = cell.statusView.mediaGridContainerView.mediaViews - guard index < mediaViews.count else { return nil } - let mediaView = mediaViews[index] - let parameters = UIPreviewParameters() - parameters.backgroundColor = .clear - parameters.visiblePath = UIBezierPath(roundedRect: mediaView.bounds, cornerRadius: MediaView.cornerRadius) - return UITargetedPreview(view: mediaView, parameters: parameters) - } else { - return nil - } + return nil +// guard let configuration = configuration as? TimelineTableViewCellContextMenuConfiguration else { return nil } +// guard let indexPath = configuration.indexPath, let index = configuration.index else { return nil } +// if let cell = tableView.cellForRow(at: indexPath) as? StatusViewContainerTableViewCell { +// let mediaViews = cell.statusView.mediaGridContainerView.mediaViews +// guard index < mediaViews.count else { return nil } +// let mediaView = mediaViews[index] +// let parameters = UIPreviewParameters() +// parameters.backgroundColor = .clear +// parameters.visiblePath = UIBezierPath(roundedRect: mediaView.bounds, cornerRadius: MediaView.cornerRadius) +// return UITargetedPreview(view: mediaView, parameters: parameters) +// } else { +// return nil +// } } func aspectTableView( @@ -275,37 +276,37 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid animator: UIContextMenuInteractionCommitAnimating ) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - guard let configuration = configuration as? TimelineTableViewCellContextMenuConfiguration else { return } - guard let indexPath = configuration.indexPath, let index = configuration.index else { return } - guard let cell = tableView.cellForRow(at: indexPath) as? StatusViewContainerTableViewCell else { return } - let mediaViews = cell.statusView.mediaGridContainerView.mediaViews - guard index < mediaViews.count else { return } - let mediaView = mediaViews[index] - - animator.addCompletion { - Task { [weak self] in - guard let self = self else { return } - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await self.item(from: source) else { - assertionFailure() - return - } - guard let status = await item.status(in: self.context.managedObjectContext) else { - assertionFailure("only works for status data provider") - return - } - await DataSourceFacade.coordinateToMediaPreviewScene( - provider: self, - target: .status, - status: status, - mediaPreviewContext: DataSourceFacade.MediaPreviewContext( - containerView: .mediaGridContainerView(cell.statusView.mediaGridContainerView), - mediaView: mediaView, - index: index - ) - ) - } // end Task - } +// guard let configuration = configuration as? TimelineTableViewCellContextMenuConfiguration else { return } +// guard let indexPath = configuration.indexPath, let index = configuration.index else { return } +// guard let cell = tableView.cellForRow(at: indexPath) as? StatusViewContainerTableViewCell else { return } +// let mediaViews = cell.statusView.mediaGridContainerView.mediaViews +// guard index < mediaViews.count else { return } +// let mediaView = mediaViews[index] +// +// animator.addCompletion { +// Task { [weak self] in +// guard let self = self else { return } +// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) +// guard let item = await self.item(from: source) else { +// assertionFailure() +// return +// } +// guard let status = await item.status(in: self.context.managedObjectContext) else { +// assertionFailure("only works for status data provider") +// return +// } +// await DataSourceFacade.coordinateToMediaPreviewScene( +// provider: self, +// target: .status, +// status: status, +// mediaPreviewContext: DataSourceFacade.MediaPreviewContext( +// containerView: .mediaGridContainerView(cell.statusView.mediaGridContainerView), +// mediaView: mediaView, +// index: index +// ) +// ) +// } // end Task +// } } // end func } diff --git a/TwidereX/Scene/Account/List/AccountListViewModel.swift b/TwidereX/Scene/Account/List/AccountListViewModel.swift index f18b6352..057f6dcc 100644 --- a/TwidereX/Scene/Account/List/AccountListViewModel.swift +++ b/TwidereX/Scene/Account/List/AccountListViewModel.swift @@ -46,7 +46,15 @@ final class AccountListViewModel: NSObject { super.init() authenticationIndexFetchedResultsController.delegate = self - try? authenticationIndexFetchedResultsController.performFetch() + do { + try authenticationIndexFetchedResultsController.performFetch() + let authenticationIndexes = authenticationIndexFetchedResultsController.fetchedObjects ?? [] + items = authenticationIndexes.map { authenticationIndex in + UserItem.authenticationIndex(record: authenticationIndex.asRecrod) + } + } catch { + assertionFailure(error.localizedDescription) + } } deinit { diff --git a/TwidereX/Scene/Compose/ComposeViewModel.swift b/TwidereX/Scene/Compose/ComposeViewModel.swift index 51838c6e..7f47f77d 100644 --- a/TwidereX/Scene/Compose/ComposeViewModel.swift +++ b/TwidereX/Scene/Compose/ComposeViewModel.swift @@ -9,6 +9,7 @@ import Foundation import Combine import TwidereCore +import TwidereUI final class ComposeViewModel { @@ -16,6 +17,7 @@ final class ComposeViewModel { // input let context: AppContext + @Published public var viewLayoutFrame = ViewLayoutFrame() // output @Published var title = L10n.Scene.Compose.Title.compose diff --git a/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift b/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift index 95d3b22b..9bc1b392 100644 --- a/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift +++ b/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift @@ -24,7 +24,8 @@ extension StatusHistoryViewModel { statusViewConfigurationContext: .init( authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider() + twitterTextProvider: OfficialTwitterTextProvider(), + viewLayoutFramePublisher: $viewLayoutFrame ), userViewTableViewCellDelegate: nil, userViewConfigurationContext: .init( diff --git a/TwidereX/Scene/History/Status/StatusHistoryViewModel.swift b/TwidereX/Scene/History/Status/StatusHistoryViewModel.swift index 7790afb6..3924da0b 100644 --- a/TwidereX/Scene/History/Status/StatusHistoryViewModel.swift +++ b/TwidereX/Scene/History/Status/StatusHistoryViewModel.swift @@ -25,6 +25,8 @@ final class StatusHistoryViewModel { let authContext: AuthContext let historyFetchedResultsController: HistoryFetchedResultsController + @Published public var viewLayoutFrame = ViewLayoutFrame() + // output var diffableDataSource: UITableViewDiffableDataSource? diff --git a/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift b/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift index 5ae491cd..fbc5e4b7 100644 --- a/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift +++ b/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift @@ -24,7 +24,8 @@ extension UserHistoryViewModel { statusViewConfigurationContext: .init( authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider() + twitterTextProvider: OfficialTwitterTextProvider(), + viewLayoutFramePublisher: $viewLayoutFrame ), userViewTableViewCellDelegate: userViewTableViewCellDelegate, userViewConfigurationContext: .init( diff --git a/TwidereX/Scene/History/User/UserHistoryViewModel.swift b/TwidereX/Scene/History/User/UserHistoryViewModel.swift index e186b0c6..304bdf08 100644 --- a/TwidereX/Scene/History/User/UserHistoryViewModel.swift +++ b/TwidereX/Scene/History/User/UserHistoryViewModel.swift @@ -24,7 +24,9 @@ final class UserHistoryViewModel { let context: AppContext let authContext: AuthContext let historyFetchedResultsController: HistoryFetchedResultsController - + + @Published public var viewLayoutFrame = ViewLayoutFrame() + // output var diffableDataSource: UITableViewDiffableDataSource? diff --git a/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift b/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift index 0b79cb65..b0f654ff 100644 --- a/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift +++ b/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift @@ -141,7 +141,8 @@ extension MediaPreviewViewController { configurationContext: .init( authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider() + twitterTextProvider: OfficialTwitterTextProvider(), + viewLayoutFramePublisher: viewModel.$viewLayoutFrame ) ) } else { diff --git a/TwidereX/Scene/MediaPreview/MediaPreviewViewModel.swift b/TwidereX/Scene/MediaPreview/MediaPreviewViewModel.swift index 38b769d6..f247e93b 100644 --- a/TwidereX/Scene/MediaPreview/MediaPreviewViewModel.swift +++ b/TwidereX/Scene/MediaPreview/MediaPreviewViewModel.swift @@ -26,6 +26,8 @@ final class MediaPreviewViewModel: NSObject { let item: Item let transitionItem: MediaPreviewTransitionItem + @Published public var viewLayoutFrame = ViewLayoutFrame() + @Published var currentPage: Int // output diff --git a/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView+ViewModel.swift b/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView+ViewModel.swift index 883c8809..cdf5977b 100644 --- a/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView+ViewModel.swift +++ b/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView+ViewModel.swift @@ -98,64 +98,64 @@ extension MediaInfoDescriptionView { extension MediaInfoDescriptionView.ViewModel { func bind(view: MediaInfoDescriptionView) { - // avatar - $authorAvatarImageURL - .sink { url in - let configuration = AvatarImageView.Configuration(url: url) - view.avatarView.avatarButton.avatarImageView.configure(configuration: configuration) - } - .store(in: &disposeBag) - UserDefaults.shared - .observe(\.avatarStyle, options: [.initial, .new]) { defaults, _ in - let avatarStyle = defaults.avatarStyle - let animator = UIViewPropertyAnimator(duration: 0.3, timingParameters: UISpringTimingParameters()) - animator.addAnimations { [weak view] in - guard let view = view else { return } - switch avatarStyle { - case .circle: - view.avatarView.avatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .circle)) - case .roundedSquare: - view.avatarView.avatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .scale(ratio: 4))) - } - } - animator.startAnimation() - } - .store(in: &observations) - // name - $authorName - .sink { metaContent in - let metaContent = metaContent ?? PlaintextMetaContent(string: "") - view.nameMetaLabel.setupAttributes(style: StatusView.authorNameLabelStyle) - view.nameMetaLabel.configure(content: metaContent) - } - .store(in: &disposeBag) - // content - $content - .sink { metaContent in - guard let content = metaContent else { - view.contentTextView.reset() - return - } - view.contentTextView.configure(content: content) - } - .store(in: &disposeBag) - // toolbar - $platform - .assign(to: \.platform, on: view.toolbar.viewModel) - .store(in: &disposeBag) - Publishers.CombineLatest( - $isRepost, - $isRepostEnabled - ) - .sink { isRepost, isEnabled in - view.toolbar.setupRepost(count: 0, isEnabled: isEnabled, isHighlighted: isRepost) - } - .store(in: &disposeBag) - $isLike - .sink { isLike in - view.toolbar.setupLike(count: 0, isHighlighted: isLike) - } - .store(in: &disposeBag) +// // avatar +// $authorAvatarImageURL +// .sink { url in +// let configuration = AvatarImageView.Configuration(url: url) +// view.avatarView.avatarButton.avatarImageView.configure(configuration: configuration) +// } +// .store(in: &disposeBag) +// UserDefaults.shared +// .observe(\.avatarStyle, options: [.initial, .new]) { defaults, _ in +// let avatarStyle = defaults.avatarStyle +// let animator = UIViewPropertyAnimator(duration: 0.3, timingParameters: UISpringTimingParameters()) +// animator.addAnimations { [weak view] in +// guard let view = view else { return } +// switch avatarStyle { +// case .circle: +// view.avatarView.avatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .circle)) +// case .roundedSquare: +// view.avatarView.avatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .scale(ratio: 4))) +// } +// } +// animator.startAnimation() +// } +// .store(in: &observations) +// // name +// $authorName +// .sink { metaContent in +// let metaContent = metaContent ?? PlaintextMetaContent(string: "") +// view.nameMetaLabel.setupAttributes(style: StatusView.authorNameLabelStyle) +// view.nameMetaLabel.configure(content: metaContent) +// } +// .store(in: &disposeBag) +// // content +// $content +// .sink { metaContent in +// guard let content = metaContent else { +// view.contentTextView.reset() +// return +// } +// view.contentTextView.configure(content: content) +// } +// .store(in: &disposeBag) +// // toolbar +// $platform +// .assign(to: \.platform, on: view.toolbar.viewModel) +// .store(in: &disposeBag) +// Publishers.CombineLatest( +// $isRepost, +// $isRepostEnabled +// ) +// .sink { isRepost, isEnabled in +// view.toolbar.setupRepost(count: 0, isEnabled: isEnabled, isHighlighted: isRepost) +// } +// .store(in: &disposeBag) +// $isLike +// .sink { isLike in +// view.toolbar.setupLike(count: 0, isHighlighted: isLike) +// } +// .store(in: &disposeBag) } } diff --git a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift index 1bd791fc..84769697 100644 --- a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift @@ -27,7 +27,8 @@ extension NotificationTimelineViewModel { statusViewConfigurationContext: .init( authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider() + twitterTextProvider: OfficialTwitterTextProvider(), + viewLayoutFramePublisher: $viewLayoutFrame ), userViewConfigurationContext: .init( authContext: authContext, diff --git a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift index 1e7e4789..5a60a03b 100644 --- a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift +++ b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift @@ -28,6 +28,8 @@ final class NotificationTimelineViewModel { let fetchedResultsController: FeedFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() + @Published public var viewLayoutFrame = ViewLayoutFrame() + @Published var isLoadingLatest = false @Published var lastAutomaticFetchTimestamp: Date? diff --git a/TwidereX/Scene/Profile/ProfileViewController.swift b/TwidereX/Scene/Profile/ProfileViewController.swift index bc865ba2..bdb3b832 100644 --- a/TwidereX/Scene/Profile/ProfileViewController.swift +++ b/TwidereX/Scene/Profile/ProfileViewController.swift @@ -316,7 +316,8 @@ extension ProfileViewController { statusViewConfigureContext: .init( authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider() + twitterTextProvider: OfficialTwitterTextProvider(), + viewLayoutFramePublisher: composeViewModel.$viewLayoutFrame ) ) ) diff --git a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceView.swift b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceView.swift index da89acb5..93a177ba 100644 --- a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceView.swift +++ b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceView.swift @@ -24,16 +24,16 @@ struct DisplayPreferenceView: View { var body: some View { List { Section { - PrototypeStatusViewRepresentable( - style: .timeline, - configurationContext: StatusView.ConfigurationContext( - authContext: viewModel.authContext, - dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider() - ), - height: $timelineStatusViewHeight - ) - .frame(height: timelineStatusViewHeight) +// PrototypeStatusViewRepresentable( +// style: .timeline, +// configurationContext: StatusView.ConfigurationContext( +// authContext: viewModel.authContext, +// dateTimeProvider: DateTimeSwiftProvider(), +// twitterTextProvider: OfficialTwitterTextProvider() +// ), +// height: $timelineStatusViewHeight +// ) +// .frame(height: timelineStatusViewHeight) } header: { Text(verbatim: L10n.Scene.Settings.Display.SectionHeader.preview) .textCase(nil) diff --git a/TwidereX/Scene/Share/View/CollectionViewCell/StatusCollectionViewCell.swift b/TwidereX/Scene/Share/View/CollectionViewCell/StatusCollectionViewCell.swift index 50bb9172..a930996f 100644 --- a/TwidereX/Scene/Share/View/CollectionViewCell/StatusCollectionViewCell.swift +++ b/TwidereX/Scene/Share/View/CollectionViewCell/StatusCollectionViewCell.swift @@ -13,12 +13,12 @@ final class StatusCollectionViewCell: UICollectionViewCell { var disposeBag = Set() - private(set) lazy var statusView = StatusView() +// private(set) lazy var statusView = StatusView() override func prepareForReuse() { super.prepareForReuse() - statusView.prepareForReuse() +// statusView.prepareForReuse() disposeBag.removeAll() } @@ -37,14 +37,14 @@ final class StatusCollectionViewCell: UICollectionViewCell { extension StatusCollectionViewCell { private func _init() { - statusView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(statusView) - NSLayoutConstraint.activate([ - statusView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), - statusView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - statusView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), - statusView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - ]) +// statusView.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(statusView) +// NSLayoutConstraint.activate([ +// statusView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), +// statusView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), +// statusView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), +// statusView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// ]) } } diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell+ViewModel.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell+ViewModel.swift index 46f6ec2a..e6f48415 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell+ViewModel.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell+ViewModel.swift @@ -36,61 +36,61 @@ extension StatusTableViewCell { configurationContext: StatusView.ConfigurationContext, delegate: StatusViewTableViewCellDelegate? ) { - if statusView.frame == .zero { - // set status view width - statusView.frame.size.width = tableView.readableContentGuide.layoutFrame.width - let contentMaxLayoutWidth = statusView.contentMaxLayoutWidth - statusView.quoteStatusView?.frame.size.width = contentMaxLayoutWidth - // set preferredMaxLayoutWidth for content - statusView.spoilerContentTextView.preferredMaxLayoutWidth = contentMaxLayoutWidth - statusView.contentTextView.preferredMaxLayoutWidth = contentMaxLayoutWidth - statusView.quoteStatusView?.contentTextView.preferredMaxLayoutWidth = statusView.quoteStatusView?.contentMaxLayoutWidth - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): did layout for new cell") - } - - switch viewModel.value { - case .feed(let feed): - statusView.configure( - feed: feed, - configurationContext: configurationContext - ) - configureSeparator(style: feed.hasMore ? .edge : .inset) - case .statusObject(let object): - statusView.configure( - statusObject: object, - configurationContext: configurationContext - ) - configureSeparator(style: .inset) - case .twitterStatus(let status): - statusView.configure( - status: status, - configurationContext: configurationContext - ) - configureSeparator(style: .inset) - case .mastodonStatus(let status): - statusView.configure( - status: status, - notification: nil, - configurationContext: configurationContext - ) - configureSeparator(style: .inset) - } - - self.delegate = delegate - - statusView.viewModel.$isContentReveal - .removeDuplicates() - .dropFirst() - .receive(on: DispatchQueue.main) - .sink { [weak tableView, weak self] _ in - guard let tableView = tableView else { return } - guard let _ = self else { return } - UIView.setAnimationsEnabled(false) - tableView.beginUpdates() - tableView.endUpdates() - UIView.setAnimationsEnabled(true) - } - .store(in: &disposeBag) +// if statusView.frame == .zero { +// // set status view width +// statusView.frame.size.width = tableView.readableContentGuide.layoutFrame.width +// let contentMaxLayoutWidth = statusView.contentMaxLayoutWidth +// statusView.quoteStatusView?.frame.size.width = contentMaxLayoutWidth +// // set preferredMaxLayoutWidth for content +// statusView.spoilerContentTextView.preferredMaxLayoutWidth = contentMaxLayoutWidth +// statusView.contentTextView.preferredMaxLayoutWidth = contentMaxLayoutWidth +// statusView.quoteStatusView?.contentTextView.preferredMaxLayoutWidth = statusView.quoteStatusView?.contentMaxLayoutWidth +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): did layout for new cell") +// } +// +// switch viewModel.value { +// case .feed(let feed): +// statusView.configure( +// feed: feed, +// configurationContext: configurationContext +// ) +// configureSeparator(style: feed.hasMore ? .edge : .inset) +// case .statusObject(let object): +// statusView.configure( +// statusObject: object, +// configurationContext: configurationContext +// ) +// configureSeparator(style: .inset) +// case .twitterStatus(let status): +// statusView.configure( +// status: status, +// configurationContext: configurationContext +// ) +// configureSeparator(style: .inset) +// case .mastodonStatus(let status): +// statusView.configure( +// status: status, +// notification: nil, +// configurationContext: configurationContext +// ) +// configureSeparator(style: .inset) +// } +// +// self.delegate = delegate +// +// statusView.viewModel.$isContentReveal +// .removeDuplicates() +// .dropFirst() +// .receive(on: DispatchQueue.main) +// .sink { [weak tableView, weak self] _ in +// guard let tableView = tableView else { return } +// guard let _ = self else { return } +// UIView.setAnimationsEnabled(false) +// tableView.beginUpdates() +// tableView.endUpdates() +// UIView.setAnimationsEnabled(true) +// } +// .store(in: &disposeBag) } } @@ -103,28 +103,28 @@ extension StatusTableViewCell { } func configureSeparator(style: SeparatorStyle) { - separator.removeFromSuperview() - separator.removeConstraints(separator.constraints) - - switch style { - case .edge: - separator.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(separator) - NSLayoutConstraint.activate([ - separator.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - separator.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - separator.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), - ]) - case .inset: - separator.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(separator) - NSLayoutConstraint.activate([ - separator.leadingAnchor.constraint(equalTo: statusView.toolbar.leadingAnchor), - separator.trailingAnchor.constraint(equalTo: statusView.toolbar.trailingAnchor), - separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - separator.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), - ]) - } +// separator.removeFromSuperview() +// separator.removeConstraints(separator.constraints) +// +// switch style { +// case .edge: +// separator.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(separator) +// NSLayoutConstraint.activate([ +// separator.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), +// separator.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), +// separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// separator.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), +// ]) +// case .inset: +// separator.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(separator) +// NSLayoutConstraint.activate([ +// separator.leadingAnchor.constraint(equalTo: statusView.toolbar.leadingAnchor), +// separator.trailingAnchor.constraint(equalTo: statusView.toolbar.trailingAnchor), +// separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// separator.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), +// ]) +// } } } diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift index 2a1b6a4d..fed6ccfb 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift @@ -18,20 +18,20 @@ class StatusTableViewCell: UITableViewCell { let logger = Logger(subsystem: "StatusTableViewCell", category: "View") - weak var delegate: StatusViewTableViewCellDelegate? +// weak var delegate: StatusViewTableViewCellDelegate? - let topConversationLinkLineView = SeparatorLineView() - let statusView = StatusView() - let bottomConversationLinkLineView = SeparatorLineView() - let separator = SeparatorLineView() +// let topConversationLinkLineView = SeparatorLineView() +// let statusView = StatusView() +// let bottomConversationLinkLineView = SeparatorLineView() +// let separator = SeparatorLineView() override func prepareForReuse() { super.prepareForReuse() - statusView.prepareForReuse() +// statusView.prepareForReuse() disposeBag.removeAll() - topConversationLinkLineView.isHidden = true - bottomConversationLinkLineView.isHidden = true +// topConversationLinkLineView.isHidden = true +// bottomConversationLinkLineView.isHidden = true } override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -49,71 +49,71 @@ class StatusTableViewCell: UITableViewCell { extension StatusTableViewCell { private func _init() { - statusView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(statusView) - NSLayoutConstraint.activate([ - statusView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), - statusView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - statusView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), - statusView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - ]) - statusView.setup(style: .inline) - statusView.toolbar.setup(style: .inline) - - topConversationLinkLineView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(topConversationLinkLineView) - NSLayoutConstraint.activate([ - topConversationLinkLineView.topAnchor.constraint(equalTo: contentView.topAnchor), - topConversationLinkLineView.centerXAnchor.constraint(equalTo: statusView.authorAvatarButton.centerXAnchor), - topConversationLinkLineView.widthAnchor.constraint(equalToConstant: 1), - statusView.authorAvatarButton.topAnchor.constraint(equalTo: topConversationLinkLineView.bottomAnchor, constant: 2), - ]) - topConversationLinkLineView.isHidden = true - - bottomConversationLinkLineView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(bottomConversationLinkLineView) - NSLayoutConstraint.activate([ - bottomConversationLinkLineView.topAnchor.constraint(equalTo: statusView.authorAvatarButton.bottomAnchor, constant: 2), - bottomConversationLinkLineView.centerXAnchor.constraint(equalTo: statusView.authorAvatarButton.centerXAnchor), - bottomConversationLinkLineView.widthAnchor.constraint(equalToConstant: 1), - contentView.bottomAnchor.constraint(equalTo: bottomConversationLinkLineView.bottomAnchor), - ]) - bottomConversationLinkLineView.isHidden = true - - statusView.delegate = self +// statusView.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(statusView) +// NSLayoutConstraint.activate([ +// statusView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), +// statusView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), +// statusView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), +// statusView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// ]) +// statusView.setup(style: .inline) +// statusView.toolbar.setup(style: .inline) +// +// topConversationLinkLineView.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(topConversationLinkLineView) +// NSLayoutConstraint.activate([ +// topConversationLinkLineView.topAnchor.constraint(equalTo: contentView.topAnchor), +// topConversationLinkLineView.centerXAnchor.constraint(equalTo: statusView.authorAvatarButton.centerXAnchor), +// topConversationLinkLineView.widthAnchor.constraint(equalToConstant: 1), +// statusView.authorAvatarButton.topAnchor.constraint(equalTo: topConversationLinkLineView.bottomAnchor, constant: 2), +// ]) +// topConversationLinkLineView.isHidden = true +// +// bottomConversationLinkLineView.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(bottomConversationLinkLineView) +// NSLayoutConstraint.activate([ +// bottomConversationLinkLineView.topAnchor.constraint(equalTo: statusView.authorAvatarButton.bottomAnchor, constant: 2), +// bottomConversationLinkLineView.centerXAnchor.constraint(equalTo: statusView.authorAvatarButton.centerXAnchor), +// bottomConversationLinkLineView.widthAnchor.constraint(equalToConstant: 1), +// contentView.bottomAnchor.constraint(equalTo: bottomConversationLinkLineView.bottomAnchor), +// ]) +// bottomConversationLinkLineView.isHidden = true +// +// statusView.delegate = self // a11y - isAccessibilityElement = true - statusView.viewModel.$groupedAccessibilityLabel - .receive(on: DispatchQueue.main) - .sink { [weak self] accessibilityLabel in - guard let self = self else { return } - self.accessibilityLabel = accessibilityLabel - } - .store(in: &_disposeBag) +// isAccessibilityElement = true +// statusView.viewModel.$groupedAccessibilityLabel +// .receive(on: DispatchQueue.main) +// .sink { [weak self] accessibilityLabel in +// guard let self = self else { return } +// self.accessibilityLabel = accessibilityLabel +// } +// .store(in: &_disposeBag) } - override func accessibilityActivate() -> Bool { - delegate?.tableViewCell(self, statusView: statusView, accessibilityActivate: Void()) - return true - } +// override func accessibilityActivate() -> Bool { +// delegate?.tableViewCell(self, statusView: statusView, accessibilityActivate: Void()) +// return true +// } } extension StatusTableViewCell { - func setTopConversationLinkLineViewDisplay() { - topConversationLinkLineView.isHidden = false - } - - func setBottomConversationLinkLineViewDisplay() { - bottomConversationLinkLineView.isHidden = false - } +// func setTopConversationLinkLineViewDisplay() { +// topConversationLinkLineView.isHidden = false +// } +// +// func setBottomConversationLinkLineViewDisplay() { +// bottomConversationLinkLineView.isHidden = false +// } } // MARK: - StatusViewContainerTableViewCell -extension StatusTableViewCell: StatusViewContainerTableViewCell { } +//extension StatusTableViewCell: StatusViewContainerTableViewCell { } // MARK: - StatusViewDelegate extension StatusTableViewCell: StatusViewDelegate { } diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell+ViewModel.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell+ViewModel.swift index 66dee86b..b0e43a81 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell+ViewModel.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell+ViewModel.swift @@ -38,53 +38,53 @@ extension StatusThreadRootTableViewCell { configurationContext: StatusView.ConfigurationContext, delegate: StatusViewTableViewCellDelegate? ) { - if statusView.frame == .zero { - // set status view width - statusView.frame.size.width = tableView.readableContentGuide.layoutFrame.width - let contentMaxLayoutWidth = statusView.contentMaxLayoutWidth - statusView.quoteStatusView?.frame.size.width = contentMaxLayoutWidth - // set preferredMaxLayoutWidth for content - statusView.contentTextView.preferredMaxLayoutWidth = contentMaxLayoutWidth - statusView.quoteStatusView?.contentTextView.preferredMaxLayoutWidth = statusView.quoteStatusView?.contentMaxLayoutWidth - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): did layout for new cell") - } - - switch viewModel.value { - case .statusObject(let object): - statusView.configure( - statusObject: object, - configurationContext: configurationContext - ) - case .twitterStatus(let status): - statusView.configure( - status: status, - configurationContext: configurationContext - ) - case .mastodonStatus(let status): - statusView.configure( - status: status, - notification: nil, - configurationContext: configurationContext - ) - } - - self.delegate = delegate - - Publishers.CombineLatest( - statusView.viewModel.$isContentReveal.removeDuplicates(), - statusView.viewModel.$isTranslateButtonDisplay.removeDuplicates() - ) - .dropFirst() - .receive(on: DispatchQueue.main) - .sink { [weak tableView, weak self] _ in - guard let tableView = tableView else { return } - guard let _ = self else { return } - UIView.setAnimationsEnabled(false) - tableView.beginUpdates() - tableView.endUpdates() - UIView.setAnimationsEnabled(true) - } - .store(in: &disposeBag) +// if statusView.frame == .zero { +// // set status view width +// statusView.frame.size.width = tableView.readableContentGuide.layoutFrame.width +// let contentMaxLayoutWidth = statusView.contentMaxLayoutWidth +// statusView.quoteStatusView?.frame.size.width = contentMaxLayoutWidth +// // set preferredMaxLayoutWidth for content +// statusView.contentTextView.preferredMaxLayoutWidth = contentMaxLayoutWidth +// statusView.quoteStatusView?.contentTextView.preferredMaxLayoutWidth = statusView.quoteStatusView?.contentMaxLayoutWidth +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): did layout for new cell") +// } +// +// switch viewModel.value { +// case .statusObject(let object): +// statusView.configure( +// statusObject: object, +// configurationContext: configurationContext +// ) +// case .twitterStatus(let status): +// statusView.configure( +// status: status, +// configurationContext: configurationContext +// ) +// case .mastodonStatus(let status): +// statusView.configure( +// status: status, +// notification: nil, +// configurationContext: configurationContext +// ) +// } +// +// self.delegate = delegate +// +// Publishers.CombineLatest( +// statusView.viewModel.$isContentReveal.removeDuplicates(), +// statusView.viewModel.$isTranslateButtonDisplay.removeDuplicates() +// ) +// .dropFirst() +// .receive(on: DispatchQueue.main) +// .sink { [weak tableView, weak self] _ in +// guard let tableView = tableView else { return } +// guard let _ = self else { return } +// UIView.setAnimationsEnabled(false) +// tableView.beginUpdates() +// tableView.endUpdates() +// UIView.setAnimationsEnabled(true) +// } +// .store(in: &disposeBag) } } diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell.swift index 47c63bfe..d6fd996c 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell.swift @@ -19,17 +19,17 @@ final class StatusThreadRootTableViewCell: UITableViewCell { weak var delegate: StatusViewTableViewCellDelegate? - let conversationLinkLineView = SeparatorLineView() - let statusView = StatusView() - let toolbarSeparator = SeparatorLineView() - let separator = SeparatorLineView() +// let conversationLinkLineView = SeparatorLineView() +// let statusView = StatusView() +// let toolbarSeparator = SeparatorLineView() +// let separator = SeparatorLineView() override func prepareForReuse() { super.prepareForReuse() - statusView.prepareForReuse() - disposeBag.removeAll() - conversationLinkLineView.isHidden = true +// statusView.prepareForReuse() +// disposeBag.removeAll() +// conversationLinkLineView.isHidden = true } @@ -50,92 +50,92 @@ extension StatusThreadRootTableViewCell { private func _init() { selectionStyle = .none - statusView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(statusView) - NSLayoutConstraint.activate([ - statusView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), - statusView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - statusView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), - statusView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - ]) - statusView.setup(style: .plain) - statusView.toolbar.setup(style: .plain) - - conversationLinkLineView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(conversationLinkLineView) - NSLayoutConstraint.activate([ - conversationLinkLineView.topAnchor.constraint(equalTo: contentView.topAnchor), - conversationLinkLineView.centerXAnchor.constraint(equalTo: statusView.authorAvatarButton.centerXAnchor), - conversationLinkLineView.widthAnchor.constraint(equalToConstant: 1), - statusView.authorAvatarButton.topAnchor.constraint(equalTo: conversationLinkLineView.bottomAnchor, constant: 2), - ]) - conversationLinkLineView.isHidden = true - - toolbarSeparator.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(toolbarSeparator) - NSLayoutConstraint.activate([ - toolbarSeparator.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - toolbarSeparator.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - toolbarSeparator.bottomAnchor.constraint(equalTo: statusView.toolbar.topAnchor), - ]) - - separator.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(separator) - NSLayoutConstraint.activate([ - separator.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - separator.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - ]) - - statusView.delegate = self - - isAccessibilityElement = false +// statusView.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(statusView) +// NSLayoutConstraint.activate([ +// statusView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), +// statusView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), +// statusView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), +// statusView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// ]) +// statusView.setup(style: .plain) +// statusView.toolbar.setup(style: .plain) +// +// conversationLinkLineView.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(conversationLinkLineView) +// NSLayoutConstraint.activate([ +// conversationLinkLineView.topAnchor.constraint(equalTo: contentView.topAnchor), +// conversationLinkLineView.centerXAnchor.constraint(equalTo: statusView.authorAvatarButton.centerXAnchor), +// conversationLinkLineView.widthAnchor.constraint(equalToConstant: 1), +// statusView.authorAvatarButton.topAnchor.constraint(equalTo: conversationLinkLineView.bottomAnchor, constant: 2), +// ]) +// conversationLinkLineView.isHidden = true +// +// toolbarSeparator.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(toolbarSeparator) +// NSLayoutConstraint.activate([ +// toolbarSeparator.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), +// toolbarSeparator.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), +// toolbarSeparator.bottomAnchor.constraint(equalTo: statusView.toolbar.topAnchor), +// ]) +// +// separator.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(separator) +// NSLayoutConstraint.activate([ +// separator.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), +// separator.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), +// separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// ]) +// +// statusView.delegate = self +// +// isAccessibilityElement = false } } extension StatusThreadRootTableViewCell { - override var accessibilityElements: [Any]? { - get { - let elements: [UIView?] = [ - statusView.headerTextLabel, - statusView.authorAvatarButton, - statusView.authorNameLabel, - statusView.authorUsernameLabel, - statusView.visibilityImageView, - statusView.spoilerContentTextView, - statusView.expandContentButton, - statusView.contentTextView, - statusView.mediaGridContainerView, - statusView.pollTableView, - statusView.pollVoteDescriptionLabel, - statusView.pollVoteButton, - statusView.quoteStatusView, - statusView.locationLabel, - statusView.metricsDashboardView, - statusView.toolbar, - ] - - return elements - .compactMap { $0 } - .filter { !$0.isHidden } - } - set { } - } +// override var accessibilityElements: [Any]? { +// get { +// let elements: [UIView?] = [ +// statusView.headerTextLabel, +// statusView.authorAvatarButton, +// statusView.authorNameLabel, +// statusView.authorUsernameLabel, +// statusView.visibilityImageView, +// statusView.spoilerContentTextView, +// statusView.expandContentButton, +// statusView.contentTextView, +// statusView.mediaGridContainerView, +// statusView.pollTableView, +// statusView.pollVoteDescriptionLabel, +// statusView.pollVoteButton, +// statusView.quoteStatusView, +// statusView.locationLabel, +// statusView.metricsDashboardView, +// statusView.toolbar, +// ] +// +// return elements +// .compactMap { $0 } +// .filter { !$0.isHidden } +// } +// set { } +// } } extension StatusThreadRootTableViewCell { - func setConversationLinkLineViewDisplay() { - conversationLinkLineView.isHidden = false - } +// func setConversationLinkLineViewDisplay() { +// conversationLinkLineView.isHidden = false +// } } // MARK: - StatusViewContainerTableViewCell -extension StatusThreadRootTableViewCell: StatusViewContainerTableViewCell { } +//extension StatusThreadRootTableViewCell: StatusViewContainerTableViewCell { } // MARK: - StatusViewDelegate -extension StatusThreadRootTableViewCell: StatusViewDelegate { } +//extension StatusThreadRootTableViewCell: StatusViewDelegate { } diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift index c68c2721..ef9016fb 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift @@ -25,22 +25,6 @@ protocol StatusViewContainerTableViewCell: UITableViewCell, AutoGenerateProtocol // sourcery: replaceWith = "func tableViewCell(_ cell: UITableViewCell," protocol StatusViewTableViewCellDelegate: AutoGenerateProtocolDelegate { // sourcery:inline:StatusViewTableViewCellDelegate.AutoGenerateProtocolDelegate - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, headerDidPressed header: UIView) - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, expandContentButtonDidPressed button: UIButton) - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, quoteStatusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int) - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, toggleContentWarningOverlayViewDisplay contentWarningOverlayView: ContentWarningOverlayView) - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, quoteStatusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int) - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, pollTableView tableView: UITableView, didSelectRowAt indexPath: IndexPath) - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, pollVoteButtonDidPressed button: UIButton) - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, quoteStatusViewDidPressed quoteStatusView: StatusView) - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, statusToolbar: StatusToolbar, actionDidPressed action: StatusToolbar.Action, button: UIButton) - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, statusToolbar: StatusToolbar, menuActionDidPressed action: StatusToolbar.MenuAction, menuButton button: UIButton) - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, translateButtonDidPressed button: UIButton) - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, accessibilityActivate: Void) // sourcery:end } @@ -48,68 +32,5 @@ protocol StatusViewTableViewCellDelegate: AutoGenerateProtocolDelegate { // Protocol Extension extension StatusViewDelegate where Self: StatusViewContainerTableViewCell { // sourcery:inline:StatusViewContainerTableViewCell.AutoGenerateProtocolRelayDelegate - func statusView(_ statusView: StatusView, headerDidPressed header: UIView) { - delegate?.tableViewCell(self, statusView: statusView, headerDidPressed: header) - } - - func statusView(_ statusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) { - delegate?.tableViewCell(self, statusView: statusView, authorAvatarButtonDidPressed: button) - } - - func statusView(_ statusView: StatusView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) { - delegate?.tableViewCell(self, statusView: statusView, quoteStatusView: quoteStatusView, authorAvatarButtonDidPressed: button) - } - - func statusView(_ statusView: StatusView, expandContentButtonDidPressed button: UIButton) { - delegate?.tableViewCell(self, statusView: statusView, expandContentButtonDidPressed: button) - } - - func statusView(_ statusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) { - delegate?.tableViewCell(self, statusView: statusView, metaTextAreaView: metaTextAreaView, didSelectMeta: meta) - } - - func statusView(_ statusView: StatusView, quoteStatusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) { - delegate?.tableViewCell(self, statusView: statusView, quoteStatusView: quoteStatusView, metaTextAreaView: metaTextAreaView, didSelectMeta: meta) - } - - func statusView(_ statusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int) { - delegate?.tableViewCell(self, statusView: statusView, mediaGridContainerView: containerView, didTapMediaView: mediaView, at: index) - } - - func statusView(_ statusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, toggleContentWarningOverlayViewDisplay contentWarningOverlayView: ContentWarningOverlayView) { - delegate?.tableViewCell(self, statusView: statusView, mediaGridContainerView: containerView, toggleContentWarningOverlayViewDisplay: contentWarningOverlayView) - } - - func statusView(_ statusView: StatusView, quoteStatusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int) { - delegate?.tableViewCell(self, statusView: statusView, quoteStatusView: quoteStatusView, mediaGridContainerView: containerView, didTapMediaView: mediaView, at: index) - } - - func statusView(_ statusView: StatusView, pollTableView tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - delegate?.tableViewCell(self, statusView: statusView, pollTableView: tableView, didSelectRowAt: indexPath) - } - - func statusView(_ statusView: StatusView, pollVoteButtonDidPressed button: UIButton) { - delegate?.tableViewCell(self, statusView: statusView, pollVoteButtonDidPressed: button) - } - - func statusView(_ statusView: StatusView, quoteStatusViewDidPressed quoteStatusView: StatusView) { - delegate?.tableViewCell(self, statusView: statusView, quoteStatusViewDidPressed: quoteStatusView) - } - - func statusView(_ statusView: StatusView, statusToolbar: StatusToolbar, actionDidPressed action: StatusToolbar.Action, button: UIButton) { - delegate?.tableViewCell(self, statusView: statusView, statusToolbar: statusToolbar, actionDidPressed: action, button: button) - } - - func statusView(_ statusView: StatusView, statusToolbar: StatusToolbar, menuActionDidPressed action: StatusToolbar.MenuAction, menuButton button: UIButton) { - delegate?.tableViewCell(self, statusView: statusView, statusToolbar: statusToolbar, menuActionDidPressed: action, menuButton: button) - } - - func statusView(_ statusView: StatusView, translateButtonDidPressed button: UIButton) { - delegate?.tableViewCell(self, statusView: statusView, translateButtonDidPressed: button) - } - - func statusView(_ statusView: StatusView, accessibilityActivate: Void) { - delegate?.tableViewCell(self, statusView: statusView, accessibilityActivate: accessibilityActivate) - } // sourcery:end } diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift index c47e9784..af523755 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift @@ -25,7 +25,8 @@ extension StatusThreadViewModel { statusViewConfigurationContext: .init( authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider() + twitterTextProvider: OfficialTwitterTextProvider(), + viewLayoutFramePublisher: $viewLayoutFrame ) ) diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift index 05fad970..ce136152 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift @@ -15,6 +15,7 @@ import MastodonSDK import CoreData import CoreDataStack import TwidereCore +import TwidereUI final class StatusThreadViewModel { @@ -32,6 +33,8 @@ final class StatusThreadViewModel { let bottomListBatchFetchViewModel = ListBatchFetchViewModel(direction: .bottom) let viewDidAppear = PassthroughSubject() + @Published public var viewLayoutFrame = ViewLayoutFrame() + // output var diffableDataSource: UITableViewDiffableDataSource? var root: CurrentValueSubject diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift index 1ae91860..a25b1139 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift @@ -250,7 +250,8 @@ extension TimelineViewController { statusViewConfigureContext: .init( authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider() + twitterTextProvider: OfficialTwitterTextProvider(), + viewLayoutFramePublisher: _viewModel.$viewLayoutFrame ) ) ) diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift index 966cdf02..3e089f10 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift @@ -32,6 +32,8 @@ class TimelineViewModel: TimelineViewModelDriver { let listBatchFetchViewModel = ListBatchFetchViewModel() let viewDidAppear = CurrentValueSubject(Void()) + @Published public var viewLayoutFrame = ViewLayoutFrame() + @Published var enableAutoFetchLatest = false @Published var didAutoFetchLatest = false @Published var isRefreshControlEnabled = true diff --git a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift index b2dd60b0..88d3f8ab 100644 --- a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift @@ -8,6 +8,7 @@ import UIKit import TwidereCore +import TwidereUI class ListTimelineViewModel: TimelineViewModel { diff --git a/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel+Diffable.swift index 5fe26670..106722ee 100644 --- a/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel+Diffable.swift @@ -27,7 +27,8 @@ extension FederatedTimelineViewModel { statusViewConfigurationContext: StatusView.ConfigurationContext( authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider() + twitterTextProvider: OfficialTwitterTextProvider(), + viewLayoutFramePublisher: $viewLayoutFrame ) ) diffableDataSource = StatusSection.diffableDataSource( diff --git a/TwidereX/Scene/Timeline/Hashtag/HashtagTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/Hashtag/HashtagTimelineViewModel+Diffable.swift index 81b76032..5cb53767 100644 --- a/TwidereX/Scene/Timeline/Hashtag/HashtagTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/Hashtag/HashtagTimelineViewModel+Diffable.swift @@ -27,7 +27,8 @@ extension HashtagTimelineViewModel { statusViewConfigurationContext: StatusView.ConfigurationContext( authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider() + twitterTextProvider: OfficialTwitterTextProvider(), + viewLayoutFramePublisher: $viewLayoutFrame ) ) diffableDataSource = StatusSection.diffableDataSource( diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel+Diffable.swift index 669ada00..a0a340d2 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel+Diffable.swift @@ -27,7 +27,8 @@ extension HomeTimelineViewModel { statusViewConfigurationContext: .init( authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider() + twitterTextProvider: OfficialTwitterTextProvider(), + viewLayoutFramePublisher: $viewLayoutFrame ) ) diffableDataSource = StatusSection.diffableDataSource( diff --git a/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel+Diffable.swift index ec2c64fc..dbada263 100644 --- a/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel+Diffable.swift @@ -23,7 +23,8 @@ extension ListStatusTimelineViewModel { statusViewConfigurationContext: StatusView.ConfigurationContext( authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider() + twitterTextProvider: OfficialTwitterTextProvider(), + viewLayoutFramePublisher: $viewLayoutFrame ) ) diffableDataSource = StatusSection.diffableDataSource( diff --git a/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel+Diffable.swift index 77205e67..1a41362c 100644 --- a/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel+Diffable.swift @@ -23,7 +23,8 @@ extension SearchTimelineViewModel { statusViewConfigurationContext: StatusView.ConfigurationContext( authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider() + twitterTextProvider: OfficialTwitterTextProvider(), + viewLayoutFramePublisher: $viewLayoutFrame ) ) diffableDataSource = StatusSection.diffableDataSource( diff --git a/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel.swift b/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel.swift index 7d12ff63..adc3e383 100644 --- a/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel.swift @@ -9,12 +9,13 @@ import os.log import UIKit import TwidereCore +import TwidereUI final class SearchTimelineViewModel: ListTimelineViewModel { // input @Published var searchText = "" - + // output init( diff --git a/TwidereX/Scene/Timeline/User/Status/UserTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/User/Status/UserTimelineViewModel+Diffable.swift index 6e972e37..b3aab38f 100644 --- a/TwidereX/Scene/Timeline/User/Status/UserTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/User/Status/UserTimelineViewModel+Diffable.swift @@ -27,7 +27,8 @@ extension UserTimelineViewModel { statusViewConfigurationContext: StatusView.ConfigurationContext( authContext: authContext, dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider() + twitterTextProvider: OfficialTwitterTextProvider(), + viewLayoutFramePublisher: $viewLayoutFrame ) ) diffableDataSource = StatusSection.diffableDataSource( From 222f91dbe3a101c9e2e08ea18da8d9109c17ce83 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 6 Feb 2023 18:16:50 +0800 Subject: [PATCH 027/128] fix: UITextView representable control layout issue --- TwidereSDK/Package.swift | 4 +- .../TwidereUI/Content/StatusView.swift | 28 ++++- .../TextViewRepresentable.swift | 114 ++++++++++++++++++ TwidereX.xcodeproj/project.pbxproj | 12 ++ .../TableViewCell/StatusTableViewCell.swift | 2 +- .../Base/Common/TimelineViewController.swift | 14 +++ 6 files changed, 167 insertions(+), 7 deletions(-) create mode 100644 TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index dffa3a58..56d50d28 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.6 +// swift-tools-version:5.7 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -7,7 +7,7 @@ let package = Package( name: "TwidereSDK", defaultLocalization: "en", platforms: [ - .iOS(.v15), + .iOS(.v16), .macOS(.v12), ], products: [ diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index 18751253..b4a756d6 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -10,6 +10,7 @@ import os.log import Combine import UIKit import SwiftUI +import Kingfisher import MetaTextKit import MetaTextArea import MetaLabel @@ -19,6 +20,10 @@ import NIOPosix public struct StatusView: View { + var avatarViewDimension: CGFloat { + return 44 + } + @ObservedObject public private(set) var viewModel: ViewModel public init(viewModel: StatusView.ViewModel) { @@ -33,14 +38,18 @@ public struct StatusView: View { // post StatusView(viewModel: repostViewModel) } else { + // authorView + authorView // content contentView // quote if let quoteViewModel = viewModel.quoteViewModel { StatusView(viewModel: quoteViewModel) + .frame(width: viewModel.viewLayoutFrame.readableContentLayoutFrame.width) } } - } + } // end VStack + .frame(width: viewModel.viewLayoutFrame.readableContentLayoutFrame.width) } } @@ -57,11 +66,22 @@ extension StatusView { } } + public var authorView: some View { + HStack { + KFImage(viewModel.avatarURL) + .resizable() + .frame(width: avatarViewDimension, height: avatarViewDimension) + Spacer() + } + } + public var contentView: some View { HStack { - Text(viewModel.content.attributedString) - .lineLimit(nil) - .multilineTextAlignment(.leading) + TextViewRepresentable( + metaContent: viewModel.content, + width: viewModel.viewLayoutFrame.readableContentLayoutFrame.width + ) + .frame(width: viewModel.viewLayoutFrame.readableContentLayoutFrame.width) Spacer() } } diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift new file mode 100644 index 00000000..17d32fae --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift @@ -0,0 +1,114 @@ +// +// TextViewRepresentable.swift +// +// +// Created by MainasuK on 2023/2/3. +// + +import UIKit +import SwiftUI +import TwidereCore +import MetaTextKit + +public struct TextViewRepresentable: UIViewRepresentable { + + let textView: WrappedTextView = { + let textView = WrappedTextView() + textView.backgroundColor = .clear + textView.isScrollEnabled = false + textView.textContainerInset = .zero + textView.textContainer.lineFragmentPadding = 0 + textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + textView.setContentHuggingPriority(.defaultHigh, for: .vertical) + return textView + }() + + // input + public let metaContent: MetaContent + let width: CGFloat + + public init( + metaContent: MetaContent, + width: CGFloat + ) { + self.metaContent = metaContent + self.width = width + } + + public func makeUIView(context: Context) -> UITextView { + let textView = self.textView + + let attributedString = NSMutableAttributedString(string: metaContent.string) + let textAttributes: [NSAttributedString.Key: Any] = [ + .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)), + .foregroundColor: UIColor.label, + ] + let linkAttributes: [NSAttributedString.Key: Any] = [ + .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)), + .foregroundColor: UIColor.link, + ] + let paragraphStyle: NSMutableParagraphStyle = { + let style = NSMutableParagraphStyle() + style.lineSpacing = 5 + style.paragraphSpacing = 8 + return style + }() + + MetaText.setAttributes( + for: attributedString, + textAttributes: textAttributes, + linkAttributes: linkAttributes, + paragraphStyle: paragraphStyle, + content: metaContent + ) + + textView.frame.size.width = width + textView.textStorage.setAttributedString(attributedString) + textView.invalidateIntrinsicContentSize() + + return textView + } + + public func updateUIView(_ view: UITextView, context: Context) { + + } + + public func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + public class Coordinator: NSObject, UITextViewDelegate { + let view: TextViewRepresentable + + init(_ view: TextViewRepresentable) { + self.view = view + super.init() + } + } +} + +class WrappedTextView: UITextView { + +// private var lastWidth: CGFloat = 0 +// +// override func layoutSubviews() { +// super.layoutSubviews() +// +// if bounds.width != lastWidth { +// lastWidth = bounds.width +// invalidateIntrinsicContentSize() +// } +// } +// +// override var intrinsicContentSize: CGSize { +// let size = sizeThatFits(CGSize( +// width: lastWidth, +// height: UIView.layoutFittingExpandedSize.height +// )) +// return CGSize( +// width: size.width.rounded(.up), +// height: size.height.rounded(.up) +// ) +// } + +} diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 1fe487ae..cf9bc4c7 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3699,6 +3699,7 @@ INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3727,6 +3728,7 @@ INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3755,6 +3757,7 @@ INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3785,6 +3788,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3814,6 +3818,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3843,6 +3848,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3876,6 +3882,7 @@ INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3910,6 +3917,7 @@ INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3937,6 +3945,7 @@ INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3964,6 +3973,7 @@ INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4273,6 +4283,7 @@ INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4301,6 +4312,7 @@ INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift index fed6ccfb..a62fb71c 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift @@ -28,7 +28,7 @@ class StatusTableViewCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() -// statusView.prepareForReuse() + contentConfiguration = nil disposeBag.removeAll() // topConversationLinkLineView.isHidden = true // bottomConversationLinkLineView.isHidden = true diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift index a25b1139..46f7b7e7 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift @@ -167,15 +167,29 @@ extension TimelineViewController { _viewModel.viewDidAppear.send() } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + _viewModel.viewLayoutFrame.update(view: view) + } override func viewSafeAreaInsetsDidChange() { super.viewSafeAreaInsetsDidChange() + + _viewModel.viewLayoutFrame.update(view: view) DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.floatyButton.paddingY = self.view.safeAreaInsets.bottom + UIView.floatyButtonBottomMargin } } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + _viewModel.viewLayoutFrame.update(view: view) + } } From 6c92203dbd92a00f90020c9187b8868d46e50a4a Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 6 Feb 2023 19:10:40 +0800 Subject: [PATCH 028/128] feat: add UILabel with TextKit 2 for author name and username --- .../TwidereUI/Content/StatusView.swift | 34 +++++++- .../LabelRepresentable.swift | 85 +++++++++++++++++++ .../TextViewRepresentable.swift | 41 ++------- 3 files changed, 126 insertions(+), 34 deletions(-) create mode 100644 TwidereSDK/Sources/TwidereUI/UIViewRepresentable/LabelRepresentable.swift diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index b4a756d6..e800243e 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -26,6 +26,8 @@ public struct StatusView: View { @ObservedObject public private(set) var viewModel: ViewModel + @State private var authorAvatarDimension = CGFloat.zero + public init(viewModel: StatusView.ViewModel) { self.viewModel = viewModel } @@ -67,10 +69,30 @@ extension StatusView { } public var authorView: some View { - HStack { + HStack(alignment: .center) { KFImage(viewModel.avatarURL) .resizable() - .frame(width: avatarViewDimension, height: avatarViewDimension) + .aspectRatio(contentMode: .fill) + .frame(width: authorAvatarDimension, height: authorAvatarDimension) + VStack(spacing: .zero) { + LabelRepresentable( + metaContent: viewModel.authorName, + textStyle: .statusAuthorName + ) + LabelRepresentable( + metaContent: PlaintextMetaContent(string: "@" + viewModel.authorUsernme), + textStyle: .statusAuthorUsername + ) + } + .background(GeometryReader { proxy in + Color.clear.preference( + key: ViewHeightKey.self, + value: proxy.frame(in: .local).size.height + ) + }) + .onPreferenceChange(ViewHeightKey.self) { height in + self.authorAvatarDimension = height + } Spacer() } } @@ -79,6 +101,7 @@ extension StatusView { HStack { TextViewRepresentable( metaContent: viewModel.content, + textStyle: .statusContent, width: viewModel.viewLayoutFrame.readableContentLayoutFrame.width ) .frame(width: viewModel.viewLayoutFrame.readableContentLayoutFrame.width) @@ -87,6 +110,13 @@ extension StatusView { } } +struct ViewHeightKey: PreferenceKey { + static var defaultValue: CGFloat { 0 } + static func reduce(value: inout Value, nextValue: () -> Value) { + value = value + nextValue() + } +} + public protocol StatusViewDelegate: AnyObject { // func statusView(_ statusView: StatusView, headerDidPressed header: UIView) // diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/LabelRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/LabelRepresentable.swift new file mode 100644 index 00000000..8769008a --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/LabelRepresentable.swift @@ -0,0 +1,85 @@ +// +// LabelRepresentable.swift +// +// +// Created by MainasuK on 2023/2/6. +// + +import UIKit +import SwiftUI +import TwidereCore +import MetaTextKit + +public struct LabelRepresentable: UIViewRepresentable { + + let label: UILabel = { + let label = UILabel() + label.numberOfLines = 1 + label.backgroundColor = .clear + label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + label.setContentHuggingPriority(.defaultHigh, for: .vertical) + return label + }() + + // input + public let metaContent: MetaContent + public let textStyle: TextStyle + + public init( + metaContent: MetaContent, + textStyle: TextStyle + ) { + self.metaContent = metaContent + self.textStyle = textStyle + } + + public func makeUIView(context: Context) -> UILabel { + let label = self.label + + let attributedString = NSMutableAttributedString(string: metaContent.string) + let textAttributes: [NSAttributedString.Key: Any] = [ + .font: textStyle.font, + .foregroundColor: textStyle.textColor, + ] + let linkAttributes: [NSAttributedString.Key: Any] = [ + .font: textStyle.font, + .foregroundColor: UIColor.tintColor, + ] + let paragraphStyle: NSMutableParagraphStyle = { + let style = NSMutableParagraphStyle() + style.lineSpacing = 5 + style.paragraphSpacing = 8 + return style + }() + + MetaText.setAttributes( + for: attributedString, + textAttributes: textAttributes, + linkAttributes: linkAttributes, + paragraphStyle: paragraphStyle, + content: metaContent + ) + + label.attributedText = attributedString + label.invalidateIntrinsicContentSize() + + return label + } + + public func updateUIView(_ view: UILabel, context: Context) { + + } + + public func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + public class Coordinator: NSObject, UITextViewDelegate { + let view: LabelRepresentable + + init(_ view: LabelRepresentable) { + self.view = view + super.init() + } + } +} diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift index 17d32fae..db0eb476 100644 --- a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift @@ -12,8 +12,8 @@ import MetaTextKit public struct TextViewRepresentable: UIViewRepresentable { - let textView: WrappedTextView = { - let textView = WrappedTextView() + let textView: UITextView = { + let textView = UITextView() textView.backgroundColor = .clear textView.isScrollEnabled = false textView.textContainerInset = .zero @@ -25,13 +25,16 @@ public struct TextViewRepresentable: UIViewRepresentable { // input public let metaContent: MetaContent + public let textStyle: TextStyle let width: CGFloat public init( metaContent: MetaContent, + textStyle: TextStyle, width: CGFloat ) { self.metaContent = metaContent + self.textStyle = textStyle self.width = width } @@ -40,12 +43,12 @@ public struct TextViewRepresentable: UIViewRepresentable { let attributedString = NSMutableAttributedString(string: metaContent.string) let textAttributes: [NSAttributedString.Key: Any] = [ - .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)), - .foregroundColor: UIColor.label, + .font: textStyle.font, + .foregroundColor: textStyle.textColor, ] let linkAttributes: [NSAttributedString.Key: Any] = [ - .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)), - .foregroundColor: UIColor.link, + .font: textStyle.font, + .foregroundColor: UIColor.tintColor, ] let paragraphStyle: NSMutableParagraphStyle = { let style = NSMutableParagraphStyle() @@ -86,29 +89,3 @@ public struct TextViewRepresentable: UIViewRepresentable { } } } - -class WrappedTextView: UITextView { - -// private var lastWidth: CGFloat = 0 -// -// override func layoutSubviews() { -// super.layoutSubviews() -// -// if bounds.width != lastWidth { -// lastWidth = bounds.width -// invalidateIntrinsicContentSize() -// } -// } -// -// override var intrinsicContentSize: CGSize { -// let size = sizeThatFits(CGSize( -// width: lastWidth, -// height: UIView.layoutFittingExpandedSize.height -// )) -// return CGSize( -// width: size.width.rounded(.up), -// height: size.height.rounded(.up) -// ) -// } - -} From c756e40d159312b40567175c0a8b5ade3bda5b2c Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 6 Feb 2023 19:12:13 +0800 Subject: [PATCH 029/128] feat: clip the avatar to circle --- .../Sources/TwidereUI/Content/StatusView.swift | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index e800243e..8409abe9 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -70,15 +70,25 @@ extension StatusView { public var authorView: some View { HStack(alignment: .center) { - KFImage(viewModel.avatarURL) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: authorAvatarDimension, height: authorAvatarDimension) + // avatar + Button { + + } label: { + KFImage(viewModel.avatarURL) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: authorAvatarDimension, height: authorAvatarDimension) + .clipShape(Circle()) + } + .buttonStyle(.borderless) + // info VStack(spacing: .zero) { + // name LabelRepresentable( metaContent: viewModel.authorName, textStyle: .statusAuthorName ) + // username LabelRepresentable( metaContent: PlaintextMetaContent(string: "@" + viewModel.authorUsernme), textStyle: .statusAuthorUsername From 40d1b1113812de05c1aa918b10769f4994a74373 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 20 Feb 2023 17:42:13 +0800 Subject: [PATCH 030/128] fix: status content layout not display all the content issue. (perfect rotation now!) --- .../TwidereUI/Content/StatusHeaderView.swift | 58 ++++++++- .../Content/StatusView+ViewModel.swift | 87 ++++++++++--- .../TwidereUI/Content/StatusView.swift | 115 ++++++++++-------- .../LabelRepresentable.swift | 5 +- .../TextViewRepresentable.swift | 43 ++++++- .../TwidereUI/Utility/ViewHeightKey.swift | 16 +++ TwidereX.xcodeproj/project.pbxproj | 2 + .../xcshareddata/swiftpm/Package.resolved | 9 -- .../Provider/DataSourceFacade+Meta.swift | 6 + .../Base/Common/TimelineViewController.swift | 9 +- 10 files changed, 265 insertions(+), 85 deletions(-) create mode 100644 TwidereSDK/Sources/TwidereUI/Utility/ViewHeightKey.swift diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift index 0abc4801..1d7833f8 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift @@ -8,22 +8,74 @@ import SwiftUI import TwidereAsset import TwidereLocalization +import Meta +import Kingfisher + +protocol StatusHeaderViewDelegate: AnyObject { + func viewDidPressed(_ viewModel: StatusHeaderView.ViewModel) +} public struct StatusHeaderView: View { + static var iconImageTrailingSpacing: CGFloat { 4.0 } + @ObservedObject public var viewModel: ViewModel + @State private var iconImageDimension = CGFloat.zero + public var body: some View { - Text("Repost") + HStack(spacing: .zero) { + if viewModel.hasHangingAvatar { + let width = viewModel.avatarDimension + + StatusView.hangingAvatarButtonTrailingSapcing + - iconImageDimension + - StatusHeaderView.iconImageTrailingSpacing + Color.clear + .frame(width: max(.zero, width)) + } + Button { + + } label: { + HStack(spacing: StatusHeaderView.iconImageTrailingSpacing) { + Image(uiImage: viewModel.image) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: iconImageDimension, height: iconImageDimension) + .clipShape(Circle()) + LabelRepresentable( + metaContent: viewModel.label, + textStyle: .statusHeader + ) + .background(GeometryReader { proxy in + Color.clear.preference( + key: ViewHeightKey.self, + value: proxy.frame(in: .local).size.height + ) + }) + .onPreferenceChange(ViewHeightKey.self) { height in + self.iconImageDimension = height + } + .border(.red, width: 1) + } + } // end Button + } } } extension StatusHeaderView { public class ViewModel: ObservableObject { - @Published public var label: AttributedString + @Published public var image: UIImage + @Published public var label: MetaContent + + @Published public var hasHangingAvatar: Bool = false + @Published public var avatarDimension: CGFloat = StatusView.hangingAvatarButtonDimension - public init(label: AttributedString) { + public init( + image: UIImage, + label: MetaContent + ) { + self.image = image self.label = label } } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index e43e9a3d..77eee2e2 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -27,6 +27,8 @@ extension StatusView { let logger = Logger(subsystem: "StatusView", category: "ViewModel") + var disposeBag = Set() + @Published public var viewLayoutFrame = ViewLayoutFrame() @Published public var repostViewModel: StatusView.ViewModel? @@ -36,7 +38,13 @@ extension StatusView { public let kind: Kind public weak var delegate: StatusViewDelegate? + weak var parentViewModel: StatusView.ViewModel? + // @Published public var authorAvatarDimension: CGFloat = .zero + // output + + // header + @Published public var statusHeaderViewModel: StatusHeaderView.ViewModel? // author @Published public var avatarURL: URL? @@ -881,6 +889,35 @@ extension StatusView.ViewModel { } } +extension StatusView.ViewModel { + var hasHangingAvatar: Bool { + switch kind { + case .conversationRoot, .quote: + return false + default: + return true + } + } + + public var margin: CGFloat { + switch kind { + case .quote: + return 12 + default: + return .zero + } + } + + public var hasToolbar: Bool { + switch kind { + case .timeline, .conversationRoot, .conversationThread: + return true + default: + return false + } + } +} + extension StatusView.ViewModel { public convenience init?( feed: Feed, @@ -893,6 +930,7 @@ extension StatusView.ViewModel { status: status, kind: .timeline, delegate: delegate, + parentViewModel: nil, viewLayoutFramePublisher: viewLayoutFramePublisher ) case .mastodon(let status): @@ -900,6 +938,7 @@ extension StatusView.ViewModel { status: status, kind: .timeline, delegate: delegate, + parentViewModel: nil, viewLayoutFramePublisher: viewLayoutFramePublisher ) case .mastodonNotification(let notification): @@ -915,6 +954,7 @@ extension StatusView.ViewModel { status: TwitterStatus, kind: Kind, delegate: StatusViewDelegate?, + parentViewModel: StatusView.ViewModel?, viewLayoutFramePublisher: Published.Publisher? ) { self.init( @@ -922,24 +962,42 @@ extension StatusView.ViewModel { delegate: delegate, viewLayoutFramePublisher: viewLayoutFramePublisher ) + self.parentViewModel = parentViewModel if let repost = status.repost { - repostViewModel = .init( + let _repostViewModel = StatusView.ViewModel( status: repost, kind: .repost, delegate: delegate, + parentViewModel: self, viewLayoutFramePublisher: viewLayoutFramePublisher ) + repostViewModel = _repostViewModel + + // header - repost + let _statusHeaderViewModel = StatusHeaderView.ViewModel( + image: Asset.Media.repeat.image.withRenderingMode(.alwaysTemplate), + label: { + let name = status.author.name + let userRepostText = L10n.Common.Controls.Status.userRetweeted(name) + let label = PlaintextMetaContent(string: userRepostText) + return label + }() + ) + _statusHeaderViewModel.hasHangingAvatar = _repostViewModel.hasHangingAvatar + _repostViewModel.statusHeaderViewModel = _statusHeaderViewModel } if let quote = status.quote { quoteViewModel = .init( status: quote, kind: .quote, delegate: delegate, + parentViewModel: self, viewLayoutFramePublisher: viewLayoutFramePublisher ) } - + + // author status.author.publisher(for: \.profileImageURL) .map { _ in status.author.avatarImageURL() } .assign(to: &$avatarURL) @@ -949,17 +1007,15 @@ extension StatusView.ViewModel { status.author.publisher(for: \.username) .assign(to: &$authorUsernme) - status.publisher(for: \.text) - .map { _ in - let content = TwitterContent(content: status.displayText) - let metaContent = TwitterMetaContent.convert( - content: content, - urlMaximumLength: 20, - twitterTextProvider: SwiftTwitterTextProvider() - ) - return metaContent - } - .assign(to: &$content) + // content + let content = TwitterContent(content: status.displayText) + let metaContent = TwitterMetaContent.convert( + content: content, + urlMaximumLength: 20, + twitterTextProvider: SwiftTwitterTextProvider(), + useParagraphMark: true + ) + self.content = metaContent } } @@ -968,6 +1024,7 @@ extension StatusView.ViewModel { status: MastodonStatus, kind: Kind, delegate: StatusViewDelegate?, + parentViewModel: StatusView.ViewModel?, viewLayoutFramePublisher: Published.Publisher? ) { self.init( @@ -975,12 +1032,14 @@ extension StatusView.ViewModel { delegate: delegate, viewLayoutFramePublisher: viewLayoutFramePublisher ) + self.parentViewModel = parentViewModel if let repost = status.repost { repostViewModel = .init( status: repost, kind: .repost, delegate: delegate, + parentViewModel: self, viewLayoutFramePublisher: viewLayoutFramePublisher ) } @@ -996,7 +1055,7 @@ extension StatusView.ViewModel { do { let content = MastodonContent(content: status.content, emojis: status.emojis.asDictionary) - let metaContent = try MastodonMetaContent.convert(document: content) + let metaContent = try MastodonMetaContent.convert(document: content, useParagraphMark: true) self.content = metaContent // viewModel.sharePlaintextContent = metaContent.original } catch { diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index 8409abe9..8692f6a0 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -20,67 +20,62 @@ import NIOPosix public struct StatusView: View { - var avatarViewDimension: CGFloat { - return 44 - } + static var hangingAvatarButtonDimension: CGFloat { 44.0 } + static var hangingAvatarButtonTrailingSapcing: CGFloat { 10.0 } @ObservedObject public private(set) var viewModel: ViewModel - @State private var authorAvatarDimension = CGFloat.zero - public init(viewModel: StatusView.ViewModel) { self.viewModel = viewModel } public var body: some View { - VStack(spacing: .zero) { + VStack { if let repostViewModel = viewModel.repostViewModel { // header - statusHeaderView + if let statusHeaderViewModel = repostViewModel.statusHeaderViewModel { + StatusHeaderView(viewModel: statusHeaderViewModel) + } // post StatusView(viewModel: repostViewModel) } else { - // authorView - authorView - // content - contentView - // quote - if let quoteViewModel = viewModel.quoteViewModel { - StatusView(viewModel: quoteViewModel) - .frame(width: viewModel.viewLayoutFrame.readableContentLayoutFrame.width) - } + HStack(alignment: .top, spacing: .zero) { + if viewModel.hasHangingAvatar { + avatarButton + .padding(.trailing, StatusView.hangingAvatarButtonTrailingSapcing) + } + VStack { + // authorView + authorView + .padding(.horizontal, viewModel.margin) + // content + contentView + .padding(.horizontal, viewModel.margin) + // quote + if let quoteViewModel = viewModel.quoteViewModel { + StatusView(viewModel: quoteViewModel) + .background { + Color(uiColor: .label.withAlphaComponent(0.04)) + } + .cornerRadius(12) + } + } // end VStack + } // end HStack + .padding(.top, viewModel.margin) + .padding(.bottom, viewModel.hasToolbar ? .zero : viewModel.margin) } } // end VStack - .frame(width: viewModel.viewLayoutFrame.readableContentLayoutFrame.width) } } extension StatusView { - public var statusHeaderView: some View { - Button { - - } label: { - HStack { - Text("Header") - Spacer() - } - } - } - public var authorView: some View { HStack(alignment: .center) { - // avatar - Button { - - } label: { - KFImage(viewModel.avatarURL) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: authorAvatarDimension, height: authorAvatarDimension) - .clipShape(Circle()) + if !viewModel.hasHangingAvatar { + // avatar + avatarButton } - .buttonStyle(.borderless) // info VStack(spacing: .zero) { // name @@ -88,11 +83,13 @@ extension StatusView { metaContent: viewModel.authorName, textStyle: .statusAuthorName ) + .border(.red, width: 1) // username LabelRepresentable( metaContent: PlaintextMetaContent(string: "@" + viewModel.authorUsernme), textStyle: .statusAuthorUsername ) + .border(.red, width: 1) } .background(GeometryReader { proxy in Color.clear.preference( @@ -101,29 +98,47 @@ extension StatusView { ) }) .onPreferenceChange(ViewHeightKey.self) { height in - self.authorAvatarDimension = height + // self.viewModel.authorAvatarDimension = height } - Spacer() } } + public var avatarButton: some View { + Button { + + } label: { + KFImage(viewModel.avatarURL) + .placeholder { progress in + Color(uiColor: .placeholderText) + } + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: StatusView.hangingAvatarButtonDimension, height: StatusView.hangingAvatarButtonDimension) + .clipShape(Circle()) + } + .buttonStyle(.borderless) + + } + public var contentView: some View { - HStack { + HStack(spacing: .zero) { + let width: CGFloat = { + switch viewModel.kind { + case .conversationRoot: + return viewModel.viewLayoutFrame.readableContentLayoutFrame.width - 2 * viewModel.margin + default: + return viewModel.viewLayoutFrame.readableContentLayoutFrame.width - 2 * viewModel.margin - StatusView.hangingAvatarButtonDimension - StatusView.hangingAvatarButtonTrailingSapcing + } + }() TextViewRepresentable( metaContent: viewModel.content, textStyle: .statusContent, - width: viewModel.viewLayoutFrame.readableContentLayoutFrame.width + width: width ) - .frame(width: viewModel.viewLayoutFrame.readableContentLayoutFrame.width) + .frame(width: width) Spacer() } - } -} - -struct ViewHeightKey: PreferenceKey { - static var defaultValue: CGFloat { 0 } - static func reduce(value: inout Value, nextValue: () -> Value) { - value = value + nextValue() + .border(.red, width: 1) } } diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/LabelRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/LabelRepresentable.swift index 8769008a..29d50d8e 100644 --- a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/LabelRepresentable.swift +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/LabelRepresentable.swift @@ -47,8 +47,9 @@ public struct LabelRepresentable: UIViewRepresentable { ] let paragraphStyle: NSMutableParagraphStyle = { let style = NSMutableParagraphStyle() - style.lineSpacing = 5 - style.paragraphSpacing = 8 + let fontMargin = textStyle.font.lineHeight - textStyle.font.pointSize + style.lineSpacing = 3 - fontMargin + style.paragraphSpacing = 8 - fontMargin return style }() diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift index db0eb476..5686f273 100644 --- a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift @@ -12,10 +12,11 @@ import MetaTextKit public struct TextViewRepresentable: UIViewRepresentable { - let textView: UITextView = { - let textView = UITextView() + let textView: WrappedTextView = { + let textView = WrappedTextView() textView.backgroundColor = .clear textView.isScrollEnabled = false + textView.isEditable = false textView.textContainerInset = .zero textView.textContainer.lineFragmentPadding = 0 textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) @@ -52,8 +53,9 @@ public struct TextViewRepresentable: UIViewRepresentable { ] let paragraphStyle: NSMutableParagraphStyle = { let style = NSMutableParagraphStyle() - style.lineSpacing = 5 - style.paragraphSpacing = 8 + let fontMargin = textStyle.font.lineHeight - textStyle.font.pointSize + style.lineSpacing = 3 - fontMargin + style.paragraphSpacing = 8 - fontMargin return style }() @@ -68,12 +70,17 @@ public struct TextViewRepresentable: UIViewRepresentable { textView.frame.size.width = width textView.textStorage.setAttributedString(attributedString) textView.invalidateIntrinsicContentSize() + textView.setNeedsLayout() + textView.layoutIfNeeded() return textView } public func updateUIView(_ view: UITextView, context: Context) { - + textView.frame.size.width = width + textView.invalidateIntrinsicContentSize() + textView.setNeedsLayout() + textView.layoutIfNeeded() } public func makeCoordinator() -> Coordinator { @@ -89,3 +96,29 @@ public struct TextViewRepresentable: UIViewRepresentable { } } } + +class WrappedTextView: UITextView { + + private var lastWidth: CGFloat = 0 + + override func layoutSubviews() { + super.layoutSubviews() + + if bounds.width != lastWidth { + lastWidth = bounds.width + invalidateIntrinsicContentSize() + } + } + + override var intrinsicContentSize: CGSize { + let size = sizeThatFits(CGSize( + width: lastWidth, + height: UIView.layoutFittingExpandedSize.height + )) + return CGSize( + width: lastWidth, + height: size.height.rounded(.up) + ) + } + +} diff --git a/TwidereSDK/Sources/TwidereUI/Utility/ViewHeightKey.swift b/TwidereSDK/Sources/TwidereUI/Utility/ViewHeightKey.swift new file mode 100644 index 00000000..417f059e --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Utility/ViewHeightKey.swift @@ -0,0 +1,16 @@ +// +// ViewHeightKey.swift +// +// +// Created by MainasuK on 2023/2/9. +// + +import UIKit +import SwiftUI + +struct ViewHeightKey: PreferenceKey { + static var defaultValue: CGFloat { 0 } + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = value + nextValue() + } +} diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index cf9bc4c7..6d9710c8 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -836,6 +836,7 @@ DBBBBE8E2744FB42007ACB4B /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = ""; }; DBBF70F626D7B62F00971656 /* AccountListTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountListTableViewCell+ViewModel.swift"; sourceTree = ""; }; DBC17A5B26E0A4C1005C0D79 /* TimelineMiddleLoaderTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimelineMiddleLoaderTableViewCell+ViewModel.swift"; sourceTree = ""; }; + DBC36A982993A4F300C67212 /* MetaTextKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MetaTextKit; path = ../MetaTextKit; sourceTree = ""; }; DBC747E0259DBD5400787EEF /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = ""; }; DBC8E04A2576337F00401E20 /* DisposeBagCollectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisposeBagCollectable.swift; sourceTree = ""; }; DBC8E04F257653E100401E20 /* SavePhotoActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavePhotoActivity.swift; sourceTree = ""; }; @@ -2130,6 +2131,7 @@ DBDA8E1524FCF8A3006750DC = { isa = PBXGroup; children = ( + DBC36A982993A4F300C67212 /* MetaTextKit */, DB33E5B42719637400EC2225 /* CoverFlowStackCollectionViewLayout */, DB51DC372716AEF000A0D8FB /* TabBarPager */, DB43239D251491EB004FAAEC /* TwidereSDK */, diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index 90627cc0..aa3637f8 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -118,15 +118,6 @@ "version": "7.2.2" } }, - { - "package": "MetaTextKit", - "repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git", - "state": { - "branch": null, - "revision": "f788c7ebb359ee6f7bbd99e0c2c3904fda7a84ea", - "version": "4.1.2" - } - }, { "package": "Pageboy", "repositoryURL": "https://github.com/uias/Pageboy", diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Meta.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Meta.swift index 91d23615..b7da9337 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Meta.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Meta.swift @@ -47,6 +47,9 @@ extension DataSourceFacade { case .hashtag(_, let hashtag, _): let hashtagViewModel = HashtagTimelineViewModel(context: provider.context, authContext: provider.authContext, hashtag: hashtag) await provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagViewModel), from: provider, transition: .show) + case .cashtag(let text, _, _): + let hashtagViewModel = HashtagTimelineViewModel(context: provider.context, authContext: provider.authContext, hashtag: text) + await provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagViewModel), from: provider, transition: .show) case .mention(_, let mention, let userInfo): await DataSourceFacade.coordinateToProfileScene( provider: provider, @@ -72,6 +75,9 @@ extension DataSourceFacade { case .hashtag(_, let hashtag, _): let hashtagViewModel = HashtagTimelineViewModel(context: provider.context, authContext: provider.authContext, hashtag: hashtag) await provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagViewModel), from: provider, transition: .show) + case .cashtag(let text, _, _): + let hashtagViewModel = HashtagTimelineViewModel(context: provider.context, authContext: provider.authContext, hashtag: text) + await provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagViewModel), from: provider, transition: .show) case .mention(_, let mention, let userInfo): await coordinateToProfileScene( provider: provider, diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift index 46f7b7e7..3f9d6f95 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift @@ -187,8 +187,13 @@ extension TimelineViewController { override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) - - _viewModel.viewLayoutFrame.update(view: view) + + coordinator.animate {[weak self] _ in + guard let self = self else { return } + self._viewModel.viewLayoutFrame.update(view: self.view) + } completion: { _ in + // do nothing + } } } From b2ab33986ed4744757f67c16308781bdaa925d15 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 23 Feb 2023 19:28:17 +0800 Subject: [PATCH 031/128] feat: restore the MediaView and MediaGridContainerView --- TwidereSDK/Package.swift | 2 + .../MediaGridContainerView+ViewModel.swift | 62 +- .../Container/MediaGridContainerView.swift | 897 ++++++++++++------ .../Content/MediaView+Configuration.swift | 177 ---- .../Content/MediaView+ViewModel.swift | 234 +++++ .../Sources/TwidereUI/Content/MediaView.swift | 660 +++++++------ .../TwidereUI/Content/StatusHeaderView.swift | 7 +- .../Content/StatusView+ViewModel.swift | 26 +- .../TwidereUI/Content/StatusView.swift | 68 +- .../Content/TimestampLabelView.swift | 49 + .../LabelRepresentable.swift | 12 +- .../TextViewRepresentable.swift | 5 +- .../xcshareddata/swiftpm/Package.resolved | 9 + .../CoverFlowStack/CoverFlowStackItem.swift | 2 +- .../CoverFlowStackSection.swift | 2 +- .../Provider/DataSourceFacade+Media.swift | 105 +- .../MediaPreviewViewController.swift | 46 +- ...owStackMediaCollectionCell+ViewModel.swift | 18 +- .../CoverFlowStackMediaCollectionCell.swift | 34 +- ...MediaGalleryCollectionCell+ViewModel.swift | 158 +-- .../StatusMediaGalleryCollectionCell.swift | 154 +-- .../Grid/GridTimelineViewController.swift | 96 +- .../MediaPreviewTransitionItem.swift | 34 +- .../MediaPreviewableViewController.swift | 10 +- 24 files changed, 1679 insertions(+), 1188 deletions(-) delete mode 100644 TwidereSDK/Sources/TwidereUI/Content/MediaView+Configuration.swift create mode 100644 TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift create mode 100644 TwidereSDK/Sources/TwidereUI/Content/TimestampLabelView.swift diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index 56d50d28..7b84d2eb 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -45,6 +45,7 @@ let package = Package( .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", from: "4.2.2"), .package(url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.4"), .package(url: "https://github.com/TwidereProject/twitter-text.git", exact: "0.0.3"), + .package(url: "https://github.com/MainasuK/DateTools", branch: "master"), .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"), ], targets: [ @@ -114,6 +115,7 @@ let package = Package( .product(name: "AlamofireNetworkActivityIndicator", package: "AlamofireNetworkActivityIndicator"), .product(name: "MetaTextKit", package: "MetaTextKit"), .product(name: "TwitterText", package: "twitter-text"), + .product(name: "DateToolsSwift", package: "DateTools"), ] ), .target( diff --git a/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView+ViewModel.swift index 09e6dfa3..5861ad09 100644 --- a/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView+ViewModel.swift @@ -19,39 +19,39 @@ extension MediaGridContainerView { extension MediaGridContainerView.ViewModel { - func resetContentWarningOverlay() { - isContentWarningOverlayDisplay = nil - } - - func bind(view: MediaGridContainerView) { - $isSensitiveToggleButtonDisplay - .sink { isDisplay in - view.sensitiveToggleButtonBlurVisualEffectView.isHidden = !isDisplay - } - .store(in: &disposeBag) - $isContentWarningOverlayDisplay - .sink { isDisplay in - assert(Thread.isMainThread) - guard let isDisplay = isDisplay else { return } - let withAnimation = self.isContentWarningOverlayDisplay != nil - view.configureOverlayDisplay(isDisplay: isDisplay, animated: withAnimation) - } - .store(in: &disposeBag) - } +// func resetContentWarningOverlay() { +// isContentWarningOverlayDisplay = nil +// } +// +// func bind(view: MediaGridContainerView) { +// $isSensitiveToggleButtonDisplay +// .sink { isDisplay in +// view.sensitiveToggleButtonBlurVisualEffectView.isHidden = !isDisplay +// } +// .store(in: &disposeBag) +// $isContentWarningOverlayDisplay +// .sink { isDisplay in +// assert(Thread.isMainThread) +// guard let isDisplay = isDisplay else { return } +// let withAnimation = self.isContentWarningOverlayDisplay != nil +// view.configureOverlayDisplay(isDisplay: isDisplay, animated: withAnimation) +// } +// .store(in: &disposeBag) +// } } extension MediaGridContainerView { - func configureOverlayDisplay(isDisplay: Bool, animated: Bool) { - if animated { - UIView.animate(withDuration: 0.33, delay: 0, options: .curveEaseInOut) { - self.contentWarningOverlayView.blurVisualEffectView.alpha = isDisplay ? 1 : 0 - } - } else { - contentWarningOverlayView.blurVisualEffectView.alpha = isDisplay ? 1 : 0 - } - - contentWarningOverlayView.isUserInteractionEnabled = isDisplay - contentWarningOverlayView.tapGestureRecognizer.isEnabled = isDisplay - } +// func configureOverlayDisplay(isDisplay: Bool, animated: Bool) { +// if animated { +// UIView.animate(withDuration: 0.33, delay: 0, options: .curveEaseInOut) { +// self.contentWarningOverlayView.blurVisualEffectView.alpha = isDisplay ? 1 : 0 +// } +// } else { +// contentWarningOverlayView.blurVisualEffectView.alpha = isDisplay ? 1 : 0 +// } +// +// contentWarningOverlayView.isUserInteractionEnabled = isDisplay +// contentWarningOverlayView.tapGestureRecognizer.isEnabled = isDisplay +// } } diff --git a/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift b/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift index 4ab3117a..a9e396a9 100644 --- a/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift +++ b/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift @@ -8,6 +8,7 @@ import os.log import UIKit +import SwiftUI import func AVFoundation.AVMakeRect public protocol MediaGridContainerViewDelegate: AnyObject { @@ -15,342 +16,620 @@ public protocol MediaGridContainerViewDelegate: AnyObject { func mediaGridContainerView(_ container: MediaGridContainerView, toggleContentWarningOverlayViewDisplay contentWarningOverlayView: ContentWarningOverlayView) } -public final class MediaGridContainerView: UIView { +public struct MediaGridContainerView: View { - public static let maxCount = 9 + static var spacing: CGFloat { 8 } + static var cornerRadius: CGFloat { 8 } - let logger = Logger(subsystem: "MediaGridContainerView", category: "UI") + public let viewModels: [MediaView.ViewModel] - public weak var delegate: MediaGridContainerViewDelegate? - public private(set) lazy var viewModel: ViewModel = { - let viewModel = ViewModel() - viewModel.bind(view: self) - return viewModel - }() - - // lazy var is required here to setup gesture recognizer target-action - // Swift not doesn't emit compiler error if without `lazy` here - private(set) lazy var _mediaViews: [MediaView] = { - var mediaViews: [MediaView] = [] - for i in 0.. 0 { + Color.black.opacity(0.3) + .overlay { + Text("+\(remains)") + .font(.system(size: 27, weight: .semibold, design: .default)) + .foregroundColor(.white) + } + } // end if + } + } + } + default: + EmptyView() + } + } // end Group +// .frame(width: idealWidth) + .border(Color.blue, width: 1) + } // end body } extension MediaGridContainerView { - private func _init() { - sensitiveToggleButton.addTarget(self, action: #selector(MediaGridContainerView.sensitiveToggleButtonDidPressed(_:)), for: .touchUpInside) - contentWarningOverlayView.delegate = self - } -} - -extension MediaGridContainerView { - @objc private func mediaViewTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { - guard let index = _mediaViews.firstIndex(where: { $0.container === sender.view }) else { return } - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(index)") - let mediaView = _mediaViews[index] - delegate?.mediaGridContainerView(self, didTapMediaView: mediaView, at: index) - } - - @objc private func sensitiveToggleButtonDidPressed(_ sender: UIButton) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - delegate?.mediaGridContainerView(self, toggleContentWarningOverlayViewDisplay: contentWarningOverlayView) - } -} - -extension MediaGridContainerView { - - public func dequeueMediaView(adaptiveLayout layout: AdaptiveLayout) -> MediaView { - prepareForReuse() - - let mediaView = _mediaViews[0] - layout.layout(in: self, mediaView: mediaView) - - layoutSensitiveToggleButton() - bringSubviewToFront(sensitiveToggleButtonBlurVisualEffectView) - - layoutContentOverlayView(on: mediaView) - bringSubviewToFront(contentWarningOverlayView) - - return mediaView - } - public func dequeueMediaView(gridLayout layout: GridLayout) -> [MediaView] { - prepareForReuse() - - let mediaViews = Array(_mediaViews[0.. some View { + Rectangle() + .fill(Color(uiColor: .placeholderText)) + .frame(width: width, height: height) + .overlay( + MediaView(viewModel: viewModels[index]) + .aspectRatio(contentMode: .fill) + ) + .cornerRadius(MediaGridContainerView.cornerRadius) + .clipped() + .overlay( + RoundedRectangle(cornerRadius: MediaGridContainerView.cornerRadius) + .stroke(Color(uiColor: .placeholderText).opacity(0.5), lineWidth: 1) + ) + .contentShape(Rectangle()) + .onTapGesture { +// let actionContext = ActionContext(index: index, viewModels: viewModels) +// action(actionContext) + } } - public func prepareForReuse() { - _mediaViews.forEach { view in - view.removeFromSuperview() - view.removeConstraints(view.constraints) - view.prepareForReuse() - } - - subviews.forEach { view in - view.removeFromSuperview() + private func height(for rows: Int) -> CGFloat { + guard let idealWidth = self.idealWidth else { + // fix grid height + let margins = CGFloat(rows - 1) * MediaGridContainerView.spacing + let height = (idealHeight - margins) / CGFloat(rows) + return height } - removeConstraints(constraints) - } - -} - -extension MediaGridContainerView { - private func layoutSensitiveToggleButton() { - sensitiveToggleButtonBlurVisualEffectView.translatesAutoresizingMaskIntoConstraints = false - addSubview(sensitiveToggleButtonBlurVisualEffectView) - NSLayoutConstraint.activate([ - sensitiveToggleButtonBlurVisualEffectView.topAnchor.constraint(equalTo: topAnchor, constant: 8), - sensitiveToggleButtonBlurVisualEffectView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8), - ]) - - sensitiveToggleButtonVibrancyVisualEffectView.translatesAutoresizingMaskIntoConstraints = false - sensitiveToggleButtonBlurVisualEffectView.contentView.addSubview(sensitiveToggleButtonVibrancyVisualEffectView) - NSLayoutConstraint.activate([ - sensitiveToggleButtonVibrancyVisualEffectView.topAnchor.constraint(equalTo: sensitiveToggleButtonBlurVisualEffectView.contentView.topAnchor), - sensitiveToggleButtonVibrancyVisualEffectView.leadingAnchor.constraint(equalTo: sensitiveToggleButtonBlurVisualEffectView.contentView.leadingAnchor), - sensitiveToggleButtonVibrancyVisualEffectView.trailingAnchor.constraint(equalTo: sensitiveToggleButtonBlurVisualEffectView.contentView.trailingAnchor), - sensitiveToggleButtonVibrancyVisualEffectView.bottomAnchor.constraint(equalTo: sensitiveToggleButtonBlurVisualEffectView.contentView.bottomAnchor), - ]) - - sensitiveToggleButton.translatesAutoresizingMaskIntoConstraints = false - sensitiveToggleButtonVibrancyVisualEffectView.contentView.addSubview(sensitiveToggleButton) - NSLayoutConstraint.activate([ - sensitiveToggleButton.topAnchor.constraint(equalTo: sensitiveToggleButtonVibrancyVisualEffectView.contentView.topAnchor, constant: 4), - sensitiveToggleButton.leadingAnchor.constraint(equalTo: sensitiveToggleButtonVibrancyVisualEffectView.contentView.leadingAnchor, constant: 4), - sensitiveToggleButtonVibrancyVisualEffectView.contentView.trailingAnchor.constraint(equalTo: sensitiveToggleButton.trailingAnchor, constant: 4), - sensitiveToggleButtonVibrancyVisualEffectView.contentView.bottomAnchor.constraint(equalTo: sensitiveToggleButton.bottomAnchor, constant: 4), - ]) - } - - private func layoutContentOverlayView(on view: UIView) { - contentWarningOverlayView.translatesAutoresizingMaskIntoConstraints = false - addSubview(contentWarningOverlayView) // should add to container - NSLayoutConstraint.activate([ - contentWarningOverlayView.topAnchor.constraint(equalTo: view.topAnchor), - contentWarningOverlayView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - contentWarningOverlayView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - contentWarningOverlayView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) + // make tiem square + let cols = rows < 3 ? 2 : 3 + let margins = CGFloat(cols - 1) * MediaGridContainerView.spacing + let width = (idealWidth - margins) / CGFloat(cols) + return width } } -extension MediaGridContainerView { +public struct MediaViewFrameModifer: ViewModifier { - public var mediaViews: [MediaView] { - _mediaViews.filter { $0.superview != nil } - } + let asepctRatio: CGFloat? + let idealWidth: CGFloat? + let idealHeight: CGFloat - public func setAlpha(_ alpha: CGFloat) { - _mediaViews.forEach { $0.alpha = alpha } + public init( + asepctRatio: CGFloat?, + idealWidth: CGFloat?, + idealHeight: CGFloat + ) { + self.asepctRatio = asepctRatio + self.idealWidth = idealWidth + self.idealHeight = idealHeight } - public func setAlpha(_ alpha: CGFloat, index: Int) { - if index < _mediaViews.count { - _mediaViews[index].alpha = alpha + public func body(content: Content) -> some View { + if let idealWidth = idealWidth { + content + .frame(width: idealWidth, height: idealWidth / (asepctRatio ?? 1.0)) + } else { + content + .frame(maxHeight: idealHeight) } } - } -extension MediaGridContainerView { - public struct AdaptiveLayout { - let aspectRatio: CGSize - let maxSize: CGSize - - func layout(in view: UIView, mediaView: MediaView) { - let imageViewSize = AVMakeRect(aspectRatio: aspectRatio, insideRect: CGRect(origin: .zero, size: maxSize)).size - mediaView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(mediaView) - NSLayoutConstraint.activate([ - mediaView.topAnchor.constraint(equalTo: view.topAnchor), - mediaView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - mediaView.trailingAnchor.constraint(equalTo: view.trailingAnchor).priority(.defaultLow), - mediaView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - mediaView.widthAnchor.constraint(equalToConstant: imageViewSize.width).priority(.required - 1), - mediaView.heightAnchor.constraint(equalToConstant: imageViewSize.height).priority(.required - 1), - ]) - } - } +struct MediaGridContainerView_Previews: PreviewProvider { - public struct GridLayout { - static let spacing: CGFloat = 8 - - let count: Int - let maxSize: CGSize - - init(count: Int, maxSize: CGSize) { - self.count = min(count, 9) - self.maxSize = maxSize - - } - - private func createStackView(axis: NSLayoutConstraint.Axis) -> UIStackView { - let stackView = UIStackView() - stackView.axis = axis - stackView.semanticContentAttribute = .forceLeftToRight - stackView.spacing = GridLayout.spacing - stackView.distribution = .fillEqually - return stackView - } - - public func layout(in view: UIView, mediaViews: [MediaView]) { - let containerVerticalStackView = createStackView(axis: .vertical) - containerVerticalStackView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(containerVerticalStackView) - NSLayoutConstraint.activate([ - containerVerticalStackView.topAnchor.constraint(equalTo: view.topAnchor), - containerVerticalStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - containerVerticalStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - containerVerticalStackView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - - let count = mediaViews.count - switch count { - case 1: - assertionFailure("should use Adaptive Layout") - containerVerticalStackView.addArrangedSubview(mediaViews[0]) - case 2: - let horizontalStackView = createStackView(axis: .horizontal) - containerVerticalStackView.addArrangedSubview(horizontalStackView) - horizontalStackView.addArrangedSubview(mediaViews[0]) - horizontalStackView.addArrangedSubview(mediaViews[1]) - case 3: - let horizontalStackView = createStackView(axis: .horizontal) - containerVerticalStackView.addArrangedSubview(horizontalStackView) - horizontalStackView.addArrangedSubview(mediaViews[0]) - - let verticalStackView = createStackView(axis: .vertical) - horizontalStackView.addArrangedSubview(verticalStackView) - verticalStackView.addArrangedSubview(mediaViews[1]) - verticalStackView.addArrangedSubview(mediaViews[2]) - case 4: - let topHorizontalStackView = createStackView(axis: .horizontal) - containerVerticalStackView.addArrangedSubview(topHorizontalStackView) - topHorizontalStackView.addArrangedSubview(mediaViews[0]) - topHorizontalStackView.addArrangedSubview(mediaViews[1]) - - let bottomHorizontalStackView = createStackView(axis: .horizontal) - containerVerticalStackView.addArrangedSubview(bottomHorizontalStackView) - bottomHorizontalStackView.addArrangedSubview(mediaViews[2]) - bottomHorizontalStackView.addArrangedSubview(mediaViews[3]) - case 5...9: - let topHorizontalStackView = createStackView(axis: .horizontal) - containerVerticalStackView.addArrangedSubview(topHorizontalStackView) - topHorizontalStackView.addArrangedSubview(mediaViews[0]) - topHorizontalStackView.addArrangedSubview(mediaViews[1]) - topHorizontalStackView.addArrangedSubview(mediaViews[2]) - - func mediaViewOrPlaceholderView(at index: Int) -> UIView { - return index < mediaViews.count ? mediaViews[index] : UIView() - } - let middleHorizontalStackView = createStackView(axis: .horizontal) - containerVerticalStackView.addArrangedSubview(middleHorizontalStackView) - middleHorizontalStackView.addArrangedSubview(mediaViews[3]) - middleHorizontalStackView.addArrangedSubview(mediaViews[4]) - middleHorizontalStackView.addArrangedSubview(mediaViewOrPlaceholderView(at: 5)) - - if count > 6 { - let bottomHorizontalStackView = createStackView(axis: .horizontal) - containerVerticalStackView.addArrangedSubview(bottomHorizontalStackView) - bottomHorizontalStackView.addArrangedSubview(mediaViewOrPlaceholderView(at: 6)) - bottomHorizontalStackView.addArrangedSubview(mediaViewOrPlaceholderView(at: 7)) - bottomHorizontalStackView.addArrangedSubview(mediaViewOrPlaceholderView(at: 8)) - } - default: - assertionFailure() - return + static let viewModels = { + let models = [ + MediaView.ViewModel( + mediaKind: .photo, + aspectRatio: CGSize(width: 2048, height: 1186), + altText: nil, + previewURL: URL(string: "https://www.nasa.gov/sites/default/files/thumbnails/image/web_first_images_release.png"), + assetURL: URL(string: "https://www.nasa.gov/sites/default/files/thumbnails/image/web_first_images_release.png"), + downloadURL: URL(string: "https://www.nasa.gov/sites/default/files/thumbnails/image/web_first_images_release.png"), + durationMS: nil + ), + MediaView.ViewModel( + mediaKind: .photo, + aspectRatio: CGSize(width: 6096, height: 5173), + altText: nil, + previewURL: URL(string: "https://www.nasa.gov/sites/default/files/thumbnails/image/chandra_ixpe_v3magentahires.jpg"), + assetURL: URL(string: "https://www.nasa.gov/sites/default/files/thumbnails/image/chandra_ixpe_v3magentahires.jpg"), + downloadURL: URL(string: "https://www.nasa.gov/sites/default/files/thumbnails/image/chandra_ixpe_v3magentahires.jpg"), + durationMS: nil + ), + MediaView.ViewModel( + mediaKind: .photo, + aspectRatio: CGSize(width: 3482, height: 1959), + altText: nil, + previewURL: URL(string: "https://www.nasa.gov/sites/default/files/thumbnails/image/pia20027_updated.jpg"), + assetURL: URL(string: "https://www.nasa.gov/sites/default/files/thumbnails/image/pia20027_updated.jpg"), + downloadURL: URL(string: "https://www.nasa.gov/sites/default/files/thumbnails/image/pia20027_updated.jpg"), + durationMS: nil + ), + MediaView.ViewModel( + mediaKind: .photo, + aspectRatio: CGSize(width: 2016, height: 2016), + altText: nil, + previewURL: URL(string: "https://www.nasa.gov/sites/default/files/images/671506main_PIA15628_full.jpg"), + assetURL: URL(string: "https://www.nasa.gov/sites/default/files/images/671506main_PIA15628_full.jpg"), + downloadURL: URL(string: "https://www.nasa.gov/sites/default/files/images/671506main_PIA15628_full.jpg"), + durationMS: nil + ), + ] + return Array(repeating: models, count: 3).flatMap { $0 } + }() + + static var previews: some View { + Group { + ForEach(0.. 6 ? containerWidth : containerWidth * 2 / 3 - NSLayoutConstraint.activate([ - view.widthAnchor.constraint(equalToConstant: containerWidth).priority(.required - 1), - view.heightAnchor.constraint(equalToConstant: containerHeight).priority(.required - 1), - ]) } } } -// MARK: - ContentWarningOverlayViewDelegate -extension MediaGridContainerView: ContentWarningOverlayViewDelegate { - public func contentWarningOverlayViewDidPressed(_ contentWarningOverlayView: ContentWarningOverlayView) { - delegate?.mediaGridContainerView(self, toggleContentWarningOverlayViewDisplay: contentWarningOverlayView) - } -} - -extension MediaGridContainerView { - public override var accessibilityElements: [Any]? { - get { - if viewModel.isContentWarningOverlayDisplay == true { - return [contentWarningOverlayView] - } else { - return [sensitiveToggleButton] + mediaViews - } - - } - set { } - } -} +//public final class MediaGridContainerView: UIView { +// +// public static let maxCount = 9 +// +// let logger = Logger(subsystem: "MediaGridContainerView", category: "UI") +// +// public weak var delegate: MediaGridContainerViewDelegate? +// public private(set) lazy var viewModel: ViewModel = { +// let viewModel = ViewModel() +// viewModel.bind(view: self) +// return viewModel +// }() +// +// // lazy var is required here to setup gesture recognizer target-action +// // Swift not doesn't emit compiler error if without `lazy` here +// private(set) lazy var _mediaViews: [MediaView] = { +// var mediaViews: [MediaView] = [] +// for i in 0.. MediaView { +// prepareForReuse() +// +// let mediaView = _mediaViews[0] +// layout.layout(in: self, mediaView: mediaView) +// +// layoutSensitiveToggleButton() +// bringSubviewToFront(sensitiveToggleButtonBlurVisualEffectView) +// +// layoutContentOverlayView(on: mediaView) +// bringSubviewToFront(contentWarningOverlayView) +// +// return mediaView +// } +// +// public func dequeueMediaView(gridLayout layout: GridLayout) -> [MediaView] { +// prepareForReuse() +// +// let mediaViews = Array(_mediaViews[0.. UIStackView { +// let stackView = UIStackView() +// stackView.axis = axis +// stackView.semanticContentAttribute = .forceLeftToRight +// stackView.spacing = GridLayout.spacing +// stackView.distribution = .fillEqually +// return stackView +// } +// +// public func layout(in view: UIView, mediaViews: [MediaView]) { +// let containerVerticalStackView = createStackView(axis: .vertical) +// containerVerticalStackView.translatesAutoresizingMaskIntoConstraints = false +// view.addSubview(containerVerticalStackView) +// NSLayoutConstraint.activate([ +// containerVerticalStackView.topAnchor.constraint(equalTo: view.topAnchor), +// containerVerticalStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor), +// containerVerticalStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor), +// containerVerticalStackView.bottomAnchor.constraint(equalTo: view.bottomAnchor), +// ]) +// +// let count = mediaViews.count +// switch count { +// case 1: +// assertionFailure("should use Adaptive Layout") +// containerVerticalStackView.addArrangedSubview(mediaViews[0]) +// case 2: +// let horizontalStackView = createStackView(axis: .horizontal) +// containerVerticalStackView.addArrangedSubview(horizontalStackView) +// horizontalStackView.addArrangedSubview(mediaViews[0]) +// horizontalStackView.addArrangedSubview(mediaViews[1]) +// case 3: +// let horizontalStackView = createStackView(axis: .horizontal) +// containerVerticalStackView.addArrangedSubview(horizontalStackView) +// horizontalStackView.addArrangedSubview(mediaViews[0]) +// +// let verticalStackView = createStackView(axis: .vertical) +// horizontalStackView.addArrangedSubview(verticalStackView) +// verticalStackView.addArrangedSubview(mediaViews[1]) +// verticalStackView.addArrangedSubview(mediaViews[2]) +// case 4: +// let topHorizontalStackView = createStackView(axis: .horizontal) +// containerVerticalStackView.addArrangedSubview(topHorizontalStackView) +// topHorizontalStackView.addArrangedSubview(mediaViews[0]) +// topHorizontalStackView.addArrangedSubview(mediaViews[1]) +// +// let bottomHorizontalStackView = createStackView(axis: .horizontal) +// containerVerticalStackView.addArrangedSubview(bottomHorizontalStackView) +// bottomHorizontalStackView.addArrangedSubview(mediaViews[2]) +// bottomHorizontalStackView.addArrangedSubview(mediaViews[3]) +// case 5...9: +// let topHorizontalStackView = createStackView(axis: .horizontal) +// containerVerticalStackView.addArrangedSubview(topHorizontalStackView) +// topHorizontalStackView.addArrangedSubview(mediaViews[0]) +// topHorizontalStackView.addArrangedSubview(mediaViews[1]) +// topHorizontalStackView.addArrangedSubview(mediaViews[2]) +// +// func mediaViewOrPlaceholderView(at index: Int) -> UIView { +// return index < mediaViews.count ? mediaViews[index] : UIView() +// } +// let middleHorizontalStackView = createStackView(axis: .horizontal) +// containerVerticalStackView.addArrangedSubview(middleHorizontalStackView) +// middleHorizontalStackView.addArrangedSubview(mediaViews[3]) +// middleHorizontalStackView.addArrangedSubview(mediaViews[4]) +// middleHorizontalStackView.addArrangedSubview(mediaViewOrPlaceholderView(at: 5)) +// +// if count > 6 { +// let bottomHorizontalStackView = createStackView(axis: .horizontal) +// containerVerticalStackView.addArrangedSubview(bottomHorizontalStackView) +// bottomHorizontalStackView.addArrangedSubview(mediaViewOrPlaceholderView(at: 6)) +// bottomHorizontalStackView.addArrangedSubview(mediaViewOrPlaceholderView(at: 7)) +// bottomHorizontalStackView.addArrangedSubview(mediaViewOrPlaceholderView(at: 8)) +// } +// default: +// assertionFailure() +// return +// } +// +// let containerWidth = maxSize.width +// let containerHeight = count > 6 ? containerWidth : containerWidth * 2 / 3 +// NSLayoutConstraint.activate([ +// view.widthAnchor.constraint(equalToConstant: containerWidth).priority(.required - 1), +// view.heightAnchor.constraint(equalToConstant: containerHeight).priority(.required - 1), +// ]) +// } +// } +//} +// +//// MARK: - ContentWarningOverlayViewDelegate +//extension MediaGridContainerView: ContentWarningOverlayViewDelegate { +// public func contentWarningOverlayViewDidPressed(_ contentWarningOverlayView: ContentWarningOverlayView) { +// delegate?.mediaGridContainerView(self, toggleContentWarningOverlayViewDisplay: contentWarningOverlayView) +// } +//} +// +//extension MediaGridContainerView { +// public override var accessibilityElements: [Any]? { +// get { +// if viewModel.isContentWarningOverlayDisplay == true { +// return [contentWarningOverlayView] +// } else { +// return [sensitiveToggleButton] + mediaViews +// } +// +// } +// set { } +// } +//} diff --git a/TwidereSDK/Sources/TwidereUI/Content/MediaView+Configuration.swift b/TwidereSDK/Sources/TwidereUI/Content/MediaView+Configuration.swift deleted file mode 100644 index 758a749c..00000000 --- a/TwidereSDK/Sources/TwidereUI/Content/MediaView+Configuration.swift +++ /dev/null @@ -1,177 +0,0 @@ -// -// MediaView+Configuration.swift -// TwidereX -// -// Created by Cirno MainasuK on 2021-10-14. -// Copyright © 2021 Twidere. All rights reserved. -// - -import UIKit -import Combine -import CoreData -import CoreDataStack -import Photos - -extension MediaView { - public enum Configuration: Hashable { - case image(info: ImageInfo) - case gif(info: VideoInfo) - case video(info: VideoInfo) - - public var aspectRadio: CGSize { - switch self { - case .image(let info): return info.aspectRadio - case .gif(let info): return info.aspectRadio - case .video(let info): return info.aspectRadio - } - } - - public var assetURL: String? { - switch self { - case .image(let info): - return info.assetURL - case .gif(let info): - return info.assetURL - case .video(let info): - return info.assetURL - } - } - - public var downloadURL: String? { - switch self { - case .image(let info): - return info.downloadURL ?? info.assetURL - case .gif(let info): - return info.assetURL - case .video(let info): - return info.assetURL - } - } - - public var resourceType: PHAssetResourceType { - switch self { - case .image: - return .photo - case .gif: - return .video - case .video: - return .video - } - } - - public struct ImageInfo: Hashable { - public let aspectRadio: CGSize - public let assetURL: String? - public let downloadURL: String? - - public init( - aspectRadio: CGSize, - assetURL: String?, - downloadURL: String? - ) { - self.aspectRadio = aspectRadio - self.assetURL = assetURL - self.downloadURL = downloadURL - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(aspectRadio.width) - hasher.combine(aspectRadio.height) - assetURL.flatMap { hasher.combine($0) } - } - } - - public struct VideoInfo: Hashable { - public let aspectRadio: CGSize - public let assetURL: String? - public let previewURL: String? - public let durationMS: Int? - - public init( - aspectRadio: CGSize, - assetURL: String?, - previewURL: String?, - durationMS: Int? - ) { - self.aspectRadio = aspectRadio - self.assetURL = assetURL - self.previewURL = previewURL - self.durationMS = durationMS - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(aspectRadio.width) - hasher.combine(aspectRadio.height) - assetURL.flatMap { hasher.combine($0) } - previewURL.flatMap { hasher.combine($0) } - durationMS.flatMap { hasher.combine($0) } - } - } - } -} - -extension MediaView { - public static func configuration(twitterStatus status: TwitterStatus) -> [MediaView.Configuration] { - func videoInfo(from attachment: TwitterAttachment) -> MediaView.Configuration.VideoInfo { - MediaView.Configuration.VideoInfo( - aspectRadio: attachment.size, - assetURL: attachment.assetURL, - previewURL: attachment.previewURL, - durationMS: attachment.durationMS - ) - } - - let status = status.repost ?? status - return status.attachments.map { attachment -> MediaView.Configuration in - switch attachment.kind { - case .photo: - let info = MediaView.Configuration.ImageInfo( - aspectRadio: attachment.size, - assetURL: attachment.assetURL, - downloadURL: attachment.downloadURL - ) - return .image(info: info) - case .video: - let info = videoInfo(from: attachment) - return .video(info: info) - case .animatedGIF: - let info = videoInfo(from: attachment) - return .gif(info: info) - } - } - } - - public static func configuration(mastodonStatus status: MastodonStatus) -> [MediaView.Configuration] { - func videoInfo(from attachment: MastodonAttachment) -> MediaView.Configuration.VideoInfo { - MediaView.Configuration.VideoInfo( - aspectRadio: attachment.size, - assetURL: attachment.assetURL, - previewURL: attachment.previewURL, - durationMS: attachment.durationMS - ) - } - - let status = status.repost ?? status - return status.attachments.map { attachment -> MediaView.Configuration in - switch attachment.kind { - case .image: - let info = MediaView.Configuration.ImageInfo( - aspectRadio: attachment.size, - assetURL: attachment.assetURL, - downloadURL: attachment.downloadURL - ) - return .image(info: info) - case .video: - let info = videoInfo(from: attachment) - return .video(info: info) - case .gifv: - let info = videoInfo(from: attachment) - return .gif(info: info) - case .audio: - // TODO: - let info = videoInfo(from: attachment) - return .video(info: info) - } - } - } -} diff --git a/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift new file mode 100644 index 00000000..2751d45b --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift @@ -0,0 +1,234 @@ +// +// MediaView+ViewModel.swift +// TwidereX +// +// Created by Cirno MainasuK on 2021-10-14. +// Copyright © 2021 Twidere. All rights reserved. +// + +import UIKit +import SwiftUI +import Combine +import CoreData +import CoreDataStack +import Photos + +extension MediaView { + public class ViewModel: ObservableObject, Hashable { + + // input + public let mediaKind: MediaKind + public let aspectRatio: CGSize + public let altText: String? + + public let previewURL: URL? + public let assetURL: URL? + public let downloadURL: URL? + + // video duration in MS + public let durationMS: Int? + + public init( + mediaKind: MediaKind, + aspectRatio: CGSize, + altText: String?, + previewURL: URL?, + assetURL: URL?, + downloadURL: URL?, + durationMS: Int? + ) { + self.mediaKind = mediaKind + self.aspectRatio = aspectRatio + self.altText = altText + self.previewURL = previewURL + self.assetURL = assetURL + self.downloadURL = downloadURL + self.durationMS = durationMS + } + + public static func == (lhs: MediaView.ViewModel, rhs: MediaView.ViewModel) -> Bool { + return lhs.mediaKind == rhs.mediaKind + && lhs.aspectRatio == rhs.aspectRatio + && lhs.altText == rhs.altText + && lhs.previewURL == rhs.previewURL + && lhs.assetURL == rhs.assetURL + && lhs.downloadURL == rhs.downloadURL + && lhs.durationMS == rhs.durationMS + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(mediaKind) + hasher.combine(aspectRatio.width) + hasher.combine(aspectRatio.height) + hasher.combine(altText) + hasher.combine(previewURL) + hasher.combine(assetURL) + hasher.combine(downloadURL) + hasher.combine(durationMS) + } + + } +} + +extension MediaView.ViewModel { + public enum MediaKind { + case video + case photo + case animatedGIF + } +} + + +//extension MediaView { +// public enum Configuration: Hashable { +// case image(info: ImageInfo) +// case gif(info: VideoInfo) +// case video(info: VideoInfo) +// +// public var aspectRadio: CGSize { +// switch self { +// case .image(let info): return info.aspectRadio +// case .gif(let info): return info.aspectRadio +// case .video(let info): return info.aspectRadio +// } +// } +// +// public var assetURL: String? { +// switch self { +// case .image(let info): +// return info.assetURL +// case .gif(let info): +// return info.assetURL +// case .video(let info): +// return info.assetURL +// } +// } +// +// public var downloadURL: String? { +// switch self { +// case .image(let info): +// return info.downloadURL ?? info.assetURL +// case .gif(let info): +// return info.assetURL +// case .video(let info): +// return info.assetURL +// } +// } +// +// public var resourceType: PHAssetResourceType { +// switch self { +// case .image: +// return .photo +// case .gif: +// return .video +// case .video: +// return .video +// } +// } +// +// public struct ImageInfo: Hashable { +// public let aspectRadio: CGSize +// public let assetURL: String? +// public let downloadURL: String? +// +// public init( +// aspectRadio: CGSize, +// assetURL: String?, +// downloadURL: String? +// ) { +// self.aspectRadio = aspectRadio +// self.assetURL = assetURL +// self.downloadURL = downloadURL +// } +// +// public func hash(into hasher: inout Hasher) { +// hasher.combine(aspectRadio.width) +// hasher.combine(aspectRadio.height) +// assetURL.flatMap { hasher.combine($0) } +// } +// } +// +// public struct VideoInfo: Hashable { +// public let aspectRadio: CGSize +// public let assetURL: String? +// public let previewURL: String? +// public let durationMS: Int? +// +// public init( +// aspectRadio: CGSize, +// assetURL: String?, +// previewURL: String?, +// durationMS: Int? +// ) { +// self.aspectRadio = aspectRadio +// self.assetURL = assetURL +// self.previewURL = previewURL +// self.durationMS = durationMS +// } +// +// public func hash(into hasher: inout Hasher) { +// hasher.combine(aspectRadio.width) +// hasher.combine(aspectRadio.height) +// assetURL.flatMap { hasher.combine($0) } +// previewURL.flatMap { hasher.combine($0) } +// durationMS.flatMap { hasher.combine($0) } +// } +// } +// } +//} +// +extension MediaView.ViewModel { + public static func viewModels(from status: TwitterStatus) -> [MediaView.ViewModel] { + return status.attachments.map { attachment -> MediaView.ViewModel in + MediaView.ViewModel( + mediaKind: { + switch attachment.kind { + case .photo: return .photo + case .video: return .video + case .animatedGIF: return .animatedGIF + } + }(), + aspectRatio: attachment.size, + altText: attachment.altDescription, + previewURL: (attachment.previewURL ?? attachment.assetURL).flatMap { URL(string: $0) }, + assetURL: attachment.assetURL.flatMap { URL(string: $0) }, + downloadURL: attachment.downloadURL.flatMap { URL(string: $0) }, + durationMS: attachment.durationMS + ) + } + } + +// public static func configuration(mastodonStatus status: MastodonStatus) -> [MediaView.Configuration] { +// func videoInfo(from attachment: MastodonAttachment) -> MediaView.Configuration.VideoInfo { +// MediaView.Configuration.VideoInfo( +// aspectRadio: attachment.size, +// assetURL: attachment.assetURL, +// previewURL: attachment.previewURL, +// durationMS: attachment.durationMS +// ) +// } +// +// let status = status.repost ?? status +// return status.attachments.map { attachment -> MediaView.Configuration in +// switch attachment.kind { +// case .image: +// let info = MediaView.Configuration.ImageInfo( +// aspectRadio: attachment.size, +// assetURL: attachment.assetURL, +// downloadURL: attachment.downloadURL +// ) +// return .image(info: info) +// case .video: +// let info = videoInfo(from: attachment) +// return .video(info: info) +// case .gifv: +// let info = videoInfo(from: attachment) +// return .gif(info: info) +// case .audio: +// // TODO: +// let info = videoInfo(from: attachment) +// return .video(info: info) +// } +// } +// } +} diff --git a/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift b/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift index f114de3e..1cfb62d8 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift @@ -8,323 +8,369 @@ import AVKit import UIKit +import SwiftUI import Combine import TwidereAsset +import Kingfisher -public final class MediaView: UIView { +public struct MediaView: View { - var disposeBag = Set() + @ObservedObject var viewModel: ViewModel - public static let cornerRadius: CGFloat = 8 - public static let durationFormatter: DateComponentsFormatter = { - let formatter = DateComponentsFormatter() - formatter.zeroFormattingBehavior = .pad - formatter.allowedUnits = [.minute, .second] - return formatter - }() - public static let borderColor: UIColor = UIColor.label.withAlphaComponent(0.05) - public static let borderWidth: CGFloat = 1 - - public let container = TouchBlockingView() - - public private(set) var configuration: Configuration? - - private(set) lazy var imageView: UIImageView = { - let imageView = UIImageView() - imageView.contentMode = .scaleAspectFill - imageView.layer.masksToBounds = true - imageView.layer.cornerCurve = .continuous - imageView.layer.cornerRadius = MediaView.cornerRadius - imageView.layer.borderColor = MediaView.borderColor.cgColor - imageView.layer.borderWidth = MediaView.borderWidth - imageView.isUserInteractionEnabled = false - return imageView - }() - - private(set) lazy var playerViewController: AVPlayerViewController = { - let playerViewController = AVPlayerViewController() - playerViewController.view.layer.masksToBounds = true - playerViewController.view.layer.cornerCurve = .continuous - playerViewController.view.layer.cornerRadius = MediaView.cornerRadius - playerViewController.view.layer.borderColor = MediaView.borderColor.cgColor - playerViewController.view.layer.borderWidth = MediaView.borderWidth - playerViewController.view.isUserInteractionEnabled = false - return playerViewController - }() - private var playerLooper: AVPlayerLooper? - - private(set) lazy var indicatorBlurEffectView: UIVisualEffectView = { - let effectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterial)) - effectView.layer.masksToBounds = true - effectView.layer.cornerCurve = .continuous - effectView.layer.cornerRadius = 4 - return effectView - }() - private(set) lazy var indicatorVibrancyEffectView = UIVisualEffectView( - effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .systemUltraThinMaterial)) - ) - private(set) lazy var playerIndicatorLabel: UILabel = { - let label = UILabel() - label.font = .preferredFont(forTextStyle: .caption1) - label.textColor = .secondaryLabel - return label - }() - - public override init(frame: CGRect) { - super.init(frame: frame) - _init() + public init(viewModel: MediaView.ViewModel) { + self.viewModel = viewModel } - public required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension MediaView { - - @MainActor - public func thumbnail() async -> UIImage? { - return imageView.image - } - - public func thumbnail() -> UIImage? { - return imageView.image - } - -} - -extension MediaView { - private func _init() { - // lazy load content later - - imageView.isAccessibilityElement = true - } - - public func setup(configuration: Configuration) { - self.configuration = configuration - - setupContainerViewHierarchy() - - switch configuration { - case .image(let info): - configure(image: info, containerView: container) - case .gif(let info): - configure(gif: info) - case .video(let info): - configure(video: info) - } - } - private func configure( - image info: Configuration.ImageInfo, - containerView: UIView - ) { - imageView.translatesAutoresizingMaskIntoConstraints = false - containerView.addSubview(imageView) - NSLayoutConstraint.activate([ - imageView.topAnchor.constraint(equalTo: containerView.topAnchor), - imageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), - imageView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), - imageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), - ]) - - let placeholder = Asset.Logo.mediaPlaceholder.image - imageView.contentMode = .center - imageView.backgroundColor = .systemGray6 - - guard let urlString = info.assetURL, - let url = URL(string: urlString) else { - imageView.image = placeholder - return - } - - imageView.af.setImage( - withURL: url, - placeholderImage: placeholder, - completion: { [weak imageView] response in - assert(Thread.isMainThread) - switch response.result { - case .success: - imageView?.contentMode = .scaleAspectFill - case .failure: - break - } - }) - } - - private func configure(gif info: Configuration.VideoInfo) { - // use view controller as View here - playerViewController.view.translatesAutoresizingMaskIntoConstraints = false - container.addSubview(playerViewController.view) - NSLayoutConstraint.activate([ - playerViewController.view.topAnchor.constraint(equalTo: container.topAnchor), - playerViewController.view.leadingAnchor.constraint(equalTo: container.leadingAnchor), - playerViewController.view.trailingAnchor.constraint(equalTo: container.trailingAnchor), - playerViewController.view.bottomAnchor.constraint(equalTo: container.bottomAnchor), - ]) - - assert(playerViewController.contentOverlayView != nil) - if let contentOverlayView = playerViewController.contentOverlayView { - let imageInfo = Configuration.ImageInfo( - aspectRadio: info.aspectRadio, - assetURL: info.previewURL, - downloadURL: info.previewURL - ) - configure(image: imageInfo, containerView: contentOverlayView) - - indicatorBlurEffectView.translatesAutoresizingMaskIntoConstraints = false - contentOverlayView.addSubview(indicatorBlurEffectView) - NSLayoutConstraint.activate([ - contentOverlayView.trailingAnchor.constraint(equalTo: indicatorBlurEffectView.trailingAnchor, constant: 11), - contentOverlayView.bottomAnchor.constraint(equalTo: indicatorBlurEffectView.bottomAnchor, constant: 8), - ]) - setupIndicatorViewHierarchy() - } - playerIndicatorLabel.attributedText = NSAttributedString(AttributedString("GIF")) - - guard let player = setupGIFPlayer(info: info) else { - // assertionFailure() - return - } - setupPlayerLooper(player: player) - playerViewController.player = player - playerViewController.showsPlaybackControls = false - - playerViewController.publisher(for: \.isReadyForDisplay) - .receive(on: DispatchQueue.main) - .sink { [weak self] isReadyForDisplay in - guard let self = self else { return } - self.imageView.isHidden = isReadyForDisplay + public var body: some View { + KFImage(viewModel.previewURL) + .cancelOnDisappear(true) + .resizable() + .placeholder { progress in + Image(uiImage: Asset.Logo.mediaPlaceholder.image.withRenderingMode(.alwaysTemplate)) } - .store(in: &disposeBag) - - // auto play for GIF - player.play() - } - - private func configure(video info: Configuration.VideoInfo) { - let imageInfo = Configuration.ImageInfo( - aspectRadio: info.aspectRadio, - assetURL: info.previewURL, - downloadURL: info.previewURL - ) - configure(image: imageInfo, containerView: container) - - indicatorBlurEffectView.translatesAutoresizingMaskIntoConstraints = false - imageView.addSubview(indicatorBlurEffectView) - NSLayoutConstraint.activate([ - imageView.trailingAnchor.constraint(equalTo: indicatorBlurEffectView.trailingAnchor, constant: 11), - imageView.bottomAnchor.constraint(equalTo: indicatorBlurEffectView.bottomAnchor, constant: 8), - ]) - setupIndicatorViewHierarchy() - - playerIndicatorLabel.attributedText = { - let imageAttachment = NSTextAttachment(image: UIImage(systemName: "play.fill")!) - let imageAttributedString = AttributedString(NSAttributedString(attachment: imageAttachment)) - let duration: String = { - guard let durationMS = info.durationMS else { return "" } - let timeInterval = TimeInterval(durationMS / 1000) - guard timeInterval > 0 else { return "" } - guard let text = MediaView.durationFormatter.string(from: timeInterval) else { return "" } - return " \(text)" - }() - let textAttributedString = AttributedString("\(duration)") - var attributedString = imageAttributedString + textAttributedString - attributedString.foregroundColor = .secondaryLabel - return NSAttributedString(attributedString) - }() - +// Color.clear +// .overlay { +// KFImage(viewModel.previewURL) +// .cancelOnDisappear(true) +// .resizable() +// .placeholder { progress in +// +// } +// } +// .aspectRatio(viewModel.aspectRatio, contentMode: .fill) +// .placeholder { +// ZStack { +// let gradient = Gradient(stops: [ +// .init(color: Color(uiColor: UIColor(hex: 0x50A85E)), location: 0.0), +// .init(color: Color(uiColor: UIColor(hex: 0x567CA6)), location: 0.3490), +// .init(color: Color(uiColor: UIColor(hex: 0x765BAE)), location: 0.7031), +// .init(color: Color(uiColor: UIColor(hex: 0xAB5886)), location: 1.0), +// ]) +// LinearGradient(gradient: gradient, startPoint: .topLeading, endPoint: .bottomTrailing) +// Image(uiImage: Asset.Image.TimeLine.placeholder.image.withRenderingMode(.alwaysTemplate)) +// } +// } +// .sizeFitContainer(shape: Rectangle(), aspectRatio: viewModel._asepctRatio) +// .accessibilityLabel(viewModel.altText ?? "") } - public func prepareForReuse() { - // reset appearance - alpha = 1 - - // reset image - imageView.removeFromSuperview() - imageView.removeConstraints(imageView.constraints) - imageView.af.cancelImageRequest() - imageView.image = nil - imageView.isHidden = false - - // reset player - playerViewController.view.removeFromSuperview() - playerViewController.contentOverlayView.flatMap { view in - view.removeConstraints(view.constraints) - } - playerViewController.player?.pause() - playerViewController.player = nil - playerLooper = nil - - // reset indicator - indicatorBlurEffectView.removeFromSuperview() - - // reset container - container.removeFromSuperview() - container.removeConstraints(container.constraints) - - // reset configuration - configuration = nil - - disposeBag.removeAll() - } } -extension MediaView { - private func setupGIFPlayer(info: Configuration.VideoInfo) -> AVPlayer? { - guard let urlString = info.assetURL, - let url = URL(string: urlString) - else { return nil } - let playerItem = AVPlayerItem(url: url) - let player = AVQueuePlayer(playerItem: playerItem) - player.isMuted = true - return player - } - - private func setupPlayerLooper(player: AVPlayer) { - guard let queuePlayer = player as? AVQueuePlayer else { return } - guard let templateItem = queuePlayer.items().first else { return } - playerLooper = AVPlayerLooper(player: queuePlayer, templateItem: templateItem) - } - - private func setupContainerViewHierarchy() { - guard container.superview == nil else { return } - container.translatesAutoresizingMaskIntoConstraints = false - addSubview(container) - NSLayoutConstraint.activate([ - container.topAnchor.constraint(equalTo: topAnchor), - container.leadingAnchor.constraint(equalTo: leadingAnchor), - container.trailingAnchor.constraint(equalTo: trailingAnchor), - container.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - } - - private func setupIndicatorViewHierarchy() { - let blurEffectView = indicatorBlurEffectView - let vibrancyEffectView = indicatorVibrancyEffectView - - if vibrancyEffectView.superview == nil { - vibrancyEffectView.translatesAutoresizingMaskIntoConstraints = false - blurEffectView.contentView.addSubview(vibrancyEffectView) - NSLayoutConstraint.activate([ - vibrancyEffectView.topAnchor.constraint(equalTo: blurEffectView.contentView.topAnchor), - vibrancyEffectView.leadingAnchor.constraint(equalTo: blurEffectView.contentView.leadingAnchor), - vibrancyEffectView.trailingAnchor.constraint(equalTo: blurEffectView.contentView.trailingAnchor), - vibrancyEffectView.bottomAnchor.constraint(equalTo: blurEffectView.contentView.bottomAnchor), - ]) - } - - if playerIndicatorLabel.superview == nil { - playerIndicatorLabel.translatesAutoresizingMaskIntoConstraints = false - vibrancyEffectView.contentView.addSubview(playerIndicatorLabel) - NSLayoutConstraint.activate([ - playerIndicatorLabel.topAnchor.constraint(equalTo: vibrancyEffectView.contentView.topAnchor), - playerIndicatorLabel.leadingAnchor.constraint(equalTo: vibrancyEffectView.contentView.leadingAnchor, constant: 3), - vibrancyEffectView.contentView.trailingAnchor.constraint(equalTo: playerIndicatorLabel.trailingAnchor, constant: 3), - playerIndicatorLabel.bottomAnchor.constraint(equalTo: vibrancyEffectView.contentView.bottomAnchor), - ]) - } - } -} +//public final class MediaView: UIView { +// +// var disposeBag = Set() +// +// public static let cornerRadius: CGFloat = 8 +// public static let durationFormatter: DateComponentsFormatter = { +// let formatter = DateComponentsFormatter() +// formatter.zeroFormattingBehavior = .pad +// formatter.allowedUnits = [.minute, .second] +// return formatter +// }() +// public static let borderColor: UIColor = UIColor.label.withAlphaComponent(0.05) +// public static let borderWidth: CGFloat = 1 +// +// public let container = TouchBlockingView() +// +// public private(set) var configuration: Configuration? +// +// private(set) lazy var imageView: UIImageView = { +// let imageView = UIImageView() +// imageView.contentMode = .scaleAspectFill +// imageView.layer.masksToBounds = true +// imageView.layer.cornerCurve = .continuous +// imageView.layer.cornerRadius = MediaView.cornerRadius +// imageView.layer.borderColor = MediaView.borderColor.cgColor +// imageView.layer.borderWidth = MediaView.borderWidth +// imageView.isUserInteractionEnabled = false +// return imageView +// }() +// +// private(set) lazy var playerViewController: AVPlayerViewController = { +// let playerViewController = AVPlayerViewController() +// playerViewController.view.layer.masksToBounds = true +// playerViewController.view.layer.cornerCurve = .continuous +// playerViewController.view.layer.cornerRadius = MediaView.cornerRadius +// playerViewController.view.layer.borderColor = MediaView.borderColor.cgColor +// playerViewController.view.layer.borderWidth = MediaView.borderWidth +// playerViewController.view.isUserInteractionEnabled = false +// return playerViewController +// }() +// private var playerLooper: AVPlayerLooper? +// +// private(set) lazy var indicatorBlurEffectView: UIVisualEffectView = { +// let effectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterial)) +// effectView.layer.masksToBounds = true +// effectView.layer.cornerCurve = .continuous +// effectView.layer.cornerRadius = 4 +// return effectView +// }() +// private(set) lazy var indicatorVibrancyEffectView = UIVisualEffectView( +// effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .systemUltraThinMaterial)) +// ) +// private(set) lazy var playerIndicatorLabel: UILabel = { +// let label = UILabel() +// label.font = .preferredFont(forTextStyle: .caption1) +// label.textColor = .secondaryLabel +// return label +// }() +// +// public override init(frame: CGRect) { +// super.init(frame: frame) +// _init() +// } +// +// public required init?(coder: NSCoder) { +// super.init(coder: coder) +// _init() +// } +// +//} +// +//extension MediaView { +// +// @MainActor +// public func thumbnail() async -> UIImage? { +// return imageView.image +// } +// +// public func thumbnail() -> UIImage? { +// return imageView.image +// } +// +//} +// +//extension MediaView { +// private func _init() { +// // lazy load content later +// +// imageView.isAccessibilityElement = true +// } +// +// public func setup(configuration: Configuration) { +// self.configuration = configuration +// +// setupContainerViewHierarchy() +// +// switch configuration { +// case .image(let info): +// configure(image: info, containerView: container) +// case .gif(let info): +// configure(gif: info) +// case .video(let info): +// configure(video: info) +// } +// } +// +// private func configure( +// image info: Configuration.ImageInfo, +// containerView: UIView +// ) { +// imageView.translatesAutoresizingMaskIntoConstraints = false +// containerView.addSubview(imageView) +// NSLayoutConstraint.activate([ +// imageView.topAnchor.constraint(equalTo: containerView.topAnchor), +// imageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), +// imageView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), +// imageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), +// ]) +// +// let placeholder = Asset.Logo.mediaPlaceholder.image +// imageView.contentMode = .center +// imageView.backgroundColor = .systemGray6 +// +// guard let urlString = info.assetURL, +// let url = URL(string: urlString) else { +// imageView.image = placeholder +// return +// } +// +// imageView.af.setImage( +// withURL: url, +// placeholderImage: placeholder, +// completion: { [weak imageView] response in +// assert(Thread.isMainThread) +// switch response.result { +// case .success: +// imageView?.contentMode = .scaleAspectFill +// case .failure: +// break +// } +// }) +// } +// +// private func configure(gif info: Configuration.VideoInfo) { +// // use view controller as View here +// playerViewController.view.translatesAutoresizingMaskIntoConstraints = false +// container.addSubview(playerViewController.view) +// NSLayoutConstraint.activate([ +// playerViewController.view.topAnchor.constraint(equalTo: container.topAnchor), +// playerViewController.view.leadingAnchor.constraint(equalTo: container.leadingAnchor), +// playerViewController.view.trailingAnchor.constraint(equalTo: container.trailingAnchor), +// playerViewController.view.bottomAnchor.constraint(equalTo: container.bottomAnchor), +// ]) +// +// assert(playerViewController.contentOverlayView != nil) +// if let contentOverlayView = playerViewController.contentOverlayView { +// let imageInfo = Configuration.ImageInfo( +// aspectRadio: info.aspectRadio, +// assetURL: info.previewURL, +// downloadURL: info.previewURL +// ) +// configure(image: imageInfo, containerView: contentOverlayView) +// +// indicatorBlurEffectView.translatesAutoresizingMaskIntoConstraints = false +// contentOverlayView.addSubview(indicatorBlurEffectView) +// NSLayoutConstraint.activate([ +// contentOverlayView.trailingAnchor.constraint(equalTo: indicatorBlurEffectView.trailingAnchor, constant: 11), +// contentOverlayView.bottomAnchor.constraint(equalTo: indicatorBlurEffectView.bottomAnchor, constant: 8), +// ]) +// setupIndicatorViewHierarchy() +// } +// playerIndicatorLabel.attributedText = NSAttributedString(AttributedString("GIF")) +// +// guard let player = setupGIFPlayer(info: info) else { +// // assertionFailure() +// return +// } +// setupPlayerLooper(player: player) +// playerViewController.player = player +// playerViewController.showsPlaybackControls = false +// +// playerViewController.publisher(for: \.isReadyForDisplay) +// .receive(on: DispatchQueue.main) +// .sink { [weak self] isReadyForDisplay in +// guard let self = self else { return } +// self.imageView.isHidden = isReadyForDisplay +// } +// .store(in: &disposeBag) +// +// // auto play for GIF +// player.play() +// } +// +// private func configure(video info: Configuration.VideoInfo) { +// let imageInfo = Configuration.ImageInfo( +// aspectRadio: info.aspectRadio, +// assetURL: info.previewURL, +// downloadURL: info.previewURL +// ) +// configure(image: imageInfo, containerView: container) +// +// indicatorBlurEffectView.translatesAutoresizingMaskIntoConstraints = false +// imageView.addSubview(indicatorBlurEffectView) +// NSLayoutConstraint.activate([ +// imageView.trailingAnchor.constraint(equalTo: indicatorBlurEffectView.trailingAnchor, constant: 11), +// imageView.bottomAnchor.constraint(equalTo: indicatorBlurEffectView.bottomAnchor, constant: 8), +// ]) +// setupIndicatorViewHierarchy() +// +// playerIndicatorLabel.attributedText = { +// let imageAttachment = NSTextAttachment(image: UIImage(systemName: "play.fill")!) +// let imageAttributedString = AttributedString(NSAttributedString(attachment: imageAttachment)) +// let duration: String = { +// guard let durationMS = info.durationMS else { return "" } +// let timeInterval = TimeInterval(durationMS / 1000) +// guard timeInterval > 0 else { return "" } +// guard let text = MediaView.durationFormatter.string(from: timeInterval) else { return "" } +// return " \(text)" +// }() +// let textAttributedString = AttributedString("\(duration)") +// var attributedString = imageAttributedString + textAttributedString +// attributedString.foregroundColor = .secondaryLabel +// return NSAttributedString(attributedString) +// }() +// +// } +// +// public func prepareForReuse() { +// // reset appearance +// alpha = 1 +// +// // reset image +// imageView.removeFromSuperview() +// imageView.removeConstraints(imageView.constraints) +// imageView.af.cancelImageRequest() +// imageView.image = nil +// imageView.isHidden = false +// +// // reset player +// playerViewController.view.removeFromSuperview() +// playerViewController.contentOverlayView.flatMap { view in +// view.removeConstraints(view.constraints) +// } +// playerViewController.player?.pause() +// playerViewController.player = nil +// playerLooper = nil +// +// // reset indicator +// indicatorBlurEffectView.removeFromSuperview() +// +// // reset container +// container.removeFromSuperview() +// container.removeConstraints(container.constraints) +// +// // reset configuration +// configuration = nil +// +// disposeBag.removeAll() +// } +//} +// +//extension MediaView { +// private func setupGIFPlayer(info: Configuration.VideoInfo) -> AVPlayer? { +// guard let urlString = info.assetURL, +// let url = URL(string: urlString) +// else { return nil } +// let playerItem = AVPlayerItem(url: url) +// let player = AVQueuePlayer(playerItem: playerItem) +// player.isMuted = true +// return player +// } +// +// private func setupPlayerLooper(player: AVPlayer) { +// guard let queuePlayer = player as? AVQueuePlayer else { return } +// guard let templateItem = queuePlayer.items().first else { return } +// playerLooper = AVPlayerLooper(player: queuePlayer, templateItem: templateItem) +// } +// +// private func setupContainerViewHierarchy() { +// guard container.superview == nil else { return } +// container.translatesAutoresizingMaskIntoConstraints = false +// addSubview(container) +// NSLayoutConstraint.activate([ +// container.topAnchor.constraint(equalTo: topAnchor), +// container.leadingAnchor.constraint(equalTo: leadingAnchor), +// container.trailingAnchor.constraint(equalTo: trailingAnchor), +// container.bottomAnchor.constraint(equalTo: bottomAnchor), +// ]) +// } +// +// private func setupIndicatorViewHierarchy() { +// let blurEffectView = indicatorBlurEffectView +// let vibrancyEffectView = indicatorVibrancyEffectView +// +// if vibrancyEffectView.superview == nil { +// vibrancyEffectView.translatesAutoresizingMaskIntoConstraints = false +// blurEffectView.contentView.addSubview(vibrancyEffectView) +// NSLayoutConstraint.activate([ +// vibrancyEffectView.topAnchor.constraint(equalTo: blurEffectView.contentView.topAnchor), +// vibrancyEffectView.leadingAnchor.constraint(equalTo: blurEffectView.contentView.leadingAnchor), +// vibrancyEffectView.trailingAnchor.constraint(equalTo: blurEffectView.contentView.trailingAnchor), +// vibrancyEffectView.bottomAnchor.constraint(equalTo: blurEffectView.contentView.bottomAnchor), +// ]) +// } +// +// if playerIndicatorLabel.superview == nil { +// playerIndicatorLabel.translatesAutoresizingMaskIntoConstraints = false +// vibrancyEffectView.contentView.addSubview(playerIndicatorLabel) +// NSLayoutConstraint.activate([ +// playerIndicatorLabel.topAnchor.constraint(equalTo: vibrancyEffectView.contentView.topAnchor), +// playerIndicatorLabel.leadingAnchor.constraint(equalTo: vibrancyEffectView.contentView.leadingAnchor, constant: 3), +// vibrancyEffectView.contentView.trailingAnchor.constraint(equalTo: playerIndicatorLabel.trailingAnchor, constant: 3), +// playerIndicatorLabel.bottomAnchor.constraint(equalTo: vibrancyEffectView.contentView.bottomAnchor), +// ]) +// } +// } +//} diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift index 1d7833f8..2729273b 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift @@ -44,7 +44,10 @@ public struct StatusHeaderView: View { .clipShape(Circle()) LabelRepresentable( metaContent: viewModel.label, - textStyle: .statusHeader + textStyle: .statusHeader, + setupLabel: { label in + // do nothing + } ) .background(GeometryReader { proxy in Color.clear.preference( @@ -55,7 +58,7 @@ public struct StatusHeaderView: View { .onPreferenceChange(ViewHeightKey.self) { height in self.iconImageDimension = height } - .border(.red, width: 1) + Spacer() } } // end Button } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index 77eee2e2..83904eaf 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -39,7 +39,7 @@ extension StatusView { public weak var delegate: StatusViewDelegate? weak var parentViewModel: StatusView.ViewModel? - // @Published public var authorAvatarDimension: CGFloat = .zero + @Published public var authorAvatarDimension: CGFloat = .zero // output @@ -83,7 +83,8 @@ extension StatusView { // // @Published public var language: String? // @Published public var isTranslateButtonDisplay = false -// +// + @Published public var mediaViewModels: [MediaView.ViewModel] = [] // @Published public var mediaViewConfigurations: [MediaView.Configuration] = [] // // @Published public var isContentSensitive: Bool = false @@ -138,11 +139,9 @@ extension StatusView { // @Published public var isDeletable = false // // @Published public var groupedAccessibilityLabel = "" -// -// let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common) -// .autoconnect() -// .share() -// .eraseToAnyPublisher() +// + @Published public var timestampLabelViewModel: TimestampLabelView.ViewModel? + // // // public let contentRevealChangePublisher = PassthroughSubject() // @@ -1007,6 +1006,14 @@ extension StatusView.ViewModel { status.author.publisher(for: \.username) .assign(to: &$authorUsernme) + // timestamp + switch kind { + case .timeline, .repost: + timestampLabelViewModel = TimestampLabelView.ViewModel(timestamp: status.createdAt) + default: + break + } + // content let content = TwitterContent(content: status.displayText) let metaContent = TwitterMetaContent.convert( @@ -1016,7 +1023,10 @@ extension StatusView.ViewModel { useParagraphMark: true ) self.content = metaContent - } + + // media + mediaViewModels = MediaView.ViewModel.viewModels(from: status) + } // end init } extension StatusView.ViewModel { diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index 8692f6a0..75d543e9 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -48,9 +48,19 @@ public struct StatusView: View { // authorView authorView .padding(.horizontal, viewModel.margin) + .border(Color.cyan, width: 1) // content contentView .padding(.horizontal, viewModel.margin) + // media + if !viewModel.mediaViewModels.isEmpty { + MediaGridContainerView( + viewModels: viewModel.mediaViewModels, + idealWidth: contentWidth, + idealHeight: 280 + ) + .padding(.horizontal, viewModel.margin) + } // quote if let quoteViewModel = viewModel.quoteViewModel { StatusView(viewModel: quoteViewModel) @@ -70,6 +80,15 @@ public struct StatusView: View { } extension StatusView { + var contentWidth: CGFloat { + switch viewModel.kind { + case .conversationRoot: + return viewModel.viewLayoutFrame.readableContentLayoutFrame.width - 2 * viewModel.margin + default: + return viewModel.viewLayoutFrame.readableContentLayoutFrame.width - 2 * viewModel.margin - StatusView.hangingAvatarButtonDimension - StatusView.hangingAvatarButtonTrailingSapcing + } + } + public var authorView: some View { HStack(alignment: .center) { if !viewModel.hasHangingAvatar { @@ -77,19 +96,30 @@ extension StatusView { avatarButton } // info - VStack(spacing: .zero) { + HStack(alignment: .firstTextBaseline, spacing: 5) { // name LabelRepresentable( metaContent: viewModel.authorName, - textStyle: .statusAuthorName + textStyle: .statusAuthorName, + setupLabel: { label in + label.setContentHuggingPriority(.defaultHigh, for: .horizontal) + label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + } ) - .border(.red, width: 1) // username LabelRepresentable( metaContent: PlaintextMetaContent(string: "@" + viewModel.authorUsernme), - textStyle: .statusAuthorUsername + textStyle: .statusAuthorUsername, + setupLabel: { label in + label.setContentHuggingPriority(.defaultHigh, for: .horizontal) + label.setContentCompressionResistancePriority(.defaultLow - 100, for: .horizontal) + } ) - .border(.red, width: 1) + Spacer() + // timestamp + if let timestampLabelViewModel = viewModel.timestampLabelViewModel { + TimestampLabelView(viewModel: timestampLabelViewModel) + } } .background(GeometryReader { proxy in Color.clear.preference( @@ -98,7 +128,7 @@ extension StatusView { ) }) .onPreferenceChange(ViewHeightKey.self) { height in - // self.viewModel.authorAvatarDimension = height + self.viewModel.authorAvatarDimension = height } } } @@ -107,36 +137,34 @@ extension StatusView { Button { } label: { + let dimension: CGFloat = { + switch viewModel.kind { + case .quote: + return viewModel.authorAvatarDimension + default: + return StatusView.hangingAvatarButtonDimension + } + }() KFImage(viewModel.avatarURL) .placeholder { progress in Color(uiColor: .placeholderText) } .resizable() .aspectRatio(contentMode: .fill) - .frame(width: StatusView.hangingAvatarButtonDimension, height: StatusView.hangingAvatarButtonDimension) + .frame(width: dimension, height: dimension) .clipShape(Circle()) } .buttonStyle(.borderless) - } public var contentView: some View { - HStack(spacing: .zero) { - let width: CGFloat = { - switch viewModel.kind { - case .conversationRoot: - return viewModel.viewLayoutFrame.readableContentLayoutFrame.width - 2 * viewModel.margin - default: - return viewModel.viewLayoutFrame.readableContentLayoutFrame.width - 2 * viewModel.margin - StatusView.hangingAvatarButtonDimension - StatusView.hangingAvatarButtonTrailingSapcing - } - }() + VStack(alignment: .leading, spacing: .zero) { TextViewRepresentable( metaContent: viewModel.content, textStyle: .statusContent, - width: width + width: contentWidth ) - .frame(width: width) - Spacer() + .frame(width: contentWidth) } .border(.red, width: 1) } diff --git a/TwidereSDK/Sources/TwidereUI/Content/TimestampLabelView.swift b/TwidereSDK/Sources/TwidereUI/Content/TimestampLabelView.swift new file mode 100644 index 00000000..4aa48537 --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Content/TimestampLabelView.swift @@ -0,0 +1,49 @@ +// +// TimestampLabelView.swift +// +// +// Created by MainasuK on 2023/2/21. +// + +import SwiftUI +import Combine +import Meta +import TwidereCore +import DateToolsSwift + +public struct TimestampLabelView: View { + + @ObservedObject public var viewModel: ViewModel + + @State var int = 0 + + public init(viewModel: TimestampLabelView.ViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + TimelineView(.periodic(from: .now, by: 1.0)) { timeline in + let timeAgo = viewModel.timeAgo(now: timeline.date) + Text("\(timeAgo)") + .font(Font.subheadline.monospacedDigit()) + .foregroundColor(Color(uiColor: TextStyle.statusTimestamp.textColor)) + } + } +} + +extension TimestampLabelView { + public class ViewModel: ObservableObject { + // input + public let timestamp: Date + + public init(timestamp: Date) { + self.timestamp = timestamp + // end init + } + + func timeAgo(now: Date) -> String { + return timestamp.shortTimeAgo(since: now) + } + + } // end class +} diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/LabelRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/LabelRepresentable.swift index 29d50d8e..76d24fa2 100644 --- a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/LabelRepresentable.swift +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/LabelRepresentable.swift @@ -16,25 +16,31 @@ public struct LabelRepresentable: UIViewRepresentable { let label = UILabel() label.numberOfLines = 1 label.backgroundColor = .clear + label.adjustsFontSizeToFitWidth = false + label.lineBreakMode = .byTruncatingTail + label.setContentHuggingPriority(.defaultHigh, for: .horizontal) // always try grow vertical label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - label.setContentHuggingPriority(.defaultHigh, for: .vertical) return label }() // input public let metaContent: MetaContent public let textStyle: TextStyle + let setupLabel: (UILabel) -> Void public init( metaContent: MetaContent, - textStyle: TextStyle + textStyle: TextStyle, + setupLabel: @escaping (UILabel) -> Void ) { self.metaContent = metaContent self.textStyle = textStyle + self.setupLabel = setupLabel } public func makeUIView(context: Context) -> UILabel { let label = self.label + setupLabel(label) let attributedString = NSMutableAttributedString(string: metaContent.string) let textAttributes: [NSAttributedString.Key: Any] = [ @@ -62,7 +68,7 @@ public struct LabelRepresentable: UIViewRepresentable { ) label.attributedText = attributedString - label.invalidateIntrinsicContentSize() +// label.invalidateIntrinsicContentSize() return label } diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift index 5686f273..956e8cbd 100644 --- a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift @@ -53,9 +53,8 @@ public struct TextViewRepresentable: UIViewRepresentable { ] let paragraphStyle: NSMutableParagraphStyle = { let style = NSMutableParagraphStyle() - let fontMargin = textStyle.font.lineHeight - textStyle.font.pointSize - style.lineSpacing = 3 - fontMargin - style.paragraphSpacing = 8 - fontMargin + style.lineSpacing = 3 + style.paragraphSpacing = 8 return style }() diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index aa3637f8..144e18bb 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -46,6 +46,15 @@ "version": "1.5.1" } }, + { + "package": "DateToolsSwift", + "repositoryURL": "https://github.com/MainasuK/DateTools", + "state": { + "branch": "master", + "revision": "b7f0415ecb9a2b684fc53a2e4093fc09d368356a", + "version": null + } + }, { "package": "FLAnimatedImage", "repositoryURL": "https://github.com/Flipboard/FLAnimatedImage.git", diff --git a/TwidereX/Diffable/Misc/CoverFlowStack/CoverFlowStackItem.swift b/TwidereX/Diffable/Misc/CoverFlowStack/CoverFlowStackItem.swift index a166c28c..40728633 100644 --- a/TwidereX/Diffable/Misc/CoverFlowStack/CoverFlowStackItem.swift +++ b/TwidereX/Diffable/Misc/CoverFlowStack/CoverFlowStackItem.swift @@ -9,5 +9,5 @@ import Foundation enum CoverFlowStackItem: Hashable { - case media(configuration: MediaView.Configuration) + case media(viewModel: MediaView.ViewModel) } diff --git a/TwidereX/Diffable/Misc/CoverFlowStack/CoverFlowStackSection.swift b/TwidereX/Diffable/Misc/CoverFlowStack/CoverFlowStackSection.swift index f5963e34..8ccb1512 100644 --- a/TwidereX/Diffable/Misc/CoverFlowStack/CoverFlowStackSection.swift +++ b/TwidereX/Diffable/Misc/CoverFlowStack/CoverFlowStackSection.swift @@ -22,7 +22,7 @@ extension CoverFlowStackSection { configuration: Configuration ) -> UICollectionViewDiffableDataSource { - let mediaCell = UICollectionView.CellRegistration { cell, indexPath, configuration in + let mediaCell = UICollectionView.CellRegistration { cell, indexPath, configuration in cell.configure(configuration: configuration) } diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift index 6ebf2598..1c2e18a7 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift @@ -25,16 +25,17 @@ extension DataSourceFacade { } func thumbnails() async -> [UIImage?] { - switch containerView { - case .mediaView(let mediaView): - let thumbnail = await mediaView.thumbnail() - return [thumbnail] - case .mediaGridContainerView(let mediaGridContainerView): - let thumbnails = await mediaGridContainerView.mediaViews.parallelMap { mediaView in - return await mediaView.thumbnail() - } - return thumbnails - } + return [] +// switch containerView { +// case .mediaView(let mediaView): +// let thumbnail = await mediaView.thumbnail() +// return [thumbnail] +// case .mediaGridContainerView(let mediaGridContainerView): +// let thumbnails = await mediaGridContainerView.mediaViews.parallelMap { mediaView in +// return await mediaView.thumbnail() +// } +// return thumbnails +// } } } @@ -125,48 +126,48 @@ extension DataSourceFacade { } }() - await coordinateToMediaPreviewScene( - provider: provider, - status: status, - mediaPreviewItem: .statusAttachment(.init( - status: status, - attachments: attachments, - initialIndex: mediaPreviewContext.index, - preloadThumbnails: thumbnails - )), - mediaPreviewTransitionItem: { - // FIXME: allow other source - let item = MediaPreviewTransitionItem( - source: source, - previewableViewController: provider - ) - let mediaView = mediaPreviewContext.mediaView - - item.initialFrame = { - let initialFrame = mediaView.superview!.convert(mediaView.frame, to: nil) - assert(initialFrame != .zero) - return initialFrame - }() - - let thumbnail = mediaView.thumbnail() - item.image = thumbnail - - item.aspectRatio = { - if let thumbnail = thumbnail { - return thumbnail.size - } - let index = mediaPreviewContext.index - guard index < attachments.count else { return nil } - let size = attachments[index].size - return size - }() - - item.sourceImageViewCornerRadius = MediaView.cornerRadius - - return item - }(), - mediaPreviewContext: mediaPreviewContext - ) +// await coordinateToMediaPreviewScene( +// provider: provider, +// status: status, +// mediaPreviewItem: .statusAttachment(.init( +// status: status, +// attachments: attachments, +// initialIndex: mediaPreviewContext.index, +// preloadThumbnails: thumbnails +// )), +// mediaPreviewTransitionItem: { +// // FIXME: allow other source +// let item = MediaPreviewTransitionItem( +// source: source, +// previewableViewController: provider +// ) +// let mediaView = mediaPreviewContext.mediaView +// +// item.initialFrame = { +// let initialFrame = mediaView.superview!.convert(mediaView.frame, to: nil) +// assert(initialFrame != .zero) +// return initialFrame +// }() +// +// let thumbnail = mediaView.thumbnail() +// item.image = thumbnail +// +// item.aspectRatio = { +// if let thumbnail = thumbnail { +// return thumbnail.size +// } +// let index = mediaPreviewContext.index +// guard index < attachments.count else { return nil } +// let size = attachments[index].size +// return size +// }() +// +// item.sourceImageViewCornerRadius = MediaView.cornerRadius +// +// return item +// }(), +// mediaPreviewContext: mediaPreviewContext +// ) } @MainActor diff --git a/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift b/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift index b0f654ff..2a4c74d4 100644 --- a/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift +++ b/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift @@ -165,29 +165,29 @@ extension MediaPreviewViewController { closeButton.addTarget(self, action: #selector(MediaPreviewViewController.closeButtonPressed(_:)), for: .touchUpInside) // bind view model - viewModel.$currentPage - .receive(on: DispatchQueue.main) - .sink { [weak self] index in - guard let self = self else { return } - // update page control - self.pageControl.currentPage = index - - // update mediaGridContainerView - switch self.viewModel.transitionItem.source { - case .none: - break - case .attachment: - break - case .attachments(let mediaGridContainerView): - UIView.animate(withDuration: 0.3) { - mediaGridContainerView.setAlpha(1) - mediaGridContainerView.setAlpha(0, index: index) - } - case .profileAvatar, .profileBanner: - break - } - } - .store(in: &disposeBag) +// viewModel.$currentPage +// .receive(on: DispatchQueue.main) +// .sink { [weak self] index in +// guard let self = self else { return } +// // update page control +// self.pageControl.currentPage = index +// +// // update mediaGridContainerView +// switch self.viewModel.transitionItem.source { +// case .none: +// break +// case .attachment: +// break +// case .attachments(let mediaGridContainerView): +// UIView.animate(withDuration: 0.3) { +// mediaGridContainerView.setAlpha(1) +// mediaGridContainerView.setAlpha(0, index: index) +// } +// case .profileAvatar, .profileBanner: +// break +// } +// } +// .store(in: &disposeBag) viewModel.$currentPage .receive(on: DispatchQueue.main) diff --git a/TwidereX/Scene/Share/View/CollectionViewCell/CoverFlowStackMediaCollectionCell+ViewModel.swift b/TwidereX/Scene/Share/View/CollectionViewCell/CoverFlowStackMediaCollectionCell+ViewModel.swift index 997b76c4..210b2dba 100644 --- a/TwidereX/Scene/Share/View/CollectionViewCell/CoverFlowStackMediaCollectionCell+ViewModel.swift +++ b/TwidereX/Scene/Share/View/CollectionViewCell/CoverFlowStackMediaCollectionCell+ViewModel.swift @@ -14,24 +14,24 @@ extension CoverFlowStackMediaCollectionCell { final class ViewModel: ObservableObject { var disposeBag = Set() - @Published var mediaViewConfiguration: MediaView.Configuration? + @Published var mediaViewModel: MediaView.ViewModel? } } extension CoverFlowStackMediaCollectionCell.ViewModel { func bind(cell: CoverFlowStackMediaCollectionCell) { - $mediaViewConfiguration - .sink { configuration in - guard let configuration = configuration else { return } - cell.mediaView.setup(configuration: configuration) - } - .store(in: &disposeBag) +// $mediaViewConfiguration +// .sink { configuration in +// guard let configuration = configuration else { return } +// cell.mediaView.setup(configuration: configuration) +// } +// .store(in: &disposeBag) } } extension CoverFlowStackMediaCollectionCell { - func configure(configuration: MediaView.Configuration) { - viewModel.mediaViewConfiguration = configuration + func configure(configuration: MediaView.ViewModel) { +// viewModel.mediaViewConfiguration = configuration } } diff --git a/TwidereX/Scene/Share/View/CollectionViewCell/CoverFlowStackMediaCollectionCell.swift b/TwidereX/Scene/Share/View/CollectionViewCell/CoverFlowStackMediaCollectionCell.swift index 9e859c53..a77baa50 100644 --- a/TwidereX/Scene/Share/View/CollectionViewCell/CoverFlowStackMediaCollectionCell.swift +++ b/TwidereX/Scene/Share/View/CollectionViewCell/CoverFlowStackMediaCollectionCell.swift @@ -18,12 +18,12 @@ final class CoverFlowStackMediaCollectionCell: UICollectionViewCell { return viewModel }() - let mediaView = MediaView() +// let mediaView = MediaView() override func prepareForReuse() { super.prepareForReuse() - mediaView.prepareForReuse() +// mediaView.prepareForReuse() } override init(frame: CGRect) { super.init(frame: frame) @@ -40,21 +40,21 @@ final class CoverFlowStackMediaCollectionCell: UICollectionViewCell { extension CoverFlowStackMediaCollectionCell { private func _init() { - contentView.layer.masksToBounds = true - contentView.layer.cornerRadius = MediaView.cornerRadius - contentView.layer.cornerCurve = .continuous - - mediaView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(mediaView) - NSLayoutConstraint.activate([ - mediaView.topAnchor.constraint(equalTo: contentView.topAnchor), - mediaView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - mediaView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - mediaView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - ]) - - // delegate user interactive to collection view - mediaView.isUserInteractionEnabled = false +// contentView.layer.masksToBounds = true +// contentView.layer.cornerRadius = MediaView.cornerRadius +// contentView.layer.cornerCurve = .continuous +// +// mediaView.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(mediaView) +// NSLayoutConstraint.activate([ +// mediaView.topAnchor.constraint(equalTo: contentView.topAnchor), +// mediaView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), +// mediaView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), +// mediaView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// ]) +// +// // delegate user interactive to collection view +// mediaView.isUserInteractionEnabled = false } } diff --git a/TwidereX/Scene/Share/View/CollectionViewCell/StatusMediaGalleryCollectionCell+ViewModel.swift b/TwidereX/Scene/Share/View/CollectionViewCell/StatusMediaGalleryCollectionCell+ViewModel.swift index 232be892..1da539e3 100644 --- a/TwidereX/Scene/Share/View/CollectionViewCell/StatusMediaGalleryCollectionCell+ViewModel.swift +++ b/TwidereX/Scene/Share/View/CollectionViewCell/StatusMediaGalleryCollectionCell+ViewModel.swift @@ -15,7 +15,7 @@ extension StatusMediaGalleryCollectionCell { final class ViewModel: ObservableObject { var disposeBag = Set() - @Published var mediaViewConfigurations: [MediaView.Configuration] = [] + @Published var mediaViewViewModels: [MediaView.ViewModel] = [] // input @Published public var isMediaSensitive: Bool = false @@ -57,90 +57,90 @@ extension StatusMediaGalleryCollectionCell.ViewModel { } func bind(cell: StatusMediaGalleryCollectionCell) { - $mediaViewConfigurations - .sink { [weak self] configurations in - guard let self = self else { return } - - switch configurations.count { - case 0: - cell.mediaView.isHidden = true - cell.collectionView.isHidden = true - case 1: - cell.mediaView.setup(configuration: configurations[0]) - cell.mediaView.isHidden = false - cell.collectionView.isHidden = true - default: - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.main]) - let items: [CoverFlowStackItem] = configurations.map { .media(configuration: $0) } - snapshot.appendItems(items, toSection: .main) - cell.diffableDataSource?.applySnapshotUsingReloadData(snapshot) - cell.mediaView.isHidden = true - cell.collectionView.isHidden = false - } - } - .store(in: &disposeBag) - $isSensitiveToggleButtonDisplay - .sink { isDisplay in - cell.sensitiveToggleButtonBlurVisualEffectView.isHidden = !isDisplay - } - .store(in: &disposeBag) - $isContentWarningOverlayDisplay - .sink { isDisplay in - assert(Thread.isMainThread) - - let isDisplay = isDisplay ?? false - let withAnimation = self.isContentWarningOverlayDisplay != nil - - if withAnimation { - UIView.animate(withDuration: 0.33, delay: 0, options: .curveEaseInOut) { - cell.contentWarningOverlayView.blurVisualEffectView.alpha = isDisplay ? 1 : 0 - } - } else { - cell.contentWarningOverlayView.blurVisualEffectView.alpha = isDisplay ? 1 : 0 - } - - cell.contentWarningOverlayView.isUserInteractionEnabled = isDisplay - cell.contentWarningOverlayView.tapGestureRecognizer.isEnabled = isDisplay - } - .store(in: &disposeBag) +// $mediaViewConfigurations +// .sink { [weak self] configurations in +// guard let self = self else { return } +// +// switch configurations.count { +// case 0: +// cell.mediaView.isHidden = true +// cell.collectionView.isHidden = true +// case 1: +// cell.mediaView.setup(configuration: configurations[0]) +// cell.mediaView.isHidden = false +// cell.collectionView.isHidden = true +// default: +// var snapshot = NSDiffableDataSourceSnapshot() +// snapshot.appendSections([.main]) +// let items: [CoverFlowStackItem] = configurations.map { .media(configuration: $0) } +// snapshot.appendItems(items, toSection: .main) +// cell.diffableDataSource?.applySnapshotUsingReloadData(snapshot) +// cell.mediaView.isHidden = true +// cell.collectionView.isHidden = false +// } +// } +// .store(in: &disposeBag) +// $isSensitiveToggleButtonDisplay +// .sink { isDisplay in +// cell.sensitiveToggleButtonBlurVisualEffectView.isHidden = !isDisplay +// } +// .store(in: &disposeBag) +// $isContentWarningOverlayDisplay +// .sink { isDisplay in +// assert(Thread.isMainThread) +// +// let isDisplay = isDisplay ?? false +// let withAnimation = self.isContentWarningOverlayDisplay != nil +// +// if withAnimation { +// UIView.animate(withDuration: 0.33, delay: 0, options: .curveEaseInOut) { +// cell.contentWarningOverlayView.blurVisualEffectView.alpha = isDisplay ? 1 : 0 +// } +// } else { +// cell.contentWarningOverlayView.blurVisualEffectView.alpha = isDisplay ? 1 : 0 +// } +// +// cell.contentWarningOverlayView.isUserInteractionEnabled = isDisplay +// cell.contentWarningOverlayView.tapGestureRecognizer.isEnabled = isDisplay +// } +// .store(in: &disposeBag) } } extension StatusMediaGalleryCollectionCell { func configure(status object: StatusObject) { - switch object { - case .twitter(let status): - configure(twitterStatus: status) - case .mastodon(let status): - configure(mastodonStatus: status) - } +// switch object { +// case .twitter(let status): +// configure(twitterStatus: status) +// case .mastodon(let status): +// configure(mastodonStatus: status) +// } } - private func configure(twitterStatus status: TwitterStatus) { - let status = status.repost ?? status - - viewModel.resetContentWarningOverlay() - viewModel.isMediaSensitive = false - viewModel.isMediaSensitiveToggled = false - viewModel.isMediaSensitiveSwitchable = false - viewModel.mediaViewConfigurations = MediaView.configuration(twitterStatus: status) - } - - private func configure(mastodonStatus status: MastodonStatus) { - let status = status.repost ?? status - - viewModel.resetContentWarningOverlay() - viewModel.isMediaSensitiveSwitchable = true - viewModel.mediaViewConfigurations = MediaView.configuration(mastodonStatus: status) - status.publisher(for: \.isMediaSensitive) - .receive(on: DispatchQueue.main) - .assign(to: \.isMediaSensitive, on: viewModel) - .store(in: &disposeBag) - status.publisher(for: \.isMediaSensitiveToggled) - .receive(on: DispatchQueue.main) - .assign(to: \.isMediaSensitiveToggled, on: viewModel) - .store(in: &disposeBag) - } +// private func configure(twitterStatus status: TwitterStatus) { +// let status = status.repost ?? status +// +// viewModel.resetContentWarningOverlay() +// viewModel.isMediaSensitive = false +// viewModel.isMediaSensitiveToggled = false +// viewModel.isMediaSensitiveSwitchable = false +// viewModel.mediaViewConfigurations = MediaView.configuration(twitterStatus: status) +// } +// +// private func configure(mastodonStatus status: MastodonStatus) { +// let status = status.repost ?? status +// +// viewModel.resetContentWarningOverlay() +// viewModel.isMediaSensitiveSwitchable = true +// viewModel.mediaViewConfigurations = MediaView.configuration(mastodonStatus: status) +// status.publisher(for: \.isMediaSensitive) +// .receive(on: DispatchQueue.main) +// .assign(to: \.isMediaSensitive, on: viewModel) +// .store(in: &disposeBag) +// status.publisher(for: \.isMediaSensitiveToggled) +// .receive(on: DispatchQueue.main) +// .assign(to: \.isMediaSensitiveToggled, on: viewModel) +// .store(in: &disposeBag) +// } } diff --git a/TwidereX/Scene/Share/View/CollectionViewCell/StatusMediaGalleryCollectionCell.swift b/TwidereX/Scene/Share/View/CollectionViewCell/StatusMediaGalleryCollectionCell.swift index e7428307..d13570c1 100644 --- a/TwidereX/Scene/Share/View/CollectionViewCell/StatusMediaGalleryCollectionCell.swift +++ b/TwidereX/Scene/Share/View/CollectionViewCell/StatusMediaGalleryCollectionCell.swift @@ -44,15 +44,15 @@ final class StatusMediaGalleryCollectionCell: UICollectionViewCell { return button }() - public let contentWarningOverlayView: ContentWarningOverlayView = { - let overlay = ContentWarningOverlayView() - overlay.layer.masksToBounds = true - overlay.layer.cornerRadius = MediaView.cornerRadius - overlay.layer.cornerCurve = .continuous - return overlay - }() +// public let contentWarningOverlayView: ContentWarningOverlayView = { +// let overlay = ContentWarningOverlayView() +// overlay.layer.masksToBounds = true +// overlay.layer.cornerRadius = MediaView.cornerRadius +// overlay.layer.cornerCurve = .continuous +// return overlay +// }() - let mediaView = MediaView() +// let mediaView = MediaView() let collectionViewLayout: CoverFlowStackCollectionViewLayout = { let layout = CoverFlowStackCollectionViewLayout() @@ -63,7 +63,7 @@ final class StatusMediaGalleryCollectionCell: UICollectionViewCell { let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) collectionView.backgroundColor = .clear collectionView.layer.masksToBounds = true - collectionView.layer.cornerRadius = MediaView.cornerRadius +// collectionView.layer.cornerRadius = MediaView.cornerRadius collectionView.layer.cornerCurve = .continuous return collectionView }() @@ -73,7 +73,7 @@ final class StatusMediaGalleryCollectionCell: UICollectionViewCell { super.prepareForReuse() disposeBag.removeAll() - mediaView.prepareForReuse() +// mediaView.prepareForReuse() diffableDataSource?.applySnapshotUsingReloadData(.init()) } @@ -92,72 +92,72 @@ final class StatusMediaGalleryCollectionCell: UICollectionViewCell { extension StatusMediaGalleryCollectionCell { private func _init() { - mediaView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(mediaView) - NSLayoutConstraint.activate([ - mediaView.topAnchor.constraint(equalTo: contentView.topAnchor), - mediaView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - mediaView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - mediaView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - ]) - - collectionView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(collectionView) - NSLayoutConstraint.activate([ - collectionView.topAnchor.constraint(equalTo: contentView.topAnchor), - collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - ]) - - // sensitiveToggleButton - sensitiveToggleButtonBlurVisualEffectView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(sensitiveToggleButtonBlurVisualEffectView) - NSLayoutConstraint.activate([ - sensitiveToggleButtonBlurVisualEffectView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), - sensitiveToggleButtonBlurVisualEffectView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8), - ]) - - sensitiveToggleButtonVibrancyVisualEffectView.translatesAutoresizingMaskIntoConstraints = false - sensitiveToggleButtonBlurVisualEffectView.contentView.addSubview(sensitiveToggleButtonVibrancyVisualEffectView) - NSLayoutConstraint.activate([ - sensitiveToggleButtonVibrancyVisualEffectView.topAnchor.constraint(equalTo: sensitiveToggleButtonBlurVisualEffectView.contentView.topAnchor), - sensitiveToggleButtonVibrancyVisualEffectView.leadingAnchor.constraint(equalTo: sensitiveToggleButtonBlurVisualEffectView.contentView.leadingAnchor), - sensitiveToggleButtonVibrancyVisualEffectView.trailingAnchor.constraint(equalTo: sensitiveToggleButtonBlurVisualEffectView.contentView.trailingAnchor), - sensitiveToggleButtonVibrancyVisualEffectView.bottomAnchor.constraint(equalTo: sensitiveToggleButtonBlurVisualEffectView.contentView.bottomAnchor), - ]) - - sensitiveToggleButton.translatesAutoresizingMaskIntoConstraints = false - sensitiveToggleButtonVibrancyVisualEffectView.contentView.addSubview(sensitiveToggleButton) - NSLayoutConstraint.activate([ - sensitiveToggleButton.topAnchor.constraint(equalTo: sensitiveToggleButtonVibrancyVisualEffectView.contentView.topAnchor, constant: 4), - sensitiveToggleButton.leadingAnchor.constraint(equalTo: sensitiveToggleButtonVibrancyVisualEffectView.contentView.leadingAnchor, constant: 4), - sensitiveToggleButtonVibrancyVisualEffectView.contentView.trailingAnchor.constraint(equalTo: sensitiveToggleButton.trailingAnchor, constant: 4), - sensitiveToggleButtonVibrancyVisualEffectView.contentView.bottomAnchor.constraint(equalTo: sensitiveToggleButton.bottomAnchor, constant: 4), - ]) - - // contentWarningOverlayView - contentWarningOverlayView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(contentWarningOverlayView) // should add to container - NSLayoutConstraint.activate([ - contentWarningOverlayView.topAnchor.constraint(equalTo: contentView.topAnchor), - contentWarningOverlayView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - contentWarningOverlayView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - contentWarningOverlayView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - ]) - - // delegate interaction to collection view - mediaView.isUserInteractionEnabled = false - - collectionView.delegate = self - let configuration = CoverFlowStackSection.Configuration() - diffableDataSource = CoverFlowStackSection.diffableDataSource( - collectionView: collectionView, - configuration: configuration - ) - - sensitiveToggleButton.addTarget(self, action: #selector(StatusMediaGalleryCollectionCell.sensitiveToggleButtonDidPressed(_:)), for: .touchUpInside) - contentWarningOverlayView.delegate = self +// mediaView.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(mediaView) +// NSLayoutConstraint.activate([ +// mediaView.topAnchor.constraint(equalTo: contentView.topAnchor), +// mediaView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), +// mediaView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), +// mediaView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// ]) +// +// collectionView.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(collectionView) +// NSLayoutConstraint.activate([ +// collectionView.topAnchor.constraint(equalTo: contentView.topAnchor), +// collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), +// collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), +// collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// ]) +// +// // sensitiveToggleButton +// sensitiveToggleButtonBlurVisualEffectView.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(sensitiveToggleButtonBlurVisualEffectView) +// NSLayoutConstraint.activate([ +// sensitiveToggleButtonBlurVisualEffectView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), +// sensitiveToggleButtonBlurVisualEffectView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8), +// ]) +// +// sensitiveToggleButtonVibrancyVisualEffectView.translatesAutoresizingMaskIntoConstraints = false +// sensitiveToggleButtonBlurVisualEffectView.contentView.addSubview(sensitiveToggleButtonVibrancyVisualEffectView) +// NSLayoutConstraint.activate([ +// sensitiveToggleButtonVibrancyVisualEffectView.topAnchor.constraint(equalTo: sensitiveToggleButtonBlurVisualEffectView.contentView.topAnchor), +// sensitiveToggleButtonVibrancyVisualEffectView.leadingAnchor.constraint(equalTo: sensitiveToggleButtonBlurVisualEffectView.contentView.leadingAnchor), +// sensitiveToggleButtonVibrancyVisualEffectView.trailingAnchor.constraint(equalTo: sensitiveToggleButtonBlurVisualEffectView.contentView.trailingAnchor), +// sensitiveToggleButtonVibrancyVisualEffectView.bottomAnchor.constraint(equalTo: sensitiveToggleButtonBlurVisualEffectView.contentView.bottomAnchor), +// ]) +// +// sensitiveToggleButton.translatesAutoresizingMaskIntoConstraints = false +// sensitiveToggleButtonVibrancyVisualEffectView.contentView.addSubview(sensitiveToggleButton) +// NSLayoutConstraint.activate([ +// sensitiveToggleButton.topAnchor.constraint(equalTo: sensitiveToggleButtonVibrancyVisualEffectView.contentView.topAnchor, constant: 4), +// sensitiveToggleButton.leadingAnchor.constraint(equalTo: sensitiveToggleButtonVibrancyVisualEffectView.contentView.leadingAnchor, constant: 4), +// sensitiveToggleButtonVibrancyVisualEffectView.contentView.trailingAnchor.constraint(equalTo: sensitiveToggleButton.trailingAnchor, constant: 4), +// sensitiveToggleButtonVibrancyVisualEffectView.contentView.bottomAnchor.constraint(equalTo: sensitiveToggleButton.bottomAnchor, constant: 4), +// ]) +// +// // contentWarningOverlayView +// contentWarningOverlayView.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(contentWarningOverlayView) // should add to container +// NSLayoutConstraint.activate([ +// contentWarningOverlayView.topAnchor.constraint(equalTo: contentView.topAnchor), +// contentWarningOverlayView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), +// contentWarningOverlayView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), +// contentWarningOverlayView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// ]) +// +// // delegate interaction to collection view +// mediaView.isUserInteractionEnabled = false +// +// collectionView.delegate = self +// let configuration = CoverFlowStackSection.Configuration() +// diffableDataSource = CoverFlowStackSection.diffableDataSource( +// collectionView: collectionView, +// configuration: configuration +// ) +// +// sensitiveToggleButton.addTarget(self, action: #selector(StatusMediaGalleryCollectionCell.sensitiveToggleButtonDidPressed(_:)), for: .touchUpInside) +// contentWarningOverlayView.delegate = self } } @@ -165,7 +165,7 @@ extension StatusMediaGalleryCollectionCell { extension StatusMediaGalleryCollectionCell { @objc private func sensitiveToggleButtonDidPressed(_ sender: UIButton) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - delegate?.statusMediaGalleryCollectionCell(self, toggleContentWarningOverlayViewDisplay: contentWarningOverlayView) +// delegate?.statusMediaGalleryCollectionCell(self, toggleContentWarningOverlayViewDisplay: contentWarningOverlayView) } } diff --git a/TwidereX/Scene/Timeline/Base/Grid/GridTimelineViewController.swift b/TwidereX/Scene/Timeline/Base/Grid/GridTimelineViewController.swift index 2de151ff..110924b9 100644 --- a/TwidereX/Scene/Timeline/Base/Grid/GridTimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/Grid/GridTimelineViewController.swift @@ -131,60 +131,60 @@ extension GridTimelineViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): did select \(indexPath.debugDescription)") guard let cell = collectionView.cellForItem(at: indexPath) as? StatusMediaGalleryCollectionCell else { return } - Task { - let source = DataSourceItem.Source(collectionViewCell: nil, indexPath: indexPath) - guard let item = await item(from: source) else { - assertionFailure() - return - } - guard let status = await item.status(in: self.context.managedObjectContext) else { - assertionFailure("only works for status data provider") - return - } - await DataSourceFacade.coordinateToMediaPreviewScene( - provider: self, - target: .status, - status: status, - mediaPreviewContext: DataSourceFacade.MediaPreviewContext( - containerView: .mediaView(cell.mediaView), - mediaView: cell.mediaView, - index: 0 // <-- only one attachment - ) - ) - } +// Task { +// let source = DataSourceItem.Source(collectionViewCell: nil, indexPath: indexPath) +// guard let item = await item(from: source) else { +// assertionFailure() +// return +// } +// guard let status = await item.status(in: self.context.managedObjectContext) else { +// assertionFailure("only works for status data provider") +// return +// } +// await DataSourceFacade.coordinateToMediaPreviewScene( +// provider: self, +// target: .status, +// status: status, +// mediaPreviewContext: DataSourceFacade.MediaPreviewContext( +// containerView: .mediaView(cell.mediaView), +// mediaView: cell.mediaView, +// index: 0 // <-- only one attachment +// ) +// ) +// } } } // MARK: - StatusMediaGalleryCollectionCellDelegate extension GridTimelineViewController: StatusMediaGalleryCollectionCellDelegate { func statusMediaGalleryCollectionCell(_ cell: StatusMediaGalleryCollectionCell, coverFlowCollectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - Task { - let source = DataSourceItem.Source(collectionViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { - assertionFailure() - return - } - guard let status = await item.status(in: self.context.managedObjectContext) else { - assertionFailure("only works for status data provider") - return - } - - guard let cell = coverFlowCollectionView.cellForItem(at: indexPath) as? CoverFlowStackMediaCollectionCell else { - assertionFailure() - return - } - - await DataSourceFacade.coordinateToMediaPreviewScene( - provider: self, - target: .status, - status: status, - mediaPreviewContext: DataSourceFacade.MediaPreviewContext( - containerView: .mediaView(cell.mediaView), - mediaView: cell.mediaView, - index: indexPath.row - ) - ) - } +// Task { +// let source = DataSourceItem.Source(collectionViewCell: cell, indexPath: nil) +// guard let item = await item(from: source) else { +// assertionFailure() +// return +// } +// guard let status = await item.status(in: self.context.managedObjectContext) else { +// assertionFailure("only works for status data provider") +// return +// } +// +// guard let cell = coverFlowCollectionView.cellForItem(at: indexPath) as? CoverFlowStackMediaCollectionCell else { +// assertionFailure() +// return +// } +// +// await DataSourceFacade.coordinateToMediaPreviewScene( +// provider: self, +// target: .status, +// status: status, +// mediaPreviewContext: DataSourceFacade.MediaPreviewContext( +// containerView: .mediaView(cell.mediaView), +// mediaView: cell.mediaView, +// index: indexPath.row +// ) +// ) +// } } func statusMediaGalleryCollectionCell(_ cell: StatusMediaGalleryCollectionCell, toggleContentWarningOverlayViewDisplay contentWarningOverlayView: ContentWarningOverlayView) { diff --git a/TwidereX/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift b/TwidereX/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift index 337755cd..34203093 100644 --- a/TwidereX/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift +++ b/TwidereX/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift @@ -57,23 +57,23 @@ extension MediaPreviewTransitionItem { position: UIViewAnimatingPosition, index: Int? ) { - let alpha: CGFloat = position == .end ? 1 : 0 - switch self { - case .none: - break - case .attachment(let mediaView): - mediaView.alpha = alpha - case .attachments(let mediaGridContainerView): - if let index = index { - mediaGridContainerView.setAlpha(0, index: index) - } else { - mediaGridContainerView.setAlpha(alpha) - } - case .profileAvatar(let profileHeaderView): - profileHeaderView.avatarView.avatarButton.alpha = alpha - case .profileBanner: - break // keep source - } +// let alpha: CGFloat = position == .end ? 1 : 0 +// switch self { +// case .none: +// break +// case .attachment(let mediaView): +// mediaView.alpha = alpha +// case .attachments(let mediaGridContainerView): +// if let index = index { +// mediaGridContainerView.setAlpha(0, index: index) +// } else { +// mediaGridContainerView.setAlpha(alpha) +// } +// case .profileAvatar(let profileHeaderView): +// profileHeaderView.avatarView.avatarButton.alpha = alpha +// case .profileBanner: +// break // keep source +// } } } } diff --git a/TwidereX/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift b/TwidereX/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift index 3689ffb0..d55407da 100644 --- a/TwidereX/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift +++ b/TwidereX/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift @@ -25,11 +25,13 @@ extension MediaPreviewableViewController { ) return frame case .attachment(let mediaView): - return mediaView.superview?.convert(mediaView.frame, to: nil) + return nil +// return mediaView.superview?.convert(mediaView.frame, to: nil) case .attachments(let mediaGridContainerView): - guard index < mediaGridContainerView.mediaViews.count else { return nil } - let mediaView = mediaGridContainerView.mediaViews[index] - return mediaView.superview?.convert(mediaView.frame, to: nil) + return nil +// guard index < mediaGridContainerView.mediaViews.count else { return nil } +// let mediaView = mediaGridContainerView.mediaViews[index] +// return mediaView.superview?.convert(mediaView.frame, to: nil) case .profileAvatar: return nil // TODO: case .profileBanner: From 7e9c149d60b7a04d624c12845ef2d9da894a4709 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 27 Feb 2023 18:53:20 +0800 Subject: [PATCH 032/128] feat: add context menu preview for media view. --- Podfile | 2 +- Podfile.lock | 6 +- .../Container/MediaGridContainerView.swift | 152 +++++++++++- .../Content/MediaView+ViewModel.swift | 22 ++ .../Sources/TwidereUI/Content/MediaView.swift | 58 ++++- .../Content/StatusView+ViewModel.swift | 5 + .../TwidereUI/Content/StatusView.swift | 12 +- ...ableViewCellContextMenuConfiguration.swift | 1 + .../ContextMenuInteractionRepresentable.swift | 69 ++++++ ...ewHeightKey.swift => PreferenceKeys.swift} | 9 +- TwidereX/Diffable/Status/StatusSection.swift | 6 +- .../Provider/DataSourceFacade+Media.swift | 207 ++++++++--------- ...ider+StatusViewTableViewCellDelegate.swift | 31 ++- ...taSourceProvider+UITableViewDelegate.swift | 216 +----------------- .../MediaPreview/MediaPreviewViewModel.swift | 34 ++- .../MediaPreviewVideoViewController.swift | 7 + .../TableViewCell/StatusTableViewCell.swift | 14 +- .../StatusViewTableViewCellDelegate.swift | 10 +- ...meTimelineViewController+DebugAction.swift | 11 +- ...wViewControllerAnimatedTransitioning.swift | 158 ++++++------- .../MediaPreviewTransitionItem.swift | 3 +- .../MediaPreviewableViewController.swift | 11 +- 22 files changed, 547 insertions(+), 497 deletions(-) create mode 100644 TwidereSDK/Sources/TwidereUI/UIViewRepresentable/ContextMenuInteractionRepresentable.swift rename TwidereSDK/Sources/TwidereUI/Utility/{ViewHeightKey.swift => PreferenceKeys.swift} (56%) diff --git a/Podfile b/Podfile index 65025ada..33e4b4a4 100644 --- a/Podfile +++ b/Podfile @@ -29,7 +29,7 @@ target 'TwidereX' do pod 'Sourcery', '~> 1.8.1' # Debug - pod 'FLEX', '~> 4.7.0', :configurations => ['Debug'] + # pod 'FLEX', '~> 4.7.0', :configurations => ['Debug'] pod 'ZIPFoundation', '~> 0.9.11', :configurations => ['Debug'] target 'TwidereXTests' do diff --git a/Podfile.lock b/Podfile.lock index 43dbbccd..56bfd2fa 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -64,7 +64,6 @@ PODS: - FirebaseInstallations (~> 9.0) - GoogleUtilities/Environment (~> 7.7) - "GoogleUtilities/NSData+zlib (~> 7.7)" - - FLEX (4.7.0) - GoogleAppMeasurement/WithoutAdIdSupport (9.2.0): - GoogleUtilities/AppDelegateSwizzler (~> 7.7) - GoogleUtilities/MethodSwizzler (~> 7.7) @@ -115,7 +114,6 @@ DEPENDENCIES: - FirebaseCrashlytics - FirebaseMessaging - FirebasePerformance - - FLEX (~> 4.7.0) - Sourcery (~> 1.8.1) - SwiftGen (~> 6.5.1) - twitter-text (~> 3.1.0) @@ -136,7 +134,6 @@ SPEC REPOS: - FirebaseMessaging - FirebasePerformance - FirebaseRemoteConfig - - FLEX - GoogleAppMeasurement - GoogleDataTransport - GoogleUtilities @@ -161,7 +158,6 @@ SPEC CHECKSUMS: FirebaseMessaging: 4eaf1b8a7464b2c5e619ad66e9b20ee3e3206b24 FirebasePerformance: 5a8d2a9e645a398dfcc02657853f4b946675d5d4 FirebaseRemoteConfig: 16e29297f0dd0c7d2415c4506d614fe0b54875d1 - FLEX: bdc9ac7d4a239e3d04c298c01221203257d63a80 GoogleAppMeasurement: 7a33224321f975d58c166657260526775d9c6b1a GoogleDataTransport: 5fffe35792f8b96ec8d6775f5eccd83c998d5a3b GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1 @@ -173,6 +169,6 @@ SPEC CHECKSUMS: XLPagerTabStrip: 61c57fd61f611ee5f01ff1495ad6fbee8bf496c5 ZIPFoundation: ae5b4b813d216d3bf0a148773267fff14bd51d37 -PODFILE CHECKSUM: 9a116be6704a7b6b4940f5dfb93bf4c4214dbc93 +PODFILE CHECKSUM: a6441556e77318b291664364cc5bfbdd86dbd781 COCOAPODS: 1.11.3 diff --git a/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift b/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift index a9e396a9..d2876846 100644 --- a/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift +++ b/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift @@ -18,13 +18,15 @@ public protocol MediaGridContainerViewDelegate: AnyObject { public struct MediaGridContainerView: View { - static var spacing: CGFloat { 8 } - static var cornerRadius: CGFloat { 8 } + static public var spacing: CGFloat { 8 } + static public var cornerRadius: CGFloat { 8 } public let viewModels: [MediaView.ViewModel] public let idealWidth: CGFloat? public let idealHeight: CGFloat // ideal height for grid exclude single media + + public let previewAction: (MediaView.ViewModel) -> Void public var body: some View { VStack { @@ -37,6 +39,48 @@ public struct MediaGridContainerView: View { idealWidth: idealWidth, idealHeight: viewModel.mediaKind == .video ? idealHeight : 2 * idealHeight) ) + .cornerRadius(MediaGridContainerView.cornerRadius) + .clipped() + .background(GeometryReader { proxy in + Color.clear.preference( + key: ViewFrameKey.self, + value: proxy.frame(in: .global) + ) + .onPreferenceChange(ViewFrameKey.self) { frame in + viewModel.frameInWindow = frame + } + }) + .overlay( + RoundedRectangle(cornerRadius: MediaGridContainerView.cornerRadius) + .stroke(Color(uiColor: .placeholderText).opacity(0.5), lineWidth: 1) + ) + .contentShape(Rectangle()) + .onTapGesture { + // let actionContext = ActionContext(index: index, viewModels: viewModels) + // action(actionContext) + } + .contextMenu(contextMenuContentPreviewProvider: { + guard let thumbnail = viewModel.thumbnail else { return nil } + let contextMenuImagePreviewViewModel = ContextMenuImagePreviewViewModel(aspectRatio: thumbnail.size, thumbnail: thumbnail) + let previewProvider = ContextMenuImagePreviewViewController() + previewProvider.viewModel = contextMenuImagePreviewViewModel + return previewProvider + + }, contextMenuActionProvider: { _ in + let children: [UIAction] = [ + UIAction( + title: L10n.Common.Controls.Actions.copy, + image: UIImage(systemName: "doc.on.doc"), + attributes: [], + state: .off + ) { _ in + print("Hi copy") + } + ] + return UIMenu(title: "", image: nil, identifier: nil, options: [], children: children) + }, previewAction: { + previewAction(viewModel) + }) case 2: let height = height(for: 1) HStack(spacing: MediaGridContainerView.spacing) { @@ -77,7 +121,6 @@ public struct MediaGridContainerView: View { } VStack(spacing: MediaGridContainerView.spacing) { mediaView(at: 2, width: nil, height: height) - } } case 6: @@ -165,11 +208,19 @@ public struct MediaGridContainerView: View { EmptyView() } } // end Group -// .frame(width: idealWidth) - .border(Color.blue, width: 1) } // end body } +extension MediaGridContainerView { + public func contextMenuItems(for viewModel: MediaView.ViewModel) -> some View { + Button { + + } label: { + Label(L10n.Common.Controls.Actions.save, systemImage: "square.and.arrow.down") + } + } +} + extension MediaGridContainerView { private func mediaView(at index: Int, width: CGFloat?, height: CGFloat?) -> some View { @@ -182,6 +233,15 @@ extension MediaGridContainerView { ) .cornerRadius(MediaGridContainerView.cornerRadius) .clipped() + .background(GeometryReader { proxy in + Color.clear.preference( + key: ViewFrameKey.self, + value: proxy.frame(in: .global) + ) + .onPreferenceChange(ViewFrameKey.self) { frame in + viewModels[index].frameInWindow = frame + } + }) .overlay( RoundedRectangle(cornerRadius: MediaGridContainerView.cornerRadius) .stroke(Color(uiColor: .placeholderText).opacity(0.5), lineWidth: 1) @@ -191,6 +251,29 @@ extension MediaGridContainerView { // let actionContext = ActionContext(index: index, viewModels: viewModels) // action(actionContext) } + .contextMenu(contextMenuContentPreviewProvider: { + let viewModel = viewModels[index] + guard let thumbnail = viewModel.thumbnail else { return nil } + let contextMenuImagePreviewViewModel = ContextMenuImagePreviewViewModel(aspectRatio: thumbnail.size, thumbnail: thumbnail) + let previewProvider = ContextMenuImagePreviewViewController() + previewProvider.viewModel = contextMenuImagePreviewViewModel + return previewProvider + + }, contextMenuActionProvider: { _ in + let children: [UIAction] = [ + UIAction( + title: L10n.Common.Controls.Actions.copy, + image: UIImage(systemName: "doc.on.doc"), + attributes: [], + state: .off + ) { _ in + print("Hi copy") + } + ] + return UIMenu(title: "", image: nil, identifier: nil, options: [], children: children) + }, previewAction: { + previewAction(viewModels[index]) + }) } private func height(for rows: Int) -> CGFloat { @@ -268,6 +351,15 @@ struct MediaGridContainerView_Previews: PreviewProvider { downloadURL: URL(string: "https://www.nasa.gov/sites/default/files/thumbnails/image/pia20027_updated.jpg"), durationMS: nil ), + MediaView.ViewModel( + mediaKind: .video, + aspectRatio: CGSize(width: 1200, height: 675), + altText: nil, + previewURL: URL(string: "https://pbs.twimg.com/ext_tw_video_thumb/1629081362555899904/pu/img/em5qGBhoV0R1aGfv.jpg"), + assetURL: URL(string: "https://pbs.twimg.com/ext_tw_video_thumb/1629081362555899904/pu/img/em5qGBhoV0R1aGfv.jpg"), + downloadURL: URL(string: "https://video.twimg.com/ext_tw_video/1629081362555899904/pu/vid/1280x720/4OGsKDg67adqojtX.mp4?tag=12"), + durationMS: 105553160985088 + ), MediaView.ViewModel( mediaKind: .photo, aspectRatio: CGSize(width: 2016, height: 2016), @@ -284,16 +376,56 @@ struct MediaGridContainerView_Previews: PreviewProvider { static var previews: some View { Group { ForEach(0.. Void + ) -> some View { + modifier(ContextMenuViewModifier( + contextMenuContentPreviewProvider: contextMenuContentPreviewProvider, + contextMenuActionProvider: contextMenuActionProvider, + previewAction: previewAction + )) + } +} + +struct ContextMenuViewModifier: ViewModifier { + let contextMenuContentPreviewProvider: UIContextMenuContentPreviewProvider + let contextMenuActionProvider: UIContextMenuActionProvider + let previewAction: () -> Void + + func body(content: Content) -> some View { + ContextMenuInteractionRepresentable( + contextMenuContentPreviewProvider: contextMenuContentPreviewProvider, + contextMenuActionProvider: contextMenuActionProvider + ) { + content + } previewAction: { + previewAction() + } + } +} + + + //public final class MediaGridContainerView: UIView { // // public static let maxCount = 9 diff --git a/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift index 2751d45b..8ddb02de 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift @@ -16,6 +16,13 @@ import Photos extension MediaView { public class ViewModel: ObservableObject, Hashable { + public static let durationFormatter: DateComponentsFormatter = { + let formatter = DateComponentsFormatter() + formatter.zeroFormattingBehavior = .pad + formatter.allowedUnits = [.minute, .second] + return formatter + }() + // input public let mediaKind: MediaKind public let aspectRatio: CGSize @@ -28,6 +35,13 @@ extension MediaView { // video duration in MS public let durationMS: Int? + @Published public var inContextMenuPreviewing = false + + // output + public var durationText: String? + public var thumbnail: UIImage? = nil + public var frameInWindow: CGRect = .zero + public init( mediaKind: MediaKind, aspectRatio: CGSize, @@ -44,6 +58,14 @@ extension MediaView { self.assetURL = assetURL self.downloadURL = downloadURL self.durationMS = durationMS + // end init + + self.durationText = durationMS.flatMap { durationMS -> String? in + let timeInterval = TimeInterval(durationMS / 1000) + guard timeInterval > 0 else { return nil } + guard let text = MediaView.ViewModel.durationFormatter.string(from: timeInterval) else { return nil } + return text + } } public static func == (lhs: MediaView.ViewModel, rhs: MediaView.ViewModel) -> Bool { diff --git a/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift b/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift index 1cfb62d8..ea8176d1 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift @@ -21,14 +21,32 @@ public struct MediaView: View { self.viewModel = viewModel } - public var body: some View { KFImage(viewModel.previewURL) + .onSuccess { result in + viewModel.thumbnail = result.image + } .cancelOnDisappear(true) .resizable() .placeholder { progress in Image(uiImage: Asset.Logo.mediaPlaceholder.image.withRenderingMode(.alwaysTemplate)) } + .overlay(alignment: .bottomTrailing) { + if let durationText = viewModel.durationText { + Label { + Text(durationText) + } icon: { + Image(systemName: "play.fill") + } + .font(.footnote) + .padding(.horizontal, 5) + .padding(.vertical, 3) + .background(.regularMaterial) + .cornerRadius(4) + .padding(EdgeInsets(top: 0, leading: 0, bottom: 8, trailing: 11)) + } + } + // Color.clear // .overlay { // KFImage(viewModel.previewURL) @@ -57,6 +75,44 @@ public struct MediaView: View { } +struct MediaView_Previews: PreviewProvider { + + static let viewModels = { + let models = [ + MediaView.ViewModel( + mediaKind: .photo, + aspectRatio: CGSize(width: 2048, height: 1186), + altText: nil, + previewURL: URL(string: "https://www.nasa.gov/sites/default/files/thumbnails/image/web_first_images_release.png"), + assetURL: URL(string: "https://www.nasa.gov/sites/default/files/thumbnails/image/web_first_images_release.png"), + downloadURL: URL(string: "https://www.nasa.gov/sites/default/files/thumbnails/image/web_first_images_release.png"), + durationMS: nil + ), + MediaView.ViewModel( + mediaKind: .video, + aspectRatio: CGSize(width: 1200, height: 675), + altText: nil, + previewURL: URL(string: "https://pbs.twimg.com/ext_tw_video_thumb/1630058212258115584/pu/img/slS0fYBeGKp8LXzC.jpg"), + assetURL: URL(string: "https://pbs.twimg.com/ext_tw_video_thumb/1630058212258115584/pu/img/slS0fYBeGKp8LXzC.jpg"), + downloadURL: URL(string: "https://video.twimg.com/ext_tw_video/1630058212258115584/pu/vid/1280x720/V-Jq9fMqwxTbZdxD.mp4?tag=12"), + durationMS: 27375 + ), + ] + return models + }() + + static var previews: some View { + Group { + ForEach(viewModels, id: \.self) { viewModel in + MediaView(viewModel: viewModel) + .frame(width: 300, height: 168) + .previewLayout(.fixed(width: 300, height: 168)) + .previewDisplayName(String(describing: viewModel.mediaKind)) + } + } + } +} + //public final class MediaView: UIView { // // var disposeBag = Set() diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index 83904eaf..1ca229b2 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -35,6 +35,7 @@ extension StatusView { @Published public var quoteViewModel: StatusView.ViewModel? // input + public let status: StatusRecord? public let kind: Kind public weak var delegate: StatusViewDelegate? @@ -161,10 +162,12 @@ extension StatusView { // } // init( + status: StatusRecord?, kind: Kind, delegate: StatusViewDelegate?, viewLayoutFramePublisher: Published.Publisher? ) { + self.status = status self.kind = kind self.delegate = delegate // end init @@ -957,6 +960,7 @@ extension StatusView.ViewModel { viewLayoutFramePublisher: Published.Publisher? ) { self.init( + status: .twitter(record: status.asRecrod), kind: kind, delegate: delegate, viewLayoutFramePublisher: viewLayoutFramePublisher @@ -1038,6 +1042,7 @@ extension StatusView.ViewModel { viewLayoutFramePublisher: Published.Publisher? ) { self.init( + status: .mastodon(record: status.asRecrod), kind: kind, delegate: delegate, viewLayoutFramePublisher: viewLayoutFramePublisher diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index 75d543e9..f0005108 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -48,7 +48,6 @@ public struct StatusView: View { // authorView authorView .padding(.horizontal, viewModel.margin) - .border(Color.cyan, width: 1) // content contentView .padding(.horizontal, viewModel.margin) @@ -57,7 +56,10 @@ public struct StatusView: View { MediaGridContainerView( viewModels: viewModel.mediaViewModels, idealWidth: contentWidth, - idealHeight: 280 + idealHeight: 280, + previewAction: { mediaViewModel in + viewModel.delegate?.statusView(viewModel, previewActionForMediaViewModel: mediaViewModel) + } ) .padding(.horizontal, viewModel.margin) } @@ -166,7 +168,6 @@ extension StatusView { ) .frame(width: contentWidth) } - .border(.red, width: 1) } } @@ -180,8 +181,9 @@ public protocol StatusViewDelegate: AnyObject { // // func statusView(_ statusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) // func statusView(_ statusView: StatusView, quoteStatusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) -// -// func statusView(_ statusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int) + + // media + func statusView(_ viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel) // func statusView(_ statusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, toggleContentWarningOverlayViewDisplay contentWarningOverlayView: ContentWarningOverlayView) // func statusView(_ statusView: StatusView, quoteStatusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int) // diff --git a/TwidereSDK/Sources/TwidereUI/ContextMenu/TimelineTableViewCellContextMenuConfiguration.swift b/TwidereSDK/Sources/TwidereUI/ContextMenu/TimelineTableViewCellContextMenuConfiguration.swift index f4a75d58..3d4fd858 100644 --- a/TwidereSDK/Sources/TwidereUI/ContextMenu/TimelineTableViewCellContextMenuConfiguration.swift +++ b/TwidereSDK/Sources/TwidereUI/ContextMenu/TimelineTableViewCellContextMenuConfiguration.swift @@ -13,5 +13,6 @@ public final class TimelineTableViewCellContextMenuConfiguration: UIContextMenuC public var indexPath: IndexPath? public var index: Int? + public var mediaViewModel: MediaView.ViewModel? } diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/ContextMenuInteractionRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/ContextMenuInteractionRepresentable.swift new file mode 100644 index 00000000..b22a7bd2 --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/ContextMenuInteractionRepresentable.swift @@ -0,0 +1,69 @@ +// +// ContextMenuInteractionRepresentable.swift +// +// +// Created by MainasuK on 2023/2/27. +// + +import UIKit +import SwiftUI + +struct ContextMenuInteractionRepresentable: UIViewRepresentable { + + let contextMenuContentPreviewProvider: UIContextMenuContentPreviewProvider + let contextMenuActionProvider: UIContextMenuActionProvider + @ViewBuilder var view: Content + let previewAction: () -> Void + + func makeUIView(context: Context) -> UIView { + let hostingController = UIHostingController(rootView: view) + hostingController.view.backgroundColor = .clear + context.coordinator.hostingViewController = hostingController + let interaction = UIContextMenuInteraction(delegate: context.coordinator) + hostingController.view.addInteraction(interaction) + hostingController.view.setContentHuggingPriority(.defaultHigh, for: .vertical) + return hostingController.view + } + + func updateUIView(_ view: UIView, context: Context) { + // do nothing + } + + func makeCoordinator() -> Coordinator { + return Coordinator(representable: self) + } + + class Coordinator: NSObject, UIContextMenuInteractionDelegate { + let representable: ContextMenuInteractionRepresentable + + var hostingViewController: UIHostingController? + + init(representable: ContextMenuInteractionRepresentable) { + self.representable = representable + } + + func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { + UIContextMenuConfiguration(identifier: nil, previewProvider: representable.contextMenuContentPreviewProvider, actionProvider: representable.contextMenuActionProvider) + } + + func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configuration: UIContextMenuConfiguration, highlightPreviewForItemWithIdentifier identifier: NSCopying) -> UITargetedPreview? { + guard let hostingViewController = self.hostingViewController else { return nil } + let parameters = UIPreviewParameters() + parameters.backgroundColor = .clear + parameters.visiblePath = UIBezierPath(roundedRect: hostingViewController.view.bounds, cornerRadius: MediaGridContainerView.cornerRadius) + return UITargetedPreview(view: hostingViewController.view, parameters: parameters) + } + + func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { + representable.previewAction() + } + + func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willDisplayMenuFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) { + print(#function) + } + + func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willEndFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) { + print(#function) + } + } +} diff --git a/TwidereSDK/Sources/TwidereUI/Utility/ViewHeightKey.swift b/TwidereSDK/Sources/TwidereUI/Utility/PreferenceKeys.swift similarity index 56% rename from TwidereSDK/Sources/TwidereUI/Utility/ViewHeightKey.swift rename to TwidereSDK/Sources/TwidereUI/Utility/PreferenceKeys.swift index 417f059e..6b5ae8ab 100644 --- a/TwidereSDK/Sources/TwidereUI/Utility/ViewHeightKey.swift +++ b/TwidereSDK/Sources/TwidereUI/Utility/PreferenceKeys.swift @@ -1,5 +1,5 @@ // -// ViewHeightKey.swift +// PreferenceKeys.swift // // // Created by MainasuK on 2023/2/9. @@ -14,3 +14,10 @@ struct ViewHeightKey: PreferenceKey { value = value + nextValue() } } + +struct ViewFrameKey: PreferenceKey { + static var defaultValue: CGRect { .zero } + static func reduce(value: inout CGRect, nextValue: () -> CGRect) { + value = nextValue() + } +} diff --git a/TwidereX/Diffable/Status/StatusSection.swift b/TwidereX/Diffable/Status/StatusSection.swift index fe29d3ce..1c47fecc 100644 --- a/TwidereX/Diffable/Status/StatusSection.swift +++ b/TwidereX/Diffable/Status/StatusSection.swift @@ -49,11 +49,7 @@ extension StatusSection { switch item { case .feed(let record): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell -// setupStatusPollDataSource( -// context: context, -// statusView: cell.statusView, -// configurationContext: configuration.statusViewConfigurationContext -// ) + cell.delegate = configuration.statusViewTableViewCellDelegate context.managedObjectContext.performAndWait { guard let feed = record.object(in: context.managedObjectContext) else { return } let _viewModel = StatusView.ViewModel( diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift index 1c2e18a7..eb5eb430 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift @@ -45,25 +45,25 @@ extension DataSourceFacade { status: StatusRecord, mediaPreviewContext: MediaPreviewContext ) async { - let _redirectRecord = await DataSourceFacade.status( - managedObjectContext: provider.context.managedObjectContext, - status: status, - target: target - ) - guard let redirectRecord = _redirectRecord else { return } - - await coordinateToMediaPreviewScene( - provider: provider, - status: redirectRecord, - mediaPreviewContext: mediaPreviewContext - ) - - Task { - await recordStatusHistory( - denpendency: provider, - status: status - ) - } // end Task +// let _redirectRecord = await DataSourceFacade.status( +// managedObjectContext: provider.context.managedObjectContext, +// status: status, +// target: target +// ) +// guard let redirectRecord = _redirectRecord else { return } +// +// await coordinateToMediaPreviewScene( +// provider: provider, +// status: redirectRecord, +// mediaPreviewContext: mediaPreviewContext +// ) +// +// Task { +// await recordStatusHistory( +// denpendency: provider, +// status: status +// ) +// } // end Task } } @@ -74,100 +74,88 @@ extension DataSourceFacade { static func coordinateToMediaPreviewScene( provider: DataSourceProvider & AuthContextProvider & MediaPreviewableViewController, status: StatusRecord, - mediaPreviewContext: MediaPreviewContext + statusViewModel: StatusView.ViewModel, + mediaViewModel: MediaView.ViewModel ) async { - let attachments: [AttachmentObject] = await provider.context.managedObjectContext.perform { - guard let status = status.object(in: provider.context.managedObjectContext) else { return [] } - return status.attachments +// let attachments: [AttachmentObject] = await provider.context.managedObjectContext.perform { +// guard let status = status.object(in: provider.context.managedObjectContext) else { return [] } +// return status.attachments +// } + guard let index = statusViewModel.mediaViewModels.firstIndex(of: mediaViewModel) else { + assertionFailure("invalid callback") + return } - let thumbnails = await mediaPreviewContext.thumbnails() + let thumbnails = statusViewModel.mediaViewModels.map { $0.thumbnail } // use standard video player - if let first = attachments.first, first.kind == .video || first.kind == .audio { - Task { @MainActor [weak provider] in - guard let provider = provider else { return } - // workaround Twitter Video assertURL missing from V2 API issue - var assetURL: URL - if let url = first.assetURL { - assetURL = url - } else if case let .twitter(record) = status { - let _statusID: String? = await provider.context.managedObjectContext.perform { - let status = record.object(in: provider.context.managedObjectContext) - return status?.id - } - guard let statusID = _statusID, - case let .twitter(authenticationContext) = provider.authContext.authenticationContext - else { return } - - let _response = try? await provider.context.apiService.twitterStatusV1(statusIDs: [statusID], authenticationContext: authenticationContext) - guard let status = _response?.value.first, - let url = status.extendedEntities?.media?.first?.assetURL.flatMap({ URL(string: $0) }) - else { return } - assetURL = url - } else { - assertionFailure() - return - } - let playerViewController = AVPlayerViewController() - playerViewController.player = AVPlayer(url: assetURL) - playerViewController.player?.play() - playerViewController.delegate = provider.context.playerService - provider.present(playerViewController, animated: true, completion: nil) - } // end Task - return - } +// if let first = attachments.first, first.kind == .video || first.kind == .audio { +// Task { @MainActor [weak provider] in +// guard let provider = provider else { return } +// // workaround Twitter Video assertURL missing from V2 API issue +// var assetURL: URL +// if let url = first.assetURL { +// assetURL = url +// } else if case let .twitter(record) = status { +// let _statusID: String? = await provider.context.managedObjectContext.perform { +// let status = record.object(in: provider.context.managedObjectContext) +// return status?.id +// } +// guard let statusID = _statusID, +// case let .twitter(authenticationContext) = provider.authContext.authenticationContext +// else { return } +// +// let _response = try? await provider.context.apiService.twitterStatusV1(statusIDs: [statusID], authenticationContext: authenticationContext) +// guard let status = _response?.value.first, +// let url = status.extendedEntities?.media?.first?.assetURL.flatMap({ URL(string: $0) }) +// else { return } +// assetURL = url +// } else { +// assertionFailure() +// return +// } +// let playerViewController = AVPlayerViewController() +// playerViewController.player = AVPlayer(url: assetURL) +// playerViewController.player?.play() +// playerViewController.delegate = provider.context.playerService +// provider.present(playerViewController, animated: true, completion: nil) +// } // end Task +// return +// } - let source: MediaPreviewTransitionItem.Source = { - switch mediaPreviewContext.containerView { - case .mediaView(let mediaView): - return .attachment(mediaView) - case .mediaGridContainerView(let mediaGridContainerView): - return .attachments(mediaGridContainerView) - } - }() -// await coordinateToMediaPreviewScene( -// provider: provider, -// status: status, -// mediaPreviewItem: .statusAttachment(.init( -// status: status, -// attachments: attachments, -// initialIndex: mediaPreviewContext.index, -// preloadThumbnails: thumbnails -// )), -// mediaPreviewTransitionItem: { -// // FIXME: allow other source -// let item = MediaPreviewTransitionItem( -// source: source, -// previewableViewController: provider -// ) -// let mediaView = mediaPreviewContext.mediaView -// -// item.initialFrame = { -// let initialFrame = mediaView.superview!.convert(mediaView.frame, to: nil) -// assert(initialFrame != .zero) -// return initialFrame -// }() -// -// let thumbnail = mediaView.thumbnail() -// item.image = thumbnail -// -// item.aspectRatio = { -// if let thumbnail = thumbnail { -// return thumbnail.size -// } -// let index = mediaPreviewContext.index -// guard index < attachments.count else { return nil } -// let size = attachments[index].size -// return size -// }() -// -// item.sourceImageViewCornerRadius = MediaView.cornerRadius -// -// return item -// }(), -// mediaPreviewContext: mediaPreviewContext -// ) + await coordinateToMediaPreviewScene( + provider: provider, + status: status, + mediaPreviewItem: .statusMedia(.init( + status: status, + mediaViewModels: statusViewModel.mediaViewModels, + initialIndex: index, + preloadThumbnails: thumbnails + )), + mediaPreviewTransitionItem: { + let source = MediaPreviewTransitionItem.Source.mediaView(mediaViewModel) + let item = MediaPreviewTransitionItem( + source: source, + previewableViewController: provider + ) + + item.initialFrame = mediaViewModel.frameInWindow + + let thumbnail = mediaViewModel.thumbnail + item.image = thumbnail + + item.aspectRatio = { + if let thumbnail = thumbnail { + return thumbnail.size + } + return mediaViewModel.aspectRatio + }() + + item.sourceImageViewCornerRadius = MediaGridContainerView.cornerRadius + + return item + }() + ) // end coordinateToMediaPreviewScene } @MainActor @@ -175,8 +163,7 @@ extension DataSourceFacade { provider: DataSourceProvider & AuthContextProvider & MediaPreviewableViewController, status: StatusRecord, mediaPreviewItem: MediaPreviewViewModel.Item, - mediaPreviewTransitionItem: MediaPreviewTransitionItem, - mediaPreviewContext: MediaPreviewContext + mediaPreviewTransitionItem: MediaPreviewTransitionItem ) async { let mediaPreviewViewModel = MediaPreviewViewModel( context: provider.context, @@ -190,5 +177,5 @@ extension DataSourceFacade { transition: .custom(transitioningDelegate: provider.mediaPreviewTransitionController) ) } - + } diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift index 1e7ac848..96a92667 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift @@ -166,34 +166,33 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider & MediaPreviewableViewController { func tableViewCell( _ cell: UITableViewCell, - statusView: StatusView, - mediaGridContainerView containerView: MediaGridContainerView, - didTapMediaView mediaView: MediaView, - at index: Int + viewModel: StatusView.ViewModel, + previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel ) { Task { - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { + guard let status = viewModel.status else { assertionFailure() return } - guard let status = await item.status(in: self.context.managedObjectContext) else { - assertionFailure("only works for status data provider") - return - } // end switch await DataSourceFacade.coordinateToMediaPreviewScene( provider: self, - target: .status, status: status, - mediaPreviewContext: DataSourceFacade.MediaPreviewContext( - containerView: .mediaGridContainerView(containerView), - mediaView: mediaView, - index: index - ) + statusViewModel: viewModel, + mediaViewModel: mediaViewModel ) } // end Task } + + func tableViewCell( + _ cell: UITableViewCell, + statusView: StatusView, + mediaGridContainerView containerView: MediaGridContainerView, + didTapMediaView mediaView: MediaView, + at index: Int + ) { + + } func tableViewCell( _ cell: UITableViewCell, diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift index 2df8fcff..3278820a 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -8,6 +8,7 @@ import UIKit import TwidereUI +import Photos extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvider { @@ -58,178 +59,9 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid } extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvider & MediaPreviewableViewController { - + func aspectTableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - -// guard let cell = tableView.cellForRow(at: indexPath) as? StatusViewContainerTableViewCell else { return nil } -// -// defer { -// Task { -// guard let item = await item(from: .init(tableViewCell: cell, indexPath: indexPath)) else { return } -// guard let status = await item.status(in: context.managedObjectContext) else { return } -// await DataSourceFacade.recordStatusHistory( -// denpendency: self, -// status: status -// ) -// } // end Task -// } -// -// // TODO: -// // this must call before check `isContentWarningOverlayDisplay`. otherwise, will get BadAccess exception -// let mediaViews = cell.statusView.mediaGridContainerView.mediaViews -// -// if cell.statusView.mediaGridContainerView.viewModel.isContentWarningOverlayDisplay == true { -// return nil -// } -// -// for (i, mediaView) in mediaViews.enumerated() { -// let pointInMediaView = mediaView.convert(point, from: tableView) -// guard mediaView.point(inside: pointInMediaView, with: nil) else { -// continue -// } -// guard let image = mediaView.thumbnail(), -// let assetURLString = mediaView.configuration?.downloadURL, -// let assetURL = URL(string: assetURLString), -// let resourceType = mediaView.configuration?.resourceType -// else { -// // not provide preview unless thumbnail ready -// return nil -// } -// -// let contextMenuImagePreviewViewModel = ContextMenuImagePreviewViewModel(aspectRatio: image.size, thumbnail: image) -// -// let configuration = TimelineTableViewCellContextMenuConfiguration(identifier: nil) { () -> UIViewController? in -// if UIDevice.current.userInterfaceIdiom == .pad && mediaViews.count == 1 { -// return nil -// } -// let previewProvider = ContextMenuImagePreviewViewController() -// previewProvider.viewModel = contextMenuImagePreviewViewModel -// return previewProvider -// -// } actionProvider: { _ -> UIMenu? in -// return UIMenu( -// title: "", -// image: nil, -// identifier: nil, -// options: [], -// children: [ -// UIAction( -// title: L10n.Common.Controls.Actions.save, -// image: UIImage(systemName: "square.and.arrow.down"), -// attributes: [], -// state: .off -// ) { [weak self] _ in -// guard let self = self else { return } -// Task { @MainActor in -// let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) -// let notificationFeedbackGenerator = UINotificationFeedbackGenerator() -// -// do { -// impactFeedbackGenerator.impactOccurred() -// try await self.context.photoLibraryService.save( -// source: .remote(url: assetURL), -// resourceType: resourceType -// ) -// self.context.photoLibraryService.presentSuccessNotification(title: L10n.Common.Alerts.PhotoSaved.title) -// notificationFeedbackGenerator.notificationOccurred(.success) -// } catch { -// self.context.photoLibraryService.presentFailureNotification( -// error: error, -// title: L10n.Common.Alerts.PhotoSaveFail.title, -// message: L10n.Common.Alerts.PhotoSaveFail.message -// ) -// notificationFeedbackGenerator.notificationOccurred(.error) -// } -// } // end Task -// }, -// UIAction( -// title: L10n.Common.Controls.Actions.copy, -// image: UIImage(systemName: "doc.on.doc"), -// attributes: [], -// state: .off -// ) { [weak self] _ in -// guard let self = self else { return } -// Task { @MainActor in -// let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) -// let notificationFeedbackGenerator = UINotificationFeedbackGenerator() -// -// do { -// impactFeedbackGenerator.impactOccurred() -// try await self.context.photoLibraryService.copy( -// source: .remote(url: assetURL), -// resourceType: resourceType -// ) -// self.context.photoLibraryService.presentSuccessNotification(title: L10n.Common.Alerts.PhotoCopied.title) -// notificationFeedbackGenerator.notificationOccurred(.success) -// } catch { -// self.context.photoLibraryService.presentFailureNotification( -// error: error, -// title: L10n.Common.Alerts.PhotoCopied.title, -// message: L10n.Common.Alerts.PhotoCopyFail.message -// ) -// notificationFeedbackGenerator.notificationOccurred(.error) -// } -// } // end Task -// }, -// UIMenu( -// title: L10n.Common.Controls.Actions.share, -// image: UIImage(systemName: "square.and.arrow.up"), -// identifier: nil, -// options: [], -// children: [ -// UIAction( -// title: L10n.Common.Controls.Actions.ShareMediaMenu.link, -// image: UIImage(systemName: "link"), -// attributes: [], -// state: .off -// ) { [weak self] _ in -// guard let self = self else { return } -// Task { @MainActor in -// let applicationActivities: [UIActivity] = [ -// SafariActivity(sceneCoordinator: self.coordinator) -// ] -// let activityViewController = UIActivityViewController( -// activityItems: [assetURL], -// applicationActivities: applicationActivities -// ) -// activityViewController.popoverPresentationController?.sourceView = mediaView -// self.present(activityViewController, animated: true, completion: nil) -// } // end Task -// }, -// UIAction( -// title: L10n.Common.Controls.Actions.ShareMediaMenu.media, -// image: UIImage(systemName: "photo"), -// attributes: [], -// state: .off -// ) { [weak self] _ in -// guard let self = self else { return } -// Task { @MainActor in -// let applicationActivities: [UIActivity] = [ -// SafariActivity(sceneCoordinator: self.coordinator) -// ] -// // FIXME: handle error -// guard let url = try await self.context.photoLibraryService.file(from: .remote(url: assetURL)) else { -// return -// } -// let activityViewController = UIActivityViewController( -// activityItems: [url], -// applicationActivities: applicationActivities -// ) -// activityViewController.popoverPresentationController?.sourceView = mediaView -// self.present(activityViewController, animated: true, completion: nil) -// } // end Task -// }, -// ] -// ), -// ] // end children -// ) // end return UIMenu -// } -// configuration.indexPath = indexPath -// configuration.index = i -// return configuration -// } // end for … in … - return nil } @@ -255,19 +87,6 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid configuration: UIContextMenuConfiguration ) -> UITargetedPreview? { return nil -// guard let configuration = configuration as? TimelineTableViewCellContextMenuConfiguration else { return nil } -// guard let indexPath = configuration.indexPath, let index = configuration.index else { return nil } -// if let cell = tableView.cellForRow(at: indexPath) as? StatusViewContainerTableViewCell { -// let mediaViews = cell.statusView.mediaGridContainerView.mediaViews -// guard index < mediaViews.count else { return nil } -// let mediaView = mediaViews[index] -// let parameters = UIPreviewParameters() -// parameters.backgroundColor = .clear -// parameters.visiblePath = UIBezierPath(roundedRect: mediaView.bounds, cornerRadius: MediaView.cornerRadius) -// return UITargetedPreview(view: mediaView, parameters: parameters) -// } else { -// return nil -// } } func aspectTableView( @@ -276,37 +95,6 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid animator: UIContextMenuInteractionCommitAnimating ) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") -// guard let configuration = configuration as? TimelineTableViewCellContextMenuConfiguration else { return } -// guard let indexPath = configuration.indexPath, let index = configuration.index else { return } -// guard let cell = tableView.cellForRow(at: indexPath) as? StatusViewContainerTableViewCell else { return } -// let mediaViews = cell.statusView.mediaGridContainerView.mediaViews -// guard index < mediaViews.count else { return } -// let mediaView = mediaViews[index] -// -// animator.addCompletion { -// Task { [weak self] in -// guard let self = self else { return } -// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) -// guard let item = await self.item(from: source) else { -// assertionFailure() -// return -// } -// guard let status = await item.status(in: self.context.managedObjectContext) else { -// assertionFailure("only works for status data provider") -// return -// } -// await DataSourceFacade.coordinateToMediaPreviewScene( -// provider: self, -// target: .status, -// status: status, -// mediaPreviewContext: DataSourceFacade.MediaPreviewContext( -// containerView: .mediaGridContainerView(cell.statusView.mediaGridContainerView), -// mediaView: mediaView, -// index: index -// ) -// ) -// } // end Task -// } } // end func } diff --git a/TwidereX/Scene/MediaPreview/MediaPreviewViewModel.swift b/TwidereX/Scene/MediaPreview/MediaPreviewViewModel.swift index f247e93b..9e245ceb 100644 --- a/TwidereX/Scene/MediaPreview/MediaPreviewViewModel.swift +++ b/TwidereX/Scene/MediaPreview/MediaPreviewViewModel.swift @@ -45,7 +45,7 @@ final class MediaPreviewViewModel: NSObject { self.item = item self.currentPage = { switch item { - case .statusAttachment(let previewContext): + case .statusMedia(let previewContext): return previewContext.initialIndex case .image: return 0 @@ -55,7 +55,7 @@ final class MediaPreviewViewModel: NSObject { // setup output self.status = { switch item { - case .statusAttachment(let previewContext): + case .statusMedia(let previewContext): let status = previewContext.status.object(in: context.managedObjectContext) return status case .image: @@ -65,15 +65,15 @@ final class MediaPreviewViewModel: NSObject { self.viewControllers = { var viewControllers: [UIViewController] = [] switch item { - case .statusAttachment(let previewContext): - for (i, attachment) in previewContext.attachments.enumerated() { - switch attachment.kind { - case .image: + case .statusMedia(let previewContext): + for (i, mediaViewModel) in previewContext.mediaViewModels.enumerated() { + switch mediaViewModel.mediaKind { + case .photo: let viewController = MediaPreviewImageViewController() viewController.viewModel = MediaPreviewImageViewModel( context: context, item: .remote(.init( - assetURL: attachment.assetURL, + assetURL: mediaViewModel.assetURL, thumbnail: previewContext.thumbnail(at: i) )) ) @@ -83,23 +83,21 @@ final class MediaPreviewViewModel: NSObject { viewController.viewModel = MediaPreviewVideoViewModel( context: context, item: .video(.init( - assetURL: attachment.assetURL, - previewURL: attachment.previewURL + assetURL: mediaViewModel.assetURL, + previewURL: mediaViewModel.previewURL )) ) viewControllers.append(viewController) - case .gif: + case .animatedGIF: let viewController = MediaPreviewVideoViewController() viewController.viewModel = MediaPreviewVideoViewModel( context: context, item: .gif(.init( - assetURL: attachment.assetURL, - previewURL: attachment.previewURL + assetURL: mediaViewModel.assetURL, + previewURL: mediaViewModel.previewURL )) ) viewControllers.append(viewController) - case .audio: - viewControllers.append(UIViewController()) } } case .image(let previewContext): @@ -132,13 +130,13 @@ final class MediaPreviewViewModel: NSObject { extension MediaPreviewViewModel { enum Item { - case statusAttachment(StatusAttachmentPreviewContext) + case statusMedia(StatusMediaPreviewContext) case image(ImagePreviewContext) } - struct StatusAttachmentPreviewContext { + struct StatusMediaPreviewContext { let status: StatusRecord - let attachments: [AttachmentObject] + let mediaViewModels: [MediaView.ViewModel] let initialIndex: Int let preloadThumbnails: [UIImage?] @@ -171,7 +169,7 @@ extension MediaPreviewViewModel: PageboyViewControllerDataSource { func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? { switch item { - case .statusAttachment(let previewContext): + case .statusMedia(let previewContext): return .at(index: previewContext.initialIndex) case .image: return .first diff --git a/TwidereX/Scene/MediaPreview/Video/MediaPreviewVideoViewController.swift b/TwidereX/Scene/MediaPreview/Video/MediaPreviewVideoViewController.swift index 9e2d04fc..d17437d0 100644 --- a/TwidereX/Scene/MediaPreview/Video/MediaPreviewVideoViewController.swift +++ b/TwidereX/Scene/MediaPreview/Video/MediaPreviewVideoViewController.swift @@ -92,6 +92,13 @@ extension MediaPreviewVideoViewController { } } + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + // fix player not respect safe area issue + playerViewController.didMove(toParent: self) + } + } // MARK: - ShareActivityProvider diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift index a62fb71c..627fad66 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift @@ -18,20 +18,16 @@ class StatusTableViewCell: UITableViewCell { let logger = Logger(subsystem: "StatusTableViewCell", category: "View") -// weak var delegate: StatusViewTableViewCellDelegate? - -// let topConversationLinkLineView = SeparatorLineView() -// let statusView = StatusView() -// let bottomConversationLinkLineView = SeparatorLineView() -// let separator = SeparatorLineView() + weak var delegate: StatusViewTableViewCellDelegate? + var viewModel: StatusView.ViewModel? override func prepareForReuse() { super.prepareForReuse() contentConfiguration = nil + delegate = nil + viewModel = nil disposeBag.removeAll() -// topConversationLinkLineView.isHidden = true -// bottomConversationLinkLineView.isHidden = true } override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -113,7 +109,7 @@ extension StatusTableViewCell { } // MARK: - StatusViewContainerTableViewCell -//extension StatusTableViewCell: StatusViewContainerTableViewCell { } +extension StatusTableViewCell: StatusViewContainerTableViewCell { } // MARK: - StatusViewDelegate extension StatusTableViewCell: StatusViewDelegate { } diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift index ef9016fb..93925ca6 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift @@ -12,11 +12,11 @@ import MetaTextArea import Meta // sourcery: protocolName = "StatusViewDelegate" -// sourcery: replaceOf = "statusView(statusView" -// sourcery: replaceWith = "delegate?.tableViewCell(self, statusView: statusView" +// sourcery: replaceOf = "statusView(viewModel" +// sourcery: replaceWith = "delegate?.tableViewCell(self, viewModel: viewModel" protocol StatusViewContainerTableViewCell: UITableViewCell, AutoGenerateProtocolRelayDelegate { var delegate: StatusViewTableViewCellDelegate? { get } - var statusView: StatusView { get } + var viewModel: StatusView.ViewModel? { get } } // MARK: - AutoGenerateProtocolDelegate @@ -25,6 +25,7 @@ protocol StatusViewContainerTableViewCell: UITableViewCell, AutoGenerateProtocol // sourcery: replaceWith = "func tableViewCell(_ cell: UITableViewCell," protocol StatusViewTableViewCellDelegate: AutoGenerateProtocolDelegate { // sourcery:inline:StatusViewTableViewCellDelegate.AutoGenerateProtocolDelegate + func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel) // sourcery:end } @@ -32,5 +33,8 @@ protocol StatusViewTableViewCellDelegate: AutoGenerateProtocolDelegate { // Protocol Extension extension StatusViewDelegate where Self: StatusViewContainerTableViewCell { // sourcery:inline:StatusViewContainerTableViewCell.AutoGenerateProtocolRelayDelegate + func statusView(_ viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel) { + delegate?.tableViewCell(self, viewModel: viewModel, previewActionForMediaViewModel: mediaViewModel) + } // sourcery:end } diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift index cced6953..9a01e603 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift @@ -14,7 +14,6 @@ import CoreData import CoreDataStack import TwitterSDK import ZIPFoundation -import FLEX import MetaTextKit import MetaTextArea import TwidereUI @@ -243,10 +242,6 @@ extension HomeTimelineViewController { identifier: nil, options: [], children: [ - UIAction(title: "Enable FLEX", image: nil, attributes: [], handler: { [weak self] action in - guard let self = self else { return } - self.showFLEXAction(action) - }), UIAction(title: "Display TextView Frame", image: nil, attributes: [], handler: { [weak self] action in guard let self = self else { return } self.displayTextViewFrame(action) @@ -266,11 +261,7 @@ extension HomeTimelineViewController { } extension HomeTimelineViewController { - - @objc private func showFLEXAction(_ sender: UIAction) { - FLEXManager.shared.showExplorer() - } - + @objc private func showStatusByID(_ id: String) { Task { @MainActor in let authenticationContext = self.viewModel.authContext.authenticationContext diff --git a/TwidereX/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift b/TwidereX/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift index ddc40e45..b12bb5df 100644 --- a/TwidereX/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift +++ b/TwidereX/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift @@ -244,44 +244,44 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { } // calculate transition mask - let maskLayerToRect: CGRect? = { - guard case .attachments = transitionItem.source else { return nil } - guard let navigationBar = toVC.navigationController?.navigationBar, let navigationBarSuperView = navigationBar.superview else { return nil } - let navigationBarFrameInWindow = navigationBarSuperView.convert(navigationBar.frame, to: nil) - - // crop rect top edge - var rect = transitionMaskView.frame - let _toViewFrameInWindow = toVC.view.superview.flatMap { $0.convert(toVC.view.frame, to: nil) } - if let toViewFrameInWindow = _toViewFrameInWindow, toViewFrameInWindow.minY > navigationBarFrameInWindow.maxY { - rect.origin.y = toViewFrameInWindow.minY - } else { - rect.origin.y = navigationBarFrameInWindow.maxY + UIView.separatorLineHeight(of: toVC.view) // extra hairline - } - - return rect - }() - let maskLayerToPath = maskLayerToRect.flatMap { UIBezierPath(rect: $0) }?.cgPath - let maskLayerToFinalRect: CGRect? = { - guard case .attachments = transitionItem.source else { return nil } - var rect = maskLayerToRect ?? transitionMaskView.frame - // clip tabBar when bar visible - guard let tabBarController = toVC.tabBarController, - !tabBarController.tabBar.isHidden, - let tabBarSuperView = tabBarController.tabBar.superview - else { return rect } - let tabBarFrameInWindow = tabBarSuperView.convert(tabBarController.tabBar.frame, to: nil) - let offset = rect.maxY - tabBarFrameInWindow.minY - guard offset > 0 else { return rect } - rect.size.height -= offset - return rect - }() - - // FIXME: - let maskLayerToFinalPath = maskLayerToFinalRect.flatMap { UIBezierPath(rect: $0) }?.cgPath - - if let maskLayerToPath = maskLayerToPath { - maskLayer.path = maskLayerToPath - } +// let maskLayerToRect: CGRect? = { +// guard case .attachments = transitionItem.source else { return nil } +// guard let navigationBar = toVC.navigationController?.navigationBar, let navigationBarSuperView = navigationBar.superview else { return nil } +// let navigationBarFrameInWindow = navigationBarSuperView.convert(navigationBar.frame, to: nil) +// +// // crop rect top edge +// var rect = transitionMaskView.frame +// let _toViewFrameInWindow = toVC.view.superview.flatMap { $0.convert(toVC.view.frame, to: nil) } +// if let toViewFrameInWindow = _toViewFrameInWindow, toViewFrameInWindow.minY > navigationBarFrameInWindow.maxY { +// rect.origin.y = toViewFrameInWindow.minY +// } else { +// rect.origin.y = navigationBarFrameInWindow.maxY + UIView.separatorLineHeight(of: toVC.view) // extra hairline +// } +// +// return rect +// }() +// let maskLayerToPath = maskLayerToRect.flatMap { UIBezierPath(rect: $0) }?.cgPath +// let maskLayerToFinalRect: CGRect? = { +// guard case .attachments = transitionItem.source else { return nil } +// var rect = maskLayerToRect ?? transitionMaskView.frame +// // clip tabBar when bar visible +// guard let tabBarController = toVC.tabBarController, +// !tabBarController.tabBar.isHidden, +// let tabBarSuperView = tabBarController.tabBar.superview +// else { return rect } +// let tabBarFrameInWindow = tabBarSuperView.convert(tabBarController.tabBar.frame, to: nil) +// let offset = rect.maxY - tabBarFrameInWindow.minY +// guard offset > 0 else { return rect } +// rect.size.height -= offset +// return rect +// }() +// +// // FIXME: +// let maskLayerToFinalPath = maskLayerToFinalRect.flatMap { UIBezierPath(rect: $0) }?.cgPath +// +// if let maskLayerToPath = maskLayerToPath { +// maskLayer.path = maskLayerToPath +// } } mediaPreviewTransitionContext.transitionView.isHidden = true @@ -420,47 +420,47 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { let toVC = transitionItem.previewableViewController var needsMaskWithAnimation = true - let maskLayerToRect: CGRect? = { - guard case .attachments = transitionItem.source else { return nil } - guard let navigationBar = toVC.navigationController?.navigationBar, let navigationBarSuperView = navigationBar.superview else { return nil } - let navigationBarFrameInWindow = navigationBarSuperView.convert(navigationBar.frame, to: nil) - - // crop rect top edge - var rect = transitionMaskView.frame - let _toViewFrameInWindow = toVC.view.superview.flatMap { $0.convert(toVC.view.frame, to: nil) } - if let toViewFrameInWindow = _toViewFrameInWindow, toViewFrameInWindow.minY > navigationBarFrameInWindow.maxY { - rect.origin.y = toViewFrameInWindow.minY - } else { - rect.origin.y = navigationBarFrameInWindow.maxY + UIView.separatorLineHeight(of: toVC.view) // extra hairline - } - - if rect.minY < snapshot.frame.minY { - needsMaskWithAnimation = false - } - - return rect - }() - let maskLayerToPath = maskLayerToRect.flatMap { UIBezierPath(rect: $0) }?.cgPath - - if let maskLayer = transitionItem.interactiveTransitionMaskLayer, !needsMaskWithAnimation { - maskLayer.path = maskLayerToPath - } - - let maskLayerToFinalRect: CGRect? = { - guard case .attachments = transitionItem.source else { return nil } - var rect = maskLayerToRect ?? transitionMaskView.frame - // clip rect bottom when tabBar visible - guard let tabBarController = toVC.tabBarController, - !tabBarController.tabBar.isHidden, - let tabBarSuperView = tabBarController.tabBar.superview - else { return rect } - let tabBarFrameInWindow = tabBarSuperView.convert(tabBarController.tabBar.frame, to: nil) - let offset = rect.maxY - tabBarFrameInWindow.minY - guard offset > 0 else { return rect } - rect.size.height -= offset - return rect - }() - maskLayerToFinalPath = maskLayerToFinalRect.flatMap { UIBezierPath(rect: $0) }?.cgPath +// let maskLayerToRect: CGRect? = { +// guard case .attachments = transitionItem.source else { return nil } +// guard let navigationBar = toVC.navigationController?.navigationBar, let navigationBarSuperView = navigationBar.superview else { return nil } +// let navigationBarFrameInWindow = navigationBarSuperView.convert(navigationBar.frame, to: nil) +// +// // crop rect top edge +// var rect = transitionMaskView.frame +// let _toViewFrameInWindow = toVC.view.superview.flatMap { $0.convert(toVC.view.frame, to: nil) } +// if let toViewFrameInWindow = _toViewFrameInWindow, toViewFrameInWindow.minY > navigationBarFrameInWindow.maxY { +// rect.origin.y = toViewFrameInWindow.minY +// } else { +// rect.origin.y = navigationBarFrameInWindow.maxY + UIView.separatorLineHeight(of: toVC.view) // extra hairline +// } +// +// if rect.minY < snapshot.frame.minY { +// needsMaskWithAnimation = false +// } +// +// return rect +// }() +// let maskLayerToPath = maskLayerToRect.flatMap { UIBezierPath(rect: $0) }?.cgPath +// +// if let maskLayer = transitionItem.interactiveTransitionMaskLayer, !needsMaskWithAnimation { +// maskLayer.path = maskLayerToPath +// } +// +// let maskLayerToFinalRect: CGRect? = { +// guard case .attachments = transitionItem.source else { return nil } +// var rect = maskLayerToRect ?? transitionMaskView.frame +// // clip rect bottom when tabBar visible +// guard let tabBarController = toVC.tabBarController, +// !tabBarController.tabBar.isHidden, +// let tabBarSuperView = tabBarController.tabBar.superview +// else { return rect } +// let tabBarFrameInWindow = tabBarSuperView.convert(tabBarController.tabBar.frame, to: nil) +// let offset = rect.maxY - tabBarFrameInWindow.minY +// guard offset > 0 else { return rect } +// rect.size.height -= offset +// return rect +// }() +// maskLayerToFinalPath = maskLayerToFinalRect.flatMap { UIBezierPath(rect: $0) }?.cgPath } itemAnimator.addAnimations { diff --git a/TwidereX/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift b/TwidereX/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift index 34203093..75eac63b 100644 --- a/TwidereX/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift +++ b/TwidereX/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift @@ -48,8 +48,7 @@ class MediaPreviewTransitionItem: Identifiable { extension MediaPreviewTransitionItem { enum Source { case none - case attachment(MediaView) - case attachments(MediaGridContainerView) + case mediaView(MediaView.ViewModel) case profileAvatar(ProfileHeaderView) case profileBanner(ProfileHeaderView) diff --git a/TwidereX/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift b/TwidereX/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift index d55407da..8583f199 100644 --- a/TwidereX/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift +++ b/TwidereX/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift @@ -24,14 +24,9 @@ extension MediaPreviewableViewController { height: 44 ) return frame - case .attachment(let mediaView): - return nil -// return mediaView.superview?.convert(mediaView.frame, to: nil) - case .attachments(let mediaGridContainerView): - return nil -// guard index < mediaGridContainerView.mediaViews.count else { return nil } -// let mediaView = mediaGridContainerView.mediaViews[index] -// return mediaView.superview?.convert(mediaView.frame, to: nil) + case .mediaView(let mediaViewModel): + guard mediaViewModel.frameInWindow != .zero else { return nil } + return mediaViewModel.frameInWindow case .profileAvatar: return nil // TODO: case .profileBanner: From 210c61a85291c42ef383932fb8bcb6ca8ded10da Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 27 Feb 2023 19:23:18 +0800 Subject: [PATCH 033/128] feat: add video duration and GIF mark for media view --- .../Content/MediaView+ViewModel.swift | 21 ++++++++++++ .../Sources/TwidereUI/Content/MediaView.swift | 33 ++++++++++++------- .../Content/StatusView+ViewModel.swift | 13 ++++++++ 3 files changed, 55 insertions(+), 12 deletions(-) diff --git a/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift index 8ddb02de..64133672 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift @@ -219,6 +219,27 @@ extension MediaView.ViewModel { ) } } + + public static func viewModels(from status: MastodonStatus) -> [MediaView.ViewModel] { + return status.attachments.map { attachment -> MediaView.ViewModel in + MediaView.ViewModel( + mediaKind: { + switch attachment.kind { + case .image: return .photo + case .video: return .video + case .audio: return .video + case .gifv: return .animatedGIF + } + }(), + aspectRatio: attachment.size, + altText: attachment.altDescription, + previewURL: (attachment.previewURL ?? attachment.assetURL).flatMap { URL(string: $0) }, + assetURL: attachment.assetURL.flatMap { URL(string: $0) }, + downloadURL: attachment.downloadURL.flatMap { URL(string: $0) }, + durationMS: attachment.durationMS + ) + } + } // public static func configuration(mastodonStatus status: MastodonStatus) -> [MediaView.Configuration] { // func videoInfo(from attachment: MastodonAttachment) -> MediaView.Configuration.VideoInfo { diff --git a/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift b/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift index ea8176d1..bd54bb61 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift @@ -31,20 +31,29 @@ public struct MediaView: View { .placeholder { progress in Image(uiImage: Asset.Logo.mediaPlaceholder.image.withRenderingMode(.alwaysTemplate)) } - .overlay(alignment: .bottomTrailing) { - if let durationText = viewModel.durationText { - Label { - Text(durationText) - } icon: { - Image(systemName: "play.fill") + .overlay(alignment: .bottom) { + HStack { + Spacer() + if let durationText = viewModel.durationText { + Text("\(Image(systemName: "play.fill")) \(durationText)") + .foregroundColor(Color(uiColor: .label)) + .font(.system(.footnote, design: .default, weight: .medium)) + .padding(.horizontal, 5) + .padding(.vertical, 3) + .background(.regularMaterial) + .cornerRadius(4) + } + if viewModel.mediaKind == .animatedGIF { + Text("GIF") + .foregroundColor(Color(uiColor: .label)) + .font(.system(.footnote, design: .default, weight: .medium)) + .padding(.horizontal, 5) + .padding(.vertical, 3) + .background(.regularMaterial) + .cornerRadius(4) } - .font(.footnote) - .padding(.horizontal, 5) - .padding(.vertical, 3) - .background(.regularMaterial) - .cornerRadius(4) - .padding(EdgeInsets(top: 0, leading: 0, bottom: 8, trailing: 11)) } + .padding(EdgeInsets(top: 0, leading: 11, bottom: 8, trailing: 11)) } // Color.clear diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index 1ca229b2..414d0a2a 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -1059,6 +1059,7 @@ extension StatusView.ViewModel { ) } + // author status.author.publisher(for: \.avatar) .compactMap { $0.flatMap { URL(string: $0) } } .assign(to: &$avatarURL) @@ -1068,6 +1069,15 @@ extension StatusView.ViewModel { status.author.publisher(for: \.username) .assign(to: &$authorUsernme) + // timestamp + switch kind { + case .timeline, .repost: + timestampLabelViewModel = TimestampLabelView.ViewModel(timestamp: status.createdAt) + default: + break + } + + // content do { let content = MastodonContent(content: status.content, emojis: status.emojis.asDictionary) let metaContent = try MastodonMetaContent.convert(document: content, useParagraphMark: true) @@ -1077,5 +1087,8 @@ extension StatusView.ViewModel { assertionFailure(error.localizedDescription) self.content = PlaintextMetaContent(string: "") } + + // media + mediaViewModels = MediaView.ViewModel.viewModels(from: status) } } From 8f92c6efc63fce7ba0444b36eee21f19459ef622 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 28 Feb 2023 14:51:54 +0800 Subject: [PATCH 034/128] feat: make GIFV media auto play --- .../Container/MediaGridContainerView.swift | 206 +++++----- .../Sources/TwidereUI/Content/MediaView.swift | 382 +----------------- .../GIFVideoPlayerRepresentable.swift | 83 ++++ .../MediaPreviewVideoViewController.swift | 1 + 4 files changed, 199 insertions(+), 473 deletions(-) create mode 100644 TwidereSDK/Sources/TwidereUI/UIViewRepresentable/GIFVideoPlayerRepresentable.swift diff --git a/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift b/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift index d2876846..003e0228 100644 --- a/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift +++ b/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift @@ -9,12 +9,6 @@ import os.log import UIKit import SwiftUI -import func AVFoundation.AVMakeRect - -public protocol MediaGridContainerViewDelegate: AnyObject { - func mediaGridContainerView(_ container: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int) - func mediaGridContainerView(_ container: MediaGridContainerView, toggleContentWarningOverlayViewDisplay contentWarningOverlayView: ContentWarningOverlayView) -} public struct MediaGridContainerView: View { @@ -32,55 +26,7 @@ public struct MediaGridContainerView: View { VStack { switch viewModels.count { case 1: - let viewModel = viewModels[0] - MediaView(viewModel: viewModel) - .modifier(MediaViewFrameModifer( - asepctRatio: viewModel.aspectRatio.width / viewModel.aspectRatio.height, - idealWidth: idealWidth, - idealHeight: viewModel.mediaKind == .video ? idealHeight : 2 * idealHeight) - ) - .cornerRadius(MediaGridContainerView.cornerRadius) - .clipped() - .background(GeometryReader { proxy in - Color.clear.preference( - key: ViewFrameKey.self, - value: proxy.frame(in: .global) - ) - .onPreferenceChange(ViewFrameKey.self) { frame in - viewModel.frameInWindow = frame - } - }) - .overlay( - RoundedRectangle(cornerRadius: MediaGridContainerView.cornerRadius) - .stroke(Color(uiColor: .placeholderText).opacity(0.5), lineWidth: 1) - ) - .contentShape(Rectangle()) - .onTapGesture { - // let actionContext = ActionContext(index: index, viewModels: viewModels) - // action(actionContext) - } - .contextMenu(contextMenuContentPreviewProvider: { - guard let thumbnail = viewModel.thumbnail else { return nil } - let contextMenuImagePreviewViewModel = ContextMenuImagePreviewViewModel(aspectRatio: thumbnail.size, thumbnail: thumbnail) - let previewProvider = ContextMenuImagePreviewViewController() - previewProvider.viewModel = contextMenuImagePreviewViewModel - return previewProvider - - }, contextMenuActionProvider: { _ in - let children: [UIAction] = [ - UIAction( - title: L10n.Common.Controls.Actions.copy, - image: UIImage(systemName: "doc.on.doc"), - attributes: [], - state: .off - ) { _ in - print("Hi copy") - } - ] - return UIMenu(title: "", image: nil, identifier: nil, options: [], children: children) - }, previewAction: { - previewAction(viewModel) - }) + mediaView(at: 0, width: idealWidth, height: idealHeight) case 2: let height = height(for: 1) HStack(spacing: MediaGridContainerView.spacing) { @@ -224,57 +170,90 @@ extension MediaGridContainerView { extension MediaGridContainerView { private func mediaView(at index: Int, width: CGFloat?, height: CGFloat?) -> some View { - Rectangle() - .fill(Color(uiColor: .placeholderText)) - .frame(width: width, height: height) - .overlay( - MediaView(viewModel: viewModels[index]) - .aspectRatio(contentMode: .fill) - ) - .cornerRadius(MediaGridContainerView.cornerRadius) - .clipped() - .background(GeometryReader { proxy in - Color.clear.preference( - key: ViewFrameKey.self, - value: proxy.frame(in: .global) - ) - .onPreferenceChange(ViewFrameKey.self) { frame in - viewModels[index].frameInWindow = frame - } - }) - .overlay( - RoundedRectangle(cornerRadius: MediaGridContainerView.cornerRadius) - .stroke(Color(uiColor: .placeholderText).opacity(0.5), lineWidth: 1) + Group { + let viewModel = viewModels[index] + switch viewModels.count { + case 1: + MediaView(viewModel: viewModel) + .modifier(MediaViewFrameModifer( + asepctRatio: viewModel.aspectRatio.width / viewModel.aspectRatio.height, + idealWidth: idealWidth, + idealHeight: viewModel.mediaKind == .video ? idealHeight : 2 * idealHeight) + ) + default: + Rectangle() + .fill(Color(uiColor: .placeholderText)) + .frame(width: width, height: height) + .overlay( + MediaView(viewModel: viewModel) + .aspectRatio(contentMode: .fill) + ) + } + } + .cornerRadius(MediaGridContainerView.cornerRadius) + .clipped() + .background(GeometryReader { proxy in + Color.clear.preference( + key: ViewFrameKey.self, + value: proxy.frame(in: .global) ) - .contentShape(Rectangle()) - .onTapGesture { -// let actionContext = ActionContext(index: index, viewModels: viewModels) -// action(actionContext) + .onPreferenceChange(ViewFrameKey.self) { frame in + viewModels[index].frameInWindow = frame } - .contextMenu(contextMenuContentPreviewProvider: { + }) + .overlay(alignment: .bottom) { + HStack { let viewModel = viewModels[index] - guard let thumbnail = viewModel.thumbnail else { return nil } - let contextMenuImagePreviewViewModel = ContextMenuImagePreviewViewModel(aspectRatio: thumbnail.size, thumbnail: thumbnail) - let previewProvider = ContextMenuImagePreviewViewController() - previewProvider.viewModel = contextMenuImagePreviewViewModel - return previewProvider - - }, contextMenuActionProvider: { _ in - let children: [UIAction] = [ - UIAction( - title: L10n.Common.Controls.Actions.copy, - image: UIImage(systemName: "doc.on.doc"), - attributes: [], - state: .off - ) { _ in - print("Hi copy") + Spacer() + Group { + if viewModel.mediaKind == .animatedGIF { + Text("GIF") + } else if let durationText = viewModel.durationText { + Text("\(Image(systemName: "play.fill")) \(durationText)") } - ] - return UIMenu(title: "", image: nil, identifier: nil, options: [], children: children) - }, previewAction: { - previewAction(viewModels[index]) - }) - } + } + .foregroundColor(Color(uiColor: .label)) + .font(.system(.footnote, design: .default, weight: .medium)) + .padding(.horizontal, 5) + .padding(.vertical, 3) + .background(.thinMaterial) + .cornerRadius(4) + } + .padding(EdgeInsets(top: 0, leading: 11, bottom: 8, trailing: 11)) + .allowsHitTesting(false) + } + .overlay( + RoundedRectangle(cornerRadius: MediaGridContainerView.cornerRadius) + .stroke(Color(uiColor: .placeholderText).opacity(0.5), lineWidth: 1) + ) + .contentShape(Rectangle()) + .onTapGesture { + previewAction(viewModels[index]) + } + .contextMenu(contextMenuContentPreviewProvider: { + let viewModel = viewModels[index] + guard let thumbnail = viewModel.thumbnail else { return nil } + let contextMenuImagePreviewViewModel = ContextMenuImagePreviewViewModel(aspectRatio: thumbnail.size, thumbnail: thumbnail) + let previewProvider = ContextMenuImagePreviewViewController() + previewProvider.viewModel = contextMenuImagePreviewViewModel + return previewProvider + + }, contextMenuActionProvider: { _ in + let children: [UIAction] = [ + UIAction( + title: L10n.Common.Controls.Actions.copy, + image: UIImage(systemName: "doc.on.doc"), + attributes: [], + state: .off + ) { _ in + print("Hi copy") + } + ] + return UIMenu(title: "", image: nil, identifier: nil, options: [], children: children) + }, previewAction: { + previewAction(viewModels[index]) + }) + } // end func private func height(for rows: Int) -> CGFloat { guard let idealWidth = self.idealWidth else { @@ -343,13 +322,13 @@ struct MediaGridContainerView_Previews: PreviewProvider { durationMS: nil ), MediaView.ViewModel( - mediaKind: .photo, - aspectRatio: CGSize(width: 3482, height: 1959), + mediaKind: .animatedGIF, + aspectRatio: CGSize(width: 1200, height: 720), altText: nil, - previewURL: URL(string: "https://www.nasa.gov/sites/default/files/thumbnails/image/pia20027_updated.jpg"), - assetURL: URL(string: "https://www.nasa.gov/sites/default/files/thumbnails/image/pia20027_updated.jpg"), - downloadURL: URL(string: "https://www.nasa.gov/sites/default/files/thumbnails/image/pia20027_updated.jpg"), - durationMS: nil + previewURL: URL(string: "https://media.mstdn.jp/cache/media_attachments/files/109/936/306/341/672/302/small/19d1c52c1a1713b2.png"), + assetURL: URL(string: "https://media.mstdn.jp/cache/media_attachments/files/109/936/306/341/672/302/small/19d1c52c1a1713b2.png"), + downloadURL: URL(string: "https://media.mstdn.jp/cache/media_attachments/files/109/936/306/341/672/302/original/19d1c52c1a1713b2.mp4"), + durationMS: 11084 ), MediaView.ViewModel( mediaKind: .video, @@ -358,7 +337,7 @@ struct MediaGridContainerView_Previews: PreviewProvider { previewURL: URL(string: "https://pbs.twimg.com/ext_tw_video_thumb/1629081362555899904/pu/img/em5qGBhoV0R1aGfv.jpg"), assetURL: URL(string: "https://pbs.twimg.com/ext_tw_video_thumb/1629081362555899904/pu/img/em5qGBhoV0R1aGfv.jpg"), downloadURL: URL(string: "https://video.twimg.com/ext_tw_video/1629081362555899904/pu/vid/1280x720/4OGsKDg67adqojtX.mp4?tag=12"), - durationMS: 105553160985088 + durationMS: 10555 ), MediaView.ViewModel( mediaKind: .photo, @@ -369,6 +348,15 @@ struct MediaGridContainerView_Previews: PreviewProvider { downloadURL: URL(string: "https://www.nasa.gov/sites/default/files/images/671506main_PIA15628_full.jpg"), durationMS: nil ), + MediaView.ViewModel( + mediaKind: .photo, + aspectRatio: CGSize(width: 3482, height: 1959), + altText: nil, + previewURL: URL(string: "https://www.nasa.gov/sites/default/files/thumbnails/image/pia20027_updated.jpg"), + assetURL: URL(string: "https://www.nasa.gov/sites/default/files/thumbnails/image/pia20027_updated.jpg"), + downloadURL: URL(string: "https://www.nasa.gov/sites/default/files/thumbnails/image/pia20027_updated.jpg"), + durationMS: nil + ), ] return Array(repeating: models, count: 3).flatMap { $0 } }() diff --git a/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift b/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift index bd54bb61..2ea83097 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift @@ -31,55 +31,18 @@ public struct MediaView: View { .placeholder { progress in Image(uiImage: Asset.Logo.mediaPlaceholder.image.withRenderingMode(.alwaysTemplate)) } - .overlay(alignment: .bottom) { - HStack { - Spacer() - if let durationText = viewModel.durationText { - Text("\(Image(systemName: "play.fill")) \(durationText)") - .foregroundColor(Color(uiColor: .label)) - .font(.system(.footnote, design: .default, weight: .medium)) - .padding(.horizontal, 5) - .padding(.vertical, 3) - .background(.regularMaterial) - .cornerRadius(4) - } - if viewModel.mediaKind == .animatedGIF { - Text("GIF") - .foregroundColor(Color(uiColor: .label)) - .font(.system(.footnote, design: .default, weight: .medium)) - .padding(.horizontal, 5) - .padding(.vertical, 3) - .background(.regularMaterial) - .cornerRadius(4) + .overlay { + switch viewModel.mediaKind { + case .animatedGIF: + if let assetURL = viewModel.downloadURL { + GIFVideoPlayerRepresentable(assetURL: assetURL) + } else { + EmptyView() } + default: + EmptyView() } - .padding(EdgeInsets(top: 0, leading: 11, bottom: 8, trailing: 11)) } - -// Color.clear -// .overlay { -// KFImage(viewModel.previewURL) -// .cancelOnDisappear(true) -// .resizable() -// .placeholder { progress in -// -// } -// } -// .aspectRatio(viewModel.aspectRatio, contentMode: .fill) -// .placeholder { -// ZStack { -// let gradient = Gradient(stops: [ -// .init(color: Color(uiColor: UIColor(hex: 0x50A85E)), location: 0.0), -// .init(color: Color(uiColor: UIColor(hex: 0x567CA6)), location: 0.3490), -// .init(color: Color(uiColor: UIColor(hex: 0x765BAE)), location: 0.7031), -// .init(color: Color(uiColor: UIColor(hex: 0xAB5886)), location: 1.0), -// ]) -// LinearGradient(gradient: gradient, startPoint: .topLeading, endPoint: .bottomTrailing) -// Image(uiImage: Asset.Image.TimeLine.placeholder.image.withRenderingMode(.alwaysTemplate)) -// } -// } -// .sizeFitContainer(shape: Rectangle(), aspectRatio: viewModel._asepctRatio) -// .accessibilityLabel(viewModel.altText ?? "") } } @@ -106,6 +69,15 @@ struct MediaView_Previews: PreviewProvider { downloadURL: URL(string: "https://video.twimg.com/ext_tw_video/1630058212258115584/pu/vid/1280x720/V-Jq9fMqwxTbZdxD.mp4?tag=12"), durationMS: 27375 ), + MediaView.ViewModel( + mediaKind: .animatedGIF, + aspectRatio: CGSize(width: 1200, height: 720), + altText: nil, + previewURL: URL(string: "https://media.mstdn.jp/cache/media_attachments/files/109/936/306/341/672/302/small/19d1c52c1a1713b2.png"), + assetURL: URL(string: "https://media.mstdn.jp/cache/media_attachments/files/109/936/306/341/672/302/small/19d1c52c1a1713b2.png"), + downloadURL: URL(string: "https://media.mstdn.jp/cache/media_attachments/files/109/936/306/341/672/302/original/19d1c52c1a1713b2.mp4"), + durationMS: 11084 + ), ] return models }() @@ -121,321 +93,3 @@ struct MediaView_Previews: PreviewProvider { } } } - -//public final class MediaView: UIView { -// -// var disposeBag = Set() -// -// public static let cornerRadius: CGFloat = 8 -// public static let durationFormatter: DateComponentsFormatter = { -// let formatter = DateComponentsFormatter() -// formatter.zeroFormattingBehavior = .pad -// formatter.allowedUnits = [.minute, .second] -// return formatter -// }() -// public static let borderColor: UIColor = UIColor.label.withAlphaComponent(0.05) -// public static let borderWidth: CGFloat = 1 -// -// public let container = TouchBlockingView() -// -// public private(set) var configuration: Configuration? -// -// private(set) lazy var imageView: UIImageView = { -// let imageView = UIImageView() -// imageView.contentMode = .scaleAspectFill -// imageView.layer.masksToBounds = true -// imageView.layer.cornerCurve = .continuous -// imageView.layer.cornerRadius = MediaView.cornerRadius -// imageView.layer.borderColor = MediaView.borderColor.cgColor -// imageView.layer.borderWidth = MediaView.borderWidth -// imageView.isUserInteractionEnabled = false -// return imageView -// }() -// -// private(set) lazy var playerViewController: AVPlayerViewController = { -// let playerViewController = AVPlayerViewController() -// playerViewController.view.layer.masksToBounds = true -// playerViewController.view.layer.cornerCurve = .continuous -// playerViewController.view.layer.cornerRadius = MediaView.cornerRadius -// playerViewController.view.layer.borderColor = MediaView.borderColor.cgColor -// playerViewController.view.layer.borderWidth = MediaView.borderWidth -// playerViewController.view.isUserInteractionEnabled = false -// return playerViewController -// }() -// private var playerLooper: AVPlayerLooper? -// -// private(set) lazy var indicatorBlurEffectView: UIVisualEffectView = { -// let effectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterial)) -// effectView.layer.masksToBounds = true -// effectView.layer.cornerCurve = .continuous -// effectView.layer.cornerRadius = 4 -// return effectView -// }() -// private(set) lazy var indicatorVibrancyEffectView = UIVisualEffectView( -// effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .systemUltraThinMaterial)) -// ) -// private(set) lazy var playerIndicatorLabel: UILabel = { -// let label = UILabel() -// label.font = .preferredFont(forTextStyle: .caption1) -// label.textColor = .secondaryLabel -// return label -// }() -// -// public override init(frame: CGRect) { -// super.init(frame: frame) -// _init() -// } -// -// public required init?(coder: NSCoder) { -// super.init(coder: coder) -// _init() -// } -// -//} -// -//extension MediaView { -// -// @MainActor -// public func thumbnail() async -> UIImage? { -// return imageView.image -// } -// -// public func thumbnail() -> UIImage? { -// return imageView.image -// } -// -//} -// -//extension MediaView { -// private func _init() { -// // lazy load content later -// -// imageView.isAccessibilityElement = true -// } -// -// public func setup(configuration: Configuration) { -// self.configuration = configuration -// -// setupContainerViewHierarchy() -// -// switch configuration { -// case .image(let info): -// configure(image: info, containerView: container) -// case .gif(let info): -// configure(gif: info) -// case .video(let info): -// configure(video: info) -// } -// } -// -// private func configure( -// image info: Configuration.ImageInfo, -// containerView: UIView -// ) { -// imageView.translatesAutoresizingMaskIntoConstraints = false -// containerView.addSubview(imageView) -// NSLayoutConstraint.activate([ -// imageView.topAnchor.constraint(equalTo: containerView.topAnchor), -// imageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), -// imageView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), -// imageView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), -// ]) -// -// let placeholder = Asset.Logo.mediaPlaceholder.image -// imageView.contentMode = .center -// imageView.backgroundColor = .systemGray6 -// -// guard let urlString = info.assetURL, -// let url = URL(string: urlString) else { -// imageView.image = placeholder -// return -// } -// -// imageView.af.setImage( -// withURL: url, -// placeholderImage: placeholder, -// completion: { [weak imageView] response in -// assert(Thread.isMainThread) -// switch response.result { -// case .success: -// imageView?.contentMode = .scaleAspectFill -// case .failure: -// break -// } -// }) -// } -// -// private func configure(gif info: Configuration.VideoInfo) { -// // use view controller as View here -// playerViewController.view.translatesAutoresizingMaskIntoConstraints = false -// container.addSubview(playerViewController.view) -// NSLayoutConstraint.activate([ -// playerViewController.view.topAnchor.constraint(equalTo: container.topAnchor), -// playerViewController.view.leadingAnchor.constraint(equalTo: container.leadingAnchor), -// playerViewController.view.trailingAnchor.constraint(equalTo: container.trailingAnchor), -// playerViewController.view.bottomAnchor.constraint(equalTo: container.bottomAnchor), -// ]) -// -// assert(playerViewController.contentOverlayView != nil) -// if let contentOverlayView = playerViewController.contentOverlayView { -// let imageInfo = Configuration.ImageInfo( -// aspectRadio: info.aspectRadio, -// assetURL: info.previewURL, -// downloadURL: info.previewURL -// ) -// configure(image: imageInfo, containerView: contentOverlayView) -// -// indicatorBlurEffectView.translatesAutoresizingMaskIntoConstraints = false -// contentOverlayView.addSubview(indicatorBlurEffectView) -// NSLayoutConstraint.activate([ -// contentOverlayView.trailingAnchor.constraint(equalTo: indicatorBlurEffectView.trailingAnchor, constant: 11), -// contentOverlayView.bottomAnchor.constraint(equalTo: indicatorBlurEffectView.bottomAnchor, constant: 8), -// ]) -// setupIndicatorViewHierarchy() -// } -// playerIndicatorLabel.attributedText = NSAttributedString(AttributedString("GIF")) -// -// guard let player = setupGIFPlayer(info: info) else { -// // assertionFailure() -// return -// } -// setupPlayerLooper(player: player) -// playerViewController.player = player -// playerViewController.showsPlaybackControls = false -// -// playerViewController.publisher(for: \.isReadyForDisplay) -// .receive(on: DispatchQueue.main) -// .sink { [weak self] isReadyForDisplay in -// guard let self = self else { return } -// self.imageView.isHidden = isReadyForDisplay -// } -// .store(in: &disposeBag) -// -// // auto play for GIF -// player.play() -// } -// -// private func configure(video info: Configuration.VideoInfo) { -// let imageInfo = Configuration.ImageInfo( -// aspectRadio: info.aspectRadio, -// assetURL: info.previewURL, -// downloadURL: info.previewURL -// ) -// configure(image: imageInfo, containerView: container) -// -// indicatorBlurEffectView.translatesAutoresizingMaskIntoConstraints = false -// imageView.addSubview(indicatorBlurEffectView) -// NSLayoutConstraint.activate([ -// imageView.trailingAnchor.constraint(equalTo: indicatorBlurEffectView.trailingAnchor, constant: 11), -// imageView.bottomAnchor.constraint(equalTo: indicatorBlurEffectView.bottomAnchor, constant: 8), -// ]) -// setupIndicatorViewHierarchy() -// -// playerIndicatorLabel.attributedText = { -// let imageAttachment = NSTextAttachment(image: UIImage(systemName: "play.fill")!) -// let imageAttributedString = AttributedString(NSAttributedString(attachment: imageAttachment)) -// let duration: String = { -// guard let durationMS = info.durationMS else { return "" } -// let timeInterval = TimeInterval(durationMS / 1000) -// guard timeInterval > 0 else { return "" } -// guard let text = MediaView.durationFormatter.string(from: timeInterval) else { return "" } -// return " \(text)" -// }() -// let textAttributedString = AttributedString("\(duration)") -// var attributedString = imageAttributedString + textAttributedString -// attributedString.foregroundColor = .secondaryLabel -// return NSAttributedString(attributedString) -// }() -// -// } -// -// public func prepareForReuse() { -// // reset appearance -// alpha = 1 -// -// // reset image -// imageView.removeFromSuperview() -// imageView.removeConstraints(imageView.constraints) -// imageView.af.cancelImageRequest() -// imageView.image = nil -// imageView.isHidden = false -// -// // reset player -// playerViewController.view.removeFromSuperview() -// playerViewController.contentOverlayView.flatMap { view in -// view.removeConstraints(view.constraints) -// } -// playerViewController.player?.pause() -// playerViewController.player = nil -// playerLooper = nil -// -// // reset indicator -// indicatorBlurEffectView.removeFromSuperview() -// -// // reset container -// container.removeFromSuperview() -// container.removeConstraints(container.constraints) -// -// // reset configuration -// configuration = nil -// -// disposeBag.removeAll() -// } -//} -// -//extension MediaView { -// private func setupGIFPlayer(info: Configuration.VideoInfo) -> AVPlayer? { -// guard let urlString = info.assetURL, -// let url = URL(string: urlString) -// else { return nil } -// let playerItem = AVPlayerItem(url: url) -// let player = AVQueuePlayer(playerItem: playerItem) -// player.isMuted = true -// return player -// } -// -// private func setupPlayerLooper(player: AVPlayer) { -// guard let queuePlayer = player as? AVQueuePlayer else { return } -// guard let templateItem = queuePlayer.items().first else { return } -// playerLooper = AVPlayerLooper(player: queuePlayer, templateItem: templateItem) -// } -// -// private func setupContainerViewHierarchy() { -// guard container.superview == nil else { return } -// container.translatesAutoresizingMaskIntoConstraints = false -// addSubview(container) -// NSLayoutConstraint.activate([ -// container.topAnchor.constraint(equalTo: topAnchor), -// container.leadingAnchor.constraint(equalTo: leadingAnchor), -// container.trailingAnchor.constraint(equalTo: trailingAnchor), -// container.bottomAnchor.constraint(equalTo: bottomAnchor), -// ]) -// } -// -// private func setupIndicatorViewHierarchy() { -// let blurEffectView = indicatorBlurEffectView -// let vibrancyEffectView = indicatorVibrancyEffectView -// -// if vibrancyEffectView.superview == nil { -// vibrancyEffectView.translatesAutoresizingMaskIntoConstraints = false -// blurEffectView.contentView.addSubview(vibrancyEffectView) -// NSLayoutConstraint.activate([ -// vibrancyEffectView.topAnchor.constraint(equalTo: blurEffectView.contentView.topAnchor), -// vibrancyEffectView.leadingAnchor.constraint(equalTo: blurEffectView.contentView.leadingAnchor), -// vibrancyEffectView.trailingAnchor.constraint(equalTo: blurEffectView.contentView.trailingAnchor), -// vibrancyEffectView.bottomAnchor.constraint(equalTo: blurEffectView.contentView.bottomAnchor), -// ]) -// } -// -// if playerIndicatorLabel.superview == nil { -// playerIndicatorLabel.translatesAutoresizingMaskIntoConstraints = false -// vibrancyEffectView.contentView.addSubview(playerIndicatorLabel) -// NSLayoutConstraint.activate([ -// playerIndicatorLabel.topAnchor.constraint(equalTo: vibrancyEffectView.contentView.topAnchor), -// playerIndicatorLabel.leadingAnchor.constraint(equalTo: vibrancyEffectView.contentView.leadingAnchor, constant: 3), -// vibrancyEffectView.contentView.trailingAnchor.constraint(equalTo: playerIndicatorLabel.trailingAnchor, constant: 3), -// playerIndicatorLabel.bottomAnchor.constraint(equalTo: vibrancyEffectView.contentView.bottomAnchor), -// ]) -// } -// } -//} diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/GIFVideoPlayerRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/GIFVideoPlayerRepresentable.swift new file mode 100644 index 00000000..a923716a --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/GIFVideoPlayerRepresentable.swift @@ -0,0 +1,83 @@ +// +// GIFVideoPlayerRepresentable.swift +// +// +// Created by MainasuK on 2023/2/28. +// + +import UIKit +import SwiftUI +import Combine +import AVKit + +public struct GIFVideoPlayerRepresentable: UIViewRepresentable { + + let controller = AVPlayerViewController() + + // input + let assetURL: URL + + // output + + public func makeUIView(context: Context) -> UIView { + let playerItem = AVPlayerItem(url: assetURL) + let player = AVQueuePlayer(playerItem: playerItem) + player.isMuted = true + context.coordinator.player = player + let playerLooper = AVPlayerLooper(player: player, templateItem: playerItem) + context.coordinator.playerLooper = playerLooper + + controller.player = player + controller.showsPlaybackControls = false + + controller.view.alpha = 0 + context.coordinator.setupPlayer() + + return controller.view + } + + public func updateUIView(_ view: UIView, context: Context) { + // do nothing + } + + public func makeCoordinator() -> Coordinator { + return Coordinator(self) + } + + public class Coordinator { + var disposeBag = Set() + + let representable: GIFVideoPlayerRepresentable + + var player: AVPlayer? + var playerLooper: AVPlayerLooper? + + init(_ representable: GIFVideoPlayerRepresentable) { + self.representable = representable + } + + func setupPlayer() { + guard let player = self.player, + let playerItem = player.currentItem + else { + assertionFailure() + return + } + + playerItem.publisher(for: \.status) + .receive(on: DispatchQueue.main) + .sink { [weak self] status in + guard let self = self else { return } + switch status { + case .readyToPlay: + self.representable.controller.view.alpha = 1 + self.player?.play() + default: + break + } + } + .store(in: &disposeBag) + } // end func + } // end Coordinator + +} diff --git a/TwidereX/Scene/MediaPreview/Video/MediaPreviewVideoViewController.swift b/TwidereX/Scene/MediaPreview/Video/MediaPreviewVideoViewController.swift index d17437d0..b5342f16 100644 --- a/TwidereX/Scene/MediaPreview/Video/MediaPreviewVideoViewController.swift +++ b/TwidereX/Scene/MediaPreview/Video/MediaPreviewVideoViewController.swift @@ -68,6 +68,7 @@ extension MediaPreviewVideoViewController { switch viewModel.item { case .gif: playerViewController.showsPlaybackControls = false + playerViewController.view.isUserInteractionEnabled = false // disable pan to seek time default: break } From 97a7ed52a18cde00e36484b5769656b6f5805ab4 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 28 Feb 2023 19:41:35 +0800 Subject: [PATCH 035/128] feat: add media content waring overlay --- .../Sources/TwidereUI/Content/MediaView.swift | 6 +- .../Content/StatusView+ViewModel.swift | 37 +++- .../TwidereUI/Content/StatusView.swift | 73 ++++--- .../Control/ContentWarningOverlayView.swift | 195 ++++++++++++------ .../Provider/DataSourceFacade+Status.swift | 2 +- ...ider+StatusViewTableViewCellDelegate.swift | 35 +++- .../StatusMediaGalleryCollectionCell.swift | 23 +-- .../StatusViewTableViewCellDelegate.swift | 5 + 8 files changed, 247 insertions(+), 129 deletions(-) diff --git a/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift b/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift index 2ea83097..3c5c84ed 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift @@ -86,9 +86,9 @@ struct MediaView_Previews: PreviewProvider { Group { ForEach(viewModels, id: \.self) { viewModel in MediaView(viewModel: viewModel) - .frame(width: 300, height: 168) - .previewLayout(.fixed(width: 300, height: 168)) - .previewDisplayName(String(describing: viewModel.mediaKind)) + .frame(width: 300, height: 168) + .previewLayout(.fixed(width: 300, height: 168)) + .previewDisplayName(String(describing: viewModel.mediaKind)) } } } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index 414d0a2a..9e933761 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -86,11 +86,12 @@ extension StatusView { // @Published public var isTranslateButtonDisplay = false // @Published public var mediaViewModels: [MediaView.ViewModel] = [] -// @Published public var mediaViewConfigurations: [MediaView.Configuration] = [] -// -// @Published public var isContentSensitive: Bool = false -// @Published public var isContentSensitiveToggled: Bool = false -// + @Published public var isMediaSensitive: Bool = false + @Published public var isMediaSensitiveToggled: Bool = false + public var isMediaContentWarningOverlayReveal: Bool { + return isMediaSensitiveToggled ? isMediaSensitive : !isMediaSensitive + } + // @Published public var isContentReveal: Bool = false // // @Published public var isMediaSensitive: Bool = false @@ -1050,13 +1051,28 @@ extension StatusView.ViewModel { self.parentViewModel = parentViewModel if let repost = status.repost { - repostViewModel = .init( + let _repostViewModel = StatusView.ViewModel( status: repost, kind: .repost, delegate: delegate, parentViewModel: self, viewLayoutFramePublisher: viewLayoutFramePublisher ) + repostViewModel = _repostViewModel + + // header - repost + let _statusHeaderViewModel = StatusHeaderView.ViewModel( + image: Asset.Media.repeat.image.withRenderingMode(.alwaysTemplate), + label: { + let name = status.author.name + let userRepostText = L10n.Common.Controls.Status.userBoosted(name) + let text = MastodonContent(content: userRepostText, emojis: status.author.emojis.asDictionary) + let label = MastodonMetaContent.convert(text: text) + return label + }() + ) + _statusHeaderViewModel.hasHangingAvatar = _repostViewModel.hasHangingAvatar + _repostViewModel.statusHeaderViewModel = _statusHeaderViewModel } // author @@ -1090,5 +1106,14 @@ extension StatusView.ViewModel { // media mediaViewModels = MediaView.ViewModel.viewModels(from: status) + + // content warning + isMediaSensitive = status.isMediaSensitive + isMediaSensitiveToggled = status.isMediaSensitiveToggled + status.publisher(for: \.isMediaSensitiveToggled) + .receive(on: DispatchQueue.main) + .assign(to: \.isMediaSensitiveToggled, on: self) + .store(in: &disposeBag) + } } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index f0005108..41f62081 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -16,7 +16,36 @@ import MetaTextArea import MetaLabel import TwidereCommon import TwidereCore -import NIOPosix + +public protocol StatusViewDelegate: AnyObject { +// func statusView(_ statusView: StatusView, headerDidPressed header: UIView) +// +// func statusView(_ statusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) +// func statusView(_ statusView: StatusView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) +// +// func statusView(_ statusView: StatusView, expandContentButtonDidPressed button: UIButton) +// +// func statusView(_ statusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) +// func statusView(_ statusView: StatusView, quoteStatusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) + + // media + func statusView(_ viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel) + func statusView(_ viewModel: StatusView.ViewModel, toggleContentWarningOverlayDisplay isReveal: Bool) +// func statusView(_ statusView: StatusView, quoteStatusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int) +// +// func statusView(_ statusView: StatusView, pollTableView tableView: UITableView, didSelectRowAt indexPath: IndexPath) +// func statusView(_ statusView: StatusView, pollVoteButtonDidPressed button: UIButton) +// +// func statusView(_ statusView: StatusView, quoteStatusViewDidPressed quoteStatusView: StatusView) +// +// func statusView(_ statusView: StatusView, statusToolbar: StatusToolbar, actionDidPressed action: StatusToolbar.Action, button: UIButton) +// func statusView(_ statusView: StatusView, statusToolbar: StatusToolbar, menuActionDidPressed action: StatusToolbar.MenuAction, menuButton button: UIButton) +// +// func statusView(_ statusView: StatusView, translateButtonDidPressed button: UIButton) +// +// // a11y +// func statusView(_ statusView: StatusView, accessibilityActivate: Void) +} public struct StatusView: View { @@ -49,8 +78,10 @@ public struct StatusView: View { authorView .padding(.horizontal, viewModel.margin) // content - contentView - .padding(.horizontal, viewModel.margin) + if !viewModel.content.string.isEmpty { + contentView + .padding(.horizontal, viewModel.margin) + } // media if !viewModel.mediaViewModels.isEmpty { MediaGridContainerView( @@ -61,6 +92,12 @@ public struct StatusView: View { viewModel.delegate?.statusView(viewModel, previewActionForMediaViewModel: mediaViewModel) } ) + .overlay { + ContentWarningOverlayView(isReveal: viewModel.isMediaContentWarningOverlayReveal) { + viewModel.delegate?.statusView(viewModel, toggleContentWarningOverlayDisplay: !viewModel.isMediaContentWarningOverlayReveal) + } + .cornerRadius(MediaGridContainerView.cornerRadius) + } .padding(.horizontal, viewModel.margin) } // quote @@ -171,36 +208,6 @@ extension StatusView { } } -public protocol StatusViewDelegate: AnyObject { -// func statusView(_ statusView: StatusView, headerDidPressed header: UIView) -// -// func statusView(_ statusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) -// func statusView(_ statusView: StatusView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) -// -// func statusView(_ statusView: StatusView, expandContentButtonDidPressed button: UIButton) -// -// func statusView(_ statusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) -// func statusView(_ statusView: StatusView, quoteStatusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) - - // media - func statusView(_ viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel) -// func statusView(_ statusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, toggleContentWarningOverlayViewDisplay contentWarningOverlayView: ContentWarningOverlayView) -// func statusView(_ statusView: StatusView, quoteStatusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int) -// -// func statusView(_ statusView: StatusView, pollTableView tableView: UITableView, didSelectRowAt indexPath: IndexPath) -// func statusView(_ statusView: StatusView, pollVoteButtonDidPressed button: UIButton) -// -// func statusView(_ statusView: StatusView, quoteStatusViewDidPressed quoteStatusView: StatusView) -// -// func statusView(_ statusView: StatusView, statusToolbar: StatusToolbar, actionDidPressed action: StatusToolbar.Action, button: UIButton) -// func statusView(_ statusView: StatusView, statusToolbar: StatusToolbar, menuActionDidPressed action: StatusToolbar.MenuAction, menuButton button: UIButton) -// -// func statusView(_ statusView: StatusView, translateButtonDidPressed button: UIButton) -// -// // a11y -// func statusView(_ statusView: StatusView, accessibilityActivate: Void) -} - //public final class StatusView: UIView { // // private var _disposeBag = Set() // which lifetime same to view scope diff --git a/TwidereSDK/Sources/TwidereUI/Control/ContentWarningOverlayView.swift b/TwidereSDK/Sources/TwidereUI/Control/ContentWarningOverlayView.swift index 4a695f81..0f1533eb 100644 --- a/TwidereSDK/Sources/TwidereUI/Control/ContentWarningOverlayView.swift +++ b/TwidereSDK/Sources/TwidereUI/Control/ContentWarningOverlayView.swift @@ -7,78 +7,147 @@ import os.log import UIKit +import SwiftUI import TwidereAsset -public protocol ContentWarningOverlayViewDelegate: AnyObject { - func contentWarningOverlayViewDidPressed(_ contentWarningOverlayView: ContentWarningOverlayView) -} - -public final class ContentWarningOverlayView: UIView { - - public static let blurVisualEffect = UIBlurEffect(style: .systemUltraThinMaterial) - - let logger = Logger(subsystem: "ContentWarningOverlayView", category: "View") +public struct ContentWarningOverlayView: View { - public weak var delegate: ContentWarningOverlayViewDelegate? + let isReveal: Bool + let onTapGesture: () -> Void - public let blurVisualEffectView = UIVisualEffectView(effect: ContentWarningOverlayView.blurVisualEffect) - public let vibrancyVisualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: ContentWarningOverlayView.blurVisualEffect)) - let alertImageView: UIImageView = { - let imageView = UIImageView() - imageView.image = Asset.Indices.exclamationmarkTriangleLarge.image.withRenderingMode(.alwaysTemplate) - return imageView - }() - - public let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer + public var body: some View { + Color.clear + .overlay { + if !isReveal { + Image(uiImage: Asset.Indices.exclamationmarkTriangleLarge.image.withRenderingMode(.alwaysTemplate)) + .foregroundColor(.secondary) + } + } + .background(.thinMaterial) + .opacity(isReveal ? 0 : 1) + .animation(.easeInOut(duration: 0.2), value: isReveal) + .onTapGesture { + onTapGesture() + } + .overlay(alignment: .top) { + HStack { + if isReveal { + Button { + onTapGesture() + } label: { + Image(uiImage: Asset.Human.eyeSlashMini.image.withRenderingMode(.alwaysTemplate)) + .foregroundColor(.secondary) + .padding(4) + .background(.regularMaterial) + .cornerRadius(6) + .padding(8) + } + } + Spacer() + } + } + } +} + +struct ContentWarningOverlayView_Previews: PreviewProvider { - override init(frame: CGRect) { - super.init(frame: frame) - _init() + static var viewModel: MediaView.ViewModel { + MediaView.ViewModel( + mediaKind: .photo, + aspectRatio: CGSize(width: 2048, height: 1186), + altText: nil, + previewURL: URL(string: "https://www.nasa.gov/sites/default/files/thumbnails/image/web_first_images_release.png"), + assetURL: URL(string: "https://www.nasa.gov/sites/default/files/thumbnails/image/web_first_images_release.png"), + downloadURL: URL(string: "https://www.nasa.gov/sites/default/files/thumbnails/image/web_first_images_release.png"), + durationMS: nil + ) } - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() + class StateViewModel { + var isReveal = false } -} - -extension ContentWarningOverlayView { - private func _init() { - // overlay - blurVisualEffectView.translatesAutoresizingMaskIntoConstraints = false - addSubview(blurVisualEffectView) - NSLayoutConstraint.activate([ - blurVisualEffectView.topAnchor.constraint(equalTo: topAnchor), - blurVisualEffectView.leadingAnchor.constraint(equalTo: leadingAnchor), - blurVisualEffectView.trailingAnchor.constraint(equalTo: trailingAnchor), - blurVisualEffectView.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - - vibrancyVisualEffectView.translatesAutoresizingMaskIntoConstraints = false - blurVisualEffectView.contentView.addSubview(vibrancyVisualEffectView) - NSLayoutConstraint.activate([ - vibrancyVisualEffectView.topAnchor.constraint(equalTo: blurVisualEffectView.contentView.topAnchor), - vibrancyVisualEffectView.leadingAnchor.constraint(equalTo: blurVisualEffectView.contentView.leadingAnchor), - vibrancyVisualEffectView.trailingAnchor.constraint(equalTo: blurVisualEffectView.contentView.trailingAnchor), - vibrancyVisualEffectView.bottomAnchor.constraint(equalTo: blurVisualEffectView.contentView.bottomAnchor), - ]) - - alertImageView.translatesAutoresizingMaskIntoConstraints = false - vibrancyVisualEffectView.contentView.addSubview(alertImageView) - NSLayoutConstraint.activate([ - alertImageView.centerXAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.centerXAnchor), - alertImageView.centerYAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.centerYAnchor), - ]) - - tapGestureRecognizer.addTarget(self, action: #selector(ContentWarningOverlayView.tapGestureRecognizerHandler(_:))) - addGestureRecognizer(tapGestureRecognizer) + static let contentWarningOverlayViewModel = StateViewModel() + + static var previews: some View { + MediaView(viewModel: viewModel) + .frame(width: 300, height: 200) + .previewLayout(.fixed(width: 300, height: 200)) + .overlay { + ContentWarningOverlayView(isReveal: contentWarningOverlayViewModel.isReveal) { + contentWarningOverlayViewModel.isReveal.toggle() + } + } } } -extension ContentWarningOverlayView { - @objc private func tapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - delegate?.contentWarningOverlayViewDidPressed(self) - } -} + +//public final class ContentWarningOverlayView: UIView { +// +// public static let blurVisualEffect = UIBlurEffect(style: .systemUltraThinMaterial) +// +// let logger = Logger(subsystem: "ContentWarningOverlayView", category: "View") +// +// public weak var delegate: ContentWarningOverlayViewDelegate? +// +// public let blurVisualEffectView = UIVisualEffectView(effect: ContentWarningOverlayView.blurVisualEffect) +// public let vibrancyVisualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: ContentWarningOverlayView.blurVisualEffect)) +// let alertImageView: UIImageView = { +// let imageView = UIImageView() +// imageView.image = Asset.Indices.exclamationmarkTriangleLarge.image.withRenderingMode(.alwaysTemplate) +// return imageView +// }() +// +// public let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer +// +// override init(frame: CGRect) { +// super.init(frame: frame) +// _init() +// } +// +// required init?(coder: NSCoder) { +// super.init(coder: coder) +// _init() +// } +// +//} +// +//extension ContentWarningOverlayView { +// private func _init() { +// // overlay +// blurVisualEffectView.translatesAutoresizingMaskIntoConstraints = false +// addSubview(blurVisualEffectView) +// NSLayoutConstraint.activate([ +// blurVisualEffectView.topAnchor.constraint(equalTo: topAnchor), +// blurVisualEffectView.leadingAnchor.constraint(equalTo: leadingAnchor), +// blurVisualEffectView.trailingAnchor.constraint(equalTo: trailingAnchor), +// blurVisualEffectView.bottomAnchor.constraint(equalTo: bottomAnchor), +// ]) +// +// vibrancyVisualEffectView.translatesAutoresizingMaskIntoConstraints = false +// blurVisualEffectView.contentView.addSubview(vibrancyVisualEffectView) +// NSLayoutConstraint.activate([ +// vibrancyVisualEffectView.topAnchor.constraint(equalTo: blurVisualEffectView.contentView.topAnchor), +// vibrancyVisualEffectView.leadingAnchor.constraint(equalTo: blurVisualEffectView.contentView.leadingAnchor), +// vibrancyVisualEffectView.trailingAnchor.constraint(equalTo: blurVisualEffectView.contentView.trailingAnchor), +// vibrancyVisualEffectView.bottomAnchor.constraint(equalTo: blurVisualEffectView.contentView.bottomAnchor), +// ]) +// +// alertImageView.translatesAutoresizingMaskIntoConstraints = false +// vibrancyVisualEffectView.contentView.addSubview(alertImageView) +// NSLayoutConstraint.activate([ +// alertImageView.centerXAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.centerXAnchor), +// alertImageView.centerYAnchor.constraint(equalTo: vibrancyVisualEffectView.contentView.centerYAnchor), +// ]) +// +// tapGestureRecognizer.addTarget(self, action: #selector(ContentWarningOverlayView.tapGestureRecognizerHandler(_:))) +// addGestureRecognizer(tapGestureRecognizer) +// } +//} +// +//extension ContentWarningOverlayView { +// @objc private func tapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") +// delegate?.contentWarningOverlayViewDidPressed(self) +// } +//} diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift index c17a7db3..5b27c8e5 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift @@ -184,7 +184,7 @@ extension DataSourceFacade { provider: DataSourceProvider, status: StatusRecord ) async throws { - let managedObjectContext = provider.context.backgroundManagedObjectContext + let managedObjectContext = provider.context.managedObjectContext try await managedObjectContext.performChanges { guard let object = status.object(in: managedObjectContext) else { return } switch object { diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift index 96a92667..f2818613 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift @@ -225,24 +225,43 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC } } - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, toggleContentWarningOverlayViewDisplay contentWarningOverlayView: ContentWarningOverlayView) { + func tableViewCell( + _ cell: UITableViewCell, + viewModel: StatusView.ViewModel, + toggleContentWarningOverlayDisplay isReveal: Bool + ) { Task { - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { + guard let status = viewModel.status else { assertionFailure() return } - guard let status = await item.status(in: self.context.managedObjectContext) else { - assertionFailure("only works for status data provider") - return - } try await DataSourceFacade.responseToToggleMediaSensitiveAction( provider: self, target: .status, status: status ) - } + } // end Task } + + +// func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, toggleContentWarningOverlayViewDisplay contentWarningOverlayView: ContentWarningOverlayView) { +// Task { +// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) +// guard let item = await item(from: source) else { +// assertionFailure() +// return +// } +// guard let status = await item.status(in: self.context.managedObjectContext) else { +// assertionFailure("only works for status data provider") +// return +// } +// try await DataSourceFacade.responseToToggleMediaSensitiveAction( +// provider: self, +// target: .status, +// status: status +// ) +// } +// } } // MARK: - poll diff --git a/TwidereX/Scene/Share/View/CollectionViewCell/StatusMediaGalleryCollectionCell.swift b/TwidereX/Scene/Share/View/CollectionViewCell/StatusMediaGalleryCollectionCell.swift index d13570c1..4a85be4a 100644 --- a/TwidereX/Scene/Share/View/CollectionViewCell/StatusMediaGalleryCollectionCell.swift +++ b/TwidereX/Scene/Share/View/CollectionViewCell/StatusMediaGalleryCollectionCell.swift @@ -30,14 +30,14 @@ final class StatusMediaGalleryCollectionCell: UICollectionViewCell { return viewModel }() - let sensitiveToggleButtonBlurVisualEffectView: UIVisualEffectView = { - let visualEffectView = UIVisualEffectView(effect: ContentWarningOverlayView.blurVisualEffect) - visualEffectView.layer.masksToBounds = true - visualEffectView.layer.cornerRadius = 6 - visualEffectView.layer.cornerCurve = .continuous - return visualEffectView - }() - let sensitiveToggleButtonVibrancyVisualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: ContentWarningOverlayView.blurVisualEffect)) +// let sensitiveToggleButtonBlurVisualEffectView: UIVisualEffectView = { +// let visualEffectView = UIVisualEffectView(effect: ContentWarningOverlayView.blurVisualEffect) +// visualEffectView.layer.masksToBounds = true +// visualEffectView.layer.cornerRadius = 6 +// visualEffectView.layer.cornerCurve = .continuous +// return visualEffectView +// }() +// let sensitiveToggleButtonVibrancyVisualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: ContentWarningOverlayView.blurVisualEffect)) let sensitiveToggleButton: HitTestExpandedButton = { let button = HitTestExpandedButton(type: .system) button.setImage(Asset.Human.eyeSlashMini.image.withRenderingMode(.alwaysTemplate), for: .normal) @@ -176,10 +176,3 @@ extension StatusMediaGalleryCollectionCell: UICollectionViewDelegate { delegate?.statusMediaGalleryCollectionCell(self, coverFlowCollectionView: collectionView, didSelectItemAt: indexPath) } } - -// MARK: - ContentWarningOverlayViewDelegate -extension StatusMediaGalleryCollectionCell: ContentWarningOverlayViewDelegate { - func contentWarningOverlayViewDidPressed(_ contentWarningOverlayView: ContentWarningOverlayView) { - delegate?.statusMediaGalleryCollectionCell(self, toggleContentWarningOverlayViewDisplay: contentWarningOverlayView) - } -} diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift index 93925ca6..a6fc2d28 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift @@ -26,6 +26,7 @@ protocol StatusViewContainerTableViewCell: UITableViewCell, AutoGenerateProtocol protocol StatusViewTableViewCellDelegate: AutoGenerateProtocolDelegate { // sourcery:inline:StatusViewTableViewCellDelegate.AutoGenerateProtocolDelegate func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel) + func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, toggleContentWarningOverlayDisplay isReveal: Bool) // sourcery:end } @@ -36,5 +37,9 @@ extension StatusViewDelegate where Self: StatusViewContainerTableViewCell { func statusView(_ viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel) { delegate?.tableViewCell(self, viewModel: viewModel, previewActionForMediaViewModel: mediaViewModel) } + + func statusView(_ viewModel: StatusView.ViewModel, toggleContentWarningOverlayDisplay isReveal: Bool) { + delegate?.tableViewCell(self, viewModel: viewModel, toggleContentWarningOverlayDisplay: isReveal) + } // sourcery:end } From 2713a4ae1c9e018e15096589875efaeac9fcb8ac Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 13 Mar 2023 14:38:20 +0800 Subject: [PATCH 036/128] feat: add spoiler and content warning toggle reaction --- .../TwidereUI/Content/StatusHeaderView.swift | 15 +- .../Content/StatusView+ViewModel.swift | 32 +- .../TwidereUI/Content/StatusView.swift | 45 +- .../Provider/DataSourceFacade+Status.swift | 6 +- ...ider+StatusViewTableViewCellDelegate.swift | 634 +++++++++--------- .../StatusViewTableViewCellDelegate.swift | 10 + .../List/ListTimelineViewController.swift | 1 + 7 files changed, 413 insertions(+), 330 deletions(-) diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift index 2729273b..a18eb038 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift @@ -37,11 +37,8 @@ public struct StatusHeaderView: View { } label: { HStack(spacing: StatusHeaderView.iconImageTrailingSpacing) { - Image(uiImage: viewModel.image) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: iconImageDimension, height: iconImageDimension) - .clipShape(Circle()) + Color.clear + .frame(width: iconImageDimension) LabelRepresentable( metaContent: viewModel.label, textStyle: .statusHeader, @@ -55,9 +52,17 @@ public struct StatusHeaderView: View { value: proxy.frame(in: .local).size.height ) }) + .border(.blue, width: 1) .onPreferenceChange(ViewHeightKey.self) { height in self.iconImageDimension = height } + .overlay(alignment: .leading) { + Image(uiImage: viewModel.image) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: iconImageDimension, height: iconImageDimension) + .offset(x: -(StatusHeaderView.iconImageTrailingSpacing + iconImageDimension), y: 0) + } Spacer() } } // end Button diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index 9e933761..e2c8bc8f 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -77,9 +77,16 @@ extension StatusView { // // @Published public var isMyself = false // - @Published public var spoilerContent: MetaContent = PlaintextMetaContent(string: "") + @Published public var spoilerContent: MetaContent? @Published public var content: MetaContent = PlaintextMetaContent(string: "") + var isContentEmpty: Bool { content.string.isEmpty } + var isContentSensitive: Bool { spoilerContent != nil } + @Published public var isContentSensitiveToggled: Bool = false + public var isContentReveal: Bool { + return isContentSensitive ? isContentSensitiveToggled : !isContentEmpty + } + // @Published public var twitterTextProvider: TwitterTextProvider? // // @Published public var language: String? @@ -1083,6 +1090,7 @@ extension StatusView.ViewModel { .compactMap { _ in status.author.nameMetaContent } .assign(to: &$authorName) status.author.publisher(for: \.username) + .map { _ in status.author.acct } .assign(to: &$authorUsernme) // timestamp @@ -1093,21 +1101,39 @@ extension StatusView.ViewModel { break } + // spoiler content + if let spoilerText = status.spoilerText, !spoilerText.isEmpty { + do { + let content = MastodonContent(content: spoilerText, emojis: status.emojis.asDictionary) + let metaContent = try MastodonMetaContent.convert(document: content, useParagraphMark: true) + self.spoilerContent = metaContent + } catch { + assertionFailure(error.localizedDescription) + self.spoilerContent = nil + } + } + // content do { let content = MastodonContent(content: status.content, emojis: status.emojis.asDictionary) let metaContent = try MastodonMetaContent.convert(document: content, useParagraphMark: true) self.content = metaContent - // viewModel.sharePlaintextContent = metaContent.original } catch { assertionFailure(error.localizedDescription) self.content = PlaintextMetaContent(string: "") } + // content warning + isContentSensitiveToggled = status.isContentSensitiveToggled + status.publisher(for: \.isContentSensitiveToggled) + .receive(on: DispatchQueue.main) + .assign(to: \.isContentSensitiveToggled, on: self) + .store(in: &disposeBag) + // media mediaViewModels = MediaView.ViewModel.viewModels(from: status) - // content warning + // media content warning isMediaSensitive = status.isMediaSensitive isMediaSensitiveToggled = status.isMediaSensitiveToggled status.publisher(for: \.isMediaSensitiveToggled) diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index 41f62081..3044cc55 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -24,6 +24,8 @@ public protocol StatusViewDelegate: AnyObject { // func statusView(_ statusView: StatusView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) // // func statusView(_ statusView: StatusView, expandContentButtonDidPressed button: UIButton) + func statusView(_ viewModel: StatusView.ViewModel, toggleContentDisplay isReveal: Bool) + // // func statusView(_ statusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) // func statusView(_ statusView: StatusView, quoteStatusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) @@ -42,6 +44,9 @@ public protocol StatusViewDelegate: AnyObject { // func statusView(_ statusView: StatusView, statusToolbar: StatusToolbar, menuActionDidPressed action: StatusToolbar.MenuAction, menuButton button: UIButton) // // func statusView(_ statusView: StatusView, translateButtonDidPressed button: UIButton) + + func statusView(_ viewModel: StatusView.ViewModel, viewHeightDidChange: Void) + // // // a11y // func statusView(_ statusView: StatusView, accessibilityActivate: Void) @@ -77,10 +82,31 @@ public struct StatusView: View { // authorView authorView .padding(.horizontal, viewModel.margin) + if viewModel.spoilerContent != nil { + spoilerContentView + .padding(.horizontal, viewModel.margin) + .border(.red, width: 1) + if !viewModel.isContentEmpty { + Button { + viewModel.delegate?.statusView(viewModel, toggleContentDisplay: !viewModel.isContentReveal) + } label: { + HStack { + Image(uiImage: Asset.Editing.ellipsisLarge.image.withRenderingMode(.alwaysTemplate)) + .background(Color(uiColor: .tertiarySystemFill)) + .clipShape(Capsule()) + Spacer() + } + } + .buttonStyle(.borderless) + .id(UUID()) // fix animation issue + .padding(.horizontal, viewModel.margin) + .border(.red, width: 1) + } + } // content - if !viewModel.content.string.isEmpty { + if viewModel.isContentReveal { contentView - .padding(.horizontal, viewModel.margin) + .padding(.horizontal, viewModel.margin) } // media if !viewModel.mediaViewModels.isEmpty { @@ -114,6 +140,10 @@ public struct StatusView: View { .padding(.bottom, viewModel.hasToolbar ? .zero : viewModel.margin) } } // end VStack + .frame(alignment: .top) + .onReceive(viewModel.$isContentSensitiveToggled) { _ in + viewModel.delegate?.statusView(viewModel, viewHeightDidChange: Void()) + } } } @@ -196,6 +226,17 @@ extension StatusView { .buttonStyle(.borderless) } + public var spoilerContentView: some View { + VStack(alignment: .leading, spacing: .zero) { + TextViewRepresentable( + metaContent: viewModel.spoilerContent ?? PlaintextMetaContent(string: ""), + textStyle: .statusContent, + width: contentWidth + ) + .frame(width: contentWidth) + } + } + public var contentView: some View { VStack(alignment: .leading, spacing: .zero) { TextViewRepresentable( diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift index 5b27c8e5..540a6ca5 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift @@ -108,7 +108,7 @@ extension DataSourceFacade { extension DataSourceFacade { - static func responseToExpandContentAction( + static func responseToToggleContentSensitiveAction( provider: DataSourceProvider & AuthContextProvider, target: StatusTarget, status: StatusRecord @@ -120,7 +120,7 @@ extension DataSourceFacade { ) guard let redirectRecord = _redirectRecord else { return } - try await responseToExpandContentAction( + try await responseToToggleContentSensitiveAction( provider: provider, status: redirectRecord ) @@ -134,7 +134,7 @@ extension DataSourceFacade { } @MainActor - static func responseToExpandContentAction( + static func responseToToggleContentSensitiveAction( provider: DataSourceProvider, status: StatusRecord ) async throws { diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift index f2818613..307076dd 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift @@ -14,98 +14,136 @@ import Meta // MARK: - header extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { - func tableViewCell( - _ cell: UITableViewCell, - statusView: StatusView, - headerDidPressed header: UIView - ) { - Task { - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { - assertionFailure() - return - } - guard let status = await item.status(in: self.context.managedObjectContext) else { - assertionFailure("only works for status data provider") - return - } - await DataSourceFacade.coordinateToProfileScene( - provider: self, - target: .repost, - status: status - ) - } - } - +// func tableViewCell( +// _ cell: UITableViewCell, +// statusView: StatusView, +// headerDidPressed header: UIView +// ) { +// Task { +// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) +// guard let item = await item(from: source) else { +// assertionFailure() +// return +// } +// guard let status = await item.status(in: self.context.managedObjectContext) else { +// assertionFailure("only works for status data provider") +// return +// } +// await DataSourceFacade.coordinateToProfileScene( +// provider: self, +// target: .repost, +// status: status +// ) +// } +// } } // MARK: - avatar button extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { - func tableViewCell( - _ cell: UITableViewCell, - statusView: StatusView, - authorAvatarButtonDidPressed button: AvatarButton - ) { - Task { - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { - assertionFailure() - return - } - guard let status = await item.status(in: self.context.managedObjectContext) else { - assertionFailure("only works for status data provider") - return - } - await DataSourceFacade.coordinateToProfileScene( - provider: self, - target: .status, - status: status - ) - } - } - - func tableViewCell( - _ cell: UITableViewCell, - statusView: StatusView, - quoteStatusView: StatusView, - authorAvatarButtonDidPressed button: AvatarButton - ) { - Task { - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { - assertionFailure() - return - } - guard let status = await item.status(in: self.context.managedObjectContext) else { - assertionFailure("only works for status data provider") - return - } - await DataSourceFacade.coordinateToProfileScene( - provider: self, - target: .quote, - status: status - ) - } - } +// func tableViewCell( +// _ cell: UITableViewCell, +// statusView: StatusView, +// authorAvatarButtonDidPressed button: AvatarButton +// ) { +// Task { +// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) +// guard let item = await item(from: source) else { +// assertionFailure() +// return +// } +// guard let status = await item.status(in: self.context.managedObjectContext) else { +// assertionFailure("only works for status data provider") +// return +// } +// await DataSourceFacade.coordinateToProfileScene( +// provider: self, +// target: .status, +// status: status +// ) +// } +// } +// +// func tableViewCell( +// _ cell: UITableViewCell, +// statusView: StatusView, +// quoteStatusView: StatusView, +// authorAvatarButtonDidPressed button: AvatarButton +// ) { +// Task { +// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) +// guard let item = await item(from: source) else { +// assertionFailure() +// return +// } +// guard let status = await item.status(in: self.context.managedObjectContext) else { +// assertionFailure("only works for status data provider") +// return +// } +// await DataSourceFacade.coordinateToProfileScene( +// provider: self, +// target: .quote, +// status: status +// ) +// } +// } } -// MARK: - spoiler +// MARK: - content extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, expandContentButtonDidPressed button: UIButton) { - Task { - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { +// func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) { +// Task { +// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) +// guard let item = await item(from: source) else { +// assertionFailure() +// return +// } +// guard let status = await item.status(in: self.context.managedObjectContext) else { +// assertionFailure("only works for status data provider") +// return +// } +// +// await DataSourceFacade.responseToMetaTextAreaView( +// provider: self, +// target: .status, +// status: status, +// metaTextAreaView: metaTextAreaView, +// didSelectMeta: meta +// ) +// } +// } +// +// func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, quoteStatusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) { +// Task { +// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) +// guard let item = await item(from: source) else { +// assertionFailure() +// return +// } +// guard let status = await item.status(in: self.context.managedObjectContext) else { +// assertionFailure("only works for status data provider") +// return +// } +// +// await DataSourceFacade.responseToMetaTextAreaView( +// provider: self, +// target: .quote, +// status: status, +// metaTextAreaView: metaTextAreaView, +// didSelectMeta: meta +// ) +// } +// } + + func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, toggleContentDisplay isReveal: Bool) { + Task { @MainActor in + guard let status = viewModel.status else { assertionFailure() return } - guard let status = await item.status(in: self.context.managedObjectContext) else { - assertionFailure("only works for status data provider") - return - } - - try await DataSourceFacade.responseToExpandContentAction( + + try await DataSourceFacade.responseToToggleContentSensitiveAction( provider: self, target: .status, status: status @@ -114,53 +152,6 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC } } -// MARK: - content -extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) { - Task { - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { - assertionFailure() - return - } - guard let status = await item.status(in: self.context.managedObjectContext) else { - assertionFailure("only works for status data provider") - return - } - - await DataSourceFacade.responseToMetaTextAreaView( - provider: self, - target: .status, - status: status, - metaTextAreaView: metaTextAreaView, - didSelectMeta: meta - ) - } - } - - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, quoteStatusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) { - Task { - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { - assertionFailure() - return - } - guard let status = await item.status(in: self.context.managedObjectContext) else { - assertionFailure("only works for status data provider") - return - } - - await DataSourceFacade.responseToMetaTextAreaView( - provider: self, - target: .quote, - status: status, - metaTextAreaView: metaTextAreaView, - didSelectMeta: meta - ) - } - } -} - // MARK: - media extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider & MediaPreviewableViewController { @@ -184,47 +175,47 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC } // end Task } - func tableViewCell( - _ cell: UITableViewCell, - statusView: StatusView, - mediaGridContainerView containerView: MediaGridContainerView, - didTapMediaView mediaView: MediaView, - at index: Int - ) { +// func tableViewCell( +// _ cell: UITableViewCell, +// statusView: StatusView, +// mediaGridContainerView containerView: MediaGridContainerView, +// didTapMediaView mediaView: MediaView, +// at index: Int +// ) { +// +// } +// +// func tableViewCell( +// _ cell: UITableViewCell, +// statusView: StatusView, +// quoteStatusView: StatusView, +// mediaGridContainerView containerView: MediaGridContainerView, +// didTapMediaView mediaView: MediaView, +// at index: Int +// ) { +// Task { +// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) +// guard let item = await item(from: source) else { +// assertionFailure() +// return +// } +// guard let status = await item.status(in: self.context.managedObjectContext) else { +// assertionFailure("only works for status data provider") +// return +// } +// await DataSourceFacade.coordinateToMediaPreviewScene( +// provider: self, +// target: .quote, +// status: status, +// mediaPreviewContext: DataSourceFacade.MediaPreviewContext( +// containerView: .mediaGridContainerView(containerView), +// mediaView: mediaView, +// index: index +// ) +// ) +// } +// } - } - - func tableViewCell( - _ cell: UITableViewCell, - statusView: StatusView, - quoteStatusView: StatusView, - mediaGridContainerView containerView: MediaGridContainerView, - didTapMediaView mediaView: MediaView, - at index: Int - ) { - Task { - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { - assertionFailure() - return - } - guard let status = await item.status(in: self.context.managedObjectContext) else { - assertionFailure("only works for status data provider") - return - } - await DataSourceFacade.coordinateToMediaPreviewScene( - provider: self, - target: .quote, - status: status, - mediaPreviewContext: DataSourceFacade.MediaPreviewContext( - containerView: .mediaGridContainerView(containerView), - mediaView: mediaView, - index: index - ) - ) - } - } - func tableViewCell( _ cell: UITableViewCell, viewModel: StatusView.ViewModel, @@ -266,107 +257,107 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC // MARK: - poll extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, pollTableView tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - Task { - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { - assertionFailure() - return - } - guard let status = await item.status(in: self.context.managedObjectContext) else { - assertionFailure("only works for status data provider") - return - } - await DataSourceFacade.responseToStatusPollOption( - provider: self, - target: .status, - status: status, - didSelectRowAt: indexPath - ) - } - } - - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, pollVoteButtonDidPressed button: UIButton) { - Task { - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { - assertionFailure() - return - } - guard let status = await item.status(in: self.context.managedObjectContext) else { - assertionFailure("only works for status data provider") - return - } - await DataSourceFacade.responseToStatusPollOption( - provider: self, - target: .status, - status: status, - voteButtonDidPressed: button - ) - } - } +// func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, pollTableView tableView: UITableView, didSelectRowAt indexPath: IndexPath) { +// Task { +// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) +// guard let item = await item(from: source) else { +// assertionFailure() +// return +// } +// guard let status = await item.status(in: self.context.managedObjectContext) else { +// assertionFailure("only works for status data provider") +// return +// } +// await DataSourceFacade.responseToStatusPollOption( +// provider: self, +// target: .status, +// status: status, +// didSelectRowAt: indexPath +// ) +// } +// } +// +// func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, pollVoteButtonDidPressed button: UIButton) { +// Task { +// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) +// guard let item = await item(from: source) else { +// assertionFailure() +// return +// } +// guard let status = await item.status(in: self.context.managedObjectContext) else { +// assertionFailure("only works for status data provider") +// return +// } +// await DataSourceFacade.responseToStatusPollOption( +// provider: self, +// target: .status, +// status: status, +// voteButtonDidPressed: button +// ) +// } +// } } // MARK: - quote extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, quoteStatusViewDidPressed quoteStatusView: StatusView) { - Task { - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { - return - } - guard let status = await item.status(in: self.context.managedObjectContext) else { - assertionFailure("only works for status data provider") - return - } - - await DataSourceFacade.coordinateToStatusThreadScene( - provider: self, - target: .quote, - status: status - ) - } - } +// func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, quoteStatusViewDidPressed quoteStatusView: StatusView) { +// Task { +// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) +// guard let item = await item(from: source) else { +// return +// } +// guard let status = await item.status(in: self.context.managedObjectContext) else { +// assertionFailure("only works for status data provider") +// return +// } +// +// await DataSourceFacade.coordinateToStatusThreadScene( +// provider: self, +// target: .quote, +// status: status +// ) +// } +// } } // MARK: - toolbar extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { - func tableViewCell( - _ cell: UITableViewCell, - statusView: StatusView, - statusToolbar: StatusToolbar, - actionDidPressed action: StatusToolbar.Action, - button: UIButton - ) { - Task { - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { - assertionFailure() - return - } - guard let status = await item.status(in: self.context.managedObjectContext) else { - assertionFailure("only works for status data provider") - return - } - - await DataSourceFacade.responseToStatusToolbar( - provider: self, - status: status, - action: action, - sender: button, - authenticationContext: self.authContext.authenticationContext - ) - } // end Task - } // end func +// func tableViewCell( +// _ cell: UITableViewCell, +// statusView: StatusView, +// statusToolbar: StatusToolbar, +// actionDidPressed action: StatusToolbar.Action, +// button: UIButton +// ) { +// Task { +// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) +// guard let item = await item(from: source) else { +// assertionFailure() +// return +// } +// guard let status = await item.status(in: self.context.managedObjectContext) else { +// assertionFailure("only works for status data provider") +// return +// } +// +// await DataSourceFacade.responseToStatusToolbar( +// provider: self, +// status: status, +// action: action, +// sender: button, +// authenticationContext: self.authContext.authenticationContext +// ) +// } // end Task +// } // end func - func tableViewCell( - _ cell: UITableViewCell, - statusView: StatusView, - statusToolbar: StatusToolbar, - menuActionDidPressed action: StatusToolbar.MenuAction, - menuButton button: UIButton - ) { +// func tableViewCell( +// _ cell: UITableViewCell, +// statusView: StatusView, +// statusToolbar: StatusToolbar, +// menuActionDidPressed action: StatusToolbar.MenuAction, +// menuButton button: UIButton +// ) { // Task { // let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) // guard let item = await item(from: source) else { @@ -444,75 +435,84 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC // ) // } // end switch // } // end Task - } // end func +// } // end func } extension StatusViewTableViewCellDelegate where Self: DataSourceProvider { - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, translateButtonDidPressed button: UIButton) { - Task { - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { - return - } - guard let status = await item.status(in: self.context.managedObjectContext) else { - assertionFailure("only works for status data provider") - return - } - - try await DataSourceFacade.responseToStatusTranslate( - provider: self, - status: status - ) - } // end Task +// func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, translateButtonDidPressed button: UIButton) { +// Task { +// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) +// guard let item = await item(from: source) else { +// return +// } +// guard let status = await item.status(in: self.context.managedObjectContext) else { +// assertionFailure("only works for status data provider") +// return +// } +// +// try await DataSourceFacade.responseToStatusTranslate( +// provider: self, +// status: status +// ) +// } // end Task +// } +} + +extension StatusViewTableViewCellDelegate where Self: DataSourceProvider { + func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, viewHeightDidChange: Void) { + // Manually update self resize + UIView.performWithoutAnimation { + cell.invalidateIntrinsicContentSize() + } } } // MARK: - a11y extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { - func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, accessibilityActivate: Void) { - Task { - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { - return - } - switch item { - case .status(let status): - await DataSourceFacade.coordinateToStatusThreadScene( - provider: self, - target: .repost, // keep repost wrapper - status: status - ) - case .user(let user): - await DataSourceFacade.coordinateToProfileScene( - provider: self, - user: user - ) - case .notification(let notification): - let managedObjectContext = self.context.managedObjectContext - guard let object = notification.object(in: managedObjectContext) else { - assertionFailure() - return - } - switch object { - case .mastodon(let notification): - if let status = notification.status { - await DataSourceFacade.coordinateToStatusThreadScene( - provider: self, - target: .repost, // keep repost wrapper - status: .mastodon(record: .init(objectID: status.objectID)) - ) - } else { - await DataSourceFacade.coordinateToProfileScene( - provider: self, - user: .mastodon(record: .init(objectID: notification.account.objectID)) - ) - } - } - } - } // end Task - } // end func +// func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, accessibilityActivate: Void) { +// Task { +// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) +// guard let item = await item(from: source) else { +// return +// } +// switch item { +// case .status(let status): +// await DataSourceFacade.coordinateToStatusThreadScene( +// provider: self, +// target: .repost, // keep repost wrapper +// status: status +// ) +// case .user(let user): +// await DataSourceFacade.coordinateToProfileScene( +// provider: self, +// user: user +// ) +// case .notification(let notification): +// let managedObjectContext = self.context.managedObjectContext +// guard let object = notification.object(in: managedObjectContext) else { +// assertionFailure() +// return +// } +// switch object { +// case .mastodon(let notification): +// if let status = notification.status { +// await DataSourceFacade.coordinateToStatusThreadScene( +// provider: self, +// target: .repost, // keep repost wrapper +// status: .mastodon(record: .init(objectID: status.objectID)) +// ) +// } else { +// await DataSourceFacade.coordinateToProfileScene( +// provider: self, +// user: .mastodon(record: .init(objectID: notification.account.objectID)) +// ) +// } +// } +// } +// } // end Task +// } // end func } diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift index a6fc2d28..6461c41d 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift @@ -25,8 +25,10 @@ protocol StatusViewContainerTableViewCell: UITableViewCell, AutoGenerateProtocol // sourcery: replaceWith = "func tableViewCell(_ cell: UITableViewCell," protocol StatusViewTableViewCellDelegate: AutoGenerateProtocolDelegate { // sourcery:inline:StatusViewTableViewCellDelegate.AutoGenerateProtocolDelegate + func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, toggleContentDisplay isReveal: Bool) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, toggleContentWarningOverlayDisplay isReveal: Bool) + func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, viewHeightDidChange: Void) // sourcery:end } @@ -34,6 +36,10 @@ protocol StatusViewTableViewCellDelegate: AutoGenerateProtocolDelegate { // Protocol Extension extension StatusViewDelegate where Self: StatusViewContainerTableViewCell { // sourcery:inline:StatusViewContainerTableViewCell.AutoGenerateProtocolRelayDelegate + func statusView(_ viewModel: StatusView.ViewModel, toggleContentDisplay isReveal: Bool) { + delegate?.tableViewCell(self, viewModel: viewModel, toggleContentDisplay: isReveal) + } + func statusView(_ viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel) { delegate?.tableViewCell(self, viewModel: viewModel, previewActionForMediaViewModel: mediaViewModel) } @@ -41,5 +47,9 @@ extension StatusViewDelegate where Self: StatusViewContainerTableViewCell { func statusView(_ viewModel: StatusView.ViewModel, toggleContentWarningOverlayDisplay isReveal: Bool) { delegate?.tableViewCell(self, viewModel: viewModel, toggleContentWarningOverlayDisplay: isReveal) } + + func statusView(_ viewModel: StatusView.ViewModel, viewHeightDidChange: Void) { + delegate?.tableViewCell(self, viewModel: viewModel, viewHeightDidChange: viewHeightDidChange) + } // sourcery:end } diff --git a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift index 205f8bcc..d7954f89 100644 --- a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift @@ -29,6 +29,7 @@ class ListTimelineViewController: TimelineViewController { tableView.backgroundColor = .systemBackground tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .none + tableView.selfSizingInvalidation = .enabled return tableView }() From 15a8a8211294474f456439446fb88081c8996f35 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 14 Mar 2023 11:33:12 +0800 Subject: [PATCH 037/128] feat: add status toolbar UI --- TwidereSDK/Package.swift | 2 +- .../mail.mini.inline.imageset/Contents.json | 2 +- .../mail.mini.inline.pdf | Bin 2567 -> 0 bytes .../mail.mini.inline.imageset/mail.pdf | Bin 0 -> 2567 bytes .../globe.mini.inline.imageset/Contents.json | 2 +- .../{globe.mini.inline.pdf => globe.pdf} | Bin 2533 -> 2533 bytes .../lock.mini.inline.imageset/Contents.json | 2 +- .../{lock.mini.inline.pdf => lock.pdf} | Bin 3706 -> 3706 bytes .../Contents.json | 2 +- ...ock.open.mini.inline.pdf => lock-open.pdf} | Bin 3500 -> 3499 bytes .../CoreDataStack/MastodonVisibility.swift | 17 -- .../Model/Status/StatusVisibility.swift | 55 ----- .../TwidereUI/Content/StatusHeaderView.swift | 5 +- .../Content/StatusView+ViewModel.swift | 34 ++- .../TwidereUI/Content/StatusView.swift | 210 ++++++++++++++++-- .../Content/TimestampLabelView.swift | 2 - .../LabelRepresentable.swift | 2 +- .../TwidereUI/Vender/VectorImageView.swift | 7 +- TwidereX.xcodeproj/project.pbxproj | 2 - .../xcshareddata/swiftpm/Package.resolved | 9 + .../MediaInfoDescriptionView+ViewModel.swift | 6 +- 21 files changed, 236 insertions(+), 123 deletions(-) delete mode 100644 TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/mail.mini.inline.imageset/mail.mini.inline.pdf create mode 100644 TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/mail.mini.inline.imageset/mail.pdf rename TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/globe.mini.inline.imageset/{globe.mini.inline.pdf => globe.pdf} (95%) rename TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/lock.mini.inline.imageset/{lock.mini.inline.pdf => lock.pdf} (95%) rename TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/lock.open.mini.inline.imageset/{lock.open.mini.inline.pdf => lock-open.pdf} (57%) delete mode 100644 TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/MastodonVisibility.swift delete mode 100644 TwidereSDK/Sources/TwidereCore/Model/Status/StatusVisibility.swift diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index 7b84d2eb..715b5b33 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -30,7 +30,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-nio.git", from: "2.34.0"), .package(url: "https://github.com/Flipboard/FLAnimatedImage.git", from: "1.0.0"), .package(url: "https://github.com/MainasuK/CommonOSLog", from: "0.1.1"), - .package(url: "https://github.com/TwidereProject/MetaTextKit.git", exact: "4.1.2"), + .package(url: "https://github.com/TwidereProject/MetaTextKit.git", exact: "4.4.0"), .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.5.0"), .package(url: "https://github.com/Alamofire/AlamofireImage.git", from: "4.1.0"), .package(url: "https://github.com/Alamofire/AlamofireNetworkActivityIndicator.git", from: "3.1.0"), diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/mail.mini.inline.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/mail.mini.inline.imageset/Contents.json index 4164de67..f8fa0bcc 100644 --- a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/mail.mini.inline.imageset/Contents.json +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/mail.mini.inline.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "mail.mini.inline.pdf", + "filename" : "mail.pdf", "idiom" : "universal" } ], diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/mail.mini.inline.imageset/mail.mini.inline.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/mail.mini.inline.imageset/mail.mini.inline.pdf deleted file mode 100644 index 482efe65aa1b36a0092149d7e042932ee765613b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2567 zcmZuzO^?$s5WV|X__7ilNaAltLP)R+2mxa0azh+SH|!R58)ymwem!pzXW}-=rRj@j z=FJ;>#uwLDub!wP2%%&p+pph+luw_^XU{~lzooxI=Xm+9+20-Sr2)92Ro(H>tRF;q z)%@OWn$_zU^71DCx9P-Bp^B=1WHr7#;Wz%K$8WBP#u#hm=0WhX`AfIi!^Yr{6HNdpQXv%;c|gwPd=lRLV-Jjs32qDW?lfPHmPGLwQO#L&G|##ELMO30L@tzVYBTpfo#2-tTQZ&!{#S(#)C8}1?}>_tfgooA`2T_TxG`P8gzdK6Q3W4yg!i z&RrXuX5OFsgK^$IX)Yuz3qJQqalVPY_$X**r-3|9ZyMCYR9Ch3Jk+tZreR+`-|yRF zCqI9|@W)er`un4i<@NfmNx=K&cDsJj{*j-j5zZ4?@tD(g9C)LrRnxV<4x6TvbiG(5 zcbDz{*z6IJ?iM3>y*>bwEPbLdup)1z=u`wzI8q{o2g38X6p7H5#D&mpa# z&FLx(T!xN^^>)`B1g>i3n=8oisB0gZ$MVtk_VFM}xoX>^q=b8c%bV@L2JHJQ+xl?q O)6vE{adGkT!>j)t;1K}; diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/mail.mini.inline.imageset/mail.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/mail.mini.inline.imageset/mail.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e3f905676786bbbc946237903883df13f3e9069b GIT binary patch literal 2567 zcmZuzO^?$s5WV|X__7ilNd4_ZLP)R+2mxa0azh+SH|!R58)ymwem!qoXW};TVb^`} z%$s>*dwg+y_3DYPq7Ygt*?s*kqk)>>`l`a$rr>7-xp@Y(60jF)v4 z8x^&a$s}qU8`)IKD2EBiY~xI{Sk~a5s|gCmDO&^bwTa5ewE$RqTVq~V2LHXMbup+A zjf_={uy6!r(pY(Cf^A^R7zeTy_aY1_xk`r+Vnlcze71>R+QE7YthT;x8elfMP&YEb zz-gsRB(t*Jdd5GoYBLG&fxz=b0Ht(EmYgo2sVo4Kd~TFaYY15fqeqbe<-}Nm^;{?^ z!U_e(kQ!x0u^rV7b7%yW3UphdiGbDuWe2k$drf3BeiK_$*WfPLkb?2FQIS<`jE+H; z5Pi}L#U?F>bs0oR3{oOq1qIjG$~u~$X$EyD_##cl;BqF&6nqg%H{=#ZNo4-IHmGhYiwbJnw2w2E=V#)&rq2X&Jf744l1!C3>Lyw0Y}k8t!Xas zH;wPKyv&WThyB4=v1gbW7|T8j7S_;b;*)q3M8THjl}A6=LZ-@k6-X{tx@qzbVy(Ay z{bW!dg;2OJH573)$r5rDj6j)5WF!oGV+y((Gec4Nk#-hj>+NV=UWQ-PAcBH zLn;EB^R10dGrgbF!8pG@sV`)#AfI|9o^N6&J__pDxgihTn;P}8tLw%DZt9L_Z_L80# z%k1v5+a1~+80l>>fY+-%FzGW?yg&%zm&xMo<`$t!%3vu6p3CkRZ1p7m9!TLSoOq%H zdnOzczg!(w+wShv^v}NiCX|dKy=2lK8N9b%0?q?h-0=fmCV5!INka~hQ5sR4L3l%) zK}JKH(^Y7=3?25X&9>bOs@|I`$nmJ}9@@w9(dPE?QIy59>kg61 R{UN1etn=dH;^l`|{{dqw4XOYD literal 0 HcmV?d00001 diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/globe.mini.inline.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/globe.mini.inline.imageset/Contents.json index d9d3797a..3e324863 100644 --- a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/globe.mini.inline.imageset/Contents.json +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/globe.mini.inline.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "globe.mini.inline.pdf", + "filename" : "globe.pdf", "idiom" : "universal" } ], diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/globe.mini.inline.imageset/globe.mini.inline.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/globe.mini.inline.imageset/globe.pdf similarity index 95% rename from TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/globe.mini.inline.imageset/globe.mini.inline.pdf rename to TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/globe.mini.inline.imageset/globe.pdf index 1cf7abab05f09bdf44da1f98b3e7dba4de8b45ae..6935aa601df1239fb333ca62ab92cafcfe99f17e 100644 GIT binary patch delta 20 ccmaDV{8V_tWJZIHQ>L>s8Jca@<=DXp09FkLG5`Po delta 20 ccmaDV{8V_tWJZ&XQ>L>s8JTR><=DXp09HH)H2?qr diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/lock.mini.inline.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/lock.mini.inline.imageset/Contents.json index 514008d4..bc5b2098 100644 --- a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/lock.mini.inline.imageset/Contents.json +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/lock.mini.inline.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "lock.mini.inline.pdf", + "filename" : "lock.pdf", "idiom" : "universal" } ], diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/lock.mini.inline.imageset/lock.mini.inline.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/lock.mini.inline.imageset/lock.pdf similarity index 95% rename from TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/lock.mini.inline.imageset/lock.mini.inline.pdf rename to TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/lock.mini.inline.imageset/lock.pdf index ac55ddd767832828a5279730c210eb569d4cb443..dc68c97f107e8ad3989a784592a61ebe056fd3b9 100644 GIT binary patch delta 40 tcmew*^Gjwz7niY~0T?J4OqSx-+PE*8i_v7W6!$VlCPTB$hj?}{0ssW<3@-ox delta 38 tcmew*^Gjwz7l)CaB@h@}PL|--*|;y7i_v1U6!$VlCL@#0hj?}{0s#1H3=aSR diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/lock.open.mini.inline.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/lock.open.mini.inline.imageset/Contents.json index 34e6c142..7d62252e 100644 --- a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/lock.open.mini.inline.imageset/Contents.json +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/lock.open.mini.inline.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "lock.open.mini.inline.pdf", + "filename" : "lock-open.pdf", "idiom" : "universal" } ], diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/lock.open.mini.inline.imageset/lock.open.mini.inline.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/lock.open.mini.inline.imageset/lock-open.pdf similarity index 57% rename from TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/lock.open.mini.inline.imageset/lock.open.mini.inline.pdf rename to TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/lock.open.mini.inline.imageset/lock-open.pdf index ad359c8bb7ad4c6975765f2911a77126d61087c3..8e0905ba2ffa7962dd2d62ed680ad523a60039a1 100644 GIT binary patch literal 3499 zcmb7{OOM+&5XbNQ6uc~u976L=2?zotyGc>BMYm3GK@W~r*>zxBt?d+PKYeE;>QDM{ z*y^w=|1`sSaAri^+~0k8WmFQvNG*rYUxk#fU&}XdM8A9BpTgFB@pHd>JU&TBaLH1K zQ#p162d)@WRAKuB^kM*zaApR0Y>G6_!zIlZ^Jah9it->VTZ3VikKXcFnabh zsSL>#z(zT3qV=SkqPCH5vd4%Eq`Tr7`wIb>*cN70JNZ(Zy$YedDcTjQ>_Gj*P6)x; zB#|k55eY~Y2~xpUa}K~tqKt}d<5H9b!Hcf=3wE-Pu)8f)09I87>{bAXOwNb+(#w09HIkTNMN^y5cVw=jDsoiQk?(ywW_p z{PTyGopmq*w*pMl5J|}dX{bId4Y@KqA&!~IA!Ko|>gN0r7GS+`80gwe7KbYk2jLCK zUV-7}@(~v3{%=}?a&#q6Q})^KgtEm{IW1k5UJ5nT^u|c7B;@HmVLUet3(|{j&IZVs z*$XN3SY8V40*iI)0&-PZ75PdOk)i(~l5-Vn2@>1XEX%PrRae6qk$wtIlXk^McOpG( zO>3-{ZVF7xDUYNF(uqJ1Q}C&Mn+0lXk9~jr>bx? zew`s`Q!q`#g_1X%R@2e=SRuH<%VjYzPRylYejUW58|&DH@riD%GVEp`;-?ZoQGj%H zQJ_%h>_!I>!v~Sfv}#&%k`F*3#MmTC$9bUK!RHA%DNk743a8B2AH_FEMQqt7{P#** z(}qx|mv*DEkw#dRUP_HOO^n4iN5?hGxq$zO9HfdIeogIgZk5}?HO6bM`MmXL5R?Z9 zcB&JXrM@W)8&6GRor@e(u8zwnxkb+XVsEDx3ZJX{AdDEP4aPX$VR1V~e zx4YeNJjhRf&;gyU#qa<8*~`WK>aow@m;PbBdN+KRpXxa_k9P`hI`$dd%_}YY!|-k2 z^#{qH0L#4G?XWxcJF>{<{~6w|_QWK;j7C&_B{0I*!{Vp)1Gy?Wf~B18+z#JJ*If92 zAX2SFhnQ+54)6ejCGgwTakUv9PgTF~`!9kzK}@moUl~K7wj!J_s@m~R?to}coFd!$ z<^s`I#;KGu{#?;nnkKWF!*G-w@R;Dm a$MwHGc|U$ut@g(;9A{G!H#hHp{qP?m4#2bk literal 3500 zcmb7HOOM+&5We$Q@UlR12ra%vAPA7`CPmQ}-8#JmJvdrr*MV%cwo|12^_`LEkM!fP z)xn@Y&GX~Tkh;0Q`|wJuD1?@c96x^*QoepI-@Fn1_JRKjQ{#)D`|ac5Nm>GzEVV!E z`qf4(?)tyiUBCR{oxJ^6|LgYRFQHX4J+e72USWlI_V~^zKTCHT0kO-)zT1*vXIdY% zj4Dn_awA$r0Ko+t3YL_a<%Xhr4THTwJQZ!A>?MW9`=)q-nW&s|FQPHVU_OF6^rbp zvB^=jKoVw*$~;v_-l0xVo8-E#5-A`^Xq2%j+CUAK<eA;9FLTtv2;2x@4GobPwU?Ue!_bf`vlHT&iJU?f2Uac4U%>$DP0=>Bn=Fn{AP$5# zAV&oZi_2Fq(EVRpy|VOZD>YetF0W;ep|q(wiK3w+qYaYe932$em97CDXn={@3n?^N zHU)8bta}$Amz7b0pGXIX=7pmptHKh4P7XDe7-2~ zt%@ub)BF;WPe@lFJmWT1ld;YjcA8zBd&tbuEt1(RbvF| zl^;*#+**@pN0ZmdI!fFn-#SeNZnd^C5zEuPTtq_ z(r&bd=prf5OLRHihEQU&bYC;f1>#5MK&s3U*W3>0Ub!7?t6kHStw)2PJb+ZYcUkI# z0EUgHrZLtA&M8;NWtQ9`=W%hg^NGS2>wIfIUa|Q$Jzu?P&?nryX)u)=`Qq(%J0AA( z(;sw0r)Tl|KY#Xeald-(Gw@6QuwK0zzspbMtiV9il*&9n4Fxx^j+U1Fe*CuU`n}|D zfMqs!J8lpCmOSzYz!AJ(?Ff@pQ4>^sCNPB0hs96p2MSek21|MLb31+`U40S%o{-8C z-C`u9j0`wiLJP~dH?H&{{Y%^zw`hA diff --git a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/MastodonVisibility.swift b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/MastodonVisibility.swift deleted file mode 100644 index d7691a0b..00000000 --- a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/MastodonVisibility.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// MastodonVisibility.swift -// -// -// Created by MainasuK on 2021-12-6. -// - -import Foundation -import CoreDataStack -import MastodonSDK - -extension MastodonVisibility { - public var asStatusVisibility: StatusVisibility { - let visibility: Mastodon.Entity.Status.Visibility = .init(rawValue: rawValue) ?? ._other(rawValue) - return .mastodon(visibility) - } -} diff --git a/TwidereSDK/Sources/TwidereCore/Model/Status/StatusVisibility.swift b/TwidereSDK/Sources/TwidereCore/Model/Status/StatusVisibility.swift deleted file mode 100644 index a09fd54b..00000000 --- a/TwidereSDK/Sources/TwidereCore/Model/Status/StatusVisibility.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// StatusVisibility.swift -// -// -// Created by MainasuK on 2021-12-6. -// - -import UIKit -import MastodonSDK -import TwidereAsset -import TwidereLocalization - -public enum StatusVisibility { - case mastodon(Mastodon.Entity.Status.Visibility) -} - -extension StatusVisibility { - public var inlineImage: UIImage? { - switch self { - case .mastodon(let visibility): - switch visibility { - case .public: - return Asset.ObjectTools.globeMiniInline.image.withRenderingMode(.alwaysTemplate) - case .unlisted: - return Asset.ObjectTools.lockOpenMiniInline.image.withRenderingMode(.alwaysTemplate) - case .private: - return Asset.ObjectTools.lockMiniInline.image.withRenderingMode(.alwaysTemplate) - case .direct: - return Asset.Communication.mailMiniInline.image.withRenderingMode(.alwaysTemplate) - case ._other: - return nil - } - } - } -} - -extension StatusVisibility { - public var accessibilityLabel: String? { - switch self { - case .mastodon(let visibility): - switch visibility { - case .public: - return L10n.Scene.Compose.Visibility.public - case .unlisted: - return L10n.Scene.Compose.Visibility.unlisted - case .private: - return L10n.Scene.Compose.Visibility.private - case .direct: - return L10n.Scene.Compose.Visibility.direct - case ._other: - return nil - } - } - } -} diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift index a18eb038..7f07acf4 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift @@ -52,14 +52,11 @@ public struct StatusHeaderView: View { value: proxy.frame(in: .local).size.height ) }) - .border(.blue, width: 1) .onPreferenceChange(ViewHeightKey.self) { height in self.iconImageDimension = height } .overlay(alignment: .leading) { - Image(uiImage: viewModel.image) - .resizable() - .aspectRatio(contentMode: .fill) + VectorImageView(image: viewModel.image) .frame(width: iconImageDimension, height: iconImageDimension) .offset(x: -(StatusHeaderView.iconImageTrailingSpacing + iconImageDimension), y: 0) } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index e2c8bc8f..c638d218 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -134,21 +134,38 @@ extension StatusView { // @Published public var quoteCount: Int = 0 // @Published public var likeCount: Int = 0 // -// @Published public var visibility: StatusVisibility? + @Published public var visibility: MastodonVisibility? + var visibilityIconImage: UIImage? { + switch visibility { + case .public: + return Asset.ObjectTools.globeMiniInline.image.withRenderingMode(.alwaysTemplate) + case .unlisted: + return Asset.ObjectTools.lockOpenMiniInline.image.withRenderingMode(.alwaysTemplate) + case .private: + return Asset.ObjectTools.lockMiniInline.image.withRenderingMode(.alwaysTemplate) + case .direct: + return Asset.Communication.mailMiniInline.image.withRenderingMode(.alwaysTemplate) + case ._other: + assertionFailure() + return nil + case nil: + return nil + } + } // @Published public var replySettings: Twitter.Entity.V2.Tweet.ReplySettings? -// +// // @Published public var dateTimeProvider: DateTimeProvider? // @Published public var timestamp: Date? // @Published public var timeAgoStyleTimestamp: String? // @Published public var formattedStyleTimestamp: String? -// +// // @Published public var sharePlaintextContent: String? // @Published public var shareStatusURL: String? -// +// // @Published public var isDeletable = false -// -// @Published public var groupedAccessibilityLabel = "" // +// @Published public var groupedAccessibilityLabel = "" + @Published public var timestampLabelViewModel: TimestampLabelView.ViewModel? // @@ -920,7 +937,7 @@ extension StatusView.ViewModel { public var hasToolbar: Bool { switch kind { - case .timeline, .conversationRoot, .conversationThread: + case .timeline, .repost, .conversationRoot, .conversationThread: return true default: return false @@ -1093,6 +1110,9 @@ extension StatusView.ViewModel { .map { _ in status.author.acct } .assign(to: &$authorUsernme) + // visibility + visibility = status.visibility + // timestamp switch kind { case .timeline, .repost: diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index 3044cc55..372bd4b3 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -54,11 +54,17 @@ public protocol StatusViewDelegate: AnyObject { public struct StatusView: View { + static let logger = Logger(subsystem: "StatusView", category: "View") + var logger: Logger { StatusView.logger } + static var hangingAvatarButtonDimension: CGFloat { 44.0 } static var hangingAvatarButtonTrailingSapcing: CGFloat { 10.0 } @ObservedObject public private(set) var viewModel: ViewModel + @Environment(\.dynamicTypeSize) var dynamicTypeSize + @State private var visibilityIconImageDimension = CGFloat.zero + public init(viewModel: StatusView.ViewModel) { self.viewModel = viewModel } @@ -134,6 +140,9 @@ public struct StatusView: View { } .cornerRadius(12) } + if viewModel.hasToolbar { + toolbarView + } } // end VStack } // end HStack .padding(.top, viewModel.margin) @@ -165,30 +174,59 @@ extension StatusView { avatarButton } // info - HStack(alignment: .firstTextBaseline, spacing: 5) { - // name - LabelRepresentable( - metaContent: viewModel.authorName, - textStyle: .statusAuthorName, - setupLabel: { label in - label.setContentHuggingPriority(.defaultHigh, for: .horizontal) - label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + let infoLayout = dynamicTypeSize < .accessibility1 ? + AnyLayout(HStackLayout(alignment: .center, spacing: 6)) : + AnyLayout(VStackLayout(alignment: .leading, spacing: .zero)) + infoLayout { + let nameLayout = dynamicTypeSize < .accessibility1 ? + AnyLayout(HStackLayout(alignment: .firstTextBaseline, spacing: 6)) : + AnyLayout(VStackLayout(alignment: .leading, spacing: .zero)) + nameLayout { + // name + LabelRepresentable( + metaContent: viewModel.authorName, + textStyle: .statusAuthorName, + setupLabel: { label in + label.setContentHuggingPriority(.defaultHigh, for: .horizontal) + label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + } + ) + // username + LabelRepresentable( + metaContent: PlaintextMetaContent(string: "@" + viewModel.authorUsernme), + textStyle: .statusAuthorUsername, + setupLabel: { label in + label.setContentHuggingPriority(.defaultHigh, for: .horizontal) + label.setContentCompressionResistancePriority(.defaultLow - 100, for: .horizontal) + } + ) + } + Spacer() + HStack(spacing: 6) { + // mastodon visibility + if let visibilityIconImage = viewModel.visibilityIconImage, visibilityIconImageDimension > 0 { + let dimension = ceil(visibilityIconImageDimension * 0.8) + Color.clear + .frame(width: dimension) + .overlay { + VectorImageView(image: visibilityIconImage, tintColor: TextStyle.statusTimestamp.textColor) + .frame(width: dimension, height: dimension) + } } - ) - // username - LabelRepresentable( - metaContent: PlaintextMetaContent(string: "@" + viewModel.authorUsernme), - textStyle: .statusAuthorUsername, - setupLabel: { label in - label.setContentHuggingPriority(.defaultHigh, for: .horizontal) - label.setContentCompressionResistancePriority(.defaultLow - 100, for: .horizontal) + // timestamp + if let timestampLabelViewModel = viewModel.timestampLabelViewModel { + TimestampLabelView(viewModel: timestampLabelViewModel) + .background(GeometryReader { proxy in + Color.clear.preference( + key: ViewHeightKey.self, + value: proxy.frame(in: .local).size.height + ) + }) + .onPreferenceChange(ViewHeightKey.self) { height in + self.visibilityIconImageDimension = height + } } - ) - Spacer() - // timestamp - if let timestampLabelViewModel = viewModel.timestampLabelViewModel { - TimestampLabelView(viewModel: timestampLabelViewModel) - } + } } .background(GeometryReader { proxy in Color.clear.preference( @@ -247,6 +285,134 @@ extension StatusView { .frame(width: contentWidth) } } + + var toolbarView: some View { + HStack { + ToolbarButton( + action: { kind in + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): reply") + }, + kind: .reply, + image: Asset.Arrows.arrowTurnUpLeftMini.image.withRenderingMode(.alwaysTemplate), + text: "", + tintColor: nil + ) + ToolbarButton( + action: { kind in + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): repost") + }, + kind: .repost, + image: Asset.Media.repeatMini.image.withRenderingMode(.alwaysTemplate), + text: "", + tintColor: nil + ) + ToolbarButton( + action: { kind in + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): like") + }, + kind: .like, + image: Asset.Health.heartMini.image.withRenderingMode(.alwaysTemplate), + text: "", + tintColor: nil + ) + // share + Button { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): share") + } label: { + HStack { + let image: UIImage = { + switch viewModel.kind { + case .conversationRoot: + return Asset.Editing.ellipsisMini.image.withRenderingMode(.alwaysTemplate) + default: + return Asset.Editing.ellipsisMini.image.withRenderingMode(.alwaysTemplate) + } + }() + Image(uiImage: image) + .foregroundColor(.secondary) + } + } + .buttonStyle(.borderless) + .modifier(MaxWidthModifier(max: nil)) + } // end HStack + .frame(height: 48) + } +} + +extension StatusView { + public struct ToolbarButton: View { + static let numberMetricFormatter = NumberMetricFormatter() + + let action: (Kind) -> Void + let kind: Kind + let image: UIImage + let text: String + let tintColor: UIColor? + + public init( + action: @escaping (Kind) -> Void, + kind: Kind, + image: UIImage, + text: String, + tintColor: UIColor? + ) { + self.action = action + self.kind = kind + self.image = image + self.text = text + self.tintColor = tintColor + } + + public var body: some View { + Button { + action(kind) + } label: { + HStack { + Image(uiImage: image) + Text(text) + .font(.system(size: 12, weight: .medium)) + .lineLimit(1) + Spacer() + } + } + .buttonStyle(.borderless) + .tint(Color(uiColor: tintColor ?? .secondaryLabel)) + .foregroundColor(Color(uiColor: tintColor ?? .secondaryLabel)) + } + + static func metric(count: Int?) -> String { + guard let count = count, count > 0 else { + return "" + } + return ToolbarButton.numberMetricFormatter.string(from: count) ?? "" + } + + public enum Kind: Hashable { + case reply + case repost + case quote + case like + case share + } + } + + public struct MaxWidthModifier: ViewModifier { + let max: CGFloat? + + public init(max: CGFloat?) { + self.max = max + } + + @ViewBuilder + public func body(content: Content) -> some View { + if let max = max { + content + .frame(maxWidth: max) + } else { + content + } + } + } } //public final class StatusView: UIView { diff --git a/TwidereSDK/Sources/TwidereUI/Content/TimestampLabelView.swift b/TwidereSDK/Sources/TwidereUI/Content/TimestampLabelView.swift index 4aa48537..062665a4 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/TimestampLabelView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/TimestampLabelView.swift @@ -15,8 +15,6 @@ public struct TimestampLabelView: View { @ObservedObject public var viewModel: ViewModel - @State var int = 0 - public init(viewModel: TimestampLabelView.ViewModel) { self.viewModel = viewModel } diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/LabelRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/LabelRepresentable.swift index 76d24fa2..9c37e9e7 100644 --- a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/LabelRepresentable.swift +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/LabelRepresentable.swift @@ -17,6 +17,7 @@ public struct LabelRepresentable: UIViewRepresentable { label.numberOfLines = 1 label.backgroundColor = .clear label.adjustsFontSizeToFitWidth = false + label.allowsDefaultTighteningForTruncation = false label.lineBreakMode = .byTruncatingTail label.setContentHuggingPriority(.defaultHigh, for: .horizontal) // always try grow vertical label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) @@ -68,7 +69,6 @@ public struct LabelRepresentable: UIViewRepresentable { ) label.attributedText = attributedString -// label.invalidateIntrinsicContentSize() return label } diff --git a/TwidereSDK/Sources/TwidereUI/Vender/VectorImageView.swift b/TwidereSDK/Sources/TwidereUI/Vender/VectorImageView.swift index 901e6dcd..0b0f9041 100644 --- a/TwidereSDK/Sources/TwidereUI/Vender/VectorImageView.swift +++ b/TwidereSDK/Sources/TwidereUI/Vender/VectorImageView.swift @@ -19,7 +19,7 @@ public struct VectorImageView: UIViewRepresentable { public init( image: UIImage, contentMode: UIView.ContentMode = .scaleAspectFit, - tintColor: UIColor = .black + tintColor: UIColor = UIColor.tintColor ) { self.image = image self.contentMode = contentMode @@ -28,10 +28,7 @@ public struct VectorImageView: UIViewRepresentable { public func makeUIView(context: Context) -> UIImageView { let imageView = UIImageView() - imageView.setContentCompressionResistancePriority( - .fittingSizeLevel, - for: .vertical - ) + imageView.setContentCompressionResistancePriority(.fittingSizeLevel,for: .vertical) return imageView } diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 6d9710c8..cf9bc4c7 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -836,7 +836,6 @@ DBBBBE8E2744FB42007ACB4B /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = ""; }; DBBF70F626D7B62F00971656 /* AccountListTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountListTableViewCell+ViewModel.swift"; sourceTree = ""; }; DBC17A5B26E0A4C1005C0D79 /* TimelineMiddleLoaderTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimelineMiddleLoaderTableViewCell+ViewModel.swift"; sourceTree = ""; }; - DBC36A982993A4F300C67212 /* MetaTextKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MetaTextKit; path = ../MetaTextKit; sourceTree = ""; }; DBC747E0259DBD5400787EEF /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = ""; }; DBC8E04A2576337F00401E20 /* DisposeBagCollectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisposeBagCollectable.swift; sourceTree = ""; }; DBC8E04F257653E100401E20 /* SavePhotoActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavePhotoActivity.swift; sourceTree = ""; }; @@ -2131,7 +2130,6 @@ DBDA8E1524FCF8A3006750DC = { isa = PBXGroup; children = ( - DBC36A982993A4F300C67212 /* MetaTextKit */, DB33E5B42719637400EC2225 /* CoverFlowStackCollectionViewLayout */, DB51DC372716AEF000A0D8FB /* TabBarPager */, DB43239D251491EB004FAAEC /* TwidereSDK */, diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index 144e18bb..cd75096d 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -127,6 +127,15 @@ "version": "7.2.2" } }, + { + "package": "MetaTextKit", + "repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git", + "state": { + "branch": null, + "revision": "988b72a09662246f209f41c0aaa427f249dcbd6d", + "version": "4.4.0" + } + }, { "package": "Pageboy", "repositoryURL": "https://github.com/uias/Pageboy", diff --git a/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView+ViewModel.swift b/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView+ViewModel.swift index cdf5977b..6924c52f 100644 --- a/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView+ViewModel.swift +++ b/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView+ViewModel.swift @@ -35,7 +35,7 @@ extension MediaInfoDescriptionView { @Published public var content: MetaContent? - @Published public var visibility: StatusVisibility? + @Published public var visibility: MastodonVisibility? @Published public var isRepost = false @Published public var isRepostEnabled = true @@ -80,7 +80,7 @@ extension MediaInfoDescriptionView { return !protected case .mastodon: guard !isMyself else { return true } - guard case let .mastodon(visibility) = visibility else { + guard let visibility = visibility else { return true } switch visibility { @@ -328,7 +328,7 @@ extension MediaInfoDescriptionView { viewModel.content = PlaintextMetaContent(string: "") } - viewModel.visibility = status.visibility.asStatusVisibility + viewModel.visibility = status.visibility } private func configureToolbar(mastodonStatus status: MastodonStatus) { From 62a1a4818d85fd8c202f7eec76528bcb394ff57b Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 14 Mar 2023 20:08:53 +0800 Subject: [PATCH 038/128] feat: add status toolbar reaction --- .github/workflows/main.yml | 3 - .github/workflows/release.yml | 3 - ShareExtension/ComposeViewController.swift | 13 +- .../PollOptionView+Configuration.swift | 296 +++++++------- .../TwidereUI/Content/StatusToolbarView.swift | 174 ++++++++ .../Content/StatusView+Configuration.swift | 37 +- .../Content/StatusView+ViewModel.swift | 125 ++++-- .../TwidereUI/Content/StatusView.swift | 134 +----- .../ComposeContent/ComposeContentView.swift | 16 +- .../ComposeContentViewController.swift | 8 + ...oseContentViewModel+MetaTextDelegate.swift | 2 +- .../ComposeContentViewModel.swift | 45 +- .../Toolbar/StatusToolbar+ViewModel.swift | 300 -------------- .../TwidereUI/Toolbar/StatusToolbar.swift | 212 ---------- .../Misc/History/HistorySection.swift | 3 +- .../Notification/NotificationSection.swift | 14 +- TwidereX/Diffable/Status/StatusSection.swift | 7 +- .../Provider/DataSourceFacade+Status.swift | 36 +- ...der+MediaInfoDescriptionViewDelegate.swift | 48 +-- ...ider+StatusViewTableViewCellDelegate.swift | 18 + .../StatusHistoryViewModel+Diffable.swift | 9 +- .../User/UserHistoryViewModel+Diffable.swift | 9 +- .../MediaPreviewViewController.swift | 27 +- .../MediaInfoDescriptionView+ViewModel.swift | 383 +++++++++--------- .../View/MediaInfoDescriptionView.swift | 46 +-- ...tificationTimelineViewModel+Diffable.swift | 9 +- .../Scene/Profile/ProfileViewController.swift | 13 +- .../StatusTableViewCell+ViewModel.swift | 14 +- ...tusThreadRootTableViewCell+ViewModel.swift | 14 +- .../StatusViewTableViewCellDelegate.swift | 5 + .../StatusThreadViewModel+Diffable.swift | 8 +- .../Base/Common/TimelineViewController.swift | 13 +- .../List/ListTimelineViewController.swift | 1 + .../FederatedTimelineViewModel+Diffable.swift | 8 +- .../HashtagTimelineViewModel+Diffable.swift | 8 +- .../Home/HomeTimelineViewModel+Diffable.swift | 8 +- ...ListStatusTimelineViewModel+Diffable.swift | 8 +- .../SearchTimelineViewModel+Diffable.swift | 8 +- .../UserTimelineViewModel+Diffable.swift | 8 +- 39 files changed, 815 insertions(+), 1278 deletions(-) create mode 100644 TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift delete mode 100644 TwidereSDK/Sources/TwidereUI/Toolbar/StatusToolbar+ViewModel.swift delete mode 100644 TwidereSDK/Sources/TwidereUI/Toolbar/StatusToolbar.swift diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c8a45a7d..f20b87a0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,9 +19,6 @@ jobs: steps: - name: checkout uses: actions/checkout@v2 - - - name: force Xcode 13.4.1 - run: sudo xcode-select -switch /Applications/Xcode_13.4.1.app - name: setup env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1a3d7f70..eb5ca204 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,9 +28,6 @@ jobs: api-key-id: ${{ secrets.APPSTORE_KEY_ID }} api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }} - - name: force Xcode 13.4.1 - run: sudo xcode-select -switch /Applications/Xcode_13.4.1.app - - name: setup env: AppSecret: ${{ secrets.AppSecret }} diff --git a/ShareExtension/ComposeViewController.swift b/ShareExtension/ComposeViewController.swift index 91ed6426..75956cca 100644 --- a/ShareExtension/ComposeViewController.swift +++ b/ShareExtension/ComposeViewController.swift @@ -103,18 +103,7 @@ extension ComposeViewController { let composeContentViewModel = ComposeContentViewModel( context: context, authContext: authContext, - kind: .post, - configurationContext: ComposeContentViewModel.ConfigurationContext( - apiService: context.apiService, - authenticationService: context.authenticationService, - mastodonEmojiService: context.mastodonEmojiService, - statusViewConfigureContext: .init( - authContext: authContext, - dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - viewLayoutFramePublisher: viewModel.$viewLayoutFrame - ) - ) + kind: .post ) let composeContentViewController = ComposeContentViewController() composeContentViewController.viewModel = composeContentViewModel diff --git a/TwidereSDK/Sources/TwidereUI/Content/PollOptionView+Configuration.swift b/TwidereSDK/Sources/TwidereUI/Content/PollOptionView+Configuration.swift index 13662df5..b2b9a86e 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/PollOptionView+Configuration.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/PollOptionView+Configuration.swift @@ -14,161 +14,161 @@ import TwitterMeta import MastodonMeta import TwidereCore -extension PollOptionView { - public typealias ConfigurationContext = StatusView.ConfigurationContext -} +//extension PollOptionView { +// public typealias ConfigurationContext = StatusView.ConfigurationContext +//} extension PollOptionView { public func configure( - pollOption: PollOptionObject, - configurationContext: ConfigurationContext + pollOption: PollOptionObject +// configurationContext: ConfigurationContext ) { - switch pollOption { - case .twitter(let object): - configure( - pollOption: object, - configurationContext: configurationContext - ) - case .mastodon(let object): - configure( - pollOption: object, - configurationContext: configurationContext - ) - } +// switch pollOption { +// case .twitter(let object): +// configure( +// pollOption: object, +// configurationContext: configurationContext +// ) +// case .mastodon(let object): +// configure( +// pollOption: object, +// configurationContext: configurationContext +// ) +// } } - public func configure( - pollOption option: TwitterPollOption, - configurationContext: ConfigurationContext - ) { - viewModel.objects.insert(option) - - // metaContent - viewModel.metaContent = PlaintextMetaContent(string: option.label) - - // $isExpire - viewModel.isExpire = true // cannot vote for Twitter - - // isMultiple - viewModel.isMultiple = false - - // isSelect, isPollVoted, isMyPoll - viewModel.isSelect = false - viewModel.isPollVoted = false - viewModel.isMyPoll = false - - // percentage - Publishers.CombineLatest( - option.poll.publisher(for: \.updatedAt), - option.publisher(for: \.votes) - ) - .map { _, optionVotesCount -> Double? in - let pollVotesCount: Int = option.poll.options.map({ Int($0.votes) }).reduce(0, +) - guard pollVotesCount > 0, optionVotesCount >= 0 else { return 0 } - return Double(optionVotesCount) / Double(pollVotesCount) - } - .assign(to: \.percentage, on: viewModel) - .store(in: &disposeBag) - } +// public func configure( +// pollOption option: TwitterPollOption, +// configurationContext: ConfigurationContext +// ) { +// viewModel.objects.insert(option) +// +// // metaContent +// viewModel.metaContent = PlaintextMetaContent(string: option.label) +// +// // $isExpire +// viewModel.isExpire = true // cannot vote for Twitter +// +// // isMultiple +// viewModel.isMultiple = false +// +// // isSelect, isPollVoted, isMyPoll +// viewModel.isSelect = false +// viewModel.isPollVoted = false +// viewModel.isMyPoll = false +// +// // percentage +// Publishers.CombineLatest( +// option.poll.publisher(for: \.updatedAt), +// option.publisher(for: \.votes) +// ) +// .map { _, optionVotesCount -> Double? in +// let pollVotesCount: Int = option.poll.options.map({ Int($0.votes) }).reduce(0, +) +// guard pollVotesCount > 0, optionVotesCount >= 0 else { return 0 } +// return Double(optionVotesCount) / Double(pollVotesCount) +// } +// .assign(to: \.percentage, on: viewModel) +// .store(in: &disposeBag) +// } - public func configure( - pollOption option: MastodonPollOption, - configurationContext: ConfigurationContext - ) { - viewModel.objects.insert(option) - - // metaContent - Publishers.CombineLatest( - option.poll.status.publisher(for: \.emojis), - option.publisher(for: \.title) - ) - .map { emojis, title -> MetaContent? in - do { - let content = MastodonContent(content: title, emojis: emojis.asDictionary) - let metaContent = try MastodonMetaContent.convert(document: content) - return metaContent - } catch { - assertionFailure() - return PlaintextMetaContent(string: title) - } - } - .assign(to: \.metaContent, on: viewModel) - .store(in: &disposeBag) - - // $isExpire - option.poll.publisher(for: \.expired) - .assign(to: \.isExpire, on: viewModel) - .store(in: &disposeBag) - // isMultiple - viewModel.isMultiple = option.poll.multiple - - let optionIndex = option.index - let authorDomain = option.poll.status.author.domain - let authorUserID = option.poll.status.author.id - // isSelect, isPollVoted, isMyPoll - Publishers.CombineLatest4( - option.publisher(for: \.poll), - option.publisher(for: \.voteBy), - option.publisher(for: \.isSelected), - viewModel.$authenticationContext - ) - .sink { [weak self] poll, optionVoteBy, isSelected, authenticationContext in - guard let self = self else { return } - - let domain: String - let userID: String - switch authenticationContext { - case .twitter, .none: - domain = "" - userID = "" - case .mastodon(let authenticationContext): - domain = authenticationContext.domain - userID = authenticationContext.userID - } - - let options = poll.options - let pollVoteBy = poll.voteBy - - let isMyPoll = authorDomain == domain - && authorUserID == userID - - let votedOptions = options.filter { option in - option.voteBy.contains(where: { $0.id == userID && $0.domain == domain }) - } - let isRemoteVotedOption = votedOptions.contains(where: { $0.index == optionIndex }) - let isRemoteVotedPoll = pollVoteBy.contains(where: { $0.id == userID && $0.domain == domain }) - - let isLocalVotedOption = isSelected - - let isSelect: Bool? = { - if isLocalVotedOption { - return true - } else if !votedOptions.isEmpty { - return isRemoteVotedOption ? true : false - } else if isRemoteVotedPoll, votedOptions.isEmpty { - // the poll voted. But server not mark voted options - return nil - } else { - return false - } - }() - self.viewModel.isSelect = isSelect - self.viewModel.isPollVoted = isRemoteVotedPoll - self.viewModel.isMyPoll = isMyPoll - } - .store(in: &disposeBag) - // percentage - Publishers.CombineLatest( - option.poll.publisher(for: \.votesCount), - option.publisher(for: \.votesCount) - ) - .map { pollVotesCount, optionVotesCount -> Double? in - guard pollVotesCount > 0, optionVotesCount >= 0 else { return 0 } - return Double(optionVotesCount) / Double(pollVotesCount) - } - .assign(to: \.percentage, on: viewModel) - .store(in: &disposeBag) - } +// public func configure( +// pollOption option: MastodonPollOption, +// configurationContext: ConfigurationContext +// ) { +// viewModel.objects.insert(option) +// +// // metaContent +// Publishers.CombineLatest( +// option.poll.status.publisher(for: \.emojis), +// option.publisher(for: \.title) +// ) +// .map { emojis, title -> MetaContent? in +// do { +// let content = MastodonContent(content: title, emojis: emojis.asDictionary) +// let metaContent = try MastodonMetaContent.convert(document: content) +// return metaContent +// } catch { +// assertionFailure() +// return PlaintextMetaContent(string: title) +// } +// } +// .assign(to: \.metaContent, on: viewModel) +// .store(in: &disposeBag) +// +// // $isExpire +// option.poll.publisher(for: \.expired) +// .assign(to: \.isExpire, on: viewModel) +// .store(in: &disposeBag) +// // isMultiple +// viewModel.isMultiple = option.poll.multiple +// +// let optionIndex = option.index +// let authorDomain = option.poll.status.author.domain +// let authorUserID = option.poll.status.author.id +// // isSelect, isPollVoted, isMyPoll +// Publishers.CombineLatest4( +// option.publisher(for: \.poll), +// option.publisher(for: \.voteBy), +// option.publisher(for: \.isSelected), +// viewModel.$authenticationContext +// ) +// .sink { [weak self] poll, optionVoteBy, isSelected, authenticationContext in +// guard let self = self else { return } +// +// let domain: String +// let userID: String +// switch authenticationContext { +// case .twitter, .none: +// domain = "" +// userID = "" +// case .mastodon(let authenticationContext): +// domain = authenticationContext.domain +// userID = authenticationContext.userID +// } +// +// let options = poll.options +// let pollVoteBy = poll.voteBy +// +// let isMyPoll = authorDomain == domain +// && authorUserID == userID +// +// let votedOptions = options.filter { option in +// option.voteBy.contains(where: { $0.id == userID && $0.domain == domain }) +// } +// let isRemoteVotedOption = votedOptions.contains(where: { $0.index == optionIndex }) +// let isRemoteVotedPoll = pollVoteBy.contains(where: { $0.id == userID && $0.domain == domain }) +// +// let isLocalVotedOption = isSelected +// +// let isSelect: Bool? = { +// if isLocalVotedOption { +// return true +// } else if !votedOptions.isEmpty { +// return isRemoteVotedOption ? true : false +// } else if isRemoteVotedPoll, votedOptions.isEmpty { +// // the poll voted. But server not mark voted options +// return nil +// } else { +// return false +// } +// }() +// self.viewModel.isSelect = isSelect +// self.viewModel.isPollVoted = isRemoteVotedPoll +// self.viewModel.isMyPoll = isMyPoll +// } +// .store(in: &disposeBag) +// // percentage +// Publishers.CombineLatest( +// option.poll.publisher(for: \.votesCount), +// option.publisher(for: \.votesCount) +// ) +// .map { pollVotesCount, optionVotesCount -> Double? in +// guard pollVotesCount > 0, optionVotesCount >= 0 else { return 0 } +// return Double(optionVotesCount) / Double(pollVotesCount) +// } +// .assign(to: \.percentage, on: viewModel) +// .store(in: &disposeBag) +// } } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift new file mode 100644 index 00000000..5877af62 --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift @@ -0,0 +1,174 @@ +// +// StatusToolbarView.swift +// +// +// Created by MainasuK on 2023/3/14. +// + +import os.log +import SwiftUI + +public struct StatusToolbarView: View { + + static let logger = Logger(subsystem: "StatusToolbarView", category: "View") + var logger: Logger { StatusView.logger } + + @ObservedObject public var viewModel: ViewModel + let handler: (Action) -> Void + + public var body: some View { + HStack { + ToolbarButton( + handler: { action in + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): reply") + handler(action) + }, + action: .reply, + image: Asset.Arrows.arrowTurnUpLeftMini.image.withRenderingMode(.alwaysTemplate), + count: viewModel.replyCount, + tintColor: nil + ) + ToolbarButton( + handler: { action in + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): repost") + handler(action) + }, + action: .repost, + image: Asset.Media.repeatMini.image.withRenderingMode(.alwaysTemplate), + count: viewModel.repostCount, + tintColor: viewModel.isReposted ? Asset.Scene.Status.Toolbar.repost.color : nil + ) + ToolbarButton( + handler: { action in + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): like") + handler(action) + }, + action: .like, + image: viewModel.isLiked ? Asset.Health.heartFillMini.image.withRenderingMode(.alwaysTemplate) : Asset.Health.heartMini.image.withRenderingMode(.alwaysTemplate), + count: viewModel.likeCount, + tintColor: viewModel.isLiked ? Asset.Scene.Status.Toolbar.like.color : nil + ) + // share + Button { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): share") + } label: { + HStack { + let image: UIImage = { +// switch viewModel.kind { +// case .conversationRoot: + return Asset.Editing.ellipsisMini.image.withRenderingMode(.alwaysTemplate) +// default: +// return Asset.Editing.ellipsisMini.image.withRenderingMode(.alwaysTemplate) +// } + }() + Image(uiImage: image) + .foregroundColor(.secondary) + } + } + .buttonStyle(.borderless) + .modifier(MaxWidthModifier(max: nil)) + } // end HStack + } + +} + +extension StatusToolbarView { + public class ViewModel: ObservableObject { + // input + @Published var replyCount: Int? + @Published var repostCount: Int? + @Published var likeCount: Int? + + @Published var isReposted: Bool = false + @Published var isLiked: Bool = false + + public init() { + // end init + } + } +} + +extension StatusToolbarView { + public enum Action: Hashable, CaseIterable { + case reply + case repost + case quote + case like + case share + } +} + +extension StatusToolbarView { + public struct ToolbarButton: View { + static let numberMetricFormatter = NumberMetricFormatter() + + let handler: (Action) -> Void + let action: Action + let image: UIImage + let count: Int? + let tintColor: UIColor? + + // output + var text: String { + Self.metric(count: count) + } + + public init( + handler: @escaping (Action) -> Void, + action: Action, + image: UIImage, + count: Int?, + tintColor: UIColor? + ) { + self.handler = handler + self.action = action + self.image = image + self.count = count + self.tintColor = tintColor + } + + public var body: some View { + Button { + handler(action) + } label: { + HStack { + Image(uiImage: image) + Text(text) + .font(.system(size: 12, weight: .medium)) + .lineLimit(1) + Spacer() + } + } + .buttonStyle(.borderless) + .tint(Color(uiColor: tintColor ?? .secondaryLabel)) + .foregroundColor(Color(uiColor: tintColor ?? .secondaryLabel)) + } + + static func metric(count: Int?) -> String { + guard let count = count, count > 0 else { + return "" + } + return ToolbarButton.numberMetricFormatter.string(from: count) ?? "" + } + } +} + +extension StatusToolbarView { + public struct MaxWidthModifier: ViewModifier { + let max: CGFloat? + + public init(max: CGFloat?) { + self.max = max + } + + @ViewBuilder + public func body(content: Content) -> some View { + if let max = max { + content + .frame(maxWidth: max) + } else { + content + } + } + } +} diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+Configuration.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+Configuration.swift index 8bae324b..47d2757e 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+Configuration.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+Configuration.swift @@ -17,26 +17,23 @@ import TwitterMeta import MastodonMeta import Meta -extension StatusView { - public struct ConfigurationContext { - public let authContext: AuthContext - public let dateTimeProvider: DateTimeProvider - public let twitterTextProvider: TwitterTextProvider - public let viewLayoutFramePublisher: Published.Publisher? - - public init( - authContext: AuthContext, - dateTimeProvider: DateTimeProvider, - twitterTextProvider: TwitterTextProvider, - viewLayoutFramePublisher: Published.Publisher? - ) { - self.authContext = authContext - self.dateTimeProvider = dateTimeProvider - self.twitterTextProvider = twitterTextProvider - self.viewLayoutFramePublisher = viewLayoutFramePublisher - } - } -} +//extension StatusView { +// public struct ConfigurationContext { +// public let dateTimeProvider: DateTimeProvider +// public let twitterTextProvider: TwitterTextProvider +// public let viewLayoutFramePublisher: Published.Publisher? +// +// public init( +// dateTimeProvider: DateTimeProvider, +// twitterTextProvider: TwitterTextProvider, +// viewLayoutFramePublisher: Published.Publisher? +// ) { +// self.dateTimeProvider = dateTimeProvider +// self.twitterTextProvider = twitterTextProvider +// self.viewLayoutFramePublisher = viewLayoutFramePublisher +// } +// } +//} //extension StatusView { // public func configure( diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index c638d218..a4b104b2 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -36,6 +36,7 @@ extension StatusView { // input public let status: StatusRecord? + public let authContext: AuthContext? public let kind: Kind public weak var delegate: StatusViewDelegate? @@ -128,12 +129,8 @@ extension StatusView { // @Published public var isRepostEnabled = true // // @Published public var isLike = false -// -// @Published public var replyCount: Int = 0 -// @Published public var repostCount: Int = 0 -// @Published public var quoteCount: Int = 0 -// @Published public var likeCount: Int = 0 -// + + @Published public var visibility: MastodonVisibility? var visibilityIconImage: UIImage? { switch visibility { @@ -168,31 +165,18 @@ extension StatusView { @Published public var timestampLabelViewModel: TimestampLabelView.ViewModel? -// -// // public let contentRevealChangePublisher = PassthroughSubject() -// - public enum Header { - case none - case repost(info: RepostInfo) - case notification(info: NotificationHeaderInfo) - // TODO: replyTo - - public struct RepostInfo { - public let authorNameMetaContent: MetaContent - } - } + // toolbar + public let toolbarViewModel = StatusToolbarView.ViewModel() -// public func prepareForReuse() { -// replySettings = nil -// } -// - init( + private init( status: StatusRecord?, + authContext: AuthContext?, kind: Kind, delegate: StatusViewDelegate?, viewLayoutFramePublisher: Published.Publisher? ) { self.status = status + self.authContext = authContext self.kind = kind self.delegate = delegate // end init @@ -948,6 +932,7 @@ extension StatusView.ViewModel { extension StatusView.ViewModel { public convenience init?( feed: Feed, + authContext: AuthContext?, delegate: StatusViewDelegate?, viewLayoutFramePublisher: Published.Publisher? ) { @@ -955,6 +940,7 @@ extension StatusView.ViewModel { case .twitter(let status): self.init( status: status, + authContext: authContext, kind: .timeline, delegate: delegate, parentViewModel: nil, @@ -963,6 +949,7 @@ extension StatusView.ViewModel { case .mastodon(let status): self.init( status: status, + authContext: authContext, kind: .timeline, delegate: delegate, parentViewModel: nil, @@ -973,12 +960,41 @@ extension StatusView.ViewModel { case .none: return nil } - } + } // end init + + public convenience init( + status: StatusObject, + authContext: AuthContext?, + delegate: StatusViewDelegate?, + viewLayoutFramePublisher: Published.Publisher? + ) { + switch status { + case .twitter(let status): + self.init( + status: status, + authContext: authContext, + kind: .timeline, + delegate: delegate, + parentViewModel: nil, + viewLayoutFramePublisher: viewLayoutFramePublisher + ) + case .mastodon(let status): + self.init( + status: status, + authContext: authContext, + kind: .timeline, + delegate: delegate, + parentViewModel: nil, + viewLayoutFramePublisher: viewLayoutFramePublisher + ) + } + } // end init } extension StatusView.ViewModel { public convenience init( status: TwitterStatus, + authContext: AuthContext?, kind: Kind, delegate: StatusViewDelegate?, parentViewModel: StatusView.ViewModel?, @@ -986,6 +1002,7 @@ extension StatusView.ViewModel { ) { self.init( status: .twitter(record: status.asRecrod), + authContext: authContext, kind: kind, delegate: delegate, viewLayoutFramePublisher: viewLayoutFramePublisher @@ -995,6 +1012,7 @@ extension StatusView.ViewModel { if let repost = status.repost { let _repostViewModel = StatusView.ViewModel( status: repost, + authContext: authContext, kind: .repost, delegate: delegate, parentViewModel: self, @@ -1018,6 +1036,7 @@ extension StatusView.ViewModel { if let quote = status.quote { quoteViewModel = .init( status: quote, + authContext: authContext, kind: .quote, delegate: delegate, parentViewModel: self, @@ -1055,12 +1074,40 @@ extension StatusView.ViewModel { // media mediaViewModels = MediaView.ViewModel.viewModels(from: status) + + // toolbar + status.publisher(for: \.replyCount) + .map { Int($0) } + .assign(to: &toolbarViewModel.$replyCount) + status.publisher(for: \.repostCount) + .map { Int($0) } + .assign(to: &toolbarViewModel.$repostCount) + status.publisher(for: \.likeCount) + .map { Int($0) } + .assign(to: &toolbarViewModel.$likeCount) + if case let .twitter(authenticationContext) = authContext?.authenticationContext { + status.publisher(for: \.likeBy) + .map { users -> Bool in + let ids = users.map { $0.id } + return ids.contains(authenticationContext.userID) + } + .assign(to: &toolbarViewModel.$isLiked) + status.publisher(for: \.repostBy) + .map { users -> Bool in + let ids = users.map { $0.id } + return ids.contains(authenticationContext.userID) + } + .assign(to: &toolbarViewModel.$isReposted) + } else { + // do nothing + } } // end init } extension StatusView.ViewModel { public convenience init( status: MastodonStatus, + authContext: AuthContext?, kind: Kind, delegate: StatusViewDelegate?, parentViewModel: StatusView.ViewModel?, @@ -1068,6 +1115,7 @@ extension StatusView.ViewModel { ) { self.init( status: .mastodon(record: status.asRecrod), + authContext: authContext, kind: kind, delegate: delegate, viewLayoutFramePublisher: viewLayoutFramePublisher @@ -1077,6 +1125,7 @@ extension StatusView.ViewModel { if let repost = status.repost { let _repostViewModel = StatusView.ViewModel( status: repost, + authContext: authContext, kind: .repost, delegate: delegate, parentViewModel: self, @@ -1161,5 +1210,31 @@ extension StatusView.ViewModel { .assign(to: \.isMediaSensitiveToggled, on: self) .store(in: &disposeBag) + // toolbar + status.publisher(for: \.replyCount) + .map { Int($0) } + .assign(to: &toolbarViewModel.$replyCount) + status.publisher(for: \.repostCount) + .map { Int($0) } + .assign(to: &toolbarViewModel.$repostCount) + status.publisher(for: \.likeCount) + .map { Int($0) } + .assign(to: &toolbarViewModel.$likeCount) + if case let .mastodon(authenticationContext) = authContext?.authenticationContext { + status.publisher(for: \.likeBy) + .map { users -> Bool in + let ids = users.map { $0.id } + return ids.contains(authenticationContext.userID) + } + .assign(to: &toolbarViewModel.$isLiked) + status.publisher(for: \.repostBy) + .map { users -> Bool in + let ids = users.map { $0.id } + return ids.contains(authenticationContext.userID) + } + .assign(to: &toolbarViewModel.$isReposted) + } else { + // do nothing + } } } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index 372bd4b3..391ffe13 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -40,7 +40,7 @@ public protocol StatusViewDelegate: AnyObject { // // func statusView(_ statusView: StatusView, quoteStatusViewDidPressed quoteStatusView: StatusView) // -// func statusView(_ statusView: StatusView, statusToolbar: StatusToolbar, actionDidPressed action: StatusToolbar.Action, button: UIButton) + func statusView(_ viewModel: StatusView.ViewModel, statusToolbarButtonDidPressed action: StatusToolbarView.Action) // func statusView(_ statusView: StatusView, statusToolbar: StatusToolbar, menuActionDidPressed action: StatusToolbar.MenuAction, menuButton button: UIButton) // // func statusView(_ statusView: StatusView, translateButtonDidPressed button: UIButton) @@ -91,7 +91,6 @@ public struct StatusView: View { if viewModel.spoilerContent != nil { spoilerContentView .padding(.horizontal, viewModel.margin) - .border(.red, width: 1) if !viewModel.isContentEmpty { Button { viewModel.delegate?.statusView(viewModel, toggleContentDisplay: !viewModel.isContentReveal) @@ -106,7 +105,6 @@ public struct StatusView: View { .buttonStyle(.borderless) .id(UUID()) // fix animation issue .padding(.horizontal, viewModel.margin) - .border(.red, width: 1) } } // content @@ -124,6 +122,7 @@ public struct StatusView: View { viewModel.delegate?.statusView(viewModel, previewActionForMediaViewModel: mediaViewModel) } ) + .clipShape(RoundedRectangle(cornerRadius: MediaGridContainerView.cornerRadius)) .overlay { ContentWarningOverlayView(isReveal: viewModel.isMediaContentWarningOverlayReveal) { viewModel.delegate?.statusView(viewModel, toggleContentWarningOverlayDisplay: !viewModel.isMediaContentWarningOverlayReveal) @@ -149,7 +148,6 @@ public struct StatusView: View { .padding(.bottom, viewModel.hasToolbar ? .zero : viewModel.margin) } } // end VStack - .frame(alignment: .top) .onReceive(viewModel.$isContentSensitiveToggled) { _ in viewModel.delegate?.statusView(viewModel, viewHeightDidChange: Void()) } @@ -287,134 +285,16 @@ extension StatusView { } var toolbarView: some View { - HStack { - ToolbarButton( - action: { kind in - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): reply") - }, - kind: .reply, - image: Asset.Arrows.arrowTurnUpLeftMini.image.withRenderingMode(.alwaysTemplate), - text: "", - tintColor: nil - ) - ToolbarButton( - action: { kind in - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): repost") - }, - kind: .repost, - image: Asset.Media.repeatMini.image.withRenderingMode(.alwaysTemplate), - text: "", - tintColor: nil - ) - ToolbarButton( - action: { kind in - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): like") - }, - kind: .like, - image: Asset.Health.heartMini.image.withRenderingMode(.alwaysTemplate), - text: "", - tintColor: nil - ) - // share - Button { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): share") - } label: { - HStack { - let image: UIImage = { - switch viewModel.kind { - case .conversationRoot: - return Asset.Editing.ellipsisMini.image.withRenderingMode(.alwaysTemplate) - default: - return Asset.Editing.ellipsisMini.image.withRenderingMode(.alwaysTemplate) - } - }() - Image(uiImage: image) - .foregroundColor(.secondary) - } + StatusToolbarView( + viewModel: viewModel.toolbarViewModel, + handler: { action in + viewModel.delegate?.statusView(viewModel, statusToolbarButtonDidPressed: action) } - .buttonStyle(.borderless) - .modifier(MaxWidthModifier(max: nil)) - } // end HStack + ) .frame(height: 48) } } -extension StatusView { - public struct ToolbarButton: View { - static let numberMetricFormatter = NumberMetricFormatter() - - let action: (Kind) -> Void - let kind: Kind - let image: UIImage - let text: String - let tintColor: UIColor? - - public init( - action: @escaping (Kind) -> Void, - kind: Kind, - image: UIImage, - text: String, - tintColor: UIColor? - ) { - self.action = action - self.kind = kind - self.image = image - self.text = text - self.tintColor = tintColor - } - - public var body: some View { - Button { - action(kind) - } label: { - HStack { - Image(uiImage: image) - Text(text) - .font(.system(size: 12, weight: .medium)) - .lineLimit(1) - Spacer() - } - } - .buttonStyle(.borderless) - .tint(Color(uiColor: tintColor ?? .secondaryLabel)) - .foregroundColor(Color(uiColor: tintColor ?? .secondaryLabel)) - } - - static func metric(count: Int?) -> String { - guard let count = count, count > 0 else { - return "" - } - return ToolbarButton.numberMetricFormatter.string(from: count) ?? "" - } - - public enum Kind: Hashable { - case reply - case repost - case quote - case like - case share - } - } - - public struct MaxWidthModifier: ViewModifier { - let max: CGFloat? - - public init(max: CGFloat?) { - self.max = max - } - - @ViewBuilder - public func body(content: Content) -> some View { - if let max = max { - content - .frame(maxWidth: max) - } else { - content - } - } - } -} - //public final class StatusView: UIView { // // private var _disposeBag = Set() // which lifetime same to view scope diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift index 17c0744d..50b6d265 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift @@ -35,16 +35,12 @@ public struct ComposeContentView: View { VStack(spacing: .zero) { // reply switch viewModel.kind { - case .reply(let status): - EmptyView() -// ReplyStatusViewRepresentable( -// statusObject: status, -// configurationContext: viewModel.configurationContext.statusViewConfigureContext, -// width: viewModel.viewSize.width - 2 * ComposeContentView.contentMargin -// ) -// .padding(.top, 8) -// .padding(.horizontal, ComposeContentView.contentMargin) -// .frame(width: viewModel.viewSize.width) + case .reply: + if let replyToStatusViewModel = viewModel.replyToStatusViewModel { + StatusView(viewModel: replyToStatusViewModel) + .frame(width: viewModel.viewLayoutFrame.readableContentLayoutFrame.width) + .padding(.top, 8) + } default: EmptyView() } diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewController.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewController.swift index a42af929..e95d49a1 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewController.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -180,11 +180,19 @@ extension ComposeContentViewController { public override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() + viewModel.viewLayoutFrame.update(view: view) + if viewModel.viewSize != view.frame.size { viewModel.viewSize = view.frame.size } } + public override func viewSafeAreaInsetsDidChange() { + super.viewSafeAreaInsetsDidChange() + + viewModel.viewLayoutFrame.update(view: view) + } + } extension ComposeContentViewController { diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel+MetaTextDelegate.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel+MetaTextDelegate.swift index 753b6fbd..90b35421 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel+MetaTextDelegate.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel+MetaTextDelegate.swift @@ -41,7 +41,7 @@ extension ComposeContentViewModel: MetaTextDelegate { let metaContent = TwitterMetaContent.convert( content: content, urlMaximumLength: .max, - twitterTextProvider: configurationContext.statusViewConfigureContext.twitterTextProvider + twitterTextProvider: SwiftTwitterTextProvider() ) return metaContent diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift index bb83ff8b..10be840e 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -33,11 +33,11 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { // MARK: - layout @Published var viewSize: CGSize = .zero + @Published public var viewLayoutFrame = ViewLayoutFrame() // input let context: AppContext public let kind: Kind - public let configurationContext: ConfigurationContext public let customEmojiPickerInputViewModel = CustomEmojiPickerInputView.ViewModel() public let platform: Platform @@ -47,6 +47,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { // reply-to public private(set) var replyTo: StatusObject? + @Published public private(set) var replyToStatusViewModel: StatusView.ViewModel? // limit @Published public var maxTextInputLimit = 500 @@ -143,7 +144,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { @Published public private(set) var availableActions: Set = Set() @Published public private(set) var isMediaToolBarButtonEnabled = true @Published public private(set) var isPollToolBarButtonEnabled = true - @Published public private(set) var isLocationToolBarButtonEnabled = CLLocationManager.locationServicesEnabled() + @Published public private(set) var isLocationToolBarButtonEnabled = false // UI state @Published public private(set) var isComposeBarButtonEnabled = true @@ -156,16 +157,18 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { context: AppContext, authContext: AuthContext, kind: Kind, - settings: Settings = Settings(), - configurationContext: ConfigurationContext + settings: Settings = Settings() ) { self.context = context self.authContext = authContext self.kind = kind - self.configurationContext = configurationContext self.platform = authContext.authenticationContext.platform super.init() // end init + + Task { + isLocationToolBarButtonEnabled = CLLocationManager.locationServicesEnabled() + } switch kind { case .post: @@ -182,6 +185,12 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { } case .reply(let status): replyTo = status + replyToStatusViewModel = StatusView.ViewModel( + status: status, + authContext: nil, + delegate: nil, + viewLayoutFramePublisher: $viewLayoutFrame + ) switch status { case .twitter(let status): @@ -270,7 +279,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { guard let author = author else { return } switch author { case .twitter: - let twitterTextProvider = configurationContext.statusViewConfigureContext.twitterTextProvider + let twitterTextProvider = SwiftTwitterTextProvider() let parseResult = twitterTextProvider.parse(text: content) self.contentWeightedLength = parseResult.weightedLength self.maxTextInputLimit = parseResult.maxWeightedLength @@ -448,7 +457,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { guard let self = self else { return nil } guard case let .mastodon(user) = author else { return nil } let domain = user.domain - guard let emojiViewModel = self.configurationContext.mastodonEmojiService.dequeueEmojiViewModel(for: domain) else { return nil } + guard let emojiViewModel = self.context.mastodonEmojiService.dequeueEmojiViewModel(for: domain) else { return nil } return emojiViewModel } .assign(to: &$emojiViewModel) @@ -478,7 +487,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { guard case let .twitter(twitterAuthenticationContext) = authContext?.authenticationContext else { return nil } do { - let response = try await self.configurationContext.apiService.geoSearch( + let response = try await self.context.apiService.geoSearch( latitude: currentLocation.coordinate.latitude, longitude: currentLocation.coordinate.longitude, granularity: "city", @@ -602,25 +611,7 @@ extension ComposeContentViewModel { self.mastodonVisibility = mastodonVisibility } } - - public struct ConfigurationContext { - public let apiService: APIService - public let authenticationService: AuthenticationService - public let mastodonEmojiService: MastodonEmojiService - public let statusViewConfigureContext: StatusView.ConfigurationContext - public init( - apiService: APIService, - authenticationService: AuthenticationService, - mastodonEmojiService: MastodonEmojiService, - statusViewConfigureContext: StatusView.ConfigurationContext - ) { - self.apiService = apiService - self.authenticationService = authenticationService - self.mastodonEmojiService = mastodonEmojiService - self.statusViewConfigureContext = statusViewConfigureContext - } - } } extension ComposeContentViewModel { @@ -753,7 +744,7 @@ extension ComposeContentViewModel { switch author { case .twitter(let author): return TwitterStatusPublisher( - apiService: configurationContext.apiService, + apiService: context.apiService, author: author, replyTo: { guard case let .twitter(status) = replyTo else { return nil } diff --git a/TwidereSDK/Sources/TwidereUI/Toolbar/StatusToolbar+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Toolbar/StatusToolbar+ViewModel.swift deleted file mode 100644 index 3bb92c95..00000000 --- a/TwidereSDK/Sources/TwidereUI/Toolbar/StatusToolbar+ViewModel.swift +++ /dev/null @@ -1,300 +0,0 @@ -// -// StatusToolbar+ViewModel.swift -// -// -// Created by MainasuK on 2022-2-23. -// - -import UIKit -import Combine -import CoreDataStack -import TwidereAsset - -extension StatusToolbar { - public final class ViewModel: ObservableObject { - var disposeBag = Set() - - @Published public var traitCollectionDidChange = CurrentValueSubject(Void()) - @Published public var platform: Platform = .none - - @Published public var replyCount: Int = 0 - @Published public var isReplyEnabled = true - - @Published public var repostCount: Int = 0 - @Published public var isRepostEnabled = true - @Published public var isRepostHighlighted = true - - @Published public var likeCount: Int = 0 - // @Published public var isLikeEnabled = true - @Published public var isLikeHighlighted = true - - func bind(view: StatusToolbar) { - // reply - Publishers.CombineLatest( - $replyCount, - $isReplyEnabled - ) - .sink { count, isEnabled in - // title - let text = ViewModel.metricText(count: count) - switch view.style { - case .none: - break - case .inline: - view.replyButton.setTitle(text, for: .normal) - view.replyButton.accessibilityHint = L10n.Count.reply(count) - case .plain: - view.replyButton.accessibilityHint = nil - } - view.replyButton.accessibilityLabel = L10n.Accessibility.Common.Status.Actions.reply - - // isEnabled - view.replyButton.isEnabled = isEnabled - } - .store(in: &disposeBag) - // repost - Publishers.CombineLatest3( - $repostCount, - $isRepostEnabled, - $platform - ) - .sink { count, isEnabled, platform in - // title - let text = ViewModel.metricText(count: count) - switch view.style { - case .none: - break - case .inline: - view.repostButton.setTitle(text, for: .normal) - view.repostButton.accessibilityHint = L10n.Count.reblog(count) - case .plain: - view.repostButton.accessibilityHint = nil - } - - switch platform { - case .none: - view.repostButton.accessibilityLabel = nil - case .twitter: - view.repostButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.retweet - case .mastodon: - view.repostButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.boost - } - - // isEnabled - view.repostButton.isEnabled = isEnabled - } - .store(in: &disposeBag) - Publishers.CombineLatest( - $isRepostHighlighted, - $traitCollectionDidChange - ) - .sink { isHighlighted, _ in - // isHighlighted - let tintColor = isHighlighted ? Asset.Scene.Status.Toolbar.repost.color : .secondaryLabel - view.repostButton.tintColor = tintColor - view.repostButton.setTitleColor(tintColor, for: .normal) - view.repostButton.setTitleColor(tintColor.withAlphaComponent(0.8), for: .highlighted) - if isHighlighted { - view.repostButton.accessibilityTraits.insert(.selected) - } else { - view.repostButton.accessibilityTraits.remove(.selected) - } - } - .store(in: &disposeBag) - // like - $likeCount - .sink { count in - // title - let text = ViewModel.metricText(count: count) - switch view.style { - case .none: - break - case .inline: - view.likeButton.setTitle(text, for: .normal) - view.likeButton.accessibilityHint = L10n.Count.reply(count) - case .plain: - view.likeButton.accessibilityHint = nil - // no titile - } - view.likeButton.accessibilityLabel = L10n.Accessibility.Common.Status.Actions.like - } - .store(in: &disposeBag) - Publishers.CombineLatest( - $isLikeHighlighted, - $traitCollectionDidChange - ) - .sink { isHighlighted, _ in - // isHighlighted - let tintColor = isHighlighted ? Asset.Scene.Status.Toolbar.like.color : .secondaryLabel - view.likeButton.tintColor = tintColor - view.likeButton.setTitleColor(tintColor, for: .normal) - view.likeButton.setTitleColor(tintColor.withAlphaComponent(0.8), for: .highlighted) - switch view.style { - case .none: - break - case .inline: - let image: UIImage = isHighlighted ? Asset.Health.heartFillMini.image : Asset.Health.heartMini.image - view.likeButton.setImage(image.withRenderingMode(.alwaysTemplate), for: .normal) - case .plain: - let image: UIImage = isHighlighted ? Asset.Health.heartFill.image : Asset.Health.heart.image - view.likeButton.setImage(image.withRenderingMode(.alwaysTemplate), for: .normal) - } - if isHighlighted { - view.likeButton.accessibilityTraits.insert(.selected) - } else { - view.likeButton.accessibilityTraits.remove(.selected) - } - } - .store(in: &disposeBag) - } - - private static func metricText(count: Int) -> String { - guard count > 0 else { return "" } - return StatusToolbar.numberMetricFormatter.string(from: count) ?? "" - } - } -} - -extension StatusToolbar { - - public func setupReply(count: Int, isEnabled: Bool) { - viewModel.replyCount = count - viewModel.isReplyEnabled = isEnabled - } - - public func setupRepost(count: Int, isEnabled: Bool, isHighlighted: Bool) { - viewModel.repostCount = count - viewModel.isRepostEnabled = isEnabled - viewModel.isRepostHighlighted = isHighlighted - } - - public func setupLike(count: Int, isHighlighted: Bool) { - viewModel.likeCount = count - viewModel.isLikeHighlighted = isHighlighted - } - - public struct MenuContext { - let shareText: String? - let shareLink: String? - let displaySaveMediaAction: Bool - let displayDeleteAction: Bool - } - - public func setupMenu(menuContext: MenuContext) { - menuButton.menu = { - var children: [UIMenuElement] = [] - - let copyTextAction = UIAction( - title: L10n.Common.Controls.Status.Actions.copyText.capitalized, - image: UIImage(systemName: "doc.on.doc"), - identifier: nil, - discoverabilityTitle: nil, - attributes: [], - state: .off - ) { _ in - guard let text = menuContext.shareText else { return } - UIPasteboard.general.string = text - } - children.append(copyTextAction) - - let copyLink = UIAction( - title: L10n.Common.Controls.Status.Actions.copyLink.capitalized, - image: UIImage(systemName: "link"), - identifier: nil, - discoverabilityTitle: nil, - attributes: [], - state: .off - ) { _ in - guard let text = menuContext.shareLink else { return } - UIPasteboard.general.string = text - } - children.append(copyLink) - - let shareLink = UIAction( - title: L10n.Common.Controls.Status.Actions.shareLink.capitalized, - image: UIImage(systemName: "square.and.arrow.up"), - identifier: nil, - discoverabilityTitle: nil, - attributes: [], - state: .off - ) { [weak self] _ in - guard let self = self else { return } - self.delegate?.statusToolbar(self, menuActionDidPressed: .share, menuButton: self.menuButton) - } - children.append(shareLink) - - if menuContext.displaySaveMediaAction { - let saveMdeiaAction = UIAction( - title: L10n.Common.Controls.Status.Actions.saveMedia.capitalized, - image: UIImage(systemName: "square.and.arrow.down"), - identifier: nil, - discoverabilityTitle: nil, - attributes: [], - state: .off - ) { [weak self] _ in - guard let self = self else { return } - self.delegate?.statusToolbar(self, menuActionDidPressed: .saveMedia, menuButton: self.menuButton) - } - children.append(saveMdeiaAction) - } - - let translateAction = UIAction( - title: L10n.Common.Controls.Status.Actions.translate.capitalized, - image: UIImage(systemName: "character.bubble"), - identifier: nil, - discoverabilityTitle: nil, - attributes: [], - state: .off - ) { [weak self] _ in - guard let self = self else { return } - self.delegate?.statusToolbar(self, menuActionDidPressed: .translate, menuButton: self.menuButton) - } - children.append(translateAction) - - if menuContext.displayDeleteAction { - let removeAction = UIAction( - title: L10n.Common.Controls.Actions.delete, - image: UIImage(systemName: "minus.circle"), - identifier: nil, - discoverabilityTitle: nil, - attributes: .destructive, - state: .off - ) { [weak self] _ in - guard let self = self else { return } - self.delegate?.statusToolbar(self, menuActionDidPressed: .remove, menuButton: self.menuButton) - } - children.append(removeAction) - } - - #if DEBUG - let copyIDAction = UIAction( - title: "Copy ID", - image: UIImage(systemName: "number.square"), - identifier: nil, - discoverabilityTitle: nil, - attributes: [], - state: .off - ) { [weak self] _ in - guard let self = self else { return } - self.delegate?.statusToolbar(self, menuActionDidPressed: .copyID, menuButton: self.menuButton) - } - let debugMenu = UIMenu(title: "", options: .displayInline, children: [copyIDAction]) - children.append(debugMenu) - #endif - - let appearEventAction = UIDeferredMenuElement.uncached { [weak self] completion in - if let self = self { - self.delegate?.statusToolbar(self, menuActionDidPressed: .appearEvent, menuButton: self.menuButton) - } - completion([]) - } - children.append(appearEventAction) - - return UIMenu(title: "", options: [], children: children) - }() - - menuButton.showsMenuAsPrimaryAction = true - menuButton.accessibilityLabel = L10n.Accessibility.Common.Status.Actions.menu - } - -} diff --git a/TwidereSDK/Sources/TwidereUI/Toolbar/StatusToolbar.swift b/TwidereSDK/Sources/TwidereUI/Toolbar/StatusToolbar.swift deleted file mode 100644 index 5f759356..00000000 --- a/TwidereSDK/Sources/TwidereUI/Toolbar/StatusToolbar.swift +++ /dev/null @@ -1,212 +0,0 @@ -// -// StatusToolbar.swift -// StatusToolbar -// -// Created by Cirno MainasuK on 2021-8-23. -// Copyright © 2021 Twidere. All rights reserved. -// - -import os.log -import UIKit -import TwidereCore - -public protocol StatusToolbarDelegate: AnyObject { - func statusToolbar(_ statusToolbar: StatusToolbar, actionDidPressed action: StatusToolbar.Action, button: UIButton) - func statusToolbar(_ statusToolbar: StatusToolbar, menuActionDidPressed action: StatusToolbar.MenuAction, menuButton button: UIButton) -} - -public final class StatusToolbar: UIView { - - public static let numberMetricFormatter = NumberMetricFormatter() - - public weak var delegate: StatusToolbarDelegate? - - public private(set) lazy var viewModel: ViewModel = { - let viewModel = ViewModel() - viewModel.bind(view: self) - return viewModel - }() - - private let logger = Logger(subsystem: "StatusToolbar", category: "Toolbar") - private let container = UIStackView() - private(set) var style: Style? - - public let replyButton = HitTestExpandedButton() - public let repostButton = HitTestExpandedButton() - public let likeButton = HitTestExpandedButton() - public let menuButton = HitTestExpandedButton() - - - public override init(frame: CGRect) { - super.init(frame: frame) - _init() - } - - public required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - - public override func willMove(toWindow newWindow: UIWindow?) { - super.willMove(toWindow: newWindow) - assert(style != nil, "Needs setup style before use") - } - -} - -extension StatusToolbar { - - private func _init() { - container.translatesAutoresizingMaskIntoConstraints = false - addSubview(container) - NSLayoutConstraint.activate([ - container.topAnchor.constraint(equalTo: topAnchor), - container.leadingAnchor.constraint(equalTo: leadingAnchor), - container.trailingAnchor.constraint(equalTo: trailingAnchor), - container.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - - replyButton.accessibilityLabel = L10n.Accessibility.Common.Status.Actions.reply - // dynamic label for repostButton - likeButton.accessibilityLabel = L10n.Accessibility.Common.Status.Actions.like - menuButton.accessibilityLabel = L10n.Accessibility.Common.Status.Actions.menu - } - - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - viewModel.traitCollectionDidChange.send() - } - - public func setup(style: Style) { - self.style = style - - container.arrangedSubviews.forEach { subview in - container.removeArrangedSubview(subview) - subview.removeFromSuperview() - } - - let buttons = [replyButton, repostButton, likeButton, menuButton] - buttons.forEach { button in - button.tintColor = .secondaryLabel - button.titleLabel?.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular) - button.setTitle("", for: .normal) - button.setTitleColor(.secondaryLabel, for: .normal) - button.setInsets(forContentPadding: .zero, imageTitlePadding: style.buttonTitleImagePadding) - button.addTarget(self, action: #selector(StatusToolbar.buttonDidPressed(_:)), for: .touchUpInside) - } - - switch style { - case .inline: - buttons.forEach { button in - button.contentHorizontalAlignment = .leading - } - replyButton.setImage(Asset.Arrows.arrowTurnUpLeftMini.image.withRenderingMode(.alwaysTemplate), for: .normal) - repostButton.setImage(Asset.Media.repeatMini.image.withRenderingMode(.alwaysTemplate), for: .normal) - likeButton.setImage(Asset.Health.heartMini.image.withRenderingMode(.alwaysTemplate), for: .normal) - menuButton.setImage(Asset.Editing.ellipsisMini.image.withRenderingMode(.alwaysTemplate), for: .normal) - - container.axis = .horizontal - container.distribution = .fill - - replyButton.translatesAutoresizingMaskIntoConstraints = false - repostButton.translatesAutoresizingMaskIntoConstraints = false - likeButton.translatesAutoresizingMaskIntoConstraints = false - menuButton.translatesAutoresizingMaskIntoConstraints = false - container.addArrangedSubview(replyButton) - container.addArrangedSubview(repostButton) - container.addArrangedSubview(likeButton) - container.addArrangedSubview(menuButton) - NSLayoutConstraint.activate([ - replyButton.heightAnchor.constraint(equalToConstant: 40).priority(.required - 10), - replyButton.heightAnchor.constraint(equalTo: repostButton.heightAnchor).priority(.defaultHigh), - replyButton.heightAnchor.constraint(equalTo: likeButton.heightAnchor).priority(.defaultHigh), - replyButton.heightAnchor.constraint(equalTo: menuButton.heightAnchor).priority(.defaultHigh), - replyButton.widthAnchor.constraint(equalTo: repostButton.widthAnchor).priority(.defaultHigh), - replyButton.widthAnchor.constraint(equalTo: likeButton.widthAnchor).priority(.defaultHigh), - ]) - menuButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) - menuButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - - case .plain: - buttons.forEach { button in - button.contentHorizontalAlignment = .center - } - replyButton.setImage(Asset.Arrows.arrowTurnUpLeft.image.withRenderingMode(.alwaysTemplate), for: .normal) - repostButton.setImage(Asset.Media.repeat.image.withRenderingMode(.alwaysTemplate), for: .normal) - likeButton.setImage(Asset.Health.heart.image.withRenderingMode(.alwaysTemplate), for: .normal) - menuButton.setImage(Asset.Editing.ellipsis.image.withRenderingMode(.alwaysTemplate), for: .normal) - - container.axis = .horizontal - container.spacing = 8 - container.distribution = .fillEqually - - container.addArrangedSubview(replyButton) - container.addArrangedSubview(repostButton) - container.addArrangedSubview(likeButton) - container.addArrangedSubview(menuButton) - - NSLayoutConstraint.activate([ - replyButton.heightAnchor.constraint(equalToConstant: 47).priority(.required - 10), - replyButton.heightAnchor.constraint(equalTo: repostButton.heightAnchor).priority(.defaultHigh), - replyButton.heightAnchor.constraint(equalTo: likeButton.heightAnchor).priority(.defaultHigh), - replyButton.heightAnchor.constraint(equalTo: menuButton.heightAnchor).priority(.defaultHigh), - ]) - } - } -} - -extension StatusToolbar { - public enum Action: String, CaseIterable { - case reply - case repost - case like - case menu - } - - public enum MenuAction: String, CaseIterable { - case saveMedia - case translate - case share - case remove - #if DEBUG - case copyID - #endif - case appearEvent - } - - public enum Style { - case inline - case plain - - var buttonTitleImagePadding: CGFloat { - switch self { - case .inline: return 4.0 - case .plain: return 0 - } - } - } -} - -extension StatusToolbar { - - @objc private func buttonDidPressed(_ sender: UIButton) { - let _action: Action? - switch sender { - case replyButton: _action = .reply - case repostButton: _action = .repost - case likeButton: _action = .like - case menuButton: _action = .menu - default: _action = nil - } - - guard let action = _action else { - assertionFailure() - return - } - - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(action.rawValue) button pressed") - delegate?.statusToolbar(self, actionDidPressed: action, button: sender) - } - -} diff --git a/TwidereX/Diffable/Misc/History/HistorySection.swift b/TwidereX/Diffable/Misc/History/HistorySection.swift index c43473dc..3fbef2f1 100644 --- a/TwidereX/Diffable/Misc/History/HistorySection.swift +++ b/TwidereX/Diffable/Misc/History/HistorySection.swift @@ -26,10 +26,9 @@ extension HistorySection { struct Configuration { weak var statusViewTableViewCellDelegate: StatusViewTableViewCellDelegate? - let statusViewConfigurationContext: StatusView.ConfigurationContext - weak var userViewTableViewCellDelegate: UserViewTableViewCellDelegate? let userViewConfigurationContext: UserView.ConfigurationContext + let viewLayoutFramePublisher: Published.Publisher? } static func diffableDataSource( diff --git a/TwidereX/Diffable/Misc/Notification/NotificationSection.swift b/TwidereX/Diffable/Misc/Notification/NotificationSection.swift index 106188c3..b9950b18 100644 --- a/TwidereX/Diffable/Misc/Notification/NotificationSection.swift +++ b/TwidereX/Diffable/Misc/Notification/NotificationSection.swift @@ -20,8 +20,8 @@ extension NotificationSection { struct Configuration { weak var statusViewTableViewCellDelegate: StatusViewTableViewCellDelegate? weak var userViewTableViewCellDelegate: UserViewTableViewCellDelegate? - let statusViewConfigurationContext: StatusView.ConfigurationContext let userViewConfigurationContext: UserView.ConfigurationContext + let viewLayoutFramePublisher: Published.Publisher? } static func diffableDataSource( @@ -107,12 +107,12 @@ extension NotificationSection { viewModel: StatusTableViewCell.ViewModel, configuration: Configuration ) { - cell.configure( - tableView: tableView, - viewModel: viewModel, - configurationContext: configuration.statusViewConfigurationContext, - delegate: configuration.statusViewTableViewCellDelegate - ) +// cell.configure( +// tableView: tableView, +// viewModel: viewModel, +// configurationContext: configuration.statusViewConfigurationContext, +// delegate: configuration.statusViewTableViewCellDelegate +// ) } static func configure( diff --git a/TwidereX/Diffable/Status/StatusSection.swift b/TwidereX/Diffable/Status/StatusSection.swift index 1c47fecc..3bff3801 100644 --- a/TwidereX/Diffable/Status/StatusSection.swift +++ b/TwidereX/Diffable/Status/StatusSection.swift @@ -29,12 +29,13 @@ extension StatusSection { struct Configuration { weak var statusViewTableViewCellDelegate: StatusViewTableViewCellDelegate? weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate? - let statusViewConfigurationContext: StatusView.ConfigurationContext + let viewLayoutFramePublisher: Published.Publisher? } static func diffableDataSource( tableView: UITableView, context: AppContext, + authContext: AuthContext, configuration: Configuration ) -> UITableViewDiffableDataSource { tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self)) @@ -54,9 +55,11 @@ extension StatusSection { guard let feed = record.object(in: context.managedObjectContext) else { return } let _viewModel = StatusView.ViewModel( feed: feed, + authContext: authContext, delegate: cell, - viewLayoutFramePublisher: configuration.statusViewConfigurationContext.viewLayoutFramePublisher + viewLayoutFramePublisher: configuration.viewLayoutFramePublisher ) + guard let viewModel = _viewModel else { return } cell.contentConfiguration = UIHostingConfiguration { StatusView(viewModel: viewModel) diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift index 540a6ca5..6f3dfeac 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift @@ -18,9 +18,7 @@ extension DataSourceFacade { static func responseToStatusToolbar( provider: DataSourceProvider & AuthContextProvider, status: StatusRecord, - action: StatusToolbar.Action, - sender: UIButton, - authenticationContext: AuthenticationContext + action: StatusToolbarView.Action ) async { defer { Task { @@ -45,18 +43,7 @@ extension DataSourceFacade { let composeContentViewModel = ComposeContentViewModel( context: provider.context, authContext: provider.authContext, - kind: .reply(status: status), - configurationContext: ComposeContentViewModel.ConfigurationContext( - apiService: provider.context.apiService, - authenticationService: provider.context.authenticationService, - mastodonEmojiService: provider.context.mastodonEmojiService, - statusViewConfigureContext: .init( - authContext: provider.authContext, - dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - viewLayoutFramePublisher: composeViewModel.$viewLayoutFrame - ) - ) + kind: .reply(status: status) ) provider.coordinator.present( scene: .compose( @@ -71,7 +58,7 @@ extension DataSourceFacade { try await DataSourceFacade.responseToStatusRepostAction( provider: provider, status: status, - authenticationContext: authenticationContext + authenticationContext: provider.authContext.authenticationContext ) // update store review count trigger @@ -79,12 +66,15 @@ extension DataSourceFacade { } catch { provider.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): update repost failure: \(error.localizedDescription)") } + + case .quote: + assertionFailure() case .like: do { try await DataSourceFacade.responseToStatusLikeAction( provider: provider, status: status, - authenticationContext: authenticationContext + authenticationContext: provider.authContext.authenticationContext ) // update store review count trigger @@ -92,16 +82,16 @@ extension DataSourceFacade { } catch { provider.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): update like failure: \(error.localizedDescription)") } - case .menu: + case .share: // media menu button trigger this let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) impactFeedbackGenerator.impactOccurred() - await DataSourceFacade.responseToStatusShareAction( - provider: provider, - status: status, - button: sender - ) +// await DataSourceFacade.responseToStatusShareAction( +// provider: provider, +// status: status, +// button: sender +// ) } // end switch action } } diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift index 1e0c12a8..b5b240c6 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift @@ -74,30 +74,30 @@ extension MediaInfoDescriptionViewDelegate where Self: DataSourceProvider & Auth } - func mediaInfoDescriptionView(_ mediaInfoDescriptionView: MediaInfoDescriptionView, statusToolbar: StatusToolbar, actionDidPressed action: StatusToolbar.Action, button: UIButton) { - Task { - let source = DataSourceItem.Source(tableViewCell: nil, indexPath: nil) - guard let item = await item(from: source) else { - assertionFailure() - return - } - guard let status = await item.status(in: self.context.managedObjectContext) else { - assertionFailure("only works for status data provider") - return - } - - await DataSourceFacade.responseToStatusToolbar( - provider: self, - status: status, - action: action, - sender: button, - authenticationContext: authContext.authenticationContext - ) - } // end Task - } // end func +// func mediaInfoDescriptionView(_ mediaInfoDescriptionView: MediaInfoDescriptionView, statusToolbar: StatusToolbar, actionDidPressed action: StatusToolbar.Action, button: UIButton) { +// Task { +// let source = DataSourceItem.Source(tableViewCell: nil, indexPath: nil) +// guard let item = await item(from: source) else { +// assertionFailure() +// return +// } +// guard let status = await item.status(in: self.context.managedObjectContext) else { +// assertionFailure("only works for status data provider") +// return +// } +// +// await DataSourceFacade.responseToStatusToolbar( +// provider: self, +// status: status, +// action: action, +// sender: button, +// authenticationContext: authContext.authenticationContext +// ) +// } // end Task +// } // end func - func mediaInfoDescriptionView(_ mediaInfoDescriptionView: MediaInfoDescriptionView, statusToolbar: StatusToolbar, menuActionDidPressed action: StatusToolbar.MenuAction, menuButton button: UIButton) { - assertionFailure("present UIAcitivityController directly") - } +// func mediaInfoDescriptionView(_ mediaInfoDescriptionView: MediaInfoDescriptionView, statusToolbar: StatusToolbar, menuActionDidPressed action: StatusToolbar.MenuAction, menuButton button: UIButton) { +// assertionFailure("present UIAcitivityController directly") +// } } diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift index 307076dd..29b07a48 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift @@ -323,6 +323,24 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC // MARK: - toolbar extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { + func tableViewCell( + _ cell: UITableViewCell, + viewModel: StatusView.ViewModel, + statusToolbarButtonDidPressed action: StatusToolbarView.Action + ) { + Task { + guard let status = viewModel.status else { + assertionFailure() + return + } + await DataSourceFacade.responseToStatusToolbar( + provider: self, + status: status, + action: action + ) + } // end Task + } + // func tableViewCell( // _ cell: UITableViewCell, // statusView: StatusView, diff --git a/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift b/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift index 9bc1b392..e4309c1e 100644 --- a/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift +++ b/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift @@ -21,17 +21,12 @@ extension StatusHistoryViewModel { context: context, configuration: .init( statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, - statusViewConfigurationContext: .init( - authContext: authContext, - dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - viewLayoutFramePublisher: $viewLayoutFrame - ), userViewTableViewCellDelegate: nil, userViewConfigurationContext: .init( authContext: authContext, listMembershipViewModel: nil - ) + ), + viewLayoutFramePublisher: $viewLayoutFrame ) ) diff --git a/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift b/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift index fbc5e4b7..1d33e374 100644 --- a/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift +++ b/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift @@ -21,17 +21,12 @@ extension UserHistoryViewModel { context: context, configuration: .init( statusViewTableViewCellDelegate: nil, - statusViewConfigurationContext: .init( - authContext: authContext, - dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - viewLayoutFramePublisher: $viewLayoutFrame - ), userViewTableViewCellDelegate: userViewTableViewCellDelegate, userViewConfigurationContext: .init( authContext: authContext, listMembershipViewModel: nil - ) + ), + viewLayoutFramePublisher: $viewLayoutFrame ) ) diff --git a/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift b/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift index 2a4c74d4..27074e12 100644 --- a/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift +++ b/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift @@ -136,15 +136,14 @@ extension MediaPreviewViewController { ]) if let status = viewModel.status { - mediaInfoDescriptionView.configure( - statusObject: status, - configurationContext: .init( - authContext: authContext, - dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - viewLayoutFramePublisher: viewModel.$viewLayoutFrame - ) - ) +// mediaInfoDescriptionView.configure( +// statusObject: status, +// configurationContext: .init( +// dateTimeProvider: DateTimeSwiftProvider(), +// twitterTextProvider: OfficialTwitterTextProvider(), +// viewLayoutFramePublisher: viewModel.$viewLayoutFrame +// ) +// ) } else { mediaInfoDescriptionView.isHidden = true } @@ -327,11 +326,11 @@ extension MediaPreviewViewController: MediaPreviewImageViewControllerDelegate { impactFeedbackGenerator.impactOccurred() // trigger menu button action - mediaInfoDescriptionView.toolbar.delegate?.statusToolbar( - mediaInfoDescriptionView.toolbar, - actionDidPressed: .menu, - button: mediaInfoDescriptionView.toolbar.menuButton - ) +// mediaInfoDescriptionView.toolbar.delegate?.statusToolbar( +// mediaInfoDescriptionView.toolbar, +// actionDidPressed: .menu, +// button: mediaInfoDescriptionView.toolbar.menuButton +// ) } } diff --git a/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView+ViewModel.swift b/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView+ViewModel.swift index 6924c52f..46d1bbc8 100644 --- a/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView+ViewModel.swift +++ b/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView+ViewModel.swift @@ -159,210 +159,205 @@ extension MediaInfoDescriptionView.ViewModel { } } - -extension MediaInfoDescriptionView { - public typealias ConfigurationContext = StatusView.ConfigurationContext -} - extension MediaInfoDescriptionView { public func configure( - statusObject object: StatusObject, - configurationContext: ConfigurationContext + statusObject object: StatusObject + // configurationContext: ConfigurationContext ) { - switch object { - case .twitter(let status): - configure( - twitterStatus: status, - configurationContext: configurationContext - ) - case .mastodon(let status): - configure( - mastodonStatus: status, - configurationContext: configurationContext - ) - } +// switch object { +// case .twitter(let status): +// configure( +// twitterStatus: status, +// configurationContext: configurationContext +// ) +// case .mastodon(let status): +// configure( +// mastodonStatus: status, +// configurationContext: configurationContext +// ) +// } } } extension MediaInfoDescriptionView { - public func configure( - twitterStatus status: TwitterStatus, - configurationContext: ConfigurationContext - ) { - viewModel.platform = .twitter - viewModel.dateTimeProvider = configurationContext.dateTimeProvider - viewModel.twitterTextProvider = configurationContext.twitterTextProvider - - configureAuthor(twitterStatus: status) - configureContent(twitterStatus: status) - configureToolbar(twitterStatus: status) - } - - private func configureAuthor(twitterStatus status: TwitterStatus) { - let author = (status.repost ?? status).author - - // author avatar - author.publisher(for: \.profileImageURL) - .map { _ in author.avatarImageURL() } - .assign(to: \.authorAvatarImageURL, on: viewModel) - .store(in: &disposeBag) - // lock - author.publisher(for: \.protected) - .assign(to: \.protected, on: viewModel) - .store(in: &disposeBag) - // author name - author.publisher(for: \.name) - .map { PlaintextMetaContent(string: $0) } - .assign(to: \.authorName, on: viewModel) - .store(in: &disposeBag) - } - - private func configureContent(twitterStatus status: TwitterStatus) { - guard let twitterTextProvider = viewModel.twitterTextProvider else { - assertionFailure() - return - } - - let status = status.repost ?? status - let content = TwitterContent(content: status.text) - let metaContent = TwitterMetaContent.convert( - content: content, - urlMaximumLength: 20, - twitterTextProvider: twitterTextProvider - ) - viewModel.content = metaContent - viewModel.visibility = nil - } - - private func configureToolbar(twitterStatus status: TwitterStatus) { - let status = status.repost ?? status - - // relationship - Publishers.CombineLatest( - viewModel.$authenticationContext, - status.publisher(for: \.repostBy) - ) - .map { authenticationContext, repostBy in - guard let authenticationContext = authenticationContext?.twitterAuthenticationContext else { - return false - } - let userID = authenticationContext.userID - return repostBy.contains(where: { $0.id == userID }) - } - .assign(to: \.isRepost, on: viewModel) - .store(in: &disposeBag) - - Publishers.CombineLatest( - viewModel.$authenticationContext, - status.publisher(for: \.likeBy) - ) - .map { authenticationContext, likeBy in - guard let authenticationContext = authenticationContext?.twitterAuthenticationContext else { - return false - } - let userID = authenticationContext.userID - return likeBy.contains(where: { $0.id == userID }) - } - .assign(to: \.isLike, on: viewModel) - .store(in: &disposeBag) - } +// public func configure( +// twitterStatus status: TwitterStatus, +// configurationContext: ConfigurationContext +// ) { +// viewModel.platform = .twitter +// viewModel.dateTimeProvider = configurationContext.dateTimeProvider +// viewModel.twitterTextProvider = configurationContext.twitterTextProvider +// +// configureAuthor(twitterStatus: status) +// configureContent(twitterStatus: status) +// configureToolbar(twitterStatus: status) +// } +// +// private func configureAuthor(twitterStatus status: TwitterStatus) { +// let author = (status.repost ?? status).author +// +// // author avatar +// author.publisher(for: \.profileImageURL) +// .map { _ in author.avatarImageURL() } +// .assign(to: \.authorAvatarImageURL, on: viewModel) +// .store(in: &disposeBag) +// // lock +// author.publisher(for: \.protected) +// .assign(to: \.protected, on: viewModel) +// .store(in: &disposeBag) +// // author name +// author.publisher(for: \.name) +// .map { PlaintextMetaContent(string: $0) } +// .assign(to: \.authorName, on: viewModel) +// .store(in: &disposeBag) +// } +// +// private func configureContent(twitterStatus status: TwitterStatus) { +// guard let twitterTextProvider = viewModel.twitterTextProvider else { +// assertionFailure() +// return +// } +// +// let status = status.repost ?? status +// let content = TwitterContent(content: status.text) +// let metaContent = TwitterMetaContent.convert( +// content: content, +// urlMaximumLength: 20, +// twitterTextProvider: twitterTextProvider +// ) +// viewModel.content = metaContent +// viewModel.visibility = nil +// } +// +// private func configureToolbar(twitterStatus status: TwitterStatus) { +// let status = status.repost ?? status +// +// // relationship +// Publishers.CombineLatest( +// viewModel.$authenticationContext, +// status.publisher(for: \.repostBy) +// ) +// .map { authenticationContext, repostBy in +// guard let authenticationContext = authenticationContext?.twitterAuthenticationContext else { +// return false +// } +// let userID = authenticationContext.userID +// return repostBy.contains(where: { $0.id == userID }) +// } +// .assign(to: \.isRepost, on: viewModel) +// .store(in: &disposeBag) +// +// Publishers.CombineLatest( +// viewModel.$authenticationContext, +// status.publisher(for: \.likeBy) +// ) +// .map { authenticationContext, likeBy in +// guard let authenticationContext = authenticationContext?.twitterAuthenticationContext else { +// return false +// } +// let userID = authenticationContext.userID +// return likeBy.contains(where: { $0.id == userID }) +// } +// .assign(to: \.isLike, on: viewModel) +// .store(in: &disposeBag) +// } } // MARK: - Mastodon extension MediaInfoDescriptionView { - public func configure( - mastodonStatus status: MastodonStatus, - configurationContext: ConfigurationContext - ) { - viewModel.platform = .mastodon - viewModel.dateTimeProvider = configurationContext.dateTimeProvider - viewModel.twitterTextProvider = configurationContext.twitterTextProvider - -// configureHeader(mastodonStatus: status, mastodonNotification: notification) - configureAuthor(mastodonStatus: status) - configureContent(mastodonStatus: status) -// configureMedia(mastodonStatus: status) - configureToolbar(mastodonStatus: status) - } - - private func configureAuthor(mastodonStatus status: MastodonStatus) { - let author = (status.repost ?? status).author - - // author avatar - author.publisher(for: \.avatar) - .map { url in url.flatMap { URL(string: $0) } } - .assign(to: \.authorAvatarImageURL, on: viewModel) - .store(in: &disposeBag) - // author name - Publishers.CombineLatest( - author.publisher(for: \.displayName), - author.publisher(for: \.emojis) - ) - .map { _, emojis in - let content = MastodonContent(content: author.name, emojis: emojis.asDictionary) - do { - let metaContent = try MastodonMetaContent.convert(document: content) - return metaContent - } catch { - assertionFailure(error.localizedDescription) - return PlaintextMetaContent(string: author.name) - } - } - .assign(to: \.authorName, on: viewModel) - .store(in: &disposeBag) - // protected - author.publisher(for: \.locked) - .assign(to: \.protected, on: viewModel) - .store(in: &disposeBag) - - } - - private func configureContent(mastodonStatus status: MastodonStatus) { - let status = status.repost ?? status - let content = MastodonContent(content: status.content, emojis: status.emojis.asDictionary) - do { - let metaContent = try MastodonMetaContent.convert(document: content) - viewModel.content = metaContent - } catch { - assertionFailure(error.localizedDescription) - viewModel.content = PlaintextMetaContent(string: "") - } - - viewModel.visibility = status.visibility - } +// public func configure( +// mastodonStatus status: MastodonStatus, +// configurationContext: ConfigurationContext +// ) { +// viewModel.platform = .mastodon +// viewModel.dateTimeProvider = configurationContext.dateTimeProvider +// viewModel.twitterTextProvider = configurationContext.twitterTextProvider +// +//// configureHeader(mastodonStatus: status, mastodonNotification: notification) +// configureAuthor(mastodonStatus: status) +// configureContent(mastodonStatus: status) +//// configureMedia(mastodonStatus: status) +// configureToolbar(mastodonStatus: status) +// } - private func configureToolbar(mastodonStatus status: MastodonStatus) { - let status = status.repost ?? status - - // relationship - Publishers.CombineLatest( - viewModel.$authenticationContext, - status.publisher(for: \.repostBy) - ) - .map { authenticationContext, repostBy in - guard let authenticationContext = authenticationContext?.mastodonAuthenticationContext else { - return false - } - let domain = authenticationContext.domain - let userID = authenticationContext.userID - return repostBy.contains(where: { $0.id == userID && $0.domain == domain }) - } - .assign(to: \.isRepost, on: viewModel) - .store(in: &disposeBag) - - Publishers.CombineLatest( - viewModel.$authenticationContext, - status.publisher(for: \.likeBy) - ) - .map { authenticationContext, likeBy in - guard let authenticationContext = authenticationContext?.mastodonAuthenticationContext else { - return false - } - let domain = authenticationContext.domain - let userID = authenticationContext.userID - return likeBy.contains(where: { $0.id == userID && $0.domain == domain }) - } - .assign(to: \.isLike, on: viewModel) - .store(in: &disposeBag) - } +// private func configureAuthor(mastodonStatus status: MastodonStatus) { +// let author = (status.repost ?? status).author +// +// // author avatar +// author.publisher(for: \.avatar) +// .map { url in url.flatMap { URL(string: $0) } } +// .assign(to: \.authorAvatarImageURL, on: viewModel) +// .store(in: &disposeBag) +// // author name +// Publishers.CombineLatest( +// author.publisher(for: \.displayName), +// author.publisher(for: \.emojis) +// ) +// .map { _, emojis in +// let content = MastodonContent(content: author.name, emojis: emojis.asDictionary) +// do { +// let metaContent = try MastodonMetaContent.convert(document: content) +// return metaContent +// } catch { +// assertionFailure(error.localizedDescription) +// return PlaintextMetaContent(string: author.name) +// } +// } +// .assign(to: \.authorName, on: viewModel) +// .store(in: &disposeBag) +// // protected +// author.publisher(for: \.locked) +// .assign(to: \.protected, on: viewModel) +// .store(in: &disposeBag) +// +// } +// +// private func configureContent(mastodonStatus status: MastodonStatus) { +// let status = status.repost ?? status +// let content = MastodonContent(content: status.content, emojis: status.emojis.asDictionary) +// do { +// let metaContent = try MastodonMetaContent.convert(document: content) +// viewModel.content = metaContent +// } catch { +// assertionFailure(error.localizedDescription) +// viewModel.content = PlaintextMetaContent(string: "") +// } +// +// viewModel.visibility = status.visibility +// } +// +// private func configureToolbar(mastodonStatus status: MastodonStatus) { +// let status = status.repost ?? status +// +// // relationship +// Publishers.CombineLatest( +// viewModel.$authenticationContext, +// status.publisher(for: \.repostBy) +// ) +// .map { authenticationContext, repostBy in +// guard let authenticationContext = authenticationContext?.mastodonAuthenticationContext else { +// return false +// } +// let domain = authenticationContext.domain +// let userID = authenticationContext.userID +// return repostBy.contains(where: { $0.id == userID && $0.domain == domain }) +// } +// .assign(to: \.isRepost, on: viewModel) +// .store(in: &disposeBag) +// +// Publishers.CombineLatest( +// viewModel.$authenticationContext, +// status.publisher(for: \.likeBy) +// ) +// .map { authenticationContext, likeBy in +// guard let authenticationContext = authenticationContext?.mastodonAuthenticationContext else { +// return false +// } +// let domain = authenticationContext.domain +// let userID = authenticationContext.userID +// return likeBy.contains(where: { $0.id == userID && $0.domain == domain }) +// } +// .assign(to: \.isLike, on: viewModel) +// .store(in: &disposeBag) +// } } diff --git a/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView.swift b/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView.swift index 19f5c40b..b6927553 100644 --- a/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView.swift +++ b/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView.swift @@ -18,8 +18,8 @@ protocol MediaInfoDescriptionViewDelegate: AnyObject { func mediaInfoDescriptionView(_ mediaInfoDescriptionView: MediaInfoDescriptionView, avatarButtonDidPressed button: UIButton) func mediaInfoDescriptionView(_ mediaInfoDescriptionView: MediaInfoDescriptionView, contentTextViewDidPressed textView: MetaTextAreaView) func mediaInfoDescriptionView(_ mediaInfoDescriptionView: MediaInfoDescriptionView, nameMetaLabelDidPressed metaLabel: MetaLabel) - func mediaInfoDescriptionView(_ mediaInfoDescriptionView: MediaInfoDescriptionView, statusToolbar: StatusToolbar, actionDidPressed action: StatusToolbar.Action, button: UIButton) - func mediaInfoDescriptionView(_ mediaInfoDescriptionView: MediaInfoDescriptionView, statusToolbar: StatusToolbar, menuActionDidPressed action: StatusToolbar.MenuAction, menuButton button: UIButton) +// func mediaInfoDescriptionView(_ mediaInfoDescriptionView: MediaInfoDescriptionView, statusToolbar: StatusToolbar, actionDidPressed action: StatusToolbar.Action, button: UIButton) +// func mediaInfoDescriptionView(_ mediaInfoDescriptionView: MediaInfoDescriptionView, statusToolbar: StatusToolbar, menuActionDidPressed action: StatusToolbar.MenuAction, menuButton button: UIButton) } final class MediaInfoDescriptionView: UIView { @@ -67,11 +67,11 @@ final class MediaInfoDescriptionView: UIView { return textView }() - let toolbar: StatusToolbar = { - let toolbar = StatusToolbar() - toolbar.setup(style: .plain) - return toolbar - }() +// let toolbar: StatusToolbar = { +// let toolbar = StatusToolbar() +// toolbar.setup(style: .plain) +// return toolbar +// }() let contentTextViewTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer let nameMetaLabelTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer @@ -125,11 +125,11 @@ extension MediaInfoDescriptionView { avatarView.heightAnchor.constraint(equalToConstant: MediaInfoDescriptionView.avatarImageViewSize.height).priority(.required - 1), ]) bottomContainerStackView.addArrangedSubview(nameMetaLabel) - toolbar.translatesAutoresizingMaskIntoConstraints = false - bottomContainerStackView.addArrangedSubview(toolbar) - NSLayoutConstraint.activate([ - toolbar.widthAnchor.constraint(equalToConstant: 180).priority(.defaultHigh), - ]) +// toolbar.translatesAutoresizingMaskIntoConstraints = false +// bottomContainerStackView.addArrangedSubview(toolbar) +// NSLayoutConstraint.activate([ +// toolbar.widthAnchor.constraint(equalToConstant: 180).priority(.defaultHigh), +// ]) avatarView.avatarButton.addTarget(self, action: #selector(MediaInfoDescriptionView.avatarButtonDidPressed(_:)), for: .touchUpInside) @@ -140,7 +140,7 @@ extension MediaInfoDescriptionView { nameMetaLabelTapGestureRecognizer.addTarget(self, action: #selector(MediaInfoDescriptionView.nameMetaLabelDidPressed(_:))) nameMetaLabel.addGestureRecognizer(nameMetaLabelTapGestureRecognizer) - toolbar.delegate = self + //toolbar.delegate = self } } @@ -167,15 +167,15 @@ extension MediaInfoDescriptionView { } // MARK: - StatusToolbarDelegate -extension MediaInfoDescriptionView: StatusToolbarDelegate { - func statusToolbar(_ statusToolbar: StatusToolbar, actionDidPressed action: StatusToolbar.Action, button: UIButton) { - delegate?.mediaInfoDescriptionView(self, statusToolbar: statusToolbar, actionDidPressed: action, button: button) - } - - func statusToolbar(_ statusToolbar: StatusToolbar, menuActionDidPressed action: StatusToolbar.MenuAction, menuButton button: UIButton) { - delegate?.mediaInfoDescriptionView(self, statusToolbar: statusToolbar, menuActionDidPressed: action, menuButton: button) - } -} +//extension MediaInfoDescriptionView: StatusToolbarDelegate { +// func statusToolbar(_ statusToolbar: StatusToolbar, actionDidPressed action: StatusToolbar.Action, button: UIButton) { +// delegate?.mediaInfoDescriptionView(self, statusToolbar: statusToolbar, actionDidPressed: action, button: button) +// } +// +// func statusToolbar(_ statusToolbar: StatusToolbar, menuActionDidPressed action: StatusToolbar.MenuAction, menuButton button: UIButton) { +// delegate?.mediaInfoDescriptionView(self, statusToolbar: statusToolbar, menuActionDidPressed: action, menuButton: button) +// } +//} extension MediaInfoDescriptionView { override var accessibilityElements: [Any]? { @@ -183,7 +183,7 @@ extension MediaInfoDescriptionView { return [ avatarView, nameMetaLabel, - toolbar, + // toolbar, ] } set { } diff --git a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift index 84769697..95380ad8 100644 --- a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift @@ -24,16 +24,11 @@ extension NotificationTimelineViewModel { let configuration = NotificationSection.Configuration( statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, userViewTableViewCellDelegate: userViewTableViewCellDelegate, - statusViewConfigurationContext: .init( - authContext: authContext, - dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - viewLayoutFramePublisher: $viewLayoutFrame - ), userViewConfigurationContext: .init( authContext: authContext, listMembershipViewModel: nil - ) + ), + viewLayoutFramePublisher: $viewLayoutFrame ) diffableDataSource = NotificationSection.diffableDataSource( tableView: tableView, diff --git a/TwidereX/Scene/Profile/ProfileViewController.swift b/TwidereX/Scene/Profile/ProfileViewController.swift index bdb3b832..04ba6906 100644 --- a/TwidereX/Scene/Profile/ProfileViewController.swift +++ b/TwidereX/Scene/Profile/ProfileViewController.swift @@ -308,18 +308,7 @@ extension ProfileViewController { } else { return .mention(user: user) } - }(), - configurationContext: ComposeContentViewModel.ConfigurationContext( - apiService: context.apiService, - authenticationService: context.authenticationService, - mastodonEmojiService: context.mastodonEmojiService, - statusViewConfigureContext: .init( - authContext: authContext, - dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - viewLayoutFramePublisher: composeViewModel.$viewLayoutFrame - ) - ) + }() ) coordinator.present(scene: .compose(viewModel: composeViewModel, contentViewModel: composeContentViewModel), from: self, transition: .modal(animated: true, completion: nil)) } diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell+ViewModel.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell+ViewModel.swift index e6f48415..d0df0aa4 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell+ViewModel.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell+ViewModel.swift @@ -30,12 +30,12 @@ extension StatusTableViewCell { } } - func configure( - tableView: UITableView, - viewModel: ViewModel, - configurationContext: StatusView.ConfigurationContext, - delegate: StatusViewTableViewCellDelegate? - ) { +// func configure( +// tableView: UITableView, +// viewModel: ViewModel, +// configurationContext: StatusView.ConfigurationContext, +// delegate: StatusViewTableViewCellDelegate? +// ) { // if statusView.frame == .zero { // // set status view width // statusView.frame.size.width = tableView.readableContentGuide.layoutFrame.width @@ -91,7 +91,7 @@ extension StatusTableViewCell { // UIView.setAnimationsEnabled(true) // } // .store(in: &disposeBag) - } +// } } diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell+ViewModel.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell+ViewModel.swift index b0e43a81..69f58b26 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell+ViewModel.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell+ViewModel.swift @@ -32,12 +32,12 @@ extension StatusThreadRootTableViewCell { } } - func configure( - tableView: UITableView, - viewModel: StatusThreadRootTableViewCell.ViewModel, - configurationContext: StatusView.ConfigurationContext, - delegate: StatusViewTableViewCellDelegate? - ) { +// func configure( +// tableView: UITableView, +// viewModel: StatusThreadRootTableViewCell.ViewModel, +// configurationContext: StatusView.ConfigurationContext, +// delegate: StatusViewTableViewCellDelegate? +// ) { // if statusView.frame == .zero { // // set status view width // statusView.frame.size.width = tableView.readableContentGuide.layoutFrame.width @@ -85,6 +85,6 @@ extension StatusThreadRootTableViewCell { // UIView.setAnimationsEnabled(true) // } // .store(in: &disposeBag) - } +// } } diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift index 6461c41d..e6c61325 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift @@ -28,6 +28,7 @@ protocol StatusViewTableViewCellDelegate: AutoGenerateProtocolDelegate { func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, toggleContentDisplay isReveal: Bool) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, toggleContentWarningOverlayDisplay isReveal: Bool) + func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, statusToolbarButtonDidPressed action: StatusToolbarView.Action) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, viewHeightDidChange: Void) // sourcery:end } @@ -48,6 +49,10 @@ extension StatusViewDelegate where Self: StatusViewContainerTableViewCell { delegate?.tableViewCell(self, viewModel: viewModel, toggleContentWarningOverlayDisplay: isReveal) } + func statusView(_ viewModel: StatusView.ViewModel, statusToolbarButtonDidPressed action: StatusToolbarView.Action) { + delegate?.tableViewCell(self, viewModel: viewModel, statusToolbarButtonDidPressed: action) + } + func statusView(_ viewModel: StatusView.ViewModel, viewHeightDidChange: Void) { delegate?.tableViewCell(self, viewModel: viewModel, viewHeightDidChange: viewHeightDidChange) } diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift index af523755..00ec9c23 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift @@ -22,17 +22,13 @@ extension StatusThreadViewModel { let configuration = StatusSection.Configuration( statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, - statusViewConfigurationContext: .init( - authContext: authContext, - dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - viewLayoutFramePublisher: $viewLayoutFrame - ) + viewLayoutFramePublisher: $viewLayoutFrame ) diffableDataSource = StatusSection.diffableDataSource( tableView: tableView, context: context, + authContext: authContext, configuration: configuration ) diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift index 3f9d6f95..dedcbd1d 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift @@ -261,18 +261,7 @@ extension TimelineViewController { break } return settings - }(), - configurationContext: ComposeContentViewModel.ConfigurationContext( - apiService: context.apiService, - authenticationService: context.authenticationService, - mastodonEmojiService: context.mastodonEmojiService, - statusViewConfigureContext: .init( - authContext: authContext, - dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - viewLayoutFramePublisher: _viewModel.$viewLayoutFrame - ) - ) + }() ) coordinator.present(scene: .compose(viewModel: composeViewModel, contentViewModel: composeContentViewModel), from: self, transition: .modal(animated: true, completion: nil)) } diff --git a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift index d7954f89..e530016a 100644 --- a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift @@ -30,6 +30,7 @@ class ListTimelineViewController: TimelineViewController { tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .none tableView.selfSizingInvalidation = .enabled + tableView.cellLayoutMarginsFollowReadableWidth = true return tableView }() diff --git a/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel+Diffable.swift index 106722ee..acfe7f30 100644 --- a/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel+Diffable.swift @@ -24,16 +24,12 @@ extension FederatedTimelineViewModel { let configuration = StatusSection.Configuration( statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, - statusViewConfigurationContext: StatusView.ConfigurationContext( - authContext: authContext, - dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - viewLayoutFramePublisher: $viewLayoutFrame - ) + viewLayoutFramePublisher: $viewLayoutFrame ) diffableDataSource = StatusSection.diffableDataSource( tableView: tableView, context: context, + authContext: authContext, configuration: configuration ) diff --git a/TwidereX/Scene/Timeline/Hashtag/HashtagTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/Hashtag/HashtagTimelineViewModel+Diffable.swift index 5cb53767..a2e8b6fc 100644 --- a/TwidereX/Scene/Timeline/Hashtag/HashtagTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/Hashtag/HashtagTimelineViewModel+Diffable.swift @@ -24,16 +24,12 @@ extension HashtagTimelineViewModel { let configuration = StatusSection.Configuration( statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, - statusViewConfigurationContext: StatusView.ConfigurationContext( - authContext: authContext, - dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - viewLayoutFramePublisher: $viewLayoutFrame - ) + viewLayoutFramePublisher: $viewLayoutFrame ) diffableDataSource = StatusSection.diffableDataSource( tableView: tableView, context: context, + authContext: authContext, configuration: configuration ) diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel+Diffable.swift index a0a340d2..57ba5a89 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel+Diffable.swift @@ -24,16 +24,12 @@ extension HomeTimelineViewModel { let configuration = StatusSection.Configuration( statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate, - statusViewConfigurationContext: .init( - authContext: authContext, - dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - viewLayoutFramePublisher: $viewLayoutFrame - ) + viewLayoutFramePublisher: $viewLayoutFrame ) diffableDataSource = StatusSection.diffableDataSource( tableView: tableView, context: context, + authContext: authContext, configuration: configuration ) diff --git a/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel+Diffable.swift index dbada263..a1696efa 100644 --- a/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel+Diffable.swift @@ -20,16 +20,12 @@ extension ListStatusTimelineViewModel { let configuration = StatusSection.Configuration( statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, - statusViewConfigurationContext: StatusView.ConfigurationContext( - authContext: authContext, - dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - viewLayoutFramePublisher: $viewLayoutFrame - ) + viewLayoutFramePublisher: $viewLayoutFrame ) diffableDataSource = StatusSection.diffableDataSource( tableView: tableView, context: context, + authContext: authContext, configuration: configuration ) diff --git a/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel+Diffable.swift index 1a41362c..7201d994 100644 --- a/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel+Diffable.swift @@ -20,16 +20,12 @@ extension SearchTimelineViewModel { let configuration = StatusSection.Configuration( statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, - statusViewConfigurationContext: StatusView.ConfigurationContext( - authContext: authContext, - dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - viewLayoutFramePublisher: $viewLayoutFrame - ) + viewLayoutFramePublisher: $viewLayoutFrame ) diffableDataSource = StatusSection.diffableDataSource( tableView: tableView, context: context, + authContext: authContext, configuration: configuration ) diff --git a/TwidereX/Scene/Timeline/User/Status/UserTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/User/Status/UserTimelineViewModel+Diffable.swift index b3aab38f..bd938af0 100644 --- a/TwidereX/Scene/Timeline/User/Status/UserTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/User/Status/UserTimelineViewModel+Diffable.swift @@ -24,16 +24,12 @@ extension UserTimelineViewModel { let configuration = StatusSection.Configuration( statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, timelineMiddleLoaderTableViewCellDelegate: nil, - statusViewConfigurationContext: StatusView.ConfigurationContext( - authContext: authContext, - dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - viewLayoutFramePublisher: $viewLayoutFrame - ) + viewLayoutFramePublisher: $viewLayoutFrame ) diffableDataSource = StatusSection.diffableDataSource( tableView: tableView, context: context, + authContext: authContext, configuration: configuration ) From 7a3146ef37de14ba6735eb68e7a7041628d90e0e Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 15 Mar 2023 17:16:39 +0800 Subject: [PATCH 039/128] feat: add tweet quote entry --- .../text.quote.imageset/Contents.json | 15 + .../text.quote.imageset/blockquote.pdf | Bin 0 -> 6904 bytes .../TwidereAsset/Generated/Assets.swift | 1 + .../TwidereUI/Content/StatusToolbarView.swift | 149 +++++++--- .../Content/StatusView+ViewModel.swift | 12 +- .../TwidereUI/Content/StatusView.swift | 32 ++- .../ComposeContent/ComposeContentView.swift | 266 ++++++++++-------- .../ComposeContentViewController.swift | 6 +- .../ComposeContentViewModel.swift | 30 +- .../Publisher/TwitterStatusPublisher.swift | 19 +- .../API/V2/Twitter+API+V2+Status.swift | 13 +- .../Provider/DataSourceFacade+Status.swift | 23 +- 12 files changed, 371 insertions(+), 195 deletions(-) create mode 100644 TwidereSDK/Sources/TwidereAsset/Assets.xcassets/TextFormatting/text.quote.imageset/Contents.json create mode 100644 TwidereSDK/Sources/TwidereAsset/Assets.xcassets/TextFormatting/text.quote.imageset/blockquote.pdf diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/TextFormatting/text.quote.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/TextFormatting/text.quote.imageset/Contents.json new file mode 100644 index 00000000..9eea54a6 --- /dev/null +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/TextFormatting/text.quote.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "blockquote.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/TextFormatting/text.quote.imageset/blockquote.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/TextFormatting/text.quote.imageset/blockquote.pdf new file mode 100644 index 0000000000000000000000000000000000000000..3ac1c391953b2da2efbdc2885725798b201f767b GIT binary patch literal 6904 zcmeHM$!^?65WVv&`eML7nA7_L7zScF4uT|zBj;x1Fd5Odf?1N8ap2_Z{fcZBTO{St z$QJ<#4+8au-Sukesz>nA>zA*dsybAa5>o7b|DzJ(*)#F{dDU#M=-*10_~OTA`~LVr z7=V}b)Zw^qmYZtvvibXZ)m(n}LR`El|63iZzbaMB_7bwcdCGVEo9}-M{mp7q@nh4Q z!)iPHZ2C2u3R^p;b##J3IG73O{g&0nKX$j_tFu`@Po;O_&tLLy{@rUYN zwXJ?RyApWY)0Hse-$M;Gd-XtTwcy9)!B~;>BEd7!zolf(EQMlYHg~!hrnDbasEgB{ z589^klcvCJkb43nMN25K+!1&u&KcWdWCeTEGd|xA=ALAXcP#2+4&&re(>bdd6cxZN z>~sY%K{ov0Ld;=$n6+8Zn;xek+Z^EaY)x?@c)znNAaSY6I(emo5vL1)IiqMy3_6N3 z%nS|_-_|ZEskA7eytGsXrnb>rYYJrm*T_T|XFF1$Y@kU$fgd*+}NLEQlRrpWUY<$#*oz{lTV0=tUabJ-bla>m73^BDYaG!Edyyv zF%YGZ2VcX4{Ku<{_ZrE--bn`sBQ1#LrGf|O&{SfgMy8BJwGsyl_7<$hPTZAD7>c$C z73}6pz6GRGHp;i}TQTY&HC2nF`X+v3Kc;NRw-ZUin^NCa1~#)`Za`GxbF>RPyu2s| zhX!F14jd{#I)hro5Oh%|O0rHmCJO}TC8A0&S`dD%jFuw6Pbn>#C7+{7Isud^SR`@a z>}zAJ388?PM-dsuy2y+z6Jv{3hW_c%B8&;tNQ1EfCzQt$4-Q6(!NJ3X;-s;aQ-U#x zR;YB6K(yB0`L@tTo)XZKq#`qp5IR(*dbe-XU<=dXU;vPhKGDU zC37dfYri=wJc0``Fo*Geb0_&WD|x)i8G)d)K)@lQ=K7Tafg9HxlWrJ}$7?qXArMHa zv;u)fP%98ZFAy9JC6YLW2gMowVoYuy%j5CN0-=)}r9iNE5(w-)d0OzJ|%&1w#B^ zAha)cw`iFA3xrmf-Vg{Rn%ik;dVye12m}o8ED(?;rji8$CN4uP28yC#tUJ)|ys{Gr5TJJw2t`PUZYL1vBSroK(SNR>FB9Eo46nW6 zuNcs@!`F?)H{0#*co1)Y!`BX6i(miwvk{Be%lAzNerm3+moIjo#M|Meo(G?{z2F$` z`kgMD!|vmL)f@zUPr1z7UF^2UW($k-F{KAzFZaNN%TV#jg&_VEv-siq3RDGoa4C8_ z7rT#Gt0w#pK#HCGjh)EB0~;J1zgQlZ>)rdCOF!+KcNNO3>c%+z72ZM53c&fo8&rEI z@F^x=20ZCv$Qy^8j3B&!j38(6GIaAPuZM-l{qlO<>??emT73T!;dp%5ZJHyJ;WogFH`o6*VBdbDTJDdnJKjfKJ$m%*&#yiMLi|LO literal 0 HcmV?d00001 diff --git a/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift b/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift index 8e1e87b0..e677c34a 100644 --- a/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift +++ b/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift @@ -211,6 +211,7 @@ public enum Asset { public static let capitalFloatLeftLarge = ImageAsset(name: "TextFormatting/capital.float.left.large") public static let listBullet = ImageAsset(name: "TextFormatting/list.bullet") public static let textHeaderRedaction = ImageAsset(name: "TextFormatting/text.header.redaction") + public static let textQuote = ImageAsset(name: "TextFormatting/text.quote") public static let textQuoteMini = ImageAsset(name: "TextFormatting/text.quote.mini") } public enum Transportation { diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift index 5877af62..85213c04 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift @@ -7,6 +7,7 @@ import os.log import SwiftUI +import CoreDataStack public struct StatusToolbarView: View { @@ -18,63 +19,119 @@ public struct StatusToolbarView: View { public var body: some View { HStack { - ToolbarButton( - handler: { action in - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): reply") - handler(action) - }, - action: .reply, - image: Asset.Arrows.arrowTurnUpLeftMini.image.withRenderingMode(.alwaysTemplate), - count: viewModel.replyCount, - tintColor: nil - ) - ToolbarButton( - handler: { action in - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): repost") - handler(action) - }, - action: .repost, - image: Asset.Media.repeatMini.image.withRenderingMode(.alwaysTemplate), - count: viewModel.repostCount, - tintColor: viewModel.isReposted ? Asset.Scene.Status.Toolbar.repost.color : nil - ) - ToolbarButton( - handler: { action in - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): like") - handler(action) - }, - action: .like, - image: viewModel.isLiked ? Asset.Health.heartFillMini.image.withRenderingMode(.alwaysTemplate) : Asset.Health.heartMini.image.withRenderingMode(.alwaysTemplate), - count: viewModel.likeCount, - tintColor: viewModel.isLiked ? Asset.Scene.Status.Toolbar.like.color : nil - ) - // share + replyButton + Group { + switch viewModel.platform { + case .twitter: + repostMenu + case .mastodon: + repostButton + case .none: + repostButton + } + } + likeButton + shareMenu + } // end HStack + } // end body + +} + +extension StatusToolbarView { + public var replyButton: some View { + ToolbarButton( + handler: { action in + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): reply") + handler(action) + }, + action: .reply, + image: Asset.Arrows.arrowTurnUpLeftMini.image.withRenderingMode(.alwaysTemplate), + count: viewModel.replyCount, + tintColor: nil + ) + } + + public var repostButton: some View { + ToolbarButton( + handler: { action in + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): repost") + handler(action) + }, + action: .repost, + image: Asset.Media.repeatMini.image.withRenderingMode(.alwaysTemplate), + count: viewModel.repostCount, + tintColor: viewModel.isReposted ? Asset.Scene.Status.Toolbar.repost.color : nil + ) + } + + public var repostMenu: some View { + Menu { + // repost Button { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): share") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): repost") + handler(.repost) } label: { - HStack { - let image: UIImage = { -// switch viewModel.kind { -// case .conversationRoot: - return Asset.Editing.ellipsisMini.image.withRenderingMode(.alwaysTemplate) -// default: -// return Asset.Editing.ellipsisMini.image.withRenderingMode(.alwaysTemplate) -// } - }() - Image(uiImage: image) - .foregroundColor(.secondary) + Label { + Text(L10n.Common.Controls.Status.Actions.retweet) + } icon: { + Image(uiImage: Asset.Media.repeat.image.withRenderingMode(.alwaysTemplate)) } } - .buttonStyle(.borderless) - .modifier(MaxWidthModifier(max: nil)) - } // end HStack + // quote + Button { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): quote") + handler(.quote) + } label: { + Label { + Text(L10n.Common.Controls.Status.Actions.quote) + } icon: { + Image(uiImage: Asset.TextFormatting.textQuote.image.withRenderingMode(.alwaysTemplate)) + } + } + } label: { + repostButton + } + } + + public var likeButton: some View { + ToolbarButton( + handler: { action in + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): like") + handler(action) + }, + action: .like, + image: viewModel.isLiked ? Asset.Health.heartFillMini.image.withRenderingMode(.alwaysTemplate) : Asset.Health.heartMini.image.withRenderingMode(.alwaysTemplate), + count: viewModel.likeCount, + tintColor: viewModel.isLiked ? Asset.Scene.Status.Toolbar.like.color : nil + ) } + public var shareMenu: some View { + Button { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): share") + } label: { + HStack { + let image: UIImage = { + // switch viewModel.kind { + // case .conversationRoot: + return Asset.Editing.ellipsisMini.image.withRenderingMode(.alwaysTemplate) + // default: + // return Asset.Editing.ellipsisMini.image.withRenderingMode(.alwaysTemplate) + // } + }() + Image(uiImage: image) + .foregroundColor(.secondary) + } + } + .buttonStyle(.borderless) + .modifier(MaxWidthModifier(max: nil)) + } } extension StatusToolbarView { public class ViewModel: ObservableObject { // input + @Published var platform: Platform = .none @Published var replyCount: Int? @Published var repostCount: Int? @Published var likeCount: Int? diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index a4b104b2..d05c33db 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -168,6 +168,8 @@ extension StatusView { // toolbar public let toolbarViewModel = StatusToolbarView.ViewModel() + @Published public var isBottomConversationLinkLineViewDisplay = false + private init( status: StatusRecord?, authContext: AuthContext?, @@ -894,7 +896,8 @@ extension StatusView.ViewModel { case timeline case repost case quote - case reference + case referenceReplyTo + case referenceQuote case conversationRoot case conversationThread } @@ -965,6 +968,7 @@ extension StatusView.ViewModel { public convenience init( status: StatusObject, authContext: AuthContext?, + kind: Kind = .timeline, delegate: StatusViewDelegate?, viewLayoutFramePublisher: Published.Publisher? ) { @@ -973,7 +977,7 @@ extension StatusView.ViewModel { self.init( status: status, authContext: authContext, - kind: .timeline, + kind: kind, delegate: delegate, parentViewModel: nil, viewLayoutFramePublisher: viewLayoutFramePublisher @@ -982,7 +986,7 @@ extension StatusView.ViewModel { self.init( status: status, authContext: authContext, - kind: .timeline, + kind: kind, delegate: delegate, parentViewModel: nil, viewLayoutFramePublisher: viewLayoutFramePublisher @@ -1076,6 +1080,7 @@ extension StatusView.ViewModel { mediaViewModels = MediaView.ViewModel.viewModels(from: status) // toolbar + toolbarViewModel.platform = .twitter status.publisher(for: \.replyCount) .map { Int($0) } .assign(to: &toolbarViewModel.$replyCount) @@ -1211,6 +1216,7 @@ extension StatusView.ViewModel { .store(in: &disposeBag) // toolbar + toolbarViewModel.platform = .mastodon status.publisher(for: \.replyCount) .map { Int($0) } .assign(to: &toolbarViewModel.$replyCount) diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index 391ffe13..d80e4cff 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -84,7 +84,7 @@ public struct StatusView: View { avatarButton .padding(.trailing, StatusView.hangingAvatarButtonTrailingSapcing) } - VStack { + VStack(spacing: .zero) { // authorView authorView .padding(.horizontal, viewModel.margin) @@ -146,6 +146,21 @@ public struct StatusView: View { } // end HStack .padding(.top, viewModel.margin) .padding(.bottom, viewModel.hasToolbar ? .zero : viewModel.margin) + .overlay { + if viewModel.isBottomConversationLinkLineViewDisplay { + HStack(alignment: .top, spacing: .zero) { + VStack(spacing: 0) { + Color.clear + .frame(width: StatusView.hangingAvatarButtonDimension, height: StatusView.hangingAvatarButtonDimension) + Rectangle() + .foregroundColor(Color(uiColor: .separator)) + .background(.clear) + .frame(width: 1) + } + Spacer() + } // end HStack + } // end if + } // end overlay } } // end VStack .onReceive(viewModel.$isContentSensitiveToggled) { _ in @@ -157,12 +172,15 @@ public struct StatusView: View { extension StatusView { var contentWidth: CGFloat { - switch viewModel.kind { - case .conversationRoot: - return viewModel.viewLayoutFrame.readableContentLayoutFrame.width - 2 * viewModel.margin - default: - return viewModel.viewLayoutFrame.readableContentLayoutFrame.width - 2 * viewModel.margin - StatusView.hangingAvatarButtonDimension - StatusView.hangingAvatarButtonTrailingSapcing - } + let width: CGFloat = { + switch viewModel.kind { + case .conversationRoot: + return viewModel.viewLayoutFrame.readableContentLayoutFrame.width - 2 * viewModel.margin + default: + return viewModel.viewLayoutFrame.readableContentLayoutFrame.width - 2 * viewModel.margin - StatusView.hangingAvatarButtonDimension - StatusView.hangingAvatarButtonTrailingSapcing + } + }() + return max(width, .leastNonzeroMagnitude) } public var authorView: some View { diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift index 50b6d265..3eb4b20a 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift @@ -14,11 +14,12 @@ import TwidereCore public struct ComposeContentView: View { - static let contentMargin: CGFloat = 16 + static let contentVerticalMargin: CGFloat = 6 static let contentRowTopPadding: CGFloat = 8 static let contentMetaTextViewHStackSpacing: CGFloat = 10 static let avatarSize = CGSize(width: 44, height: 44) + @ObservedObject var viewModel: ComposeContentViewModel @State var mentionTextHeight: CGFloat = 0 @@ -29,6 +30,10 @@ public struct ComposeContentView: View { let index: Int } @FocusState var pollField: PollField? + + var readableContentLayoutMargin: CGFloat { + abs(viewModel.viewLayoutFrame.readableContentLayoutFrame.origin.x) + } public var body: some View { ScrollView(.vertical, showsIndicators: false) { @@ -39,7 +44,7 @@ public struct ComposeContentView: View { if let replyToStatusViewModel = viewModel.replyToStatusViewModel { StatusView(viewModel: replyToStatusViewModel) .frame(width: viewModel.viewLayoutFrame.readableContentLayoutFrame.width) - .padding(.top, 8) + .padding(.top, ComposeContentView.contentRowTopPadding) } default: EmptyView() @@ -47,135 +52,40 @@ public struct ComposeContentView: View { // content HStack(alignment: .top, spacing: ComposeContentView.contentMetaTextViewHStackSpacing) { // avatar - AvatarButtonRepresentable(configuration: .init(url: viewModel.author?.avatarURL)) - .frame(width: ComposeContentView.avatarSize.width, height: ComposeContentView.avatarSize.height) - .overlay(alignment: .top) { - // draw conversation link line - switch viewModel.kind { - case .reply: - Rectangle() - .foregroundColor(Color(uiColor: .separator)) - .background(.clear) - .frame(width: 1, height: ComposeContentView.contentRowTopPadding) - .offset(x: 0, y: -ComposeContentView.contentRowTopPadding) - default: - EmptyView() - } - } + authorButtonView VStack { // mention if viewModel.isMentionPickDisplay { - Button { - viewModel.mentionPickPublisher.send() - } label: { - HStack(spacing: .zero) { - VectorImageView( - image: Asset.Communication.textBubbleSmall.image.withRenderingMode(.alwaysTemplate), - tintColor: .tintColor - ) - .frame(width: mentionTextHeight, height: mentionTextHeight, alignment: .center) - Text(viewModel.mentionPickButtonTitle) - .font(.footnote) - .background(GeometryReader { geometry in - Color.clear.preference( - key: SizeDimensionPreferenceKey.self, - value: geometry.size.height - ) - }) - .onPreferenceChange(SizeDimensionPreferenceKey.self) { - mentionTextHeight = $0 - } - Spacer() - } - } - } // end if + mentionPickerView + } // content warning if viewModel.isContentWarningComposing { - let contentWarningIconSize = CGSize(width: 24, height: 24) - let contentWarningStackSpacing: CGFloat = 8 - VStack { - HStack(spacing: contentWarningStackSpacing) { - VectorImageView( - image: Asset.Indices.exclamationmarkOctagon.image.withRenderingMode(.alwaysTemplate), - tintColor: viewModel.isContentWarningEditing ? .tintColor : .secondaryLabel - ) - .frame(width: contentWarningIconSize.width, height: contentWarningIconSize.height) - MetaTextViewRepresentable( - string: $viewModel.contentWarning, - width: { - var textViewWidth = viewModel.viewSize.width - textViewWidth -= ComposeContentView.contentMargin * 2 - textViewWidth -= ComposeContentView.contentMetaTextViewHStackSpacing - textViewWidth -= ComposeContentView.avatarSize.width - textViewWidth -= contentWarningIconSize.width - textViewWidth -= contentWarningStackSpacing - return textViewWidth - }(), - configurationHandler: { metaText in - viewModel.contentWarningMetaText = metaText - metaText.textView.attributedPlaceholder = { - var attributes = metaText.textAttributes - attributes[.foregroundColor] = UIColor.secondaryLabel - return NSAttributedString( - string: L10n.Scene.Compose.cwPlaceholder, - attributes: attributes - ) - }() - metaText.textView.tag = ComposeContentViewModel.MetaTextViewKind.contentWarning.rawValue - metaText.textView.returnKeyType = .next - metaText.textView.delegate = viewModel - metaText.textView.setContentHuggingPriority(.required - 1, for: .vertical) - metaText.delegate = viewModel - } - ) - } - Divider() - .background(viewModel.isContentWarningEditing ? .accentColor : Color(uiColor: .separator)) - } // end VStack - } // end if viewModel.isContentWarningComposing - // contentTextEditor - MetaTextViewRepresentable( - string: $viewModel.content, - width: { - var textViewWidth = viewModel.viewSize.width - textViewWidth -= ComposeContentView.contentMargin * 2 - textViewWidth -= ComposeContentView.contentMetaTextViewHStackSpacing - textViewWidth -= ComposeContentView.avatarSize.width - return textViewWidth - }(), - configurationHandler: { metaText in - viewModel.contentMetaText = metaText - metaText.textView.attributedPlaceholder = { - var attributes = metaText.textAttributes - attributes[.foregroundColor] = UIColor.secondaryLabel - return NSAttributedString( - string: L10n.Scene.Compose.placeholder, - attributes: attributes - ) - }() - metaText.textView.tag = ComposeContentViewModel.MetaTextViewKind.content.rawValue - metaText.textView.keyboardType = .twitter - metaText.textView.delegate = viewModel - metaText.delegate = viewModel - metaText.textView.becomeFirstResponder() - } - ) - .frame(minHeight: ComposeContentView.avatarSize.height) + contentWarningView + } + // content editor + contentEditorView // poll pollView - } - } // end content + } // end VStack + } // end HStack (content) + .frame(width: viewModel.viewLayoutFrame.readableContentLayoutFrame.width) .padding(.top, ComposeContentView.contentRowTopPadding) - .padding(.horizontal, ComposeContentView.contentMargin) - // mediaAttachment mediaAttachmentView - .padding(ComposeContentView.contentMargin) - + .padding(.horizontal, readableContentLayoutMargin) + .padding(.vertical, ComposeContentView.contentVerticalMargin) + // quote + if let quoteStatusViewModel = viewModel.quoteStatusViewModel { + StatusView(viewModel: quoteStatusViewModel) + .padding(.horizontal, readableContentLayoutMargin) + .padding(.vertical, 8) + .background(Color.primary.opacity(0.04)) + .padding(.vertical, ComposeContentView.contentVerticalMargin) + } Spacer() } // end VStack } // end ScrollView - .frame(width: viewModel.viewSize.width) + .frame(width: viewModel.viewLayoutFrame.layoutFrame.width) .frame(maxHeight: .infinity) .padding(.bottom, toolbarHeight) .contentShape(Rectangle()) @@ -209,6 +119,126 @@ public struct ComposeContentView: View { } extension ComposeContentView { + // MARK: - author button + var authorButtonView: some View { + AvatarButtonRepresentable(configuration: .init(url: viewModel.author?.avatarURL)) + .frame(width: ComposeContentView.avatarSize.width, height: ComposeContentView.avatarSize.height) + .overlay(alignment: .top) { + // draw conversation link line + switch viewModel.kind { + case .reply: + Rectangle() + .foregroundColor(Color(uiColor: .separator)) + .background(.clear) + .frame(width: 1, height: ComposeContentView.contentRowTopPadding) + .offset(x: 0, y: -ComposeContentView.contentRowTopPadding) + default: + EmptyView() + } + } + } // end var + + // MARK: - mention picker + var mentionPickerView: some View { + Button { + viewModel.mentionPickPublisher.send() + } label: { + HStack(spacing: .zero) { + VectorImageView( + image: Asset.Communication.textBubbleSmall.image.withRenderingMode(.alwaysTemplate), + tintColor: .tintColor + ) + .frame(width: mentionTextHeight, height: mentionTextHeight, alignment: .center) + Text(viewModel.mentionPickButtonTitle) + .font(.footnote) + .background(GeometryReader { geometry in + Color.clear.preference( + key: SizeDimensionPreferenceKey.self, + value: geometry.size.height + ) + }) + .onPreferenceChange(SizeDimensionPreferenceKey.self) { + mentionTextHeight = $0 + } + Spacer() + } + } + } + + // MARK: - content warning + var contentWarningView: some View { + VStack { + let contentWarningIconSize = CGSize(width: 24, height: 24) + let contentWarningStackSpacing: CGFloat = 8 + HStack(spacing: contentWarningStackSpacing) { + VectorImageView( + image: Asset.Indices.exclamationmarkOctagon.image.withRenderingMode(.alwaysTemplate), + tintColor: viewModel.isContentWarningEditing ? .tintColor : .secondaryLabel + ) + .frame(width: contentWarningIconSize.width, height: contentWarningIconSize.height) + MetaTextViewRepresentable( + string: $viewModel.contentWarning, + width: { + var textViewWidth = viewModel.viewLayoutFrame.readableContentLayoutFrame.width + textViewWidth -= ComposeContentView.contentMetaTextViewHStackSpacing + textViewWidth -= ComposeContentView.avatarSize.width + textViewWidth -= contentWarningIconSize.width + textViewWidth -= contentWarningStackSpacing + return textViewWidth + }(), + configurationHandler: { metaText in + viewModel.contentWarningMetaText = metaText + metaText.textView.attributedPlaceholder = { + var attributes = metaText.textAttributes + attributes[.foregroundColor] = UIColor.secondaryLabel + return NSAttributedString( + string: L10n.Scene.Compose.cwPlaceholder, + attributes: attributes + ) + }() + metaText.textView.tag = ComposeContentViewModel.MetaTextViewKind.contentWarning.rawValue + metaText.textView.returnKeyType = .next + metaText.textView.delegate = viewModel + metaText.textView.setContentHuggingPriority(.required - 1, for: .vertical) + metaText.delegate = viewModel + } + ) + } + Divider() + .background(viewModel.isContentWarningEditing ? .accentColor : Color(uiColor: .separator)) + } // end VStack + } + + // MARK: - content editor + var contentEditorView: some View { + MetaTextViewRepresentable( + string: $viewModel.content, + width: { + var textViewWidth = viewModel.viewLayoutFrame.readableContentLayoutFrame.width + textViewWidth -= ComposeContentView.contentMetaTextViewHStackSpacing + textViewWidth -= ComposeContentView.avatarSize.width + return textViewWidth + }(), + configurationHandler: { metaText in + viewModel.contentMetaText = metaText + metaText.textView.attributedPlaceholder = { + var attributes = metaText.textAttributes + attributes[.foregroundColor] = UIColor.secondaryLabel + return NSAttributedString( + string: L10n.Scene.Compose.placeholder, + attributes: attributes + ) + }() + metaText.textView.tag = ComposeContentViewModel.MetaTextViewKind.content.rawValue + metaText.textView.keyboardType = .twitter + metaText.textView.delegate = viewModel + metaText.delegate = viewModel + metaText.textView.becomeFirstResponder() + } + ) + .frame(minHeight: ComposeContentView.avatarSize.height) + } + // MARK: - attachment var mediaAttachmentView: some View { Group { diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewController.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewController.swift index e95d49a1..9237acee 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewController.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -58,7 +58,7 @@ extension ComposeContentViewController { super.viewDidLoad() view.backgroundColor = .systemBackground - viewModel.viewSize = view.frame.size + viewModel.viewLayoutFrame.update(view: view) customEmojiPickerInputView.delegate = self viewModel.setupDiffableDataSource( @@ -181,10 +181,6 @@ extension ComposeContentViewController { super.viewDidLayoutSubviews() viewModel.viewLayoutFrame.update(view: view) - - if viewModel.viewSize != view.frame.size { - viewModel.viewSize = view.frame.size - } } public override func viewSafeAreaInsetsDidChange() { diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift index 10be840e..9d0b1609 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -32,7 +32,6 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { }() // MARK: - layout - @Published var viewSize: CGSize = .zero @Published public var viewLayoutFrame = ViewLayoutFrame() // input @@ -48,6 +47,10 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { // reply-to public private(set) var replyTo: StatusObject? @Published public private(set) var replyToStatusViewModel: StatusView.ViewModel? + + // quote + public private(set) var quote: StatusObject? + @Published private(set) public var quoteStatusViewModel: StatusView.ViewModel? // limit @Published public var maxTextInputLimit = 500 @@ -188,9 +191,11 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { replyToStatusViewModel = StatusView.ViewModel( status: status, authContext: nil, + kind: .referenceReplyTo, delegate: nil, viewLayoutFramePublisher: $viewLayoutFrame ) + replyToStatusViewModel?.isBottomConversationLinkLineViewDisplay = true switch status { case .twitter(let status): @@ -254,16 +259,22 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { } content = mentionAccts.joined(separator: " ") + " " } + + case .quote(let status): + quote = status + quoteStatusViewModel = StatusView.ViewModel( + status: status, + authContext: nil, + kind: .referenceQuote, + delegate: nil, + viewLayoutFramePublisher: $viewLayoutFrame + ) } initialContent = content // bind author -// $authContext -// .receive(on: DispatchQueue.main) -// .map { authContext in -// authContext?.authenticationContext.user(in: configurationContext.apiService.) -// } + author = authContext.authenticationContext.user(in: context.managedObjectContext) // bind text $content @@ -597,6 +608,7 @@ extension ComposeContentViewModel { case hashtag(hashtag: String) case mention(user: UserObject) case reply(status: StatusObject) + case quote(status: StatusObject) } public struct Settings { @@ -748,9 +760,13 @@ extension ComposeContentViewModel { author: author, replyTo: { guard case let .twitter(status) = replyTo else { return nil } - return .init(objectID: status.objectID) + return status.asRecrod }(), excludeReplyUserIDs: Array(excludeReplyTwitterUserIDs), + quote: { + guard case let .twitter(status) = quote else { return nil } + return status.asRecrod + }(), content: content, place: isRequestLocation ? currentPlace : nil, poll: { diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Publisher/TwitterStatusPublisher.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Publisher/TwitterStatusPublisher.swift index 0c0b1393..a8ab9493 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Publisher/TwitterStatusPublisher.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Publisher/TwitterStatusPublisher.swift @@ -20,9 +20,11 @@ public final class TwitterStatusPublisher: NSObject, ProgressReporting { // author public let author: TwitterUser - // refer + // refer reply-to public let replyTo: ManagedObjectRecord? public let excludeReplyUserIDs: [Twitter.Entity.V2.User.ID] + // refer quote + public let quote: ManagedObjectRecord? // status content public let content: String // location @@ -45,6 +47,7 @@ public final class TwitterStatusPublisher: NSObject, ProgressReporting { author: TwitterUser, replyTo: ManagedObjectRecord?, excludeReplyUserIDs: [Twitter.Entity.V2.User.ID], + quote: ManagedObjectRecord?, content: String, place: Twitter.Entity.Place?, poll: Twitter.API.V2.Status.Poll?, @@ -54,6 +57,7 @@ public final class TwitterStatusPublisher: NSObject, ProgressReporting { self.author = author self.replyTo = replyTo self.excludeReplyUserIDs = excludeReplyUserIDs + self.quote = quote self.content = content self.place = place self.poll = poll @@ -84,7 +88,7 @@ extension TwitterStatusPublisher: StatusPublisher { let managedObjectContext = api.backgroundManagedObjectContext - let _authenticationContext: TwitterAuthenticationContext? = await managedObjectContext.perform { + let _authenticationContext: TwitterAuthenticationContext? = await author.managedObjectContext?.perform { guard let authentication = self.author.twitterAuthentication else { return nil } return TwitterAuthenticationContext(authentication: authentication, secret: secret) } @@ -120,6 +124,7 @@ extension TwitterStatusPublisher: StatusPublisher { // Task: status let publishResponse = try await api.publishTwitterStatus( query: Twitter.API.V2.Status.PublishQuery( + forSuperFollowersOnly: nil, geo: { guard let place = self.place else { return nil } return .init(placeID: place.id) @@ -141,7 +146,15 @@ extension TwitterStatusPublisher: StatusPublisher { inReplyToTweetID: replyToID ) }(), - forSuperFollowersOnly: nil, + quoteTweetID: { + guard let quote = self.quote else { return nil } + let _quoteID: Twitter.Entity.V2.Tweet.ID? = await managedObjectContext.perform { + guard let quote = quote.object(in: managedObjectContext) else { return nil } + return quote.id + } + guard let quoteID = _quoteID else { return nil } + return quoteID + }(), replySettings: replySettings, text: content ), diff --git a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Status.swift b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Status.swift index 3b9302ed..72803391 100644 --- a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Status.swift +++ b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Status.swift @@ -36,39 +36,42 @@ extension Twitter.API.V2.Status { } public struct PublishQuery: JSONEncodeQuery { - + public let forSuperFollowersOnly: Bool? public let geo: Twitter.Entity.V2.Tweet.Geo? public let media: Media? public let poll: Poll? + public let quoteTweetID: Twitter.Entity.V2.Tweet.ID? public let reply: Reply? - public let forSuperFollowersOnly: Bool? public let replySettings: Twitter.Entity.V2.Tweet.ReplySettings? public let text: String? enum CodingKeys: String, CodingKey { + case forSuperFollowersOnly = "for_super_followers_only" case geo case media case poll case reply - case forSuperFollowersOnly = "for_super_followers_only" + case quoteTweetID = "quote_tweet_id" case replySettings = "reply_settings" case text } public init( + forSuperFollowersOnly: Bool?, geo: Twitter.Entity.V2.Tweet.Geo?, media: Media?, poll: Poll?, reply: Reply?, - forSuperFollowersOnly: Bool?, + quoteTweetID: Twitter.Entity.V2.Tweet.ID?, replySettings: Twitter.Entity.V2.Tweet.ReplySettings?, text: String? ) { + self.forSuperFollowersOnly = forSuperFollowersOnly self.geo = geo self.media = media self.poll = poll self.reply = reply - self.forSuperFollowersOnly = forSuperFollowersOnly + self.quoteTweetID = quoteTweetID self.text = text self.replySettings = { switch replySettings { diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift index 6f3dfeac..c93db38b 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift @@ -68,7 +68,28 @@ extension DataSourceFacade { } case .quote: - assertionFailure() + guard let status = status.object(in: provider.context.managedObjectContext) else { + assertionFailure() + return + } + + let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) + impactFeedbackGenerator.impactOccurred() + + let composeViewModel = ComposeViewModel(context: provider.context) + let composeContentViewModel = ComposeContentViewModel( + context: provider.context, + authContext: provider.authContext, + kind: .quote(status: status) + ) + provider.coordinator.present( + scene: .compose( + viewModel: composeViewModel, + contentViewModel: composeContentViewModel + ), + from: provider, + transition: .modal(animated: true, completion: nil) + ) case .like: do { try await DataSourceFacade.responseToStatusLikeAction( From eeb4538e3abff0fa142de6be1d47c8360e49b994 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 15 Mar 2023 18:45:13 +0800 Subject: [PATCH 040/128] fix: menu button position may change when keyboard appear issue. And update the menu style to picker style --- .../Toolbar/ComposeContentToolbarView.swift | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.swift index 50cf4d80..43d18ca6 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.swift @@ -130,16 +130,16 @@ public struct ComposeContentToolbarView: View { var twitterReplySettingsMenuButton: some View { Menu { - ForEach(Twitter.Entity.V2.Tweet.ReplySettings.allCases, id: \.self) { replySetting in - Button { - viewModel.twitterReplySettings = replySetting - } label: { + Picker(selection: $viewModel.twitterReplySettings) { + ForEach(Twitter.Entity.V2.Tweet.ReplySettings.allCases, id: \.self) { replySetting in Label { Text(replySetting.title) } icon: { Image(uiImage: replySetting.image) } } + } label: { + Text(viewModel.twitterReplySettings.title) } } label: { HStack { @@ -154,8 +154,7 @@ public struct ComposeContentToolbarView: View { } .padding(.horizontal, 12) .contentShape(Rectangle()) - // do not padding vertical - // otherwise the poll, at, hashtag button tap area will be clipped + .ignoresSafeArea(.all, edges: .all) // fix label position jumping issue } } @@ -167,16 +166,16 @@ public struct ComposeContentToolbarView: View { .private, .direct, ] - ForEach(visibilities, id: \.self) { visibility in - Button { - viewModel.mastodonVisibility = visibility - } label: { + Picker(selection: $viewModel.mastodonVisibility) { + ForEach(visibilities, id: \.self) { visibility in Label { Text(visibility.title) } icon: { Image(uiImage: visibility.image) } } + } label: { + Text(viewModel.mastodonVisibility.title) } } label: { HStack { @@ -191,8 +190,7 @@ public struct ComposeContentToolbarView: View { } .padding(.horizontal, 12) .contentShape(Rectangle()) - // do not padding vertical - // otherwise the poll, at, hashtag button tap area will be clipped + .ignoresSafeArea(.all, edges: .all) // fix label position jumping issue } } From bf8b33397f2f89b778ce0aa26fecedfc7ee3a223 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 15 Mar 2023 19:37:28 +0800 Subject: [PATCH 041/128] fix: toggle spoiler with poor animation issue --- TwidereSDK/Sources/TwidereUI/Content/StatusView.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index d80e4cff..c3836260 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -93,6 +93,10 @@ public struct StatusView: View { .padding(.horizontal, viewModel.margin) if !viewModel.isContentEmpty { Button { + // force to trigger view update without animation + withAnimation(.none) { + viewModel.isContentSensitiveToggled.toggle() + } viewModel.delegate?.statusView(viewModel, toggleContentDisplay: !viewModel.isContentReveal) } label: { HStack { @@ -164,6 +168,7 @@ public struct StatusView: View { } } // end VStack .onReceive(viewModel.$isContentSensitiveToggled) { _ in + // trigger tableView reload to update the cell height viewModel.delegate?.statusView(viewModel, viewHeightDidChange: Void()) } } From 02890390ea58b58f2293c30854ee75d47b7aeb56 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 16 Mar 2023 16:44:55 +0800 Subject: [PATCH 042/128] feat: add status menu --- .../TwidereUI/Content/StatusToolbarView.swift | 35 +++++- .../Content/StatusView+ViewModel.swift | 114 +++++++++++------- .../TwidereUI/Content/StatusView.swift | 38 +++++- TwidereX/Diffable/Status/StatusSection.swift | 1 + .../Provider/DataSourceFacade+Status.swift | 25 +++- ...ider+StatusViewTableViewCellDelegate.swift | 29 +---- 6 files changed, 163 insertions(+), 79 deletions(-) diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift index 85213c04..d9a78c37 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift @@ -15,7 +15,8 @@ public struct StatusToolbarView: View { var logger: Logger { StatusView.logger } @ObservedObject public var viewModel: ViewModel - let handler: (Action) -> Void + public var menuActions: [Action] + public let handler: (Action) -> Void public var body: some View { HStack { @@ -109,6 +110,18 @@ extension StatusToolbarView { public var shareMenu: some View { Button { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): share") + ForEach(menuActions, id: \.self) { action in + Button { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(String(describing: action))") + handler(action) + } label: { + Label { + + } icon: { + + } + } // end Button + } // end ForEach } label: { HStack { let image: UIImage = { @@ -151,7 +164,25 @@ extension StatusToolbarView { case repost case quote case like - case share + case copyText + case copyLink + case shareLink + case saveMedia + case translate + + public var text: String { + switch self { + case .reply: return "" + case .repost: return "" + case .quote: return "" + case .like: return "" + case .copyText: return "" + case .copyLink: return "" + case .shareLink: return "" + case .saveMedia: return "" + case .translate: return "" + } + } } } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index d05c33db..7317d9f5 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -52,6 +52,7 @@ extension StatusView { @Published public var avatarURL: URL? @Published public var authorName: MetaContent = PlaintextMetaContent(string: "") @Published public var authorUsernme = "" + @Published public var authorUserIdentifier: UserIdentifier? // static let pollOptionOrdinalNumberFormatter: NumberFormatter = { // let formatter = NumberFormatter() @@ -75,9 +76,7 @@ extension StatusView { // @Published public var authorUsername: String? // // @Published public var protected: Bool = false -// -// @Published public var isMyself = false -// + @Published public var spoilerContent: MetaContent? @Published public var content: MetaContent = PlaintextMetaContent(string: "") @@ -88,11 +87,32 @@ extension StatusView { return isContentSensitive ? isContentSensitiveToggled : !isContentEmpty } -// @Published public var twitterTextProvider: TwitterTextProvider? -// -// @Published public var language: String? -// @Published public var isTranslateButtonDisplay = false -// + @Published public var language: String? + @Published public private(set) var translateButtonPreference: UserDefaults.TranslateButtonPreference? + public var isTranslateButtonDisplay: Bool { + // only display for conversation root + switch kind { + case .conversationRoot: break + default: return false + } + // check prefernece and compare device language + switch translateButtonPreference { + case .auto: + guard let language = language, !language.isEmpty else { + // default hidden + return false + } + let contentLocale = Locale(identifier: language) + guard let currentLanguageCode = Locale.current.language.languageCode?.identifier, + let contentLanguageCode = contentLocale.language.languageCode?.identifier + else { return true } + return currentLanguageCode != contentLanguageCode + case .always: return true + case .off: return false + case nil: return false + } + } + @Published public var mediaViewModels: [MediaView.ViewModel] = [] @Published public var isMediaSensitive: Bool = false @Published public var isMediaSensitiveToggled: Bool = false @@ -127,9 +147,6 @@ extension StatusView { // // @Published public var isRepost = false // @Published public var isRepostEnabled = true -// -// @Published public var isLike = false - @Published public var visibility: MastodonVisibility? var visibilityIconImage: UIImage? { @@ -150,23 +167,21 @@ extension StatusView { } } // @Published public var replySettings: Twitter.Entity.V2.Tweet.ReplySettings? -// -// @Published public var dateTimeProvider: DateTimeProvider? -// @Published public var timestamp: Date? -// @Published public var timeAgoStyleTimestamp: String? -// @Published public var formattedStyleTimestamp: String? -// -// @Published public var sharePlaintextContent: String? -// @Published public var shareStatusURL: String? -// -// @Published public var isDeletable = false -// + +////// // @Published public var groupedAccessibilityLabel = "" @Published public var timestampLabelViewModel: TimestampLabelView.ViewModel? // toolbar public let toolbarViewModel = StatusToolbarView.ViewModel() + // toolbar - share menu context + @Published public var statusLink: URL? + public var canDelete: Bool { + guard let authContext = self.authContext else { return false } + guard let authorUserIdentifier = self.authorUserIdentifier else { return false } + return authContext.authenticationContext.userIdentifier == authorUserIdentifier + } @Published public var isBottomConversationLinkLineViewDisplay = false @@ -255,28 +270,10 @@ extension StatusView { // } // } // .assign(to: &$isRepostEnabled) -// -// Publishers.CombineLatest( -// UserDefaults.shared.publisher(for: \.translateButtonPreference), -// $language -// ) -// .map { preference, language -> Bool in -// switch preference { -// case .auto: -// guard let language = language, !language.isEmpty else { -// // default hidden -// return false -// } -// let contentLocale = Locale(identifier: language) -// guard let currentLanguageCode = Locale.current.languageCode, -// let contentLanguageCode = contentLocale.languageCode -// else { return true } -// return currentLanguageCode != contentLanguageCode -// case .always: return true -// case .off: return false -// } -// } -// .assign(to: &$isTranslateButtonDisplay) + + UserDefaults.shared.publisher(for: \.translateButtonPreference) + .map { $0 } + .assign(to: &$translateButtonPreference) } } } @@ -913,6 +910,15 @@ extension StatusView.ViewModel { } } + public var cellTopMargin: CGFloat { + switch kind { + case .timeline: + return repostViewModel == nil ? 12 : 8 + default: + return .zero + } + } + public var margin: CGFloat { switch kind { case .quote: @@ -1057,6 +1063,7 @@ extension StatusView.ViewModel { .assign(to: &$authorName) status.author.publisher(for: \.username) .assign(to: &$authorUsernme) + authorUserIdentifier = .twitter(.init(id: status.author.id)) // timestamp switch kind { @@ -1076,6 +1083,18 @@ extension StatusView.ViewModel { ) self.content = metaContent + // language + status.publisher(for: \.language) + .map { language in + switch language { + case "qam", "qct", "qht", "qme", "qst", "zxx": + return nil + default: + return language + } + } + .assign(to: &$language) + // media mediaViewModels = MediaView.ViewModel.viewModels(from: status) @@ -1106,6 +1125,7 @@ extension StatusView.ViewModel { } else { // do nothing } + statusLink = status.statusURL } // end init } @@ -1163,6 +1183,7 @@ extension StatusView.ViewModel { status.author.publisher(for: \.username) .map { _ in status.author.acct } .assign(to: &$authorUsernme) + authorUserIdentifier = .mastodon(.init(domain: status.author.domain, id: status.author.id)) // visibility visibility = status.visibility @@ -1196,6 +1217,10 @@ extension StatusView.ViewModel { assertionFailure(error.localizedDescription) self.content = PlaintextMetaContent(string: "") } + + // language + status.publisher(for: \.language) + .assign(to: &$language) // content warning isContentSensitiveToggled = status.isContentSensitiveToggled @@ -1242,5 +1267,6 @@ extension StatusView.ViewModel { } else { // do nothing } + statusLink = URL(string: status.url ?? status.uri) } } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index c3836260..3708188e 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -84,7 +84,8 @@ public struct StatusView: View { avatarButton .padding(.trailing, StatusView.hangingAvatarButtonTrailingSapcing) } - VStack(spacing: .zero) { + let contentSpacing: CGFloat = 0 + VStack(spacing: contentSpacing) { // authorView authorView .padding(.horizontal, viewModel.margin) @@ -145,11 +146,20 @@ public struct StatusView: View { } if viewModel.hasToolbar { toolbarView + .padding(.top, -contentSpacing) } } // end VStack + .overlay(alignment: .bottom) { + VStack(spacing: .zero) { + Spacer() + Divider() + Color.clear + .frame(height: 1) + } + } } // end HStack - .padding(.top, viewModel.margin) - .padding(.bottom, viewModel.hasToolbar ? .zero : viewModel.margin) + .padding(.top, viewModel.margin) // container margin + .padding(.bottom, viewModel.hasToolbar ? .zero : viewModel.margin) // container margin .overlay { if viewModel.isBottomConversationLinkLineViewDisplay { HStack(alignment: .top, spacing: .zero) { @@ -165,8 +175,9 @@ public struct StatusView: View { } // end HStack } // end if } // end overlay - } + } // end if … else … } // end VStack + .padding(.top, viewModel.cellTopMargin) .onReceive(viewModel.$isContentSensitiveToggled) { _ in // trigger tableView reload to update the cell height viewModel.delegate?.statusView(viewModel, viewHeightDidChange: Void()) @@ -222,6 +233,7 @@ extension StatusView { } ) } + .frame(alignment: .leading) Spacer() HStack(spacing: 6) { // mastodon visibility @@ -310,6 +322,24 @@ extension StatusView { var toolbarView: some View { StatusToolbarView( viewModel: viewModel.toolbarViewModel, + menuActions: { + var actions: [StatusToolbarView.Action] = [] + // copyText + actions.append(.copyText) + // copyLink, shareLink + if viewModel.statusLink != nil { + actions.append(.copyLink) + actions.append(.shareLink) + } + // save media + if !viewModel.mediaViewModels.isEmpty { + actions.append(.copyText) + } + // + actions.append(.copyText) + actions.append(.copyText) + return actions + }(), handler: { action in viewModel.delegate?.statusView(viewModel, statusToolbarButtonDidPressed: action) } diff --git a/TwidereX/Diffable/Status/StatusSection.swift b/TwidereX/Diffable/Status/StatusSection.swift index 3bff3801..219c5fc6 100644 --- a/TwidereX/Diffable/Status/StatusSection.swift +++ b/TwidereX/Diffable/Status/StatusSection.swift @@ -64,6 +64,7 @@ extension StatusSection { cell.contentConfiguration = UIHostingConfiguration { StatusView(viewModel: viewModel) } + .margins(.vertical, 0) // remove vertical margins } return cell case .feedLoader(let record): diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift index c93db38b..46b4c824 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift @@ -17,6 +17,7 @@ extension DataSourceFacade { @MainActor static func responseToStatusToolbar( provider: DataSourceProvider & AuthContextProvider, + viewModel: StatusView.ViewModel, status: StatusRecord, action: StatusToolbarView.Action ) async { @@ -103,7 +104,29 @@ extension DataSourceFacade { } catch { provider.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): update like failure: \(error.localizedDescription)") } - case .share: + case .copyText: + let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) + impactFeedbackGenerator.impactOccurred() + + let plaintext = viewModel.content.string + UIPasteboard.general.string = plaintext + case .copyLink: + let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) + impactFeedbackGenerator.impactOccurred() + + guard let link = viewModel.statusLink?.absoluteString else { return } + UIPasteboard.general.string = link + case .shareLink: + let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) + impactFeedbackGenerator.impactOccurred() + + guard let link = viewModel.statusLink?.absoluteString else { return } + UIPasteboard.general.string = link + case .saveMedia: + break + case .translate: + break + // media menu button trigger this let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) impactFeedbackGenerator.impactOccurred() diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift index 29b07a48..3457a957 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift @@ -335,40 +335,13 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC } await DataSourceFacade.responseToStatusToolbar( provider: self, + viewModel: viewModel, status: status, action: action ) } // end Task } -// func tableViewCell( -// _ cell: UITableViewCell, -// statusView: StatusView, -// statusToolbar: StatusToolbar, -// actionDidPressed action: StatusToolbar.Action, -// button: UIButton -// ) { -// Task { -// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) -// guard let item = await item(from: source) else { -// assertionFailure() -// return -// } -// guard let status = await item.status(in: self.context.managedObjectContext) else { -// assertionFailure("only works for status data provider") -// return -// } -// -// await DataSourceFacade.responseToStatusToolbar( -// provider: self, -// status: status, -// action: action, -// sender: button, -// authenticationContext: self.authContext.authenticationContext -// ) -// } // end Task -// } // end func - // func tableViewCell( // _ cell: UITableViewCell, // statusView: StatusView, From ee714ff93d14e20b6aa32480b3f7dff97130994c Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 16 Mar 2023 18:43:23 +0800 Subject: [PATCH 043/128] chore: remove AppShared and cleanup framework import --- AppShared/AppShared.h | 19 - AppShared/Info.plist | 22 - AppShared/Vender/DateTimeSwiftProvider.swift | 19 - .../Vender/OfficialTwitterTextProvider.swift | 48 -- NotificationService/NotificationService.swift | 1 - Podfile | 22 +- Podfile.lock | 20 +- ShareExtension/ComposeViewController.swift | 4 +- ShareExtension/ComposeViewModel.swift | 1 - .../xcschemes/TwidereSDK.xcscheme | 67 +++ TwidereSDK/Package.swift | 10 + .../Preference/Preference+Appearance.swift | 1 - .../Model/Status/StatusRecord.swift | 1 - .../APIService+Authentication.swift | 1 - .../APIService/APIService+Notification.swift | 1 - ...donEmojiService+EmojiViewModel+State.swift | 1 - ...cationService+NotificationSubscriber.swift | 1 - .../Service/PhotoLibraryService.swift | 1 - .../Banner/NotificationBannerView.swift | 1 - .../Content/StatusMetricsDashboardView.swift | 1 - .../Content/StatusView+Configuration.swift | 1 - .../Content/StatusView+ViewModel.swift | 1 - .../TwidereUI/Content/StatusView.swift | 1 - .../Content/UserView+Configuration.swift | 1 - .../Content/UserView+ViewModel.swift | 1 - .../Attachment/AttachmentViewModel.swift | 1 - .../Publisher/MastodonStatusPublisher.swift | 1 - .../Publisher/TwitterStatusPublisher.swift | 1 - .../MentionPickViewController.swift | 1 - .../User/UserViewTableViewCellDelegate.swift | 1 - TwidereX.xcodeproj/project.pbxproj | 413 ++---------------- .../xcshareddata/swiftpm/Package.resolved | 12 +- TwidereX/Coordinator/SceneCoordinator.swift | 1 - .../CoverFlowStackSection.swift | 1 + .../Misc/History/HistorySection.swift | 2 - TwidereX/Diffable/Misc/List/ListSection.swift | 1 - .../Notification/NotificationSection.swift | 2 - .../Diffable/Misc/TabBar/TabBarItem.swift | 1 - TwidereX/Diffable/Status/StatusSection.swift | 2 - TwidereX/Diffable/User/UserItem.swift | 1 - TwidereX/Diffable/User/UserSection.swift | 1 - TwidereX/Extension/AVPlayer.swift | 1 + TwidereX/Extension/UILabel.swift | 1 + TwidereX/Extension/UIViewController.swift | 1 + TwidereX/Generated/AppIconAssets.swift | 47 ++ .../DataSourceFacade+Friendship.swift | 1 - .../Provider/DataSourceFacade+Media.swift | 1 - .../Provider/DataSourceFacade+Poll.swift | 2 +- .../Provider/DataSourceFacade+Profile.swift | 2 +- .../Provider/DataSourceFacade+Report.swift | 1 - .../Provider/DataSourceFacade+Status.swift | 3 - .../Provider/DataSourceFacade+Translate.swift | 1 - ...der+MediaInfoDescriptionViewDelegate.swift | 4 - ...ider+StatusViewTableViewCellDelegate.swift | 2 - ...taSourceProvider+UITableViewDelegate.swift | 1 - ...ovider+UserViewTableViewCellDelegate.swift | 1 - .../List/AccountListViewController.swift | 1 - .../List/AccountListViewModel+Diffable.swift | 1 + .../Account/List/AccountListViewModel.swift | 1 + .../List/View/AccountListTableViewCell.swift | 2 +- .../TwitterAccountUnlockViewController.swift | 1 + .../Scene/Compose/ComposeViewController.swift | 1 - TwidereX/Scene/Compose/ComposeViewModel.swift | 2 +- .../Status/StatusHistoryViewController.swift | 1 - .../StatusHistoryViewModel+Diffable.swift | 1 - .../Status/StatusHistoryViewModel.swift | 1 - .../User/UserHistoryViewController.swift | 1 - .../User/UserHistoryViewModel+Diffable.swift | 1 - .../History/User/UserHistoryViewModel.swift | 1 - .../CompositeListViewController.swift | 1 - .../Scene/List/EditList/EditListView.swift | 1 + .../ListUser/ListUserViewController.swift | 1 - .../ListUser/ListUserViewModel+Diffable.swift | 1 - .../MediaPreviewViewController.swift | 1 - .../MediaPreview/MediaPreviewViewModel.swift | 1 - .../MediaInfoDescriptionView+ViewModel.swift | 2 - .../View/MediaInfoDescriptionView.swift | 1 - .../NotificationTimelineViewController.swift | 1 - ...tificationTimelineViewModel+Diffable.swift | 1 - .../NotificationTimelineViewModel.swift | 1 - .../MastodonAuthenticationController.swift | 2 - ...erAuthenticationOptionViewController.swift | 2 - ...TwitterAuthenticationOptionViewModel.swift | 2 - .../TwitterAuthenticationController.swift | 3 - .../Welcome/WelcomeViewController.swift | 1 - .../Onboarding/Welcome/WelcomeViewModel.swift | 2 - .../FollowingListViewController.swift | 2 +- .../View/ProfileHeaderView+ViewModel.swift | 8 +- .../Header/View/ProfileHeaderView.swift | 1 - .../Scene/Profile/ProfileViewController.swift | 2 - .../Drawer/DrawerSidebarViewController.swift | 1 - .../DrawerSidebarHeaderView+ViewModel.swift | 1 - .../Drawer/View/DrawerSidebarHeaderView.swift | 1 - .../Root/MainTab/MainTabBarController.swift | 2 - TwidereX/Scene/Root/Sidebar/SidebarView.swift | 1 - .../Root/Sidebar/SidebarViewController.swift | 1 - .../Search/Search/SearchViewController.swift | 1 - .../Hashtag/Cell/HashtagTableViewCell.swift | 1 + .../User/SearchUserViewModel+Diffable.swift | 1 - .../User/SearchUserViewModel.swift | 1 - .../TrendPlace/TrendPlaceViewController.swift | 1 + TwidereX/Scene/Setting/About/AboutView.swift | 1 - .../AccountPreferenceView.swift | 1 - .../AppIconPreferenceView.swift | 2 +- .../BehaviorsPreferenceView.swift | 1 - .../BehaviorsPreferenceViewModel.swift | 1 - .../DisplayPreferenceView.swift | 4 - .../DisplayPreferenceViewModel.swift | 4 - .../TranslateButtonPreferenceView.swift | 1 - .../TranslationServicePreferenceView.swift | 1 - .../Scene/Setting/List/SettingListView.swift | 1 - .../AvatarBarButtonItem+ViewModel.swift | 1 - .../View/Button/AvatarBarButtonItem.swift | 1 - .../View/Button/FollowActionButton.swift | 1 + .../StatusMediaGalleryCollectionCell.swift | 1 - .../StatusTableViewCell+ViewModel.swift | 2 - .../TableViewCell/StatusTableViewCell.swift | 1 - ...tusThreadRootTableViewCell+ViewModel.swift | 3 - .../StatusThreadRootTableViewCell.swift | 1 - .../StatusViewTableViewCellDelegate.swift | 2 +- .../TimelineLoaderTableViewCell.swift | 1 + .../StatusThreadViewModel+Diffable.swift | 1 - .../StatusThread/StatusThreadViewModel.swift | 1 - .../Base/Common/TimelineViewController.swift | 3 - .../List/ListTimelineViewController.swift | 2 - .../Base/List/ListTimelineViewModel.swift | 1 - .../FederatedTimelineViewModel+Diffable.swift | 2 - .../HashtagTimelineViewModel+Diffable.swift | 2 - ...meTimelineViewController+DebugAction.swift | 2 - .../Home/HomeTimelineViewController.swift | 1 - .../Home/HomeTimelineViewModel+Diffable.swift | 1 - ...ListStatusTimelineViewModel+Diffable.swift | 2 - ...earchMediaTimelineViewModel+Diffable.swift | 1 + .../SearchTimelineViewModel+Diffable.swift | 2 - .../Status/SearchTimelineViewModel.swift | 1 - .../UserTimelineViewModel+Diffable.swift | 2 - .../DrawerSidebarAnimatedTransitioning.swift | 1 - .../MediaPreviewTransitionItem.swift | 1 - TwidereX/Supporting Files/AppDelegate.swift | 2 - TwidereX/Supporting Files/SceneDelegate.swift | 2 - .../Handler/PublishPostIntentHandler.swift | 1 - .../Handler/SwitchAccountIntentHandler.swift | 1 - TwidereXIntent/es.lproj/Intents.strings | 6 +- 143 files changed, 188 insertions(+), 703 deletions(-) delete mode 100644 AppShared/AppShared.h delete mode 100644 AppShared/Info.plist delete mode 100644 AppShared/Vender/DateTimeSwiftProvider.swift delete mode 100644 AppShared/Vender/OfficialTwitterTextProvider.swift create mode 100644 TwidereSDK/.swiftpm/xcode/xcshareddata/xcschemes/TwidereSDK.xcscheme diff --git a/AppShared/AppShared.h b/AppShared/AppShared.h deleted file mode 100644 index a78d8be5..00000000 --- a/AppShared/AppShared.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// AppShared.h -// AppShared -// -// Created by Cirno MainasuK on 2021-8-13. -// Copyright © 2021 Twidere. All rights reserved. -// - -#import - -//! Project version number for AppShared. -FOUNDATION_EXPORT double AppSharedVersionNumber; - -//! Project version string for AppShared. -FOUNDATION_EXPORT const unsigned char AppSharedVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/AppShared/Info.plist b/AppShared/Info.plist deleted file mode 100644 index 3fd95d2a..00000000 --- a/AppShared/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.4.2 - CFBundleVersion - 111 - - diff --git a/AppShared/Vender/DateTimeSwiftProvider.swift b/AppShared/Vender/DateTimeSwiftProvider.swift deleted file mode 100644 index 72a22e3e..00000000 --- a/AppShared/Vender/DateTimeSwiftProvider.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// DateTimeSwiftProvider.swift -// TwidereX -// -// Created by MainasuK on 2021/11/22. -// Copyright © 2021 Twidere. All rights reserved. -// - -import Foundation -import TwidereCore -import DateToolsSwift - -public class DateTimeSwiftProvider: DateTimeProvider { - public func shortTimeAgoSinceNow(to date: Date?) -> String? { - return date?.shortTimeAgoSinceNow - } - - public init() { } -} diff --git a/AppShared/Vender/OfficialTwitterTextProvider.swift b/AppShared/Vender/OfficialTwitterTextProvider.swift deleted file mode 100644 index fa004abd..00000000 --- a/AppShared/Vender/OfficialTwitterTextProvider.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// OfficialTwitterTextProvider.swift -// OfficialTwitterTextProvider -// -// Created by Cirno MainasuK on 2021-9-6. -// Copyright © 2021 Twidere. All rights reserved. -// - -import Foundation -import Meta -import TwitterMeta -import twitter_text - -public class OfficialTwitterTextProvider: TwitterTextProvider { - - public static let parser = TwitterTextParser.defaultParser() - - public func parse(text: String) -> ParseResult { - let result = OfficialTwitterTextProvider.parser.parseTweet(text) - - return ParseResult( - isValid: result.isValid, - weightedLength: result.weightedLength, - maxWeightedLength: OfficialTwitterTextProvider.parser.maxWeightedTweetLength(), - entities: self.entities(in: text) - ) - } - - - public func entities(in text: String) -> [TwitterTextProviderEntity] { - return TwitterText.entities(inText: text).compactMap { entity in - switch entity.type { - case .URL: return .url(range: entity.range) - case .screenName: return .screenName(range: entity.range) - case .hashtag: return .hashtag(range: entity.range) - case .listName: return .listName(range: entity.range) - case .symbol: return .symbol(range: entity.range) - case .tweetChar: return .tweetChar(range: entity.range) - case .tweetEmojiChar: return .tweetEmojiChar(range: entity.range) - @unknown default: - assertionFailure() - return nil - } - } - } - - public init() { } -} diff --git a/NotificationService/NotificationService.swift b/NotificationService/NotificationService.swift index 5fce6ce7..b53ad8ec 100644 --- a/NotificationService/NotificationService.swift +++ b/NotificationService/NotificationService.swift @@ -9,7 +9,6 @@ import os.log import UIKit import UserNotifications -import AppShared import TwidereCommon import TwidereCore import AlamofireImage diff --git a/Podfile b/Podfile index 33e4b4a4..a8b1f03e 100644 --- a/Podfile +++ b/Podfile @@ -1,19 +1,9 @@ source 'https://cdn.cocoapods.org/' platform :ios, '15.0' -def common_pods - # Misc - pod 'DateToolsSwift', '~> 5.0.0' - # Twitter - pod 'twitter-text', '~> 3.1.0' -end - target 'TwidereX' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! - - # Pods for TwidereX - common_pods ## UI pod 'XLPagerTabStrip', '~> 9.0.0' @@ -25,12 +15,8 @@ target 'TwidereX' do pod 'FirebaseMessaging' # misc - pod 'SwiftGen', '~> 6.5.1' + pod 'SwiftGen', '~> 6.6.2' pod 'Sourcery', '~> 1.8.1' - - # Debug - # pod 'FLEX', '~> 4.7.0', :configurations => ['Debug'] - pod 'ZIPFoundation', '~> 0.9.11', :configurations => ['Debug'] target 'TwidereXTests' do inherit! :search_paths @@ -43,12 +29,6 @@ target 'TwidereX' do end -target 'AppShared' do - # Comment the next line if you don't want to use dynamic frameworks - use_frameworks! - common_pods -end - post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| diff --git a/Podfile.lock b/Podfile.lock index 56bfd2fa..b1d2a85d 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,5 +1,4 @@ PODS: - - DateToolsSwift (5.0.0) - Firebase/AnalyticsWithoutAdIdSupport (9.2.0): - Firebase/CoreOnly - FirebaseAnalytics/WithoutAdIdSupport (~> 9.2.0) @@ -103,26 +102,20 @@ PODS: - Sourcery (1.8.1): - Sourcery/CLI-Only (= 1.8.1) - Sourcery/CLI-Only (1.8.1) - - SwiftGen (6.5.1) - - twitter-text (3.1.0) + - SwiftGen (6.6.2) - XLPagerTabStrip (9.0.0) - - ZIPFoundation (0.9.13) DEPENDENCIES: - - DateToolsSwift (~> 5.0.0) - Firebase/AnalyticsWithoutAdIdSupport - FirebaseCrashlytics - FirebaseMessaging - FirebasePerformance - Sourcery (~> 1.8.1) - - SwiftGen (~> 6.5.1) - - twitter-text (~> 3.1.0) + - SwiftGen (~> 6.6.2) - XLPagerTabStrip (~> 9.0.0) - - ZIPFoundation (~> 0.9.11) SPEC REPOS: trunk: - - DateToolsSwift - Firebase - FirebaseABTesting - FirebaseAnalytics @@ -141,12 +134,9 @@ SPEC REPOS: - PromisesObjC - Sourcery - SwiftGen - - twitter-text - XLPagerTabStrip - - ZIPFoundation SPEC CHECKSUMS: - DateToolsSwift: 4207ada6ad615d8dc076323d27037c94916dbfa6 Firebase: 4ba896cb8e5105d4b9e247e1c1b6222b548df55a FirebaseABTesting: cd1ec762a0078b46a7ce91dfe5b7b8991c2dff8f FirebaseAnalytics: af5a03a8dff7648c7b8486f6a78b1368e0268dd3 @@ -164,11 +154,9 @@ SPEC CHECKSUMS: nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 PromisesObjC: ab77feca74fa2823e7af4249b8326368e61014cb Sourcery: 4d44d4ea26a682a4a9875ec7c1870a1e7b8e183f - SwiftGen: a6d22010845f08fe18fbdf3a07a8e380fd22e0ea - twitter-text: 3a0d73ca52955439dc8b208ca7e123ea0abd6a51 + SwiftGen: 1366a7f71aeef49954ca5a63ba4bef6b0f24138c XLPagerTabStrip: 61c57fd61f611ee5f01ff1495ad6fbee8bf496c5 - ZIPFoundation: ae5b4b813d216d3bf0a148773267fff14bd51d37 -PODFILE CHECKSUM: a6441556e77318b291664364cc5bfbdd86dbd781 +PODFILE CHECKSUM: e2c773b0e4d6bfd3166d7794b7f8babe8f9b9b92 COCOAPODS: 1.11.3 diff --git a/ShareExtension/ComposeViewController.swift b/ShareExtension/ComposeViewController.swift index 75956cca..71f41720 100644 --- a/ShareExtension/ComposeViewController.swift +++ b/ShareExtension/ComposeViewController.swift @@ -9,11 +9,11 @@ import os.log import UIKit import Combine -import AppShared -import TwidereUI import CoreDataStack import UniformTypeIdentifiers +@_exported import TwidereUI + class ComposeViewController: UIViewController { let logger = Logger(subsystem: "ComposeViewController", category: "ViewController") diff --git a/ShareExtension/ComposeViewModel.swift b/ShareExtension/ComposeViewModel.swift index bd17b518..1f961f8e 100644 --- a/ShareExtension/ComposeViewModel.swift +++ b/ShareExtension/ComposeViewModel.swift @@ -9,7 +9,6 @@ import Foundation import Combine import TwidereCore -import TwidereUI final class ComposeViewModel { diff --git a/TwidereSDK/.swiftpm/xcode/xcshareddata/xcschemes/TwidereSDK.xcscheme b/TwidereSDK/.swiftpm/xcode/xcshareddata/xcschemes/TwidereSDK.xcscheme new file mode 100644 index 00000000..cbe2a11a --- /dev/null +++ b/TwidereSDK/.swiftpm/xcode/xcshareddata/xcschemes/TwidereSDK.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index 715b5b33..dcd94433 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -46,6 +46,11 @@ let package = Package( .package(url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.4"), .package(url: "https://github.com/TwidereProject/twitter-text.git", exact: "0.0.3"), .package(url: "https://github.com/MainasuK/DateTools", branch: "master"), + .package(url: "https://github.com/kciter/Floaty.git", branch: "master"), + .package(url: "https://github.com/MainasuK/FPSIndicator.git", from: "1.1.0"), + .package(url: "https://github.com/uias/Tabman.git", from: "3.0.1"), + .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.6.0"), + .package(url: "https://github.com/tid-kijyun/Kanna.git", from: "5.2.7"), .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"), ], targets: [ @@ -116,6 +121,8 @@ let package = Package( .product(name: "MetaTextKit", package: "MetaTextKit"), .product(name: "TwitterText", package: "twitter-text"), .product(name: "DateToolsSwift", package: "DateTools"), + .product(name: "CryptoSwift", package: "CryptoSwift"), + .product(name: "Kanna", package: "Kanna"), ] ), .target( @@ -135,6 +142,9 @@ let package = Package( .product(name: "SDWebImage", package: "SDWebImage"), .product(name: "SwiftMessages", package: "SwiftMessages"), .product(name: "UITextView+Placeholder", package: "UITextView-Placeholder"), + .product(name: "FPSIndicator", package: "FPSIndicator"), + .product(name: "Floaty", package: "Floaty"), + .product(name: "Tabman", package: "Tabman"), ] ), ] diff --git a/TwidereSDK/Sources/TwidereCore/Extension/Preference/Preference+Appearance.swift b/TwidereSDK/Sources/TwidereCore/Extension/Preference/Preference+Appearance.swift index 66927422..0dcb88f7 100644 --- a/TwidereSDK/Sources/TwidereCore/Extension/Preference/Preference+Appearance.swift +++ b/TwidereSDK/Sources/TwidereCore/Extension/Preference/Preference+Appearance.swift @@ -6,7 +6,6 @@ // import Foundation -import TwidereCommon import TwidereLocalization extension UserDefaults.TranslateButtonPreference { diff --git a/TwidereSDK/Sources/TwidereCore/Model/Status/StatusRecord.swift b/TwidereSDK/Sources/TwidereCore/Model/Status/StatusRecord.swift index d22b37d7..8ccfd9ec 100644 --- a/TwidereSDK/Sources/TwidereCore/Model/Status/StatusRecord.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/Status/StatusRecord.swift @@ -9,7 +9,6 @@ import Foundation import CoreData import CoreDataStack -import TwidereCommon public enum StatusRecord: Hashable { case twitter(record: ManagedObjectRecord) diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Authentication.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Authentication.swift index e6f8cafd..559f5727 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Authentication.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Authentication.swift @@ -9,7 +9,6 @@ import Foundation import Combine import TwitterSDK import MastodonSDK -import TwidereCommon extension APIService { diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Notification.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Notification.swift index 12f01db0..01fcce8b 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Notification.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Notification.swift @@ -8,7 +8,6 @@ import os.log import Foundation import MastodonSDK -import TwidereCommon extension APIService { diff --git a/TwidereSDK/Sources/TwidereCore/Service/MastodonEmojiService+EmojiViewModel+State.swift b/TwidereSDK/Sources/TwidereCore/Service/MastodonEmojiService+EmojiViewModel+State.swift index 52ae2e9d..130b2975 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/MastodonEmojiService+EmojiViewModel+State.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/MastodonEmojiService+EmojiViewModel+State.swift @@ -8,7 +8,6 @@ import os.log import Foundation import GameplayKit -import TwidereCommon import MastodonSDK extension MastodonEmojiService.EmojiViewModel { diff --git a/TwidereSDK/Sources/TwidereCore/Service/NotificationService+NotificationSubscriber.swift b/TwidereSDK/Sources/TwidereCore/Service/NotificationService+NotificationSubscriber.swift index b990fd9a..bc10c883 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/NotificationService+NotificationSubscriber.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/NotificationService+NotificationSubscriber.swift @@ -12,7 +12,6 @@ import CoreData import CoreDataStack import MastodonSDK import TwidereCommon -import CoreData public struct NotificationSubject { public let fcmToken: String? diff --git a/TwidereSDK/Sources/TwidereCore/Service/PhotoLibraryService.swift b/TwidereSDK/Sources/TwidereCore/Service/PhotoLibraryService.swift index 999a25de..a1852407 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/PhotoLibraryService.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/PhotoLibraryService.swift @@ -11,7 +11,6 @@ import UIKit import Photos import Alamofire import AlamofireImage -import TwidereCommon import SwiftMessages import Kingfisher diff --git a/TwidereSDK/Sources/TwidereUI/Banner/NotificationBannerView.swift b/TwidereSDK/Sources/TwidereUI/Banner/NotificationBannerView.swift index 5dd98a04..b552549f 100644 --- a/TwidereSDK/Sources/TwidereUI/Banner/NotificationBannerView.swift +++ b/TwidereSDK/Sources/TwidereUI/Banner/NotificationBannerView.swift @@ -10,7 +10,6 @@ import os.log import UIKit import SwiftUI import TwidereAsset -import TwidereCommon public final class NotificationBannerView: UIView { diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusMetricsDashboardView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusMetricsDashboardView.swift index 6bbb4066..3b45d018 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusMetricsDashboardView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusMetricsDashboardView.swift @@ -8,7 +8,6 @@ import os.log import UIKit import CoreDataStack -import TwidereCommon import TwidereCore public protocol StatusMetricsDashboardViewDelegate: AnyObject { diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+Configuration.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+Configuration.swift index 47d2757e..e2e2a690 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+Configuration.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+Configuration.swift @@ -11,7 +11,6 @@ import Combine import SwiftUI import CoreData import CoreDataStack -import TwidereCommon import TwidereCore import TwitterMeta import MastodonMeta diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index 7317d9f5..9053bf6e 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -12,7 +12,6 @@ import Combine import SwiftUI import CoreData import CoreDataStack -import TwidereCommon import TwidereAsset import TwidereLocalization import TwidereCore diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index 3708188e..db5be7d2 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -14,7 +14,6 @@ import Kingfisher import MetaTextKit import MetaTextArea import MetaLabel -import TwidereCommon import TwidereCore public protocol StatusViewDelegate: AnyObject { diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView+Configuration.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView+Configuration.swift index e2d3a13a..53a7b5f1 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView+Configuration.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView+Configuration.swift @@ -9,7 +9,6 @@ import Foundation import Combine import SwiftUI import CoreDataStack -import TwidereCommon import TwidereCore import TwidereAsset import Meta diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift index d54a25f1..69440859 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift @@ -10,7 +10,6 @@ import UIKit import Combine import SwiftUI import CoreDataStack -import TwidereCommon import TwidereCore import TwidereAsset import Meta diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift index e191f4d5..dc01102c 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift @@ -9,7 +9,6 @@ import os.log import UIKit import Combine import PhotosUI -import TwidereCommon import Kingfisher final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable { diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift index 45499341..e9051464 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift @@ -7,7 +7,6 @@ import os.log import Foundation -import TwidereCommon import TwidereCore import MastodonSDK import CoreDataStack diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Publisher/TwitterStatusPublisher.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Publisher/TwitterStatusPublisher.swift index a8ab9493..2fbde048 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Publisher/TwitterStatusPublisher.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Publisher/TwitterStatusPublisher.swift @@ -7,7 +7,6 @@ import os.log import Foundation -import TwidereCommon import TwidereCore import TwitterSDK import CoreDataStack diff --git a/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewController.swift b/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewController.swift index f7db3e06..e5549b8c 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewController.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewController.swift @@ -9,7 +9,6 @@ import os.log import UIKit import Combine import TwidereLocalization -import TwidereUI protocol MentionPickViewControllerDelegate: AnyObject { func mentionPickViewController(_ controller: MentionPickViewController, itemPickDidChange items: [MentionPickViewModel.Item]) diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserViewTableViewCellDelegate.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserViewTableViewCellDelegate.swift index a014de1a..ec846b06 100644 --- a/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserViewTableViewCellDelegate.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserViewTableViewCellDelegate.swift @@ -6,7 +6,6 @@ // import UIKit -import TwidereCommon // sourcery: protocolName = "UserViewDelegate" // sourcery: replaceOf = "userView(userView" diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index cf9bc4c7..f38ed74f 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -25,6 +25,10 @@ DB02C77527351DA2007EA0BF /* HashtagTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02C77427351DA2007EA0BF /* HashtagTableViewCell+ViewModel.swift */; }; DB02C777273520B8007EA0BF /* SearchHashtagViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02C776273520B8007EA0BF /* SearchHashtagViewModel+State.swift */; }; DB02C77927352217007EA0BF /* SearchHashtagViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02C77827352217007EA0BF /* SearchHashtagViewModel+Diffable.swift */; }; + DB05E13729C318530055BF3F /* TwidereSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB05E13629C318530055BF3F /* TwidereSDK */; }; + DB05E13929C318590055BF3F /* TwidereSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB05E13829C318590055BF3F /* TwidereSDK */; }; + DB05E13B29C3185E0055BF3F /* TwidereSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB05E13A29C3185E0055BF3F /* TwidereSDK */; }; + DB05E13D29C321960055BF3F /* TwidereSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB05E13C29C321960055BF3F /* TwidereSDK */; }; DB0618102786EC870030EE79 /* LineChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB06180F2786EC870030EE79 /* LineChartView.swift */; }; DB0AD4DF285872BE0002ABDB /* UserMediaTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AD4DE285872BE0002ABDB /* UserMediaTimelineViewController.swift */; }; DB0AD4E22858734A0002ABDB /* UserMediaTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AD4E12858734A0002ABDB /* UserMediaTimelineViewModel.swift */; }; @@ -58,7 +62,6 @@ DB25C4CF2779A06D00EC1435 /* SavedSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB25C4CE2779A06D00EC1435 /* SavedSearchViewModel.swift */; }; DB25C4D12779A37E00EC1435 /* SavedSearchViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB25C4D02779A37E00EC1435 /* SavedSearchViewModel+Diffable.swift */; }; DB25C4D32779ADD800EC1435 /* SearchResultContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB25C4D22779ADD800EC1435 /* SearchResultContainerViewController.swift */; }; - DB2611B7251B2D42004BF309 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = DB2611B6251B2D42004BF309 /* CryptoSwift */; }; DB262A332721377800D18EF3 /* DataSourceFacade+Block.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB262A322721377800D18EF3 /* DataSourceFacade+Block.swift */; }; DB262A3727213FBE00D18EF3 /* UIAlertAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB262A3627213FBE00D18EF3 /* UIAlertAction.swift */; }; DB262A392721621900D18EF3 /* DataSourceFacade+User.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB262A382721621800D18EF3 /* DataSourceFacade+User.swift */; }; @@ -180,7 +183,6 @@ DB6BCD6E277ADC0900847054 /* TrendViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6BCD6D277ADC0900847054 /* TrendViewModel.swift */; }; DB6BCD70277AEAC700847054 /* TrendTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6BCD6F277AEAC700847054 /* TrendTableViewCell.swift */; }; DB6BCD72277AEED600847054 /* TrendViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6BCD71277AEED600847054 /* TrendViewModel+Diffable.swift */; }; - DB6CF1C4269715CD001DE069 /* FPSIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = DB6CF1C3269715CD001DE069 /* FPSIndicator */; }; DB6DF3E0252060AA00E8A273 /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6DF3DF252060AA00E8A273 /* ProfileViewModel.swift */; }; DB71C7D1271EB09A00BE3819 /* DataSourceFacade+Friendship.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71C7D0271EB09A00BE3819 /* DataSourceFacade+Friendship.swift */; }; DB71C7D3271EB71800BE3819 /* ProfileViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71C7D2271EB71800BE3819 /* ProfileViewController+DataSourceProvider.swift */; }; @@ -215,13 +217,11 @@ DB76A66F276083CB00A50673 /* MediaPreviewTransitionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB76A66E276083CB00A50673 /* MediaPreviewTransitionViewController.swift */; }; DB76A67127609A8700A50673 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB76A67027609A8700A50673 /* RemoteProfileViewModel.swift */; }; DB77FF7F2847808C00182A0B /* ShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DBBBBE5E2744E8CC007ACB4B /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - DB7AB33E2744E3740035EB8A /* Floaty in Frameworks */ = {isa = PBXBuildFile; productRef = DB7AB33D2744E3740035EB8A /* Floaty */; }; DB7FF06128853A7F00BFD55E /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7FF06028853A7F00BFD55E /* NotificationService.swift */; }; DB7FF06528853A7F00BFD55E /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DB7FF05E28853A7F00BFD55E /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; DB7FF06A28853AB000BFD55E /* String+Decode85.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7FF05928853A4F00BFD55E /* String+Decode85.swift */; }; DB7FF06B28853AB300BFD55E /* NotificationService+Decrypt.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7FF05628853A4F00BFD55E /* NotificationService+Decrypt.swift */; }; DB7FF06C28853AB800BFD55E /* String+Escape.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7FF05528853A4F00BFD55E /* String+Escape.swift */; }; - DB7FF06E28853B1F00BFD55E /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB94B6BB26C65CB000A2E8A1 /* AppShared.framework */; }; DB8301F7273CED0400BF5224 /* NotificationTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8301F6273CED0400BF5224 /* NotificationTimelineViewController.swift */; }; DB8301FA273CED2E00BF5224 /* NotificationTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8301F9273CED2E00BF5224 /* NotificationTimelineViewModel.swift */; }; DB830200273D04D000BF5224 /* NotificationTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8301FF273D04D000BF5224 /* NotificationTimelineViewModel+Diffable.swift */; }; @@ -261,7 +261,6 @@ DB932E5227FEC7390036A824 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = DB932E5527FEC7390036A824 /* Intents.intentdefinition */; settings = {ATTRIBUTES = (codegen, ); }; }; DB932E5327FEC7390036A824 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = DB932E5527FEC7390036A824 /* Intents.intentdefinition */; }; DB94B6B426C65BE100A2E8A1 /* MastodonAuthenticationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB94B6B326C65BE100A2E8A1 /* MastodonAuthenticationController.swift */; }; - DB94B6BE26C65CB000A2E8A1 /* AppShared.h in Headers */ = {isa = PBXBuildFile; fileRef = DB94B6BD26C65CB000A2E8A1 /* AppShared.h */; settings = {ATTRIBUTES = (Public, ); }; }; DB969A66253064FE0053CB31 /* DispatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB969A65253064FE0053CB31 /* DispatchQueue.swift */; }; DB98DC0E2507862D0087E30F /* TimelineMiddleLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98DC0D2507862D0087E30F /* TimelineMiddleLoaderTableViewCell.swift */; }; DB98DC10250787420087E30F /* TimelineBottomLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98DC0F250787420087E30F /* TimelineBottomLoaderTableViewCell.swift */; }; @@ -292,7 +291,6 @@ DBB04A322861AE24003799CA /* TrendPlaceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB04A312861AE24003799CA /* TrendPlaceViewController.swift */; }; DBB04A352861AE4D003799CA /* TrendPlaceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB04A342861AE4D003799CA /* TrendPlaceViewModel.swift */; }; DBB04A372861AF2D003799CA /* TrendPlaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB04A362861AF2D003799CA /* TrendPlaceView.swift */; }; - DBB0E3C32760FB1200F1D45F /* TwidereSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DBB0E3C22760FB1200F1D45F /* TwidereSDK */; }; DBB47DA926EB3AA2001590F7 /* ProfileFieldListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB47DA826EB3AA2001590F7 /* ProfileFieldListView.swift */; }; DBB47DAB26EB3BF8001590F7 /* ProfileFieldContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB47DAA26EB3BF8001590F7 /* ProfileFieldContentView.swift */; }; DBB47DAD26EB440C001590F7 /* ProfileFieldCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB47DAC26EB440C001590F7 /* ProfileFieldCollectionViewCell.swift */; }; @@ -313,8 +311,6 @@ DBCB4053255B6EB100DD8D8F /* AccountListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCB4052255B6EB100DD8D8F /* AccountListViewModel.swift */; }; DBCB4060255CAC0300DD8D8F /* TwitterAuthenticationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCB405F255CAC0300DD8D8F /* TwitterAuthenticationController.swift */; }; DBCB408A255D8C2B00DD8D8F /* SafariActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCB4089255D8C2B00DD8D8F /* SafariActivity.swift */; }; - DBCC7AE5274BA10100E0986D /* DateTimeSwiftProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCC7AE3274B95C900E0986D /* DateTimeSwiftProvider.swift */; }; - DBCC7AE6274BA10100E0986D /* OfficialTwitterTextProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB01091B26E606BB005F67D7 /* OfficialTwitterTextProvider.swift */; }; DBCE2E912591A06300926D09 /* UINavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCE2E902591A06300926D09 /* UINavigationController.swift */; }; DBCE2E992591A44000926D09 /* UIViewAnimatingPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCE2E982591A44000926D09 /* UIViewAnimatingPosition.swift */; }; DBD0B4972758B57F0015A388 /* DrawerSidebarTransitionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBEFDC91255920060086F268 /* DrawerSidebarTransitionController.swift */; }; @@ -325,7 +321,6 @@ DBD0B49D2758B5F50015A388 /* SidebarSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD0B49C2758B5F50015A388 /* SidebarSection.swift */; }; DBD0B4A02758B6010015A388 /* SidebarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD0B49F2758B6010015A388 /* SidebarItem.swift */; }; DBD40B852599B9C2006E4ABC /* CombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD40B842599B9C2006E4ABC /* CombineTests.swift */; }; - DBD98489251CB88A00ED87A1 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBD98488251CB88A00ED87A1 /* Tabman */; }; DBDA7F1A27D1CBCA00BA6BE1 /* TwidereXUITests+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBDA7F1927D1CBCA00BA6BE1 /* TwidereXUITests+Onboarding.swift */; }; DBDA7F1E27D2256400BA6BE1 /* ListViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBDA7F1D27D2256400BA6BE1 /* ListViewModel+State.swift */; }; DBDA8E2224FCF8A3006750DC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBDA8E2124FCF8A3006750DC /* AppDelegate.swift */; }; @@ -342,7 +337,6 @@ DBE6357D2885557C001C114B /* PushNotificationScratchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE6357C2885557C001C114B /* PushNotificationScratchViewModel.swift */; }; DBE6357F288555AE001C114B /* PushNotificationScratchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE6357E288555AE001C114B /* PushNotificationScratchView.swift */; }; DBE76D1E2500E65D00DEB0FC /* HomeTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE76D1D2500E65D00DEB0FC /* HomeTimelineViewModel.swift */; }; - DBEA4F842511F7460007FEC5 /* Kanna in Frameworks */ = {isa = PBXBuildFile; productRef = DBEA4F832511F7460007FEC5 /* Kanna */; }; DBED96D8253F5D7800C5383A /* NamingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBED96D7253F5D7800C5383A /* NamingState.swift */; }; DBF167FD27C394830001F75E /* NeedsDependency+AvatarBarButtonItemDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF167FC27C394830001F75E /* NeedsDependency+AvatarBarButtonItemDelegate.swift */; }; DBF3309125B96E0B00A678FB /* WKNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF3309025B96E0B00A678FB /* WKNavigationDelegateShim.swift */; }; @@ -366,9 +360,6 @@ DBFA4A2025A5924C00D51703 /* ListTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFA4A1F25A5924C00D51703 /* ListTimelineViewModel+Diffable.swift */; }; DBFADA892872BEDA00B512D6 /* CoverFlowStackCollectionViewLayout in Frameworks */ = {isa = PBXBuildFile; productRef = DBFADA882872BEDA00B512D6 /* CoverFlowStackCollectionViewLayout */; }; DBFADA8B2872BEDE00B512D6 /* TabBarPager in Frameworks */ = {isa = PBXBuildFile; productRef = DBFADA8A2872BEDE00B512D6 /* TabBarPager */; }; - DBFC0AE6276118080011E99B /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB94B6BB26C65CB000A2E8A1 /* AppShared.framework */; }; - DBFC0AE9276118240011E99B /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB94B6BB26C65CB000A2E8A1 /* AppShared.framework */; }; - DBFC0AEA276118240011E99B /* AppShared.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DB94B6BB26C65CB000A2E8A1 /* AppShared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DBFCC44725667C620016698E /* UILabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCC44625667C620016698E /* UILabel.swift */; }; DBFCEF1228939C9600EEBFB1 /* UserHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCEF1128939C9600EEBFB1 /* UserHistoryViewController.swift */; }; DBFCEF1528939CAC00EEBFB1 /* UserHistoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCEF1428939CAC00EEBFB1 /* UserHistoryViewModel.swift */; }; @@ -381,8 +372,6 @@ DBFDCE4427F450FC00BE99E3 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFDCE4327F450FC00BE99E3 /* IntentHandler.swift */; }; DBFDCE4827F450FC00BE99E3 /* TwidereXIntent.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DBFDCE4027F450FC00BE99E3 /* TwidereXIntent.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; DBFDCE5027F4515E00BE99E3 /* SwitchAccountIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFDCE4F27F4515E00BE99E3 /* SwitchAccountIntentHandler.swift */; }; - DBFDCE5427F451FC00BE99E3 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB94B6BB26C65CB000A2E8A1 /* AppShared.framework */; }; - E5E65DC85CA480AE17571EFC /* Pods_AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84835D7E14A262E389DD4AB3 /* Pods_AppShared.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -400,27 +389,6 @@ remoteGlobalIDString = DB7FF05D28853A7F00BFD55E; remoteInfo = NotificationService; }; - DB7FF07028853B1F00BFD55E /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = DBDA8E1624FCF8A3006750DC /* Project object */; - proxyType = 1; - remoteGlobalIDString = DB94B6BA26C65CB000A2E8A1; - remoteInfo = AppShared; - }; - DB94B6BF26C65CB000A2E8A1 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = DBDA8E1624FCF8A3006750DC /* Project object */; - proxyType = 1; - remoteGlobalIDString = DB94B6BA26C65CB000A2E8A1; - remoteInfo = AppShared; - }; - DBBBBE732744EC27007ACB4B /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = DBDA8E1624FCF8A3006750DC /* Project object */; - proxyType = 1; - remoteGlobalIDString = DB94B6BA26C65CB000A2E8A1; - remoteInfo = AppShared; - }; DBBBBE812744F39C007ACB4B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = DBDA8E1624FCF8A3006750DC /* Project object */; @@ -456,33 +424,15 @@ remoteGlobalIDString = DBFDCE3F27F450FC00BE99E3; remoteInfo = TwidereXIntent; }; - DBFDCE5627F451FC00BE99E3 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = DBDA8E1624FCF8A3006750DC /* Project object */; - proxyType = 1; - remoteGlobalIDString = DB94B6BA26C65CB000A2E8A1; - remoteInfo = AppShared; - }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ - DB0B3B8D26C6637500501BB7 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; DBFC0AEB276118240011E99B /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( - DBFC0AEA276118240011E99B /* AppShared.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -529,7 +479,6 @@ D286181E8236434CBB750613 /* Pods-TwidereXTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TwidereXTests.profile.xcconfig"; path = "Target Support Files/Pods-TwidereXTests/Pods-TwidereXTests.profile.xcconfig"; sourceTree = ""; }; DB004BF526CE4A7F00F5C574 /* StatusCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCollectionViewCell.swift; sourceTree = ""; }; DB01091426E5EB64005F67D7 /* MastodonStatusThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonStatusThreadViewModel.swift; sourceTree = ""; }; - DB01091B26E606BB005F67D7 /* OfficialTwitterTextProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfficialTwitterTextProvider.swift; sourceTree = ""; }; DB01091F26E60756005F67D7 /* DataSourceFacade+StatusThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+StatusThread.swift"; sourceTree = ""; }; DB01092126E608B7005F67D7 /* DataSourceFacade+Repost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Repost.swift"; sourceTree = ""; }; DB01092326E6199C005F67D7 /* DataSourceProvider+StatusViewTableViewCellDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceProvider+StatusViewTableViewCellDelegate.swift"; sourceTree = ""; }; @@ -780,8 +729,6 @@ DB932E6B27FEC7490036A824 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Intents.strings"; sourceTree = ""; }; DB932E6D27FEC7490036A824 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Intents.strings; sourceTree = ""; }; DB94B6B326C65BE100A2E8A1 /* MastodonAuthenticationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAuthenticationController.swift; sourceTree = ""; }; - DB94B6BB26C65CB000A2E8A1 /* AppShared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AppShared.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - DB94B6BD26C65CB000A2E8A1 /* AppShared.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppShared.h; sourceTree = ""; }; DB969A65253064FE0053CB31 /* DispatchQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DispatchQueue.swift; sourceTree = ""; }; DB97D1F4256CF7710056F8C2 /* ProgressBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBarView.swift; sourceTree = ""; }; DB98DC0D2507862D0087E30F /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = ""; }; @@ -846,8 +793,6 @@ DBCB4052255B6EB100DD8D8F /* AccountListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountListViewModel.swift; sourceTree = ""; }; DBCB405F255CAC0300DD8D8F /* TwitterAuthenticationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterAuthenticationController.swift; sourceTree = ""; }; DBCB4089255D8C2B00DD8D8F /* SafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariActivity.swift; sourceTree = ""; }; - DBCC7AE3274B95C900E0986D /* DateTimeSwiftProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeSwiftProvider.swift; sourceTree = ""; }; - DBCDCE602760B8B000F0B78C /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DBCE2E902591A06300926D09 /* UINavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UINavigationController.swift; sourceTree = ""; }; DBCE2E982591A44000926D09 /* UIViewAnimatingPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewAnimatingPosition.swift; sourceTree = ""; }; DBCE2EA82591F23B00926D09 /* FollowingListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowingListViewController.swift; sourceTree = ""; }; @@ -948,16 +893,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB7FF06E28853B1F00BFD55E /* AppShared.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - DB94B6B826C65CB000A2E8A1 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - DBB0E3C32760FB1200F1D45F /* TwidereSDK in Frameworks */, - E5E65DC85CA480AE17571EFC /* Pods_AppShared.framework in Frameworks */, + DB05E13B29C3185E0055BF3F /* TwidereSDK in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -965,7 +901,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DBFC0AE6276118080011E99B /* AppShared.framework in Frameworks */, + DB05E13729C318530055BF3F /* TwidereSDK in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -973,14 +909,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB2611B7251B2D42004BF309 /* CryptoSwift in Frameworks */, - DBFC0AE9276118240011E99B /* AppShared.framework in Frameworks */, - DB6CF1C4269715CD001DE069 /* FPSIndicator in Frameworks */, - DBD98489251CB88A00ED87A1 /* Tabman in Frameworks */, + DB05E13D29C321960055BF3F /* TwidereSDK in Frameworks */, DBFADA892872BEDA00B512D6 /* CoverFlowStackCollectionViewLayout in Frameworks */, DBFADA8B2872BEDE00B512D6 /* TabBarPager in Frameworks */, - DBEA4F842511F7460007FEC5 /* Kanna in Frameworks */, - DB7AB33E2744E3740035EB8A /* Floaty in Frameworks */, 44CCAE5E7B79E0C359E367E5 /* Pods_TwidereX.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1005,7 +936,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DBFDCE5427F451FC00BE99E3 /* AppShared.framework in Frameworks */, + DB05E13929C318590055BF3F /* TwidereSDK in Frameworks */, DBFDCE4127F450FC00BE99E3 /* Intents.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1876,16 +1807,6 @@ path = Mastodon; sourceTree = ""; }; - DB94B6BC26C65CB000A2E8A1 /* AppShared */ = { - isa = PBXGroup; - children = ( - DBCDCE602760B8B000F0B78C /* Info.plist */, - DB94B6BD26C65CB000A2E8A1 /* AppShared.h */, - DBCC7AE7274BA10300E0986D /* Vender */, - ); - path = AppShared; - sourceTree = ""; - }; DB97D1E7256CBA5D0056F8C2 /* TableViewCell */ = { isa = PBXGroup; children = ( @@ -2079,15 +2000,6 @@ path = Activity; sourceTree = ""; }; - DBCC7AE7274BA10300E0986D /* Vender */ = { - isa = PBXGroup; - children = ( - DB01091B26E606BB005F67D7 /* OfficialTwitterTextProvider.swift */, - DBCC7AE3274B95C900E0986D /* DateTimeSwiftProvider.swift */, - ); - path = Vender; - sourceTree = ""; - }; DBCE2EAD2591F37400926D09 /* FollowingList */ = { isa = PBXGroup; children = ( @@ -2137,7 +2049,6 @@ DBDA8E3724FCF8A7006750DC /* TwidereXTests */, DBDA8E4224FCF8A7006750DC /* TwidereXUITests */, DBE76CE92500B29300DEB0FC /* StubMixer */, - DB94B6BC26C65CB000A2E8A1 /* AppShared */, DBBBBE5F2744E8CC007ACB4B /* ShareExtension */, DBFDCE4227F450FC00BE99E3 /* TwidereXIntent */, DB7FF05F28853A7F00BFD55E /* NotificationService */, @@ -2153,7 +2064,6 @@ DBDA8E1E24FCF8A3006750DC /* TwidereX.app */, DBDA8E3424FCF8A7006750DC /* TwidereXTests.xctest */, DBDA8E3F24FCF8A7006750DC /* TwidereXUITests.xctest */, - DB94B6BB26C65CB000A2E8A1 /* AppShared.framework */, DBBBBE5E2744E8CC007ACB4B /* ShareExtension.appex */, DBFDCE4027F450FC00BE99E3 /* TwidereXIntent.appex */, DB7FF05E28853A7F00BFD55E /* NotificationService.appex */, @@ -2501,17 +2411,6 @@ }; /* End PBXGroup section */ -/* Begin PBXHeadersBuildPhase section */ - DB94B6B626C65CB000A2E8A1 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - DB94B6BE26C65CB000A2E8A1 /* AppShared.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXHeadersBuildPhase section */ - /* Begin PBXNativeTarget section */ DB7FF05D28853A7F00BFD55E /* NotificationService */ = { isa = PBXNativeTarget; @@ -2524,36 +2423,15 @@ buildRules = ( ); dependencies = ( - DB7FF07128853B1F00BFD55E /* PBXTargetDependency */, ); name = NotificationService; + packageProductDependencies = ( + DB05E13A29C3185E0055BF3F /* TwidereSDK */, + ); productName = NotificationService; productReference = DB7FF05E28853A7F00BFD55E /* NotificationService.appex */; productType = "com.apple.product-type.app-extension"; }; - DB94B6BA26C65CB000A2E8A1 /* AppShared */ = { - isa = PBXNativeTarget; - buildConfigurationList = DB94B6C326C65CB100A2E8A1 /* Build configuration list for PBXNativeTarget "AppShared" */; - buildPhases = ( - 51870AB6AC62D0F5C707B21E /* [CP] Check Pods Manifest.lock */, - DB94B6B626C65CB000A2E8A1 /* Headers */, - DB94B6B726C65CB000A2E8A1 /* Sources */, - DB94B6B826C65CB000A2E8A1 /* Frameworks */, - DB94B6B926C65CB000A2E8A1 /* Resources */, - DB0B3B8D26C6637500501BB7 /* Embed Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = AppShared; - packageProductDependencies = ( - DBB0E3C22760FB1200F1D45F /* TwidereSDK */, - ); - productName = AppShared; - productReference = DB94B6BB26C65CB000A2E8A1 /* AppShared.framework */; - productType = "com.apple.product-type.framework"; - }; DBBBBE5D2744E8CC007ACB4B /* ShareExtension */ = { isa = PBXNativeTarget; buildConfigurationList = DBBBBE692744E8CC007ACB4B /* Build configuration list for PBXNativeTarget "ShareExtension" */; @@ -2565,11 +2443,13 @@ buildRules = ( ); dependencies = ( - DBBBBE742744EC27007ACB4B /* PBXTargetDependency */, DBBBBE822744F39C007ACB4B /* PBXTargetDependency */, DBBBBE892744F535007ACB4B /* PBXTargetDependency */, ); name = ShareExtension; + packageProductDependencies = ( + DB05E13629C318530055BF3F /* TwidereSDK */, + ); productName = ShareExtension; productReference = DBBBBE5E2744E8CC007ACB4B /* ShareExtension.appex */; productType = "com.apple.product-type.app-extension"; @@ -2593,20 +2473,15 @@ buildRules = ( ); dependencies = ( - DB94B6C026C65CB000A2E8A1 /* PBXTargetDependency */, DBFDCE4727F450FC00BE99E3 /* PBXTargetDependency */, DB77FF812847808C00182A0B /* PBXTargetDependency */, DB7FF06428853A7F00BFD55E /* PBXTargetDependency */, ); name = TwidereX; packageProductDependencies = ( - DBEA4F832511F7460007FEC5 /* Kanna */, - DB2611B6251B2D42004BF309 /* CryptoSwift */, - DBD98488251CB88A00ED87A1 /* Tabman */, - DB6CF1C3269715CD001DE069 /* FPSIndicator */, - DB7AB33D2744E3740035EB8A /* Floaty */, DBFADA882872BEDA00B512D6 /* CoverFlowStackCollectionViewLayout */, DBFADA8A2872BEDE00B512D6 /* TabBarPager */, + DB05E13C29C321960055BF3F /* TwidereSDK */, ); productName = TwidereX; productReference = DBDA8E1E24FCF8A3006750DC /* TwidereX.app */; @@ -2662,9 +2537,11 @@ buildRules = ( ); dependencies = ( - DBFDCE5727F451FC00BE99E3 /* PBXTargetDependency */, ); name = TwidereXIntent; + packageProductDependencies = ( + DB05E13829C318590055BF3F /* TwidereSDK */, + ); productName = TwidereXIntent; productReference = DBFDCE4027F450FC00BE99E3 /* TwidereXIntent.appex */; productType = "com.apple.product-type.app-extension"; @@ -2682,10 +2559,6 @@ DB7FF05D28853A7F00BFD55E = { CreatedOnToolsVersion = 13.4.1; }; - DB94B6BA26C65CB000A2E8A1 = { - CreatedOnToolsVersion = 13.0; - LastSwiftMigration = 1300; - }; DBBBBE5D2744E8CC007ACB4B = { CreatedOnToolsVersion = 13.1; }; @@ -2727,11 +2600,6 @@ ); mainGroup = DBDA8E1524FCF8A3006750DC; packageReferences = ( - DBEA4F822511F7460007FEC5 /* XCRemoteSwiftPackageReference "Kanna" */, - DB2611B5251B2D42004BF309 /* XCRemoteSwiftPackageReference "CryptoSwift" */, - DBD98487251CB88A00ED87A1 /* XCRemoteSwiftPackageReference "Tabman" */, - DB6CF1C2269715CD001DE069 /* XCRemoteSwiftPackageReference "FPSIndicator" */, - DB7AB33C2744E3740035EB8A /* XCRemoteSwiftPackageReference "Floaty" */, ); productRefGroup = DBDA8E1F24FCF8A3006750DC /* Products */; projectDirPath = ""; @@ -2740,7 +2608,6 @@ DBDA8E1D24FCF8A3006750DC /* TwidereX */, DBDA8E3324FCF8A7006750DC /* TwidereXTests */, DBDA8E3E24FCF8A7006750DC /* TwidereXUITests */, - DB94B6BA26C65CB000A2E8A1 /* AppShared */, DBBBBE5D2744E8CC007ACB4B /* ShareExtension */, DBFDCE3F27F450FC00BE99E3 /* TwidereXIntent */, DB7FF05D28853A7F00BFD55E /* NotificationService */, @@ -2756,13 +2623,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - DB94B6B926C65CB000A2E8A1 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; DBBBBE5C2744E8CC007ACB4B /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -2851,28 +2711,6 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-TwidereX-TwidereXUITests/Pods-TwidereX-TwidereXUITests-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 51870AB6AC62D0F5C707B21E /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-AppShared-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; 6B1901F5A04D5AA62D66599E /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -3016,15 +2854,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - DB94B6B726C65CB000A2E8A1 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - DBCC7AE5274BA10100E0986D /* DateTimeSwiftProvider.swift in Sources */, - DBCC7AE6274BA10100E0986D /* OfficialTwitterTextProvider.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; DBBBBE5A2744E8CC007ACB4B /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -3410,29 +3239,12 @@ target = DB7FF05D28853A7F00BFD55E /* NotificationService */; targetProxy = DB7FF06328853A7F00BFD55E /* PBXContainerItemProxy */; }; - DB7FF07128853B1F00BFD55E /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = DB94B6BA26C65CB000A2E8A1 /* AppShared */; - targetProxy = DB7FF07028853B1F00BFD55E /* PBXContainerItemProxy */; - }; - DB94B6C026C65CB000A2E8A1 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = DB94B6BA26C65CB000A2E8A1 /* AppShared */; - targetProxy = DB94B6BF26C65CB000A2E8A1 /* PBXContainerItemProxy */; - }; - DBBBBE742744EC27007ACB4B /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = DB94B6BA26C65CB000A2E8A1 /* AppShared */; - targetProxy = DBBBBE732744EC27007ACB4B /* PBXContainerItemProxy */; - }; DBBBBE822744F39C007ACB4B /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = DB94B6BA26C65CB000A2E8A1 /* AppShared */; targetProxy = DBBBBE812744F39C007ACB4B /* PBXContainerItemProxy */; }; DBBBBE892744F535007ACB4B /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = DB94B6BA26C65CB000A2E8A1 /* AppShared */; targetProxy = DBBBBE882744F535007ACB4B /* PBXContainerItemProxy */; }; DBDA8E3624FCF8A7006750DC /* PBXTargetDependency */ = { @@ -3450,11 +3262,6 @@ target = DBFDCE3F27F450FC00BE99E3 /* TwidereXIntent */; targetProxy = DBFDCE4627F450FC00BE99E3 /* PBXContainerItemProxy */; }; - DBFDCE5727F451FC00BE99E3 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = DB94B6BA26C65CB000A2E8A1 /* AppShared */; - targetProxy = DBFDCE5627F451FC00BE99E3 /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -3682,41 +3489,6 @@ }; name = Profile; }; - DB2F180A282B54F30001C6A8 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 8602431405CD5A4D45224B80 /* Pods-AppShared.profile.xcconfig */; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 118; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 7LFDZ96332; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 118; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = ShareExtension/Info.plist; - INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MARKETING_VERSION = 1.2.0; - PRODUCT_BUNDLE_IDENTIFIER = com.twidere.AppShared; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; DB2F180B282B54F30001C6A8 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { @@ -3865,75 +3637,6 @@ }; name = Release; }; - DB94B6C426C65CB100A2E8A1 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 5635061D4E24E9A2C69E90ED /* Pods-AppShared.debug.xcconfig */; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 118; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 7LFDZ96332; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 118; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = ShareExtension/Info.plist; - INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MARKETING_VERSION = 1.2.0; - PRODUCT_BUNDLE_IDENTIFIER = com.twidere.AppShared; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - DB94B6C526C65CB100A2E8A1 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 06C6C02930FE7CBB4E13D99E /* Pods-AppShared.release.xcconfig */; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 118; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 7LFDZ96332; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 118; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = ShareExtension/Info.plist; - INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MARKETING_VERSION = 1.2.0; - PRODUCT_BUNDLE_IDENTIFIER = com.twidere.AppShared; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; DBBBBE6A2744E8CC007ACB4B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -4342,16 +4045,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - DB94B6C326C65CB100A2E8A1 /* Build configuration list for PBXNativeTarget "AppShared" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - DB94B6C426C65CB100A2E8A1 /* Debug */, - DB2F180A282B54F30001C6A8 /* Profile */, - DB94B6C526C65CB100A2E8A1 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; DBBBBE692744E8CC007ACB4B /* Build configuration list for PBXNativeTarget "ShareExtension" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -4414,78 +4107,22 @@ }; /* End XCConfigurationList section */ -/* Begin XCRemoteSwiftPackageReference section */ - DB2611B5251B2D42004BF309 /* XCRemoteSwiftPackageReference "CryptoSwift" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/krzyzanowskim/CryptoSwift.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.3.2; - }; - }; - DB6CF1C2269715CD001DE069 /* XCRemoteSwiftPackageReference "FPSIndicator" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/MainasuK/FPSIndicator.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.0; - }; - }; - DB7AB33C2744E3740035EB8A /* XCRemoteSwiftPackageReference "Floaty" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/kciter/Floaty.git"; - requirement = { - branch = master; - kind = branch; - }; - }; - DBD98487251CB88A00ED87A1 /* XCRemoteSwiftPackageReference "Tabman" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/uias/Tabman.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 2.9.1; - }; - }; - DBEA4F822511F7460007FEC5 /* XCRemoteSwiftPackageReference "Kanna" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/tid-kijyun/Kanna.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 5.2.2; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - /* Begin XCSwiftPackageProductDependency section */ - DB2611B6251B2D42004BF309 /* CryptoSwift */ = { + DB05E13629C318530055BF3F /* TwidereSDK */ = { isa = XCSwiftPackageProductDependency; - package = DB2611B5251B2D42004BF309 /* XCRemoteSwiftPackageReference "CryptoSwift" */; - productName = CryptoSwift; - }; - DB6CF1C3269715CD001DE069 /* FPSIndicator */ = { - isa = XCSwiftPackageProductDependency; - package = DB6CF1C2269715CD001DE069 /* XCRemoteSwiftPackageReference "FPSIndicator" */; - productName = FPSIndicator; - }; - DB7AB33D2744E3740035EB8A /* Floaty */ = { - isa = XCSwiftPackageProductDependency; - package = DB7AB33C2744E3740035EB8A /* XCRemoteSwiftPackageReference "Floaty" */; - productName = Floaty; + productName = TwidereSDK; }; - DBB0E3C22760FB1200F1D45F /* TwidereSDK */ = { + DB05E13829C318590055BF3F /* TwidereSDK */ = { isa = XCSwiftPackageProductDependency; productName = TwidereSDK; }; - DBD98488251CB88A00ED87A1 /* Tabman */ = { + DB05E13A29C3185E0055BF3F /* TwidereSDK */ = { isa = XCSwiftPackageProductDependency; - package = DBD98487251CB88A00ED87A1 /* XCRemoteSwiftPackageReference "Tabman" */; - productName = Tabman; + productName = TwidereSDK; }; - DBEA4F832511F7460007FEC5 /* Kanna */ = { + DB05E13C29C321960055BF3F /* TwidereSDK */ = { isa = XCSwiftPackageProductDependency; - package = DBEA4F822511F7460007FEC5 /* XCRemoteSwiftPackageReference "Kanna" */; - productName = Kanna; + productName = TwidereSDK; }; DBFADA882872BEDA00B512D6 /* CoverFlowStackCollectionViewLayout */ = { isa = XCSwiftPackageProductDependency; diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index cd75096d..eede46b6 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -42,8 +42,8 @@ "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git", "state": { "branch": null, - "revision": "039f56c5d7960f277087a0be51f5eb04ed0ec073", - "version": "1.5.1" + "revision": "19b3c3ceed117c5cc883517c4e658548315ba70b", + "version": "1.6.0" } }, { @@ -141,8 +141,8 @@ "repositoryURL": "https://github.com/uias/Pageboy", "state": { "branch": null, - "revision": "34ecb6e7c4e0e07494960ab2f7cc9a02293915a6", - "version": "3.6.2" + "revision": "5522aa6ae88633f6c23cf504e9cd684e963822f1", + "version": "4.0.2" } }, { @@ -213,8 +213,8 @@ "repositoryURL": "https://github.com/uias/Tabman.git", "state": { "branch": null, - "revision": "a9f10cb862a32e6a22549836af013abd6b0692d3", - "version": "2.12.0" + "revision": "6a87a2825939f5da06cf52a6582d67a646488cff", + "version": "3.0.1" } }, { diff --git a/TwidereX/Coordinator/SceneCoordinator.swift b/TwidereX/Coordinator/SceneCoordinator.swift index b80797fe..d14ba4ad 100644 --- a/TwidereX/Coordinator/SceneCoordinator.swift +++ b/TwidereX/Coordinator/SceneCoordinator.swift @@ -11,7 +11,6 @@ import UIKit import Combine import SafariServices import CoreDataStack -import TwidereUI final public class SceneCoordinator { diff --git a/TwidereX/Diffable/Misc/CoverFlowStack/CoverFlowStackSection.swift b/TwidereX/Diffable/Misc/CoverFlowStack/CoverFlowStackSection.swift index 8ccb1512..7cec900a 100644 --- a/TwidereX/Diffable/Misc/CoverFlowStack/CoverFlowStackSection.swift +++ b/TwidereX/Diffable/Misc/CoverFlowStack/CoverFlowStackSection.swift @@ -7,6 +7,7 @@ // import UIKit +import TwidereCore enum CoverFlowStackSection: Hashable { case main diff --git a/TwidereX/Diffable/Misc/History/HistorySection.swift b/TwidereX/Diffable/Misc/History/HistorySection.swift index 3fbef2f1..9dba71d6 100644 --- a/TwidereX/Diffable/Misc/History/HistorySection.swift +++ b/TwidereX/Diffable/Misc/History/HistorySection.swift @@ -12,8 +12,6 @@ import Combine import CoreData import CoreDataStack import MetaTextKit -import TwidereUI -import AppShared import TwitterSDK enum HistorySection: Hashable { diff --git a/TwidereX/Diffable/Misc/List/ListSection.swift b/TwidereX/Diffable/Misc/List/ListSection.swift index 88fde9e3..2a6e24f4 100644 --- a/TwidereX/Diffable/Misc/List/ListSection.swift +++ b/TwidereX/Diffable/Misc/List/ListSection.swift @@ -9,7 +9,6 @@ import UIKit import Meta import TwidereCore -import TwidereUI enum ListSection: Hashable { case twitter(kind: TwitterListKind) diff --git a/TwidereX/Diffable/Misc/Notification/NotificationSection.swift b/TwidereX/Diffable/Misc/Notification/NotificationSection.swift index b9950b18..49f333b0 100644 --- a/TwidereX/Diffable/Misc/Notification/NotificationSection.swift +++ b/TwidereX/Diffable/Misc/Notification/NotificationSection.swift @@ -8,8 +8,6 @@ import UIKit -import AppShared -import TwidereUI enum NotificationSection: Hashable { case main diff --git a/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift b/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift index 490e5a72..08c6fbc9 100644 --- a/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift +++ b/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift @@ -9,7 +9,6 @@ import UIKit import TwidereAsset import TwidereLocalization -import TwidereUI enum TabBarItem: Int, Hashable { case home diff --git a/TwidereX/Diffable/Status/StatusSection.swift b/TwidereX/Diffable/Status/StatusSection.swift index 219c5fc6..eb47d6f6 100644 --- a/TwidereX/Diffable/Status/StatusSection.swift +++ b/TwidereX/Diffable/Status/StatusSection.swift @@ -13,8 +13,6 @@ import SwiftUI import CoreData import CoreDataStack import MetaTextKit -import TwidereUI -import AppShared import TwitterSDK enum StatusSection: Hashable { diff --git a/TwidereX/Diffable/User/UserItem.swift b/TwidereX/Diffable/User/UserItem.swift index b26f468f..07e8c151 100644 --- a/TwidereX/Diffable/User/UserItem.swift +++ b/TwidereX/Diffable/User/UserItem.swift @@ -9,7 +9,6 @@ import Foundation import CoreDataStack import TwidereCore -import TwidereUI enum UserItem: Hashable { case authenticationIndex(record: ManagedObjectRecord) diff --git a/TwidereX/Diffable/User/UserSection.swift b/TwidereX/Diffable/User/UserSection.swift index 4ec55385..0b009044 100644 --- a/TwidereX/Diffable/User/UserSection.swift +++ b/TwidereX/Diffable/User/UserSection.swift @@ -9,7 +9,6 @@ import UIKit import Combine import CoreDataStack -import TwidereUI enum UserSection { case main diff --git a/TwidereX/Extension/AVPlayer.swift b/TwidereX/Extension/AVPlayer.swift index 208eda49..33f12b11 100644 --- a/TwidereX/Extension/AVPlayer.swift +++ b/TwidereX/Extension/AVPlayer.swift @@ -6,6 +6,7 @@ // Copyright © 2020 Twidere. All rights reserved. // +import UIKit import AVKit // MARK: - CustomDebugStringConvertible diff --git a/TwidereX/Extension/UILabel.swift b/TwidereX/Extension/UILabel.swift index 7313c98f..db8cb155 100644 --- a/TwidereX/Extension/UILabel.swift +++ b/TwidereX/Extension/UILabel.swift @@ -7,6 +7,7 @@ // import UIKit +import TwidereCore extension UILabel { diff --git a/TwidereX/Extension/UIViewController.swift b/TwidereX/Extension/UIViewController.swift index 9695e480..2dd5c435 100644 --- a/TwidereX/Extension/UIViewController.swift +++ b/TwidereX/Extension/UIViewController.swift @@ -7,6 +7,7 @@ // import UIKit +import TwidereCore extension UIViewController { diff --git a/TwidereX/Generated/AppIconAssets.swift b/TwidereX/Generated/AppIconAssets.swift index 62c81ef1..6ef423b9 100644 --- a/TwidereX/Generated/AppIconAssets.swift +++ b/TwidereX/Generated/AppIconAssets.swift @@ -8,6 +8,9 @@ #elseif os(tvOS) || os(watchOS) import UIKit #endif +#if canImport(SwiftUI) + import SwiftUI +#endif // Deprecated typealiases @available(*, deprecated, renamed: "ColorAsset.Color", message: "This typealias will be removed in SwiftGen 7.0") @@ -71,6 +74,13 @@ internal final class ColorAsset { } #endif + #if canImport(SwiftUI) + @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + internal private(set) lazy var swiftUIColor: SwiftUI.Color = { + SwiftUI.Color(asset: self) + }() + #endif + fileprivate init(name: String) { self.name = name } @@ -90,6 +100,16 @@ internal extension ColorAsset.Color { } } +#if canImport(SwiftUI) +@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) +internal extension SwiftUI.Color { + init(asset: ColorAsset) { + let bundle = BundleToken.bundle + self.init(asset.name, bundle: bundle) + } +} +#endif + internal struct ImageAsset { internal fileprivate(set) var name: String @@ -126,6 +146,13 @@ internal struct ImageAsset { return result } #endif + + #if canImport(SwiftUI) + @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + internal var swiftUIImage: SwiftUI.Image { + SwiftUI.Image(asset: self) + } + #endif } internal extension ImageAsset.Image { @@ -144,6 +171,26 @@ internal extension ImageAsset.Image { } } +#if canImport(SwiftUI) +@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) +internal extension SwiftUI.Image { + init(asset: ImageAsset) { + let bundle = BundleToken.bundle + self.init(asset.name, bundle: bundle) + } + + init(asset: ImageAsset, label: Text) { + let bundle = BundleToken.bundle + self.init(asset.name, bundle: bundle, label: label) + } + + init(decorative asset: ImageAsset) { + let bundle = BundleToken.bundle + self.init(decorative: asset.name, bundle: bundle) + } +} +#endif + // swiftlint:disable convenience_type private final class BundleToken { static let bundle: Bundle = { diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Friendship.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Friendship.swift index ec433444..41a19c74 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Friendship.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Friendship.swift @@ -12,7 +12,6 @@ import CoreDataStack import TwidereCore import TwitterSDK import MastodonSDK -import TwidereUI import SwiftMessages extension DataSourceFacade { diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift index eb5eb430..22295cbc 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift @@ -9,7 +9,6 @@ import UIKit import AVKit import TwidereCore -import TwidereUI extension DataSourceFacade { diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Poll.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Poll.swift index 3beba2d6..ef5e6333 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Poll.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Poll.swift @@ -7,7 +7,7 @@ // import UIKit -import TwidereUI +import TwidereCore import CoreDataStack extension DataSourceFacade { diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Profile.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Profile.swift index f1d24520..f413c261 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Profile.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Profile.swift @@ -7,8 +7,8 @@ // import Foundation -import TwidereCore import CoreDataStack +import TwidereCore extension DataSourceFacade { diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Report.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Report.swift index 07534c06..83bf523a 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Report.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Report.swift @@ -10,7 +10,6 @@ import UIKit import TwidereAsset import TwidereLocalization import SwiftMessages -import TwidereUI extension DataSourceFacade { @MainActor diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift index 46b4c824..ab729f05 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift @@ -8,9 +8,6 @@ import os.log import UIKit -import AppShared -import TwidereCore -import TwidereUI import SwiftMessages extension DataSourceFacade { diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Translate.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Translate.swift index 3ece9937..1c6736f9 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Translate.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Translate.swift @@ -8,7 +8,6 @@ import os.log import Foundation -import TwidereCommon import TwidereCore import MastodonMeta diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift index b5b240c6..93069799 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift @@ -8,10 +8,6 @@ import UIKit import MetaTextArea -import TwidereCommon -import TwidereCore -import TwidereUI -import AppShared import MetaTextKit import MetaLabel diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift index 3457a957..b34aa9db 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift @@ -7,8 +7,6 @@ // import UIKit -import AppShared -import TwidereUI import MetaTextArea import Meta diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift index 3278820a..1b79e797 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -7,7 +7,6 @@ // import UIKit -import TwidereUI import Photos extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvider { diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift index c7f26789..eb31ebfc 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift @@ -7,7 +7,6 @@ // import UIKit -import TwidereUI import SwiftMessages // MARK: - menu button diff --git a/TwidereX/Scene/Account/List/AccountListViewController.swift b/TwidereX/Scene/Account/List/AccountListViewController.swift index 076ff230..8602e861 100644 --- a/TwidereX/Scene/Account/List/AccountListViewController.swift +++ b/TwidereX/Scene/Account/List/AccountListViewController.swift @@ -12,7 +12,6 @@ import AuthenticationServices import Combine import CoreDataStack import TwitterSDK -import TwidereCommon final class AccountListViewController: UIViewController, NeedsDependency { diff --git a/TwidereX/Scene/Account/List/AccountListViewModel+Diffable.swift b/TwidereX/Scene/Account/List/AccountListViewModel+Diffable.swift index 77029388..3df0efc2 100644 --- a/TwidereX/Scene/Account/List/AccountListViewModel+Diffable.swift +++ b/TwidereX/Scene/Account/List/AccountListViewModel+Diffable.swift @@ -10,6 +10,7 @@ import UIKit import Combine import CoreDataStack import AlamofireImage +import TwidereCore extension AccountListViewModel { diff --git a/TwidereX/Scene/Account/List/AccountListViewModel.swift b/TwidereX/Scene/Account/List/AccountListViewModel.swift index 057f6dcc..c5fbe9bb 100644 --- a/TwidereX/Scene/Account/List/AccountListViewModel.swift +++ b/TwidereX/Scene/Account/List/AccountListViewModel.swift @@ -11,6 +11,7 @@ import UIKit import Combine import CoreData import CoreDataStack +import TwidereCore final class AccountListViewModel: NSObject { diff --git a/TwidereX/Scene/Account/List/View/AccountListTableViewCell.swift b/TwidereX/Scene/Account/List/View/AccountListTableViewCell.swift index fbf39be2..98cc56f2 100644 --- a/TwidereX/Scene/Account/List/View/AccountListTableViewCell.swift +++ b/TwidereX/Scene/Account/List/View/AccountListTableViewCell.swift @@ -9,7 +9,7 @@ import os.log import UIKit import Combine -import TwidereUI +import TwidereCore final class AccountListTableViewCell: UITableViewCell { diff --git a/TwidereX/Scene/Account/TwitterAccountUnlock/TwitterAccountUnlockViewController.swift b/TwidereX/Scene/Account/TwitterAccountUnlock/TwitterAccountUnlockViewController.swift index c6eefcc4..3f70726a 100644 --- a/TwidereX/Scene/Account/TwitterAccountUnlock/TwitterAccountUnlockViewController.swift +++ b/TwidereX/Scene/Account/TwitterAccountUnlock/TwitterAccountUnlockViewController.swift @@ -8,6 +8,7 @@ import UIKit import WebKit +import TwidereCore final class TwitterAccountUnlockViewController: UIViewController, NeedsDependency { diff --git a/TwidereX/Scene/Compose/ComposeViewController.swift b/TwidereX/Scene/Compose/ComposeViewController.swift index c3f7f651..7c41a959 100644 --- a/TwidereX/Scene/Compose/ComposeViewController.swift +++ b/TwidereX/Scene/Compose/ComposeViewController.swift @@ -10,7 +10,6 @@ import os.log import UIKit import Combine import AVKit -import TwidereUI final class ComposeViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { diff --git a/TwidereX/Scene/Compose/ComposeViewModel.swift b/TwidereX/Scene/Compose/ComposeViewModel.swift index 7f47f77d..be5d7abd 100644 --- a/TwidereX/Scene/Compose/ComposeViewModel.swift +++ b/TwidereX/Scene/Compose/ComposeViewModel.swift @@ -9,7 +9,7 @@ import Foundation import Combine import TwidereCore -import TwidereUI +import TwidereLocalization final class ComposeViewModel { diff --git a/TwidereX/Scene/History/Status/StatusHistoryViewController.swift b/TwidereX/Scene/History/Status/StatusHistoryViewController.swift index 61be74eb..3509fcb0 100644 --- a/TwidereX/Scene/History/Status/StatusHistoryViewController.swift +++ b/TwidereX/Scene/History/Status/StatusHistoryViewController.swift @@ -10,7 +10,6 @@ import os.log import UIKit import Combine import CoreDataStack -import TwidereUI final class StatusHistoryViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { diff --git a/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift b/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift index e4309c1e..29a088ad 100644 --- a/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift +++ b/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift @@ -8,7 +8,6 @@ import UIKit import Combine -import AppShared extension StatusHistoryViewModel { diff --git a/TwidereX/Scene/History/Status/StatusHistoryViewModel.swift b/TwidereX/Scene/History/Status/StatusHistoryViewModel.swift index 3924da0b..999b1493 100644 --- a/TwidereX/Scene/History/Status/StatusHistoryViewModel.swift +++ b/TwidereX/Scene/History/Status/StatusHistoryViewModel.swift @@ -12,7 +12,6 @@ import Combine import CoreDataStack import MastodonSDK import TwidereCore -import TwidereUI final class StatusHistoryViewModel { diff --git a/TwidereX/Scene/History/User/UserHistoryViewController.swift b/TwidereX/Scene/History/User/UserHistoryViewController.swift index ba6ce514..519baef5 100644 --- a/TwidereX/Scene/History/User/UserHistoryViewController.swift +++ b/TwidereX/Scene/History/User/UserHistoryViewController.swift @@ -10,7 +10,6 @@ import os.log import UIKit import Combine import CoreDataStack -import TwidereUI final class UserHistoryViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { diff --git a/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift b/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift index 1d33e374..3bc1aff5 100644 --- a/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift +++ b/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift @@ -8,7 +8,6 @@ import UIKit import Combine -import AppShared extension UserHistoryViewModel { diff --git a/TwidereX/Scene/History/User/UserHistoryViewModel.swift b/TwidereX/Scene/History/User/UserHistoryViewModel.swift index 304bdf08..ee6f5ffc 100644 --- a/TwidereX/Scene/History/User/UserHistoryViewModel.swift +++ b/TwidereX/Scene/History/User/UserHistoryViewModel.swift @@ -12,7 +12,6 @@ import Combine import CoreDataStack import MastodonSDK import TwidereCore -import TwidereUI final class UserHistoryViewModel { diff --git a/TwidereX/Scene/List/CompositeList/CompositeListViewController.swift b/TwidereX/Scene/List/CompositeList/CompositeListViewController.swift index ea99fdbd..64a74dc0 100644 --- a/TwidereX/Scene/List/CompositeList/CompositeListViewController.swift +++ b/TwidereX/Scene/List/CompositeList/CompositeListViewController.swift @@ -10,7 +10,6 @@ import os.log import UIKit import Combine import TwidereLocalization -import TwidereUI import TwidereCore class CompositeListViewController: UIViewController, NeedsDependency { diff --git a/TwidereX/Scene/List/EditList/EditListView.swift b/TwidereX/Scene/List/EditList/EditListView.swift index 42941b52..ef56e7ae 100644 --- a/TwidereX/Scene/List/EditList/EditListView.swift +++ b/TwidereX/Scene/List/EditList/EditListView.swift @@ -8,6 +8,7 @@ import SwiftUI import TwidereLocalization +import TwidereCore struct EditListView: View { diff --git a/TwidereX/Scene/List/ListUser/ListUserViewController.swift b/TwidereX/Scene/List/ListUser/ListUserViewController.swift index 9a8fe5e2..42891947 100644 --- a/TwidereX/Scene/List/ListUser/ListUserViewController.swift +++ b/TwidereX/Scene/List/ListUser/ListUserViewController.swift @@ -10,7 +10,6 @@ import os.log import UIKit import Combine import CoreDataStack -import TwidereUI import SwiftMessages final class ListUserViewController: UIViewController, NeedsDependency { diff --git a/TwidereX/Scene/List/ListUser/ListUserViewModel+Diffable.swift b/TwidereX/Scene/List/ListUser/ListUserViewModel+Diffable.swift index 1c4be627..51619179 100644 --- a/TwidereX/Scene/List/ListUser/ListUserViewModel+Diffable.swift +++ b/TwidereX/Scene/List/ListUser/ListUserViewModel+Diffable.swift @@ -8,7 +8,6 @@ import UIKit import Combine -import TwidereUI extension ListUserViewModel { diff --git a/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift b/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift index 27074e12..330dfa1f 100644 --- a/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift +++ b/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift @@ -11,7 +11,6 @@ import UIKit import Combine import CoreData import CoreDataStack -import AppShared import AlamofireImage import Kingfisher import Pageboy diff --git a/TwidereX/Scene/MediaPreview/MediaPreviewViewModel.swift b/TwidereX/Scene/MediaPreview/MediaPreviewViewModel.swift index 9e245ceb..1b33ad6d 100644 --- a/TwidereX/Scene/MediaPreview/MediaPreviewViewModel.swift +++ b/TwidereX/Scene/MediaPreview/MediaPreviewViewModel.swift @@ -12,7 +12,6 @@ import CoreData import CoreDataStack import Pageboy import TwidereCore -import TwidereUI final class MediaPreviewViewModel: NSObject { diff --git a/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView+ViewModel.swift b/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView+ViewModel.swift index 46d1bbc8..6af25d85 100644 --- a/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView+ViewModel.swift +++ b/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView+ViewModel.swift @@ -10,8 +10,6 @@ import os.log import UIKit import Combine import CoreDataStack -import AppShared -import TwidereCore import TwitterMeta import MastodonMeta import Meta diff --git a/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView.swift b/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView.swift index b6927553..846feb8c 100644 --- a/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView.swift +++ b/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView.swift @@ -12,7 +12,6 @@ import Combine import MetaTextKit import MetaTextArea import MetaLabel -import TwidereUI protocol MediaInfoDescriptionViewDelegate: AnyObject { func mediaInfoDescriptionView(_ mediaInfoDescriptionView: MediaInfoDescriptionView, avatarButtonDidPressed button: UIButton) diff --git a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift index 3679d18b..a2343a77 100644 --- a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift +++ b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift @@ -9,7 +9,6 @@ import os.log import UIKit import Combine -import TwidereUI final class NotificationTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { diff --git a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift index 95380ad8..875eb763 100644 --- a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift @@ -12,7 +12,6 @@ import CoreData import CoreDataStack import TwitterSDK import MastodonSDK -import AppShared extension NotificationTimelineViewModel { diff --git a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift index 5a60a03b..1b5505f3 100644 --- a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift +++ b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift @@ -13,7 +13,6 @@ import CoreDataStack import GameplayKit import MastodonSDK import TwidereCore -import TwidereUI final class NotificationTimelineViewModel { diff --git a/TwidereX/Scene/Onboarding/Welcome/Mastodon/MastodonAuthenticationController.swift b/TwidereX/Scene/Onboarding/Welcome/Mastodon/MastodonAuthenticationController.swift index 89276fbd..08ba2e66 100644 --- a/TwidereX/Scene/Onboarding/Welcome/Mastodon/MastodonAuthenticationController.swift +++ b/TwidereX/Scene/Onboarding/Welcome/Mastodon/MastodonAuthenticationController.swift @@ -12,9 +12,7 @@ import Combine import CoreDataStack import AuthenticationServices import WebKit -import AppShared import MastodonSDK -import TwidereCommon final class MastodonAuthenticationController: NeedsDependency { diff --git a/TwidereX/Scene/Onboarding/Welcome/Twitter/Option/TwitterAuthenticationOptionViewController.swift b/TwidereX/Scene/Onboarding/Welcome/Twitter/Option/TwitterAuthenticationOptionViewController.swift index 50016c36..1a4b14f4 100644 --- a/TwidereX/Scene/Onboarding/Welcome/Twitter/Option/TwitterAuthenticationOptionViewController.swift +++ b/TwidereX/Scene/Onboarding/Welcome/Twitter/Option/TwitterAuthenticationOptionViewController.swift @@ -9,10 +9,8 @@ import os.log import UIKit import Combine -import AppShared import AuthenticationServices import TwitterSDK -import TwidereUI final class TwitterAuthenticationOptionViewController: UIViewController, NeedsDependency { diff --git a/TwidereX/Scene/Onboarding/Welcome/Twitter/Option/TwitterAuthenticationOptionViewModel.swift b/TwidereX/Scene/Onboarding/Welcome/Twitter/Option/TwitterAuthenticationOptionViewModel.swift index 7aafbafb..436ca72d 100644 --- a/TwidereX/Scene/Onboarding/Welcome/Twitter/Option/TwitterAuthenticationOptionViewModel.swift +++ b/TwidereX/Scene/Onboarding/Welcome/Twitter/Option/TwitterAuthenticationOptionViewModel.swift @@ -8,9 +8,7 @@ import UIKit import Combine -import AppShared import TwitterSDK -import TwidereCommon final class TwitterAuthenticationOptionViewModel: NSObject { diff --git a/TwidereX/Scene/Onboarding/Welcome/Twitter/TwitterAuthenticationController.swift b/TwidereX/Scene/Onboarding/Welcome/Twitter/TwitterAuthenticationController.swift index baade405..49397e73 100644 --- a/TwidereX/Scene/Onboarding/Welcome/Twitter/TwitterAuthenticationController.swift +++ b/TwidereX/Scene/Onboarding/Welcome/Twitter/TwitterAuthenticationController.swift @@ -13,9 +13,6 @@ import CoreDataStack import AuthenticationServices import WebKit import TwitterSDK -import TwidereCommon -import TwidereCore -import AppShared // Note: // use given AuthorizationContext to authorize user diff --git a/TwidereX/Scene/Onboarding/Welcome/WelcomeViewController.swift b/TwidereX/Scene/Onboarding/Welcome/WelcomeViewController.swift index c80075f6..0459002f 100644 --- a/TwidereX/Scene/Onboarding/Welcome/WelcomeViewController.swift +++ b/TwidereX/Scene/Onboarding/Welcome/WelcomeViewController.swift @@ -10,7 +10,6 @@ import os.log import UIKit import SwiftUI import Combine -import AppShared import TwitterSDK import MastodonSDK import AuthenticationServices diff --git a/TwidereX/Scene/Onboarding/Welcome/WelcomeViewModel.swift b/TwidereX/Scene/Onboarding/Welcome/WelcomeViewModel.swift index 1d46984a..a1ca8062 100644 --- a/TwidereX/Scene/Onboarding/Welcome/WelcomeViewModel.swift +++ b/TwidereX/Scene/Onboarding/Welcome/WelcomeViewModel.swift @@ -10,10 +10,8 @@ import os.log import Foundation import SwiftUI import Combine -import AppShared import TwitterSDK import MastodonSDK -import TwidereCommon protocol WelcomeViewModelDelegate: AnyObject { func presentTwitterAuthenticationOption() diff --git a/TwidereX/Scene/Profile/FriendshipList/FollowingList/FollowingListViewController.swift b/TwidereX/Scene/Profile/FriendshipList/FollowingList/FollowingListViewController.swift index 402ab604..42bc2756 100644 --- a/TwidereX/Scene/Profile/FriendshipList/FollowingList/FollowingListViewController.swift +++ b/TwidereX/Scene/Profile/FriendshipList/FollowingList/FollowingListViewController.swift @@ -10,7 +10,7 @@ import os.log import UIKit import Combine import GameplayKit -import TwidereUI +import TwidereCore import TwidereAsset import TwidereLocalization diff --git a/TwidereX/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift b/TwidereX/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift index c1722da6..aad0acad 100644 --- a/TwidereX/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift +++ b/TwidereX/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift @@ -12,8 +12,6 @@ import CoreDataStack import TwitterMeta import MastodonMeta import AlamofireImage -import AppShared -import TwidereCore extension ProfileHeaderView { class ViewModel: ObservableObject { @@ -270,7 +268,7 @@ extension ProfileHeaderView { user.publisher(for: \.bioEntities), UIContentSizeCategory.publisher ) - .map { _, _, _ in user.bioMetaContent(provider: OfficialTwitterTextProvider()) } + .map { _, _, _ in user.bioMetaContent(provider: SwiftTwitterTextProvider()) } .assign(to: \.bioMetaContent, on: viewModel) .store(in: &viewModel.configureDisposeBag) } @@ -285,7 +283,7 @@ extension ProfileHeaderView { var fields: [ProfileFieldListView.Item] = [] var index = 0 let now = Date() - if let value = user.urlMetaContent(provider: OfficialTwitterTextProvider()) { + if let value = user.urlMetaContent(provider: SwiftTwitterTextProvider()) { let item = ProfileFieldListView.Item( index: index, updateAt: now, @@ -296,7 +294,7 @@ extension ProfileHeaderView { fields.append(item) index += 1 } - if let value = user.locationMetaContent(provider: OfficialTwitterTextProvider()) { + if let value = user.locationMetaContent(provider: SwiftTwitterTextProvider()) { let item = ProfileFieldListView.Item( index: index, updateAt: now, diff --git a/TwidereX/Scene/Profile/Header/View/ProfileHeaderView.swift b/TwidereX/Scene/Profile/Header/View/ProfileHeaderView.swift index 65f026d8..19db8e54 100644 --- a/TwidereX/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/TwidereX/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -12,7 +12,6 @@ import Combine import MetaTextKit import MetaTextArea import MetaLabel -import TwidereUI protocol ProfileHeaderViewDelegate: AnyObject { func profileHeaderView(_ headerView: ProfileHeaderView, friendshipButtonPressed button: UIButton) diff --git a/TwidereX/Scene/Profile/ProfileViewController.swift b/TwidereX/Scene/Profile/ProfileViewController.swift index 04ba6906..b2445250 100644 --- a/TwidereX/Scene/Profile/ProfileViewController.swift +++ b/TwidereX/Scene/Profile/ProfileViewController.swift @@ -10,7 +10,6 @@ import UIKit import Combine import CoreData import CoreDataStack -import AppShared import Floaty import Meta import MetaTextKit @@ -18,7 +17,6 @@ import MetaTextArea import MetaLabel import TabBarPager import XLPagerTabStrip -import TwidereUI final class ProfileViewController: UIViewController, NeedsDependency, DrawerSidebarTransitionHostViewController { diff --git a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift index eaa1a294..e71a8679 100644 --- a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift +++ b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift @@ -10,7 +10,6 @@ import os.log import UIKit import Combine import AlamofireImage -import TwidereUI final class DrawerSidebarViewController: UIViewController, NeedsDependency { diff --git a/TwidereX/Scene/Root/Drawer/View/DrawerSidebarHeaderView+ViewModel.swift b/TwidereX/Scene/Root/Drawer/View/DrawerSidebarHeaderView+ViewModel.swift index ddd2812a..c2638956 100644 --- a/TwidereX/Scene/Root/Drawer/View/DrawerSidebarHeaderView+ViewModel.swift +++ b/TwidereX/Scene/Root/Drawer/View/DrawerSidebarHeaderView+ViewModel.swift @@ -9,7 +9,6 @@ import UIKit import Combine import MetaTextKit -import TwidereCommon import CoreDataStack import TwidereCore diff --git a/TwidereX/Scene/Root/Drawer/View/DrawerSidebarHeaderView.swift b/TwidereX/Scene/Root/Drawer/View/DrawerSidebarHeaderView.swift index be8c9049..cc29906d 100644 --- a/TwidereX/Scene/Root/Drawer/View/DrawerSidebarHeaderView.swift +++ b/TwidereX/Scene/Root/Drawer/View/DrawerSidebarHeaderView.swift @@ -11,7 +11,6 @@ import UIKit import TwidereCore import MetaTextKit import MetaLabel -import TwidereUI protocol DrawerSidebarHeaderViewDelegate: AnyObject { func drawerSidebarHeaderView(_ headerView: DrawerSidebarHeaderView, avatarButtonDidPressed button: UIButton) diff --git a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift index 484b915b..a5db5d55 100644 --- a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift +++ b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift @@ -13,8 +13,6 @@ import Combine import SafariServices import SwiftMessages import TwitterSDK -import TwidereUI -import TwidereCommon import func QuartzCore.CACurrentMediaTime final class MainTabBarController: UITabBarController, NeedsDependency { diff --git a/TwidereX/Scene/Root/Sidebar/SidebarView.swift b/TwidereX/Scene/Root/Sidebar/SidebarView.swift index a1eade73..54af0cdd 100644 --- a/TwidereX/Scene/Root/Sidebar/SidebarView.swift +++ b/TwidereX/Scene/Root/Sidebar/SidebarView.swift @@ -9,7 +9,6 @@ import SwiftUI import TwidereAsset import TwidereLocalization -import TwidereUI import func QuartzCore.CACurrentMediaTime struct SidebarView: View { diff --git a/TwidereX/Scene/Root/Sidebar/SidebarViewController.swift b/TwidereX/Scene/Root/Sidebar/SidebarViewController.swift index f7e7cc4a..e3b47305 100644 --- a/TwidereX/Scene/Root/Sidebar/SidebarViewController.swift +++ b/TwidereX/Scene/Root/Sidebar/SidebarViewController.swift @@ -10,7 +10,6 @@ import os.log import UIKit import SwiftUI import Combine -import TwidereUI final class SidebarViewController: UIViewController, NeedsDependency { diff --git a/TwidereX/Scene/Search/Search/SearchViewController.swift b/TwidereX/Scene/Search/Search/SearchViewController.swift index bdf64e3d..3d9853bf 100644 --- a/TwidereX/Scene/Search/Search/SearchViewController.swift +++ b/TwidereX/Scene/Search/Search/SearchViewController.swift @@ -10,7 +10,6 @@ import os.log import UIKit import Combine import TwidereLocalization -import TwidereUI // DrawerSidebarTransitionableViewController final class SearchViewController: UIViewController, NeedsDependency, DrawerSidebarTransitionHostViewController { diff --git a/TwidereX/Scene/Search/SearchResult/Hashtag/Cell/HashtagTableViewCell.swift b/TwidereX/Scene/Search/SearchResult/Hashtag/Cell/HashtagTableViewCell.swift index 99628e4c..428d436d 100644 --- a/TwidereX/Scene/Search/SearchResult/Hashtag/Cell/HashtagTableViewCell.swift +++ b/TwidereX/Scene/Search/SearchResult/Hashtag/Cell/HashtagTableViewCell.swift @@ -7,6 +7,7 @@ // import UIKit +import TwidereCore import MetaTextKit import MetaLabel diff --git a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+Diffable.swift b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+Diffable.swift index 737f403e..d47e9a1c 100644 --- a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+Diffable.swift +++ b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+Diffable.swift @@ -12,7 +12,6 @@ import CoreData import CoreDataStack import AlamofireImage import Kingfisher -import TwidereUI extension SearchUserViewModel { @MainActor func setupDiffableDataSource( diff --git a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel.swift b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel.swift index 687ad64c..fd67e318 100644 --- a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel.swift +++ b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel.swift @@ -14,7 +14,6 @@ import CoreDataStack import GameplayKit import TwitterSDK import TwidereCore -import TwidereUI final class SearchUserViewModel { diff --git a/TwidereX/Scene/Search/TrendPlace/TrendPlaceViewController.swift b/TwidereX/Scene/Search/TrendPlace/TrendPlaceViewController.swift index e0d227cb..ce09efce 100644 --- a/TwidereX/Scene/Search/TrendPlace/TrendPlaceViewController.swift +++ b/TwidereX/Scene/Search/TrendPlace/TrendPlaceViewController.swift @@ -10,6 +10,7 @@ import os.log import UIKit import SwiftUI import Combine +import TwidereCore final class TrendPlaceViewController: UIViewController, NeedsDependency { diff --git a/TwidereX/Scene/Setting/About/AboutView.swift b/TwidereX/Scene/Setting/About/AboutView.swift index 829f3355..50c93577 100644 --- a/TwidereX/Scene/Setting/About/AboutView.swift +++ b/TwidereX/Scene/Setting/About/AboutView.swift @@ -8,7 +8,6 @@ import SwiftUI import TwidereAsset -import TwidereUI struct AboutView: View { diff --git a/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceView.swift b/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceView.swift index 50fa7d4b..159b2bdb 100644 --- a/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceView.swift +++ b/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceView.swift @@ -8,7 +8,6 @@ import Foundation import SwiftUI -import TwidereUI enum AccountPreferenceListEntry: Hashable { case muted diff --git a/TwidereX/Scene/Setting/AppIconPreference/AppIconPreferenceView.swift b/TwidereX/Scene/Setting/AppIconPreference/AppIconPreferenceView.swift index 11c91f13..7878ca70 100644 --- a/TwidereX/Scene/Setting/AppIconPreference/AppIconPreferenceView.swift +++ b/TwidereX/Scene/Setting/AppIconPreference/AppIconPreferenceView.swift @@ -9,7 +9,7 @@ import os.log import Foundation import SwiftUI -import TwidereCommon +import TwidereCore struct AppIconPreferenceView: View { diff --git a/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceView.swift b/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceView.swift index dc712773..004f81d5 100644 --- a/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceView.swift +++ b/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceView.swift @@ -8,7 +8,6 @@ import SwiftUI import TwidereLocalization -import TwidereUI struct BehaviorsPreferenceView: View { diff --git a/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewModel.swift b/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewModel.swift index dc7c43e8..d748ddda 100644 --- a/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewModel.swift +++ b/TwidereX/Scene/Setting/BehaviorsPreference/BehaviorsPreferenceViewModel.swift @@ -11,7 +11,6 @@ import UIKit import SwiftUI import Combine import CoreDataStack -import TwidereCommon import TwidereCore import TwitterSDK import MastodonSDK diff --git a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceView.swift b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceView.swift index 93a177ba..3f5a6244 100644 --- a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceView.swift +++ b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceView.swift @@ -9,10 +9,6 @@ import Foundation import SwiftUI import Combine -import TwidereCore -import TwidereUI -import TwidereLocalization -import AppShared struct DisplayPreferenceView: View { diff --git a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewModel.swift b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewModel.swift index 05a3de4d..ff0a338f 100644 --- a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewModel.swift +++ b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewModel.swift @@ -9,10 +9,6 @@ import os.log import UIKit import Combine -import AppShared -import TwidereAsset -import TwidereLocalization -import TwidereUI import TwitterMeta final class DisplayPreferenceViewModel: ObservableObject { diff --git a/TwidereX/Scene/Setting/DisplayPreference/Translation/TranslateButtonPreferenceView.swift b/TwidereX/Scene/Setting/DisplayPreference/Translation/TranslateButtonPreferenceView.swift index 81f5b601..951e90b4 100644 --- a/TwidereX/Scene/Setting/DisplayPreference/Translation/TranslateButtonPreferenceView.swift +++ b/TwidereX/Scene/Setting/DisplayPreference/Translation/TranslateButtonPreferenceView.swift @@ -9,7 +9,6 @@ import os.log import Foundation import SwiftUI -import TwidereCommon struct TranslateButtonPreferenceView: View { diff --git a/TwidereX/Scene/Setting/DisplayPreference/Translation/TranslationServicePreferenceView.swift b/TwidereX/Scene/Setting/DisplayPreference/Translation/TranslationServicePreferenceView.swift index 48835457..85bff850 100644 --- a/TwidereX/Scene/Setting/DisplayPreference/Translation/TranslationServicePreferenceView.swift +++ b/TwidereX/Scene/Setting/DisplayPreference/Translation/TranslationServicePreferenceView.swift @@ -9,7 +9,6 @@ import os.log import Foundation import SwiftUI -import TwidereCommon struct TranslationServicePreferenceView: View { diff --git a/TwidereX/Scene/Setting/List/SettingListView.swift b/TwidereX/Scene/Setting/List/SettingListView.swift index 3981f32f..61b98b16 100644 --- a/TwidereX/Scene/Setting/List/SettingListView.swift +++ b/TwidereX/Scene/Setting/List/SettingListView.swift @@ -9,7 +9,6 @@ import CoreData import CoreDataStack import SwiftUI -import TwidereUI struct TextCaseEraseStyle: ViewModifier { func body(content: Content) -> some View { diff --git a/TwidereX/Scene/Share/View/Button/AvatarBarButtonItem+ViewModel.swift b/TwidereX/Scene/Share/View/Button/AvatarBarButtonItem+ViewModel.swift index b45749cd..635021c7 100644 --- a/TwidereX/Scene/Share/View/Button/AvatarBarButtonItem+ViewModel.swift +++ b/TwidereX/Scene/Share/View/Button/AvatarBarButtonItem+ViewModel.swift @@ -10,7 +10,6 @@ import UIKit import Combine import TwidereCore import CoreDataStack -import TwidereUI extension AvatarBarButtonItem { public class ViewModel: ObservableObject { diff --git a/TwidereX/Scene/Share/View/Button/AvatarBarButtonItem.swift b/TwidereX/Scene/Share/View/Button/AvatarBarButtonItem.swift index 3f99ee08..851a8656 100644 --- a/TwidereX/Scene/Share/View/Button/AvatarBarButtonItem.swift +++ b/TwidereX/Scene/Share/View/Button/AvatarBarButtonItem.swift @@ -9,7 +9,6 @@ import os.log import UIKit import Combine -import TwidereUI public protocol AvatarBarButtonItemDelegate: AnyObject { func avatarBarButtonItem(_ barButtonItem: AvatarBarButtonItem, didLongPressed sender: UILongPressGestureRecognizer) diff --git a/TwidereX/Scene/Share/View/Button/FollowActionButton.swift b/TwidereX/Scene/Share/View/Button/FollowActionButton.swift index 030b52c4..cbdcd265 100644 --- a/TwidereX/Scene/Share/View/Button/FollowActionButton.swift +++ b/TwidereX/Scene/Share/View/Button/FollowActionButton.swift @@ -7,6 +7,7 @@ // import UIKit +import TwidereCore final class FollowActionButton: UIButton { diff --git a/TwidereX/Scene/Share/View/CollectionViewCell/StatusMediaGalleryCollectionCell.swift b/TwidereX/Scene/Share/View/CollectionViewCell/StatusMediaGalleryCollectionCell.swift index 4a85be4a..0eeb5d4e 100644 --- a/TwidereX/Scene/Share/View/CollectionViewCell/StatusMediaGalleryCollectionCell.swift +++ b/TwidereX/Scene/Share/View/CollectionViewCell/StatusMediaGalleryCollectionCell.swift @@ -10,7 +10,6 @@ import os.log import UIKit import Combine import CoverFlowStackCollectionViewLayout -import TwidereUI protocol StatusMediaGalleryCollectionCellDelegate: AnyObject { func statusMediaGalleryCollectionCell(_ cell: StatusMediaGalleryCollectionCell, coverFlowCollectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell+ViewModel.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell+ViewModel.swift index d0df0aa4..6b4238fe 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell+ViewModel.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell+ViewModel.swift @@ -10,8 +10,6 @@ import UIKit import Combine import SwiftUI import CoreDataStack -import AppShared -import TwidereUI extension StatusTableViewCell { diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift index 627fad66..97cb5397 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift @@ -9,7 +9,6 @@ import os.log import UIKit import Combine -import TwidereUI class StatusTableViewCell: UITableViewCell { diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell+ViewModel.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell+ViewModel.swift index 69f58b26..bb5e88d5 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell+ViewModel.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell+ViewModel.swift @@ -10,9 +10,6 @@ import UIKit import Combine import SwiftUI import CoreDataStack -import TwidereCore -import AppShared -import TwidereUI extension StatusThreadRootTableViewCell { diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell.swift index d6fd996c..73239c05 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell.swift @@ -9,7 +9,6 @@ import os.log import UIKit import Combine -import TwidereUI final class StatusThreadRootTableViewCell: UITableViewCell { diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift index e6c61325..7fd8fcf1 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift @@ -7,7 +7,7 @@ // import UIKit -import TwidereUI +import TwidereCore import MetaTextArea import Meta diff --git a/TwidereX/Scene/Share/View/TableViewCell/TimelineLoaderTableViewCell.swift b/TwidereX/Scene/Share/View/TableViewCell/TimelineLoaderTableViewCell.swift index 576dfc1d..93146e52 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/TimelineLoaderTableViewCell.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/TimelineLoaderTableViewCell.swift @@ -8,6 +8,7 @@ import UIKit import Combine +import TwidereCore class TimelineLoaderTableViewCell: UITableViewCell { diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift index 00ec9c23..5884d838 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift @@ -11,7 +11,6 @@ import UIKit import Combine import CoreData import CoreDataStack -import AppShared extension StatusThreadViewModel { diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift index ce136152..8ea96f35 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift @@ -15,7 +15,6 @@ import MastodonSDK import CoreData import CoreDataStack import TwidereCore -import TwidereUI final class StatusThreadViewModel { diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift index dedcbd1d..2e4e2356 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift @@ -10,9 +10,6 @@ import os.log import UIKit import Combine import Floaty -import AppShared -import TwidereCore -import TwidereUI import TabBarPager class TimelineViewController: UIViewController, NeedsDependency, DrawerSidebarTransitionHostViewController, MediaPreviewableViewController { diff --git a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift index e530016a..4092ab97 100644 --- a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift @@ -10,8 +10,6 @@ import os.log import UIKit import Combine import Floaty -import AppShared -import TwidereCore import TabBarPager class ListTimelineViewController: TimelineViewController { diff --git a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift index 88d3f8ab..b2dd60b0 100644 --- a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift @@ -8,7 +8,6 @@ import UIKit import TwidereCore -import TwidereUI class ListTimelineViewModel: TimelineViewModel { diff --git a/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel+Diffable.swift index acfe7f30..face7188 100644 --- a/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel+Diffable.swift @@ -11,8 +11,6 @@ import UIKit import Combine import CoreData import CoreDataStack -import TwidereCore -import AppShared extension FederatedTimelineViewModel { diff --git a/TwidereX/Scene/Timeline/Hashtag/HashtagTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/Hashtag/HashtagTimelineViewModel+Diffable.swift index a2e8b6fc..432c8170 100644 --- a/TwidereX/Scene/Timeline/Hashtag/HashtagTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/Hashtag/HashtagTimelineViewModel+Diffable.swift @@ -11,8 +11,6 @@ import UIKit import Combine import CoreData import CoreDataStack -import TwidereCore -import AppShared extension HashtagTimelineViewModel { diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift index 9a01e603..c06c5ae4 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift @@ -13,10 +13,8 @@ import UIKit import CoreData import CoreDataStack import TwitterSDK -import ZIPFoundation import MetaTextKit import MetaTextArea -import TwidereUI import SwiftMessages extension HomeTimelineViewController { diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController.swift index 6665a7dd..51347bef 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController.swift @@ -9,7 +9,6 @@ import os.log import UIKit import Combine -import TwidereUI import TwidereLocalization final class HomeTimelineViewController: ListTimelineViewController { diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel+Diffable.swift index 57ba5a89..84caa1e8 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel+Diffable.swift @@ -11,7 +11,6 @@ import UIKit import Combine import CoreData import CoreDataStack -import AppShared extension HomeTimelineViewModel { diff --git a/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel+Diffable.swift index a1696efa..ade26ac1 100644 --- a/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel+Diffable.swift @@ -7,8 +7,6 @@ // import UIKit -import TwidereUI -import AppShared extension ListStatusTimelineViewModel { diff --git a/TwidereX/Scene/Timeline/Search/Media/SearchMediaTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/Search/Media/SearchMediaTimelineViewModel+Diffable.swift index 1027cc54..14620772 100644 --- a/TwidereX/Scene/Timeline/Search/Media/SearchMediaTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/Search/Media/SearchMediaTimelineViewModel+Diffable.swift @@ -10,6 +10,7 @@ import os.log import UIKit import CoreData import CoreDataStack +import TwidereCore extension SearchMediaTimelineViewModel { @MainActor func setupDiffableDataSource( diff --git a/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel+Diffable.swift index 7201d994..855cb060 100644 --- a/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel+Diffable.swift @@ -7,8 +7,6 @@ // import UIKit -import TwidereUI -import AppShared extension SearchTimelineViewModel { diff --git a/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel.swift b/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel.swift index adc3e383..108d75d2 100644 --- a/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Search/Status/SearchTimelineViewModel.swift @@ -9,7 +9,6 @@ import os.log import UIKit import TwidereCore -import TwidereUI final class SearchTimelineViewModel: ListTimelineViewModel { diff --git a/TwidereX/Scene/Timeline/User/Status/UserTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/User/Status/UserTimelineViewModel+Diffable.swift index bd938af0..6bf2a6b5 100644 --- a/TwidereX/Scene/Timeline/User/Status/UserTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/User/Status/UserTimelineViewModel+Diffable.swift @@ -11,8 +11,6 @@ import UIKit import Combine import CoreData import CoreDataStack -import TwidereCore -import AppShared extension UserTimelineViewModel { diff --git a/TwidereX/Scene/Transition/DrawerSidebar/DrawerSidebarAnimatedTransitioning.swift b/TwidereX/Scene/Transition/DrawerSidebar/DrawerSidebarAnimatedTransitioning.swift index 9d4ad1f8..99f099d1 100644 --- a/TwidereX/Scene/Transition/DrawerSidebar/DrawerSidebarAnimatedTransitioning.swift +++ b/TwidereX/Scene/Transition/DrawerSidebar/DrawerSidebarAnimatedTransitioning.swift @@ -8,7 +8,6 @@ import UIKit import CommonOSLog -import TwidereUI final class DrawerSidebarAnimatedTransitioning: ViewControllerAnimatedTransitioning { diff --git a/TwidereX/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift b/TwidereX/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift index 75eac63b..fbd5d623 100644 --- a/TwidereX/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift +++ b/TwidereX/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift @@ -7,7 +7,6 @@ // import UIKit -import TwidereUI class MediaPreviewTransitionItem: Identifiable { diff --git a/TwidereX/Supporting Files/AppDelegate.swift b/TwidereX/Supporting Files/AppDelegate.swift index a8dd65cd..22aa3428 100644 --- a/TwidereX/Supporting Files/AppDelegate.swift +++ b/TwidereX/Supporting Files/AppDelegate.swift @@ -14,8 +14,6 @@ import Firebase import FirebaseMessaging import FirebaseCrashlytics import Kingfisher -import AppShared -import TwidereCommon @_exported import TwidereUI diff --git a/TwidereX/Supporting Files/SceneDelegate.swift b/TwidereX/Supporting Files/SceneDelegate.swift index 7f59dc6b..983ce94a 100644 --- a/TwidereX/Supporting Files/SceneDelegate.swift +++ b/TwidereX/Supporting Files/SceneDelegate.swift @@ -11,8 +11,6 @@ import Combine import Intents import FPSIndicator import CoreDataStack -import TwidereCore -import AppShared class SceneDelegate: UIResponder, UIWindowSceneDelegate { diff --git a/TwidereXIntent/Handler/PublishPostIntentHandler.swift b/TwidereXIntent/Handler/PublishPostIntentHandler.swift index d75e6e01..8c732458 100644 --- a/TwidereXIntent/Handler/PublishPostIntentHandler.swift +++ b/TwidereXIntent/Handler/PublishPostIntentHandler.swift @@ -13,7 +13,6 @@ import CoreData import CoreDataStack import TwidereCore import TwidereCommon -import AppShared final class PublishPostIntentHandler: NSObject { diff --git a/TwidereXIntent/Handler/SwitchAccountIntentHandler.swift b/TwidereXIntent/Handler/SwitchAccountIntentHandler.swift index 39cea4b5..7e292bb2 100644 --- a/TwidereXIntent/Handler/SwitchAccountIntentHandler.swift +++ b/TwidereXIntent/Handler/SwitchAccountIntentHandler.swift @@ -12,7 +12,6 @@ import Intents import CoreData import CoreDataStack import TwidereCore -import TwidereCommon final class SwitchAccountIntentHandler: NSObject { diff --git a/TwidereXIntent/es.lproj/Intents.strings b/TwidereXIntent/es.lproj/Intents.strings index d1d20064..2ef31ab1 100644 --- a/TwidereXIntent/es.lproj/Intents.strings +++ b/TwidereXIntent/es.lproj/Intents.strings @@ -56,17 +56,17 @@ "aeaw8w" = "Directo"; -"ch7ADX" = "Compose new post"; +"ch7ADX" = "Crear una nueva publicación"; "f1YNIs" = "Publicar contenido"; "jEAHra" = "No listado"; -"kWl9zU" = "Post ${content}with ${accounts}"; +"kWl9zU" = "Publicar ${content} con ${accounts}"; "noeHVX" = "Publicación"; -"vbgnbQ" = "Post ${content}with ${accounts}"; +"vbgnbQ" = "Publicar ${content} con ${accounts}"; "xeqcBa-BS59z4" = "Hay ${count} opciones que coinciden con \"Público\"."; From 1999d3c61b52129020a91194e5cb6a3dcb1653f7 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 16 Mar 2023 18:43:36 +0800 Subject: [PATCH 044/128] chore: update i18n resources --- .../Sources/StringsConvertor/main.swift | 27 +- .../TwidereAsset/Generated/Assets.swift | 47 + .../Generated/Strings.swift | 1137 +++++++++-------- .../Resources/ar.lproj/Localizable.strings | 85 +- .../Resources/ca.lproj/Localizable.strings | 393 +++--- .../Resources/de.lproj/Localizable.strings | 7 +- .../Resources/en.lproj/Localizable.strings | 7 +- .../Resources/es.lproj/Localizable.strings | 31 +- .../es.lproj/Localizable.stringsdict | 4 +- .../Resources/eu.lproj/Localizable.strings | 7 +- .../Resources/gl.lproj/Localizable.strings | 45 +- .../Resources/ja.lproj/Localizable.strings | 71 +- .../ja.lproj/Localizable.stringsdict | 8 +- .../Resources/ko.lproj/Localizable.strings | 7 +- .../Resources/pt-BR.lproj/Localizable.strings | 45 +- .../Resources/tr.lproj/Localizable.strings | 11 +- .../zh-Hans.lproj/Localizable.strings | 7 +- TwidereXIntent/ar.lproj/Intents.strings | 2 +- TwidereXIntent/ca.lproj/Intents.strings | 2 +- TwidereXIntent/ja.lproj/Intents.strings | 64 +- 20 files changed, 1062 insertions(+), 945 deletions(-) diff --git a/Localization/StringsConvertor/Sources/StringsConvertor/main.swift b/Localization/StringsConvertor/Sources/StringsConvertor/main.swift index 8001b768..c0042445 100644 --- a/Localization/StringsConvertor/Sources/StringsConvertor/main.swift +++ b/Localization/StringsConvertor/Sources/StringsConvertor/main.swift @@ -52,19 +52,20 @@ class Helper { private func map(language: String) -> String? { switch language { - case "ar_SA": return "ar" // Arabic - case "eu_ES": return "eu" // Basque - case "en_US": return "en" - case "zh_CN": return "zh-Hans" // Chinese Simplified - case "ja_JP": return "ja" // Japanese - case "gl_ES": return "gl" // Galician - case "de_DE": return "de" // German - case "pt_BR": return "pt-BR" // Brazilian Portuguese - case "ca_ES": return "ca" // Catalan - case "es_ES": return "es" // Spanish - case "ko_KR": return "ko" // Korean - case "tr_TR": return "tr" // Turkish - default: return nil + case "Base.lproj": return "Base" + case "ar_SA": return "ar" // Arabic + case "eu_ES": return "eu" // Basque + case "en_US": return "en" + case "zh_CN": return "zh-Hans" // Chinese Simplified + case "ja_JP": return "ja" // Japanese + case "gl_ES": return "gl" // Galician + case "de_DE": return "de" // German + case "pt_BR": return "pt-BR" // Brazilian Portuguese + case "ca_ES": return "ca" // Catalan + case "es_ES": return "es" // Spanish + case "ko_KR": return "ko" // Korean + case "tr_TR": return "tr" // Turkish + default: return nil } } diff --git a/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift b/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift index e677c34a..ee6fa613 100644 --- a/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift +++ b/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift @@ -8,6 +8,9 @@ #elseif os(tvOS) || os(watchOS) import UIKit #endif +#if canImport(SwiftUI) + import SwiftUI +#endif // Deprecated typealiases @available(*, deprecated, renamed: "ColorAsset.Color", message: "This typealias will be removed in SwiftGen 7.0") @@ -252,6 +255,13 @@ public final class ColorAsset { } #endif + #if canImport(SwiftUI) + @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + public private(set) lazy var swiftUIColor: SwiftUI.Color = { + SwiftUI.Color(asset: self) + }() + #endif + fileprivate init(name: String) { self.name = name } @@ -271,6 +281,16 @@ public extension ColorAsset.Color { } } +#if canImport(SwiftUI) +@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) +public extension SwiftUI.Color { + init(asset: ColorAsset) { + let bundle = Bundle.module + self.init(asset.name, bundle: bundle) + } +} +#endif + public struct ImageAsset { public fileprivate(set) var name: String @@ -307,6 +327,13 @@ public struct ImageAsset { return result } #endif + + #if canImport(SwiftUI) + @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + public var swiftUIImage: SwiftUI.Image { + SwiftUI.Image(asset: self) + } + #endif } public extension ImageAsset.Image { @@ -324,3 +351,23 @@ public extension ImageAsset.Image { #endif } } + +#if canImport(SwiftUI) +@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) +public extension SwiftUI.Image { + init(asset: ImageAsset) { + let bundle = Bundle.module + self.init(asset.name, bundle: bundle) + } + + init(asset: ImageAsset, label: Text) { + let bundle = Bundle.module + self.init(asset.name, bundle: bundle, label: label) + } + + init(decorative asset: ImageAsset) { + let bundle = Bundle.module + self.init(decorative: asset.name, bundle: bundle) + } +} +#endif diff --git a/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift b/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift index 5ac559d8..be26567f 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift +++ b/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift @@ -3,767 +3,767 @@ import Foundation -// swiftlint:disable superfluous_disable_command file_length implicit_return +// swiftlint:disable superfluous_disable_command file_length implicit_return prefer_self_in_static_references // MARK: - Strings // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces public enum L10n { - public enum Accessibility { public enum Common { /// Back - public static let back = L10n.tr("Localizable", "Accessibility.Common.Back") + public static let back = L10n.tr("Localizable", "Accessibility.Common.Back", fallback: "Back") /// Close - public static let close = L10n.tr("Localizable", "Accessibility.Common.Close") + public static let close = L10n.tr("Localizable", "Accessibility.Common.Close", fallback: "Close") /// Done - public static let done = L10n.tr("Localizable", "Accessibility.Common.Done") + public static let done = L10n.tr("Localizable", "Accessibility.Common.Done", fallback: "Done") /// More - public static let more = L10n.tr("Localizable", "Accessibility.Common.More") + public static let more = L10n.tr("Localizable", "Accessibility.Common.More", fallback: "More") /// Network Image - public static let networkImage = L10n.tr("Localizable", "Accessibility.Common.NetworkImage") + public static let networkImage = L10n.tr("Localizable", "Accessibility.Common.NetworkImage", fallback: "Network Image") public enum Logo { /// Github Logo - public static let github = L10n.tr("Localizable", "Accessibility.Common.Logo.Github") + public static let github = L10n.tr("Localizable", "Accessibility.Common.Logo.Github", fallback: "Github Logo") /// Mastodon Logo - public static let mastodon = L10n.tr("Localizable", "Accessibility.Common.Logo.Mastodon") + public static let mastodon = L10n.tr("Localizable", "Accessibility.Common.Logo.Mastodon", fallback: "Mastodon Logo") /// Telegram Logo - public static let telegram = L10n.tr("Localizable", "Accessibility.Common.Logo.Telegram") + public static let telegram = L10n.tr("Localizable", "Accessibility.Common.Logo.Telegram", fallback: "Telegram Logo") /// Twidere X logo - public static let twidere = L10n.tr("Localizable", "Accessibility.Common.Logo.Twidere") + public static let twidere = L10n.tr("Localizable", "Accessibility.Common.Logo.Twidere", fallback: "Twidere X logo") /// Twitter Logo - public static let twitter = L10n.tr("Localizable", "Accessibility.Common.Logo.Twitter") + public static let twitter = L10n.tr("Localizable", "Accessibility.Common.Logo.Twitter", fallback: "Twitter Logo") } public enum Status { /// Author avatar - public static let authorAvatar = L10n.tr("Localizable", "Accessibility.Common.Status.AuthorAvatar") + public static let authorAvatar = L10n.tr("Localizable", "Accessibility.Common.Status.AuthorAvatar", fallback: "Author avatar") /// Boosted - public static let boosted = L10n.tr("Localizable", "Accessibility.Common.Status.Boosted") + public static let boosted = L10n.tr("Localizable", "Accessibility.Common.Status.Boosted", fallback: "Boosted") /// Content Warning - public static let contentWarning = L10n.tr("Localizable", "Accessibility.Common.Status.ContentWarning") + public static let contentWarning = L10n.tr("Localizable", "Accessibility.Common.Status.ContentWarning", fallback: "Content Warning") /// Liked - public static let liked = L10n.tr("Localizable", "Accessibility.Common.Status.Liked") + public static let liked = L10n.tr("Localizable", "Accessibility.Common.Status.Liked", fallback: "Liked") /// Location - public static let location = L10n.tr("Localizable", "Accessibility.Common.Status.Location") + public static let location = L10n.tr("Localizable", "Accessibility.Common.Status.Location", fallback: "Location") /// Media - public static let media = L10n.tr("Localizable", "Accessibility.Common.Status.Media") + public static let media = L10n.tr("Localizable", "Accessibility.Common.Status.Media", fallback: "Media") /// Poll option - public static let pollOptionOrdinalPrefix = L10n.tr("Localizable", "Accessibility.Common.Status.PollOptionOrdinalPrefix") + public static let pollOptionOrdinalPrefix = L10n.tr("Localizable", "Accessibility.Common.Status.PollOptionOrdinalPrefix", fallback: "Poll option") /// Retweeted - public static let retweeted = L10n.tr("Localizable", "Accessibility.Common.Status.Retweeted") + public static let retweeted = L10n.tr("Localizable", "Accessibility.Common.Status.Retweeted", fallback: "Retweeted") public enum Actions { /// Hide content - public static let hideContent = L10n.tr("Localizable", "Accessibility.Common.Status.Actions.HideContent") + public static let hideContent = L10n.tr("Localizable", "Accessibility.Common.Status.Actions.HideContent", fallback: "Hide content") /// Hide media - public static let hideMedia = L10n.tr("Localizable", "Accessibility.Common.Status.Actions.HideMedia") + public static let hideMedia = L10n.tr("Localizable", "Accessibility.Common.Status.Actions.HideMedia", fallback: "Hide media") /// Like - public static let like = L10n.tr("Localizable", "Accessibility.Common.Status.Actions.Like") + public static let like = L10n.tr("Localizable", "Accessibility.Common.Status.Actions.Like", fallback: "Like") /// Menu - public static let menu = L10n.tr("Localizable", "Accessibility.Common.Status.Actions.Menu") + public static let menu = L10n.tr("Localizable", "Accessibility.Common.Status.Actions.Menu", fallback: "Menu") /// Reply - public static let reply = L10n.tr("Localizable", "Accessibility.Common.Status.Actions.Reply") + public static let reply = L10n.tr("Localizable", "Accessibility.Common.Status.Actions.Reply", fallback: "Reply") /// Retweet - public static let retweet = L10n.tr("Localizable", "Accessibility.Common.Status.Actions.Retweet") + public static let retweet = L10n.tr("Localizable", "Accessibility.Common.Status.Actions.Retweet", fallback: "Retweet") /// Reveal content - public static let revealContent = L10n.tr("Localizable", "Accessibility.Common.Status.Actions.RevealContent") + public static let revealContent = L10n.tr("Localizable", "Accessibility.Common.Status.Actions.RevealContent", fallback: "Reveal content") /// Reveal media - public static let revealMedia = L10n.tr("Localizable", "Accessibility.Common.Status.Actions.RevealMedia") + public static let revealMedia = L10n.tr("Localizable", "Accessibility.Common.Status.Actions.RevealMedia", fallback: "Reveal media") } } public enum Video { /// Play video - public static let play = L10n.tr("Localizable", "Accessibility.Common.Video.Play") + public static let play = L10n.tr("Localizable", "Accessibility.Common.Video.Play", fallback: "Play video") } } public enum Scene { public enum Compose { /// Add mention - public static let addMention = L10n.tr("Localizable", "Accessibility.Scene.Compose.AddMention") + public static let addMention = L10n.tr("Localizable", "Accessibility.Scene.Compose.AddMention", fallback: "Add mention") /// Open draft - public static let draft = L10n.tr("Localizable", "Accessibility.Scene.Compose.Draft") + public static let draft = L10n.tr("Localizable", "Accessibility.Scene.Compose.Draft", fallback: "Open draft") /// Add image - public static let image = L10n.tr("Localizable", "Accessibility.Scene.Compose.Image") + public static let image = L10n.tr("Localizable", "Accessibility.Scene.Compose.Image", fallback: "Add image") /// Send - public static let send = L10n.tr("Localizable", "Accessibility.Scene.Compose.Send") + public static let send = L10n.tr("Localizable", "Accessibility.Scene.Compose.Send", fallback: "Send") /// Thread mode - public static let thread = L10n.tr("Localizable", "Accessibility.Scene.Compose.Thread") + public static let thread = L10n.tr("Localizable", "Accessibility.Scene.Compose.Thread", fallback: "Thread mode") public enum Location { /// Disable location - public static let disable = L10n.tr("Localizable", "Accessibility.Scene.Compose.Location.Disable") + public static let disable = L10n.tr("Localizable", "Accessibility.Scene.Compose.Location.Disable", fallback: "Disable location") /// Enable location - public static let enable = L10n.tr("Localizable", "Accessibility.Scene.Compose.Location.Enable") + public static let enable = L10n.tr("Localizable", "Accessibility.Scene.Compose.Location.Enable", fallback: "Enable location") } public enum MediaInsert { /// Take Photo - public static let camera = L10n.tr("Localizable", "Accessibility.Scene.Compose.MediaInsert.Camera") + public static let camera = L10n.tr("Localizable", "Accessibility.Scene.Compose.MediaInsert.Camera", fallback: "Take Photo") /// Add GIF - public static let gif = L10n.tr("Localizable", "Accessibility.Scene.Compose.MediaInsert.Gif") + public static let gif = L10n.tr("Localizable", "Accessibility.Scene.Compose.MediaInsert.Gif", fallback: "Add GIF") /// Browse Library - public static let library = L10n.tr("Localizable", "Accessibility.Scene.Compose.MediaInsert.Library") + public static let library = L10n.tr("Localizable", "Accessibility.Scene.Compose.MediaInsert.Library", fallback: "Browse Library") /// Record Video - public static let recordVideo = L10n.tr("Localizable", "Accessibility.Scene.Compose.MediaInsert.RecordVideo") + public static let recordVideo = L10n.tr("Localizable", "Accessibility.Scene.Compose.MediaInsert.RecordVideo", fallback: "Record Video") } } public enum Gif { /// Search GIF - public static let search = L10n.tr("Localizable", "Accessibility.Scene.Gif.Search") + public static let search = L10n.tr("Localizable", "Accessibility.Scene.Gif.Search", fallback: "Search GIF") /// GIPHY - public static let title = L10n.tr("Localizable", "Accessibility.Scene.Gif.Title") + public static let title = L10n.tr("Localizable", "Accessibility.Scene.Gif.Title", fallback: "GIPHY") } public enum Home { /// Compose - public static let compose = L10n.tr("Localizable", "Accessibility.Scene.Home.Compose") + public static let compose = L10n.tr("Localizable", "Accessibility.Scene.Home.Compose", fallback: "Compose") /// Menu - public static let menu = L10n.tr("Localizable", "Accessibility.Scene.Home.Menu") + public static let menu = L10n.tr("Localizable", "Accessibility.Scene.Home.Menu", fallback: "Menu") public enum Drawer { /// Account DropDown - public static let accountDropdown = L10n.tr("Localizable", "Accessibility.Scene.Home.Drawer.AccountDropdown") + public static let accountDropdown = L10n.tr("Localizable", "Accessibility.Scene.Home.Drawer.AccountDropdown", fallback: "Account DropDown") } } public enum ManageAccounts { /// Add - public static let add = L10n.tr("Localizable", "Accessibility.Scene.ManageAccounts.Add") + public static let add = L10n.tr("Localizable", "Accessibility.Scene.ManageAccounts.Add", fallback: "Add") /// Current sign-in user: %@ public static func currentSignInUser(_ p1: Any) -> String { - return L10n.tr("Localizable", "Accessibility.Scene.ManageAccounts.CurrentSignInUser", String(describing: p1)) + return L10n.tr("Localizable", "Accessibility.Scene.ManageAccounts.CurrentSignInUser", String(describing: p1), fallback: "Current sign-in user: %@") } } public enum Search { /// History - public static let history = L10n.tr("Localizable", "Accessibility.Scene.Search.History") + public static let history = L10n.tr("Localizable", "Accessibility.Scene.Search.History", fallback: "History") /// Save - public static let save = L10n.tr("Localizable", "Accessibility.Scene.Search.Save") + public static let save = L10n.tr("Localizable", "Accessibility.Scene.Search.Save", fallback: "Save") } public enum Settings { public enum Display { /// Font Size - public static let fontSize = L10n.tr("Localizable", "Accessibility.Scene.Settings.Display.FontSize") + public static let fontSize = L10n.tr("Localizable", "Accessibility.Scene.Settings.Display.FontSize", fallback: "Font Size") } } public enum SignIn { /// Please enter Mastodon domain to sign-in - public static let pleaseEnterMastodonDomainToSignIn = L10n.tr("Localizable", "Accessibility.Scene.SignIn.PleaseEnterMastodonDomainToSignIn") + public static let pleaseEnterMastodonDomainToSignIn = L10n.tr("Localizable", "Accessibility.Scene.SignIn.PleaseEnterMastodonDomainToSignIn", fallback: "Please enter Mastodon domain to sign-in") /// Twitter client authentication key setting - public static let twitterClientAuthenticationKeySetting = L10n.tr("Localizable", "Accessibility.Scene.SignIn.TwitterClientAuthenticationKeySetting") + public static let twitterClientAuthenticationKeySetting = L10n.tr("Localizable", "Accessibility.Scene.SignIn.TwitterClientAuthenticationKeySetting", fallback: "Twitter client authentication key setting") } public enum Timeline { /// Load - public static let loadGap = L10n.tr("Localizable", "Accessibility.Scene.Timeline.LoadGap") + public static let loadGap = L10n.tr("Localizable", "Accessibility.Scene.Timeline.LoadGap", fallback: "Load") } public enum User { /// Location - public static let location = L10n.tr("Localizable", "Accessibility.Scene.User.Location") + public static let location = L10n.tr("Localizable", "Accessibility.Scene.User.Location", fallback: "Location") /// Website - public static let website = L10n.tr("Localizable", "Accessibility.Scene.User.Website") + public static let website = L10n.tr("Localizable", "Accessibility.Scene.User.Website", fallback: "Website") public enum Tab { /// Favourite - public static let favourite = L10n.tr("Localizable", "Accessibility.Scene.User.Tab.Favourite") + public static let favourite = L10n.tr("Localizable", "Accessibility.Scene.User.Tab.Favourite", fallback: "Favourite") /// Media - public static let media = L10n.tr("Localizable", "Accessibility.Scene.User.Tab.Media") + public static let media = L10n.tr("Localizable", "Accessibility.Scene.User.Tab.Media", fallback: "Media") /// Statuses - public static let status = L10n.tr("Localizable", "Accessibility.Scene.User.Tab.Status") + public static let status = L10n.tr("Localizable", "Accessibility.Scene.User.Tab.Status", fallback: "Statuses") } } } public enum VoiceOver { /// Double tap and hold to display menu - public static let doubleTapAndHoldToDisplayMenu = L10n.tr("Localizable", "Accessibility.VoiceOver.DoubleTapAndHoldToDisplayMenu") + public static let doubleTapAndHoldToDisplayMenu = L10n.tr("Localizable", "Accessibility.VoiceOver.DoubleTapAndHoldToDisplayMenu", fallback: "Double tap and hold to display menu") /// Double tap and hold to open the accounts panel - public static let doubleTapAndHoldToOpenTheAccountsPanel = L10n.tr("Localizable", "Accessibility.VoiceOver.DoubleTapAndHoldToOpenTheAccountsPanel") + public static let doubleTapAndHoldToOpenTheAccountsPanel = L10n.tr("Localizable", "Accessibility.VoiceOver.DoubleTapAndHoldToOpenTheAccountsPanel", fallback: "Double tap and hold to open the accounts panel") /// Double tap to open profile - public static let doubleTapToOpenProfile = L10n.tr("Localizable", "Accessibility.VoiceOver.DoubleTapToOpenProfile") + public static let doubleTapToOpenProfile = L10n.tr("Localizable", "Accessibility.VoiceOver.DoubleTapToOpenProfile", fallback: "Double tap to open profile") /// Selected - public static let selected = L10n.tr("Localizable", "Accessibility.VoiceOver.Selected") + public static let selected = L10n.tr("Localizable", "Accessibility.VoiceOver.Selected", fallback: "Selected") } } - public enum Common { public enum Alerts { public enum AccountSuspended { /// Twitter suspends accounts which violate the %@ public static func message(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Alerts.AccountSuspended.Message", String(describing: p1)) + return L10n.tr("Localizable", "Common.Alerts.AccountSuspended.Message", String(describing: p1), fallback: "Twitter suspends accounts which violate the %@") } /// Account Suspended - public static let title = L10n.tr("Localizable", "Common.Alerts.AccountSuspended.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.AccountSuspended.Title", fallback: "Account Suspended") /// Twitter Rules - public static let twitterRules = L10n.tr("Localizable", "Common.Alerts.AccountSuspended.TwitterRules") + public static let twitterRules = L10n.tr("Localizable", "Common.Alerts.AccountSuspended.TwitterRules", fallback: "Twitter Rules") } public enum AccountTemporarilyLocked { /// Open Twitter to unlock - public static let message = L10n.tr("Localizable", "Common.Alerts.AccountTemporarilyLocked.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.AccountTemporarilyLocked.Message", fallback: "Open Twitter to unlock") /// Account Temporarily Locked - public static let title = L10n.tr("Localizable", "Common.Alerts.AccountTemporarilyLocked.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.AccountTemporarilyLocked.Title", fallback: "Account Temporarily Locked") } public enum BlockUserConfirm { /// Do you want to block %@? public static func title(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Alerts.BlockUserConfirm.Title", String(describing: p1)) + return L10n.tr("Localizable", "Common.Alerts.BlockUserConfirm.Title", String(describing: p1), fallback: "Do you want to block %@?") } } public enum BlockUserSuccess { /// %@ has been blocked public static func title(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Alerts.BlockUserSuccess.Title", String(describing: p1)) + return L10n.tr("Localizable", "Common.Alerts.BlockUserSuccess.Title", String(describing: p1), fallback: "%@ has been blocked") } } public enum CancelFollowRequest { /// Cancel follow request for %@? public static func message(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Alerts.CancelFollowRequest.Message", String(describing: p1)) + return L10n.tr("Localizable", "Common.Alerts.CancelFollowRequest.Message", String(describing: p1), fallback: "Cancel follow request for %@?") } } public enum DeleteTootConfirm { /// Do you want to delete this toot? - public static let message = L10n.tr("Localizable", "Common.Alerts.DeleteTootConfirm.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.DeleteTootConfirm.Message", fallback: "Do you want to delete this toot?") /// Delete Toot - public static let title = L10n.tr("Localizable", "Common.Alerts.DeleteTootConfirm.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.DeleteTootConfirm.Title", fallback: "Delete Toot") } public enum DeleteTweetConfirm { /// Do you want to delete this tweet? - public static let message = L10n.tr("Localizable", "Common.Alerts.DeleteTweetConfirm.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.DeleteTweetConfirm.Message", fallback: "Do you want to delete this tweet?") /// Delete Tweet - public static let title = L10n.tr("Localizable", "Common.Alerts.DeleteTweetConfirm.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.DeleteTweetConfirm.Title", fallback: "Delete Tweet") } public enum FailedToAddListMember { /// Please try again - public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToAddListMember.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToAddListMember.Message", fallback: "Please try again") /// Failed to Add List Member - public static let title = L10n.tr("Localizable", "Common.Alerts.FailedToAddListMember.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.FailedToAddListMember.Title", fallback: "Failed to Add List Member") } public enum FailedToBlockUser { /// Please try again - public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToBlockUser.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToBlockUser.Message", fallback: "Please try again") /// Failed to block %@ public static func title(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Alerts.FailedToBlockUser.Title", String(describing: p1)) + return L10n.tr("Localizable", "Common.Alerts.FailedToBlockUser.Title", String(describing: p1), fallback: "Failed to block %@") } } public enum FailedToDeleteList { /// Please try again - public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToDeleteList.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToDeleteList.Message", fallback: "Please try again") /// Failed to Delete List - public static let title = L10n.tr("Localizable", "Common.Alerts.FailedToDeleteList.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.FailedToDeleteList.Title", fallback: "Failed to Delete List") } public enum FailedToDeleteToot { /// Please try again - public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToDeleteToot.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToDeleteToot.Message", fallback: "Please try again") /// Failed to Delete Toot - public static let title = L10n.tr("Localizable", "Common.Alerts.FailedToDeleteToot.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.FailedToDeleteToot.Title", fallback: "Failed to Delete Toot") } public enum FailedToDeleteTweet { /// Please try again - public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToDeleteTweet.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToDeleteTweet.Message", fallback: "Please try again") /// Failed to Delete Tweet - public static let title = L10n.tr("Localizable", "Common.Alerts.FailedToDeleteTweet.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.FailedToDeleteTweet.Title", fallback: "Failed to Delete Tweet") } public enum FailedToFollowing { /// Please try again - public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToFollowing.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToFollowing.Message", fallback: "Please try again") /// Failed to Following - public static let title = L10n.tr("Localizable", "Common.Alerts.FailedToFollowing.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.FailedToFollowing.Title", fallback: "Failed to Following") } public enum FailedToLoad { /// Please try again - public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToLoad.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToLoad.Message", fallback: "Please try again") /// Failed to Load - public static let title = L10n.tr("Localizable", "Common.Alerts.FailedToLoad.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.FailedToLoad.Title", fallback: "Failed to Load") } public enum FailedToLoginMastodonServer { /// Server URL is incorrect. - public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToLoginMastodonServer.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToLoginMastodonServer.Message", fallback: "Server URL is incorrect.") /// Failed to Login - public static let title = L10n.tr("Localizable", "Common.Alerts.FailedToLoginMastodonServer.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.FailedToLoginMastodonServer.Title", fallback: "Failed to Login") } public enum FailedToLoginTimeout { /// Connection timeout. - public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToLoginTimeout.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToLoginTimeout.Message", fallback: "Connection timeout.") /// Failed to Login - public static let title = L10n.tr("Localizable", "Common.Alerts.FailedToLoginTimeout.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.FailedToLoginTimeout.Title", fallback: "Failed to Login") } public enum FailedToMuteUser { /// Please try again - public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToMuteUser.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToMuteUser.Message", fallback: "Please try again") /// Failed to mute %@ public static func title(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Alerts.FailedToMuteUser.Title", String(describing: p1)) + return L10n.tr("Localizable", "Common.Alerts.FailedToMuteUser.Title", String(describing: p1), fallback: "Failed to mute %@") } } public enum FailedToRemoveListMember { /// Please try again - public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToRemoveListMember.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToRemoveListMember.Message", fallback: "Please try again") /// Failed to Remove List Member - public static let title = L10n.tr("Localizable", "Common.Alerts.FailedToRemoveListMember.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.FailedToRemoveListMember.Title", fallback: "Failed to Remove List Member") } public enum FailedToReportAndBlockUser { /// Please try again - public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToReportAndBlockUser.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToReportAndBlockUser.Message", fallback: "Please try again") /// Failed to report and block %@ public static func title(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Alerts.FailedToReportAndBlockUser.Title", String(describing: p1)) + return L10n.tr("Localizable", "Common.Alerts.FailedToReportAndBlockUser.Title", String(describing: p1), fallback: "Failed to report and block %@") } } public enum FailedToReportUser { /// Please try again - public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToReportUser.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToReportUser.Message", fallback: "Please try again") /// Failed to report %@ public static func title(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Alerts.FailedToReportUser.Title", String(describing: p1)) + return L10n.tr("Localizable", "Common.Alerts.FailedToReportUser.Title", String(describing: p1), fallback: "Failed to report %@") } } public enum FailedToSendMessage { /// Failed to send message - public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToSendMessage.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToSendMessage.Message", fallback: "Failed to send message") /// Sending message - public static let title = L10n.tr("Localizable", "Common.Alerts.FailedToSendMessage.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.FailedToSendMessage.Title", fallback: "Sending message") } public enum FailedToUnblockUser { /// Please try again - public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToUnblockUser.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToUnblockUser.Message", fallback: "Please try again") /// Failed to unblock %@ public static func title(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Alerts.FailedToUnblockUser.Title", String(describing: p1)) + return L10n.tr("Localizable", "Common.Alerts.FailedToUnblockUser.Title", String(describing: p1), fallback: "Failed to unblock %@") } } public enum FailedToUnfollowing { /// Please try again - public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToUnfollowing.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToUnfollowing.Message", fallback: "Please try again") /// Failed to Unfollowing - public static let title = L10n.tr("Localizable", "Common.Alerts.FailedToUnfollowing.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.FailedToUnfollowing.Title", fallback: "Failed to Unfollowing") } public enum FailedToUnmuteUser { /// Please try again - public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToUnmuteUser.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.FailedToUnmuteUser.Message", fallback: "Please try again") /// Failed to unmute %@ public static func title(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Alerts.FailedToUnmuteUser.Title", String(describing: p1)) + return L10n.tr("Localizable", "Common.Alerts.FailedToUnmuteUser.Title", String(describing: p1), fallback: "Failed to unmute %@") } } public enum FollowingRequestSent { /// Following Request Sent - public static let title = L10n.tr("Localizable", "Common.Alerts.FollowingRequestSent.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.FollowingRequestSent.Title", fallback: "Following Request Sent") } public enum FollowingSuccess { /// Following Succeeded - public static let title = L10n.tr("Localizable", "Common.Alerts.FollowingSuccess.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.FollowingSuccess.Title", fallback: "Following Succeeded") } public enum ListDeleted { /// Please try again - public static let message = L10n.tr("Localizable", "Common.Alerts.ListDeleted.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.ListDeleted.Message", fallback: "Please try again") /// List Deleted - public static let title = L10n.tr("Localizable", "Common.Alerts.ListDeleted.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.ListDeleted.Title", fallback: "List Deleted") } public enum ListMemberRemoved { /// List Member Removed - public static let title = L10n.tr("Localizable", "Common.Alerts.ListMemberRemoved.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.ListMemberRemoved.Title", fallback: "List Member Removed") } public enum MediaSaveFail { /// Please try again - public static let message = L10n.tr("Localizable", "Common.Alerts.MediaSaveFail.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.MediaSaveFail.Message", fallback: "Please try again") /// Failed to save media - public static let title = L10n.tr("Localizable", "Common.Alerts.MediaSaveFail.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.MediaSaveFail.Title", fallback: "Failed to save media") } public enum MediaSaved { /// Media saved - public static let title = L10n.tr("Localizable", "Common.Alerts.MediaSaved.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.MediaSaved.Title", fallback: "Media saved") } public enum MediaSaving { /// Saving media - public static let title = L10n.tr("Localizable", "Common.Alerts.MediaSaving.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.MediaSaving.Title", fallback: "Saving media") } public enum MediaSharing { /// Media will be shared after download is completed - public static let title = L10n.tr("Localizable", "Common.Alerts.MediaSharing.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.MediaSharing.Title", fallback: "Media will be shared after download is completed") } public enum MuteUserConfirm { /// Do you want to mute %@? public static func title(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Alerts.MuteUserConfirm.Title", String(describing: p1)) + return L10n.tr("Localizable", "Common.Alerts.MuteUserConfirm.Title", String(describing: p1), fallback: "Do you want to mute %@?") } } public enum MuteUserSuccess { /// %@ has been muted public static func title(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Alerts.MuteUserSuccess.Title", String(describing: p1)) + return L10n.tr("Localizable", "Common.Alerts.MuteUserSuccess.Title", String(describing: p1), fallback: "%@ has been muted") } } public enum NoTweetsFound { /// No Tweets Found - public static let title = L10n.tr("Localizable", "Common.Alerts.NoTweetsFound.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.NoTweetsFound.Title", fallback: "No Tweets Found") } public enum PermissionDeniedFriendshipBlocked { /// You have been blocked from following this account at the request of the user - public static let message = L10n.tr("Localizable", "Common.Alerts.PermissionDeniedFriendshipBlocked.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.PermissionDeniedFriendshipBlocked.Message", fallback: "You have been blocked from following this account at the request of the user") /// Permission Denied - public static let title = L10n.tr("Localizable", "Common.Alerts.PermissionDeniedFriendshipBlocked.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.PermissionDeniedFriendshipBlocked.Title", fallback: "Permission Denied") } public enum PermissionDeniedNotAuthorized { /// Sorry, you are not authorized - public static let message = L10n.tr("Localizable", "Common.Alerts.PermissionDeniedNotAuthorized.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.PermissionDeniedNotAuthorized.Message", fallback: "Sorry, you are not authorized") /// Permission Denied - public static let title = L10n.tr("Localizable", "Common.Alerts.PermissionDeniedNotAuthorized.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.PermissionDeniedNotAuthorized.Title", fallback: "Permission Denied") } public enum PhotoCopied { /// Photo Copied - public static let title = L10n.tr("Localizable", "Common.Alerts.PhotoCopied.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.PhotoCopied.Title", fallback: "Photo Copied") } public enum PhotoCopyFail { /// Please try again - public static let message = L10n.tr("Localizable", "Common.Alerts.PhotoCopyFail.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.PhotoCopyFail.Message", fallback: "Please try again") /// Failed to Copy Photo - public static let title = L10n.tr("Localizable", "Common.Alerts.PhotoCopyFail.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.PhotoCopyFail.Title", fallback: "Failed to Copy Photo") } public enum PhotoSaveFail { /// Please try again - public static let message = L10n.tr("Localizable", "Common.Alerts.PhotoSaveFail.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.PhotoSaveFail.Message", fallback: "Please try again") /// Failed to Save Photo - public static let title = L10n.tr("Localizable", "Common.Alerts.PhotoSaveFail.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.PhotoSaveFail.Title", fallback: "Failed to Save Photo") } public enum PhotoSaved { /// Photo Saved - public static let title = L10n.tr("Localizable", "Common.Alerts.PhotoSaved.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.PhotoSaved.Title", fallback: "Photo Saved") } public enum PostFailInvalidPoll { /// Poll has empty field. Please fulfill the field then try again - public static let message = L10n.tr("Localizable", "Common.Alerts.PostFailInvalidPoll.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.PostFailInvalidPoll.Message", fallback: "Poll has empty field. Please fulfill the field then try again") /// Failed to Publish - public static let title = L10n.tr("Localizable", "Common.Alerts.PostFailInvalidPoll.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.PostFailInvalidPoll.Title", fallback: "Failed to Publish") } public enum RateLimitExceeded { /// Reached Twitter API usage limit - public static let message = L10n.tr("Localizable", "Common.Alerts.RateLimitExceeded.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.RateLimitExceeded.Message", fallback: "Reached Twitter API usage limit") /// Rate Limit Exceeded - public static let title = L10n.tr("Localizable", "Common.Alerts.RateLimitExceeded.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.RateLimitExceeded.Title", fallback: "Rate Limit Exceeded") } public enum ReportAndBlockUserSuccess { /// %@ has been reported for spam and blocked public static func title(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Alerts.ReportAndBlockUserSuccess.Title", String(describing: p1)) + return L10n.tr("Localizable", "Common.Alerts.ReportAndBlockUserSuccess.Title", String(describing: p1), fallback: "%@ has been reported for spam and blocked") } } public enum ReportUserSuccess { /// %@ has been reported for spam public static func title(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Alerts.ReportUserSuccess.Title", String(describing: p1)) + return L10n.tr("Localizable", "Common.Alerts.ReportUserSuccess.Title", String(describing: p1), fallback: "%@ has been reported for spam") } } public enum RequestThrottle { /// Operation too frequent. Please try again later - public static let message = L10n.tr("Localizable", "Common.Alerts.RequestThrottle.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.RequestThrottle.Message", fallback: "Operation too frequent. Please try again later") /// Request Throttle - public static let title = L10n.tr("Localizable", "Common.Alerts.RequestThrottle.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.RequestThrottle.Title", fallback: "Request Throttle") } public enum SignOutUserConfirm { /// Do you want to sign out? - public static let message = L10n.tr("Localizable", "Common.Alerts.SignOutUserConfirm.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.SignOutUserConfirm.Message", fallback: "Do you want to sign out?") /// Sign out - public static let title = L10n.tr("Localizable", "Common.Alerts.SignOutUserConfirm.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.SignOutUserConfirm.Title", fallback: "Sign out") } public enum TooManyRequests { /// Too Many Requests - public static let title = L10n.tr("Localizable", "Common.Alerts.TooManyRequests.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.TooManyRequests.Title", fallback: "Too Many Requests") } public enum TootDeleted { /// Toot Deleted - public static let title = L10n.tr("Localizable", "Common.Alerts.TootDeleted.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.TootDeleted.Title", fallback: "Toot Deleted") } public enum TootFail { /// Your toot has been saved to Drafts. - public static let draftSavedMessage = L10n.tr("Localizable", "Common.Alerts.TootFail.DraftSavedMessage") + public static let draftSavedMessage = L10n.tr("Localizable", "Common.Alerts.TootFail.DraftSavedMessage", fallback: "Your toot has been saved to Drafts.") /// Please try again - public static let message = L10n.tr("Localizable", "Common.Alerts.TootFail.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.TootFail.Message", fallback: "Please try again") /// Failed to Toot - public static let title = L10n.tr("Localizable", "Common.Alerts.TootFail.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.TootFail.Title", fallback: "Failed to Toot") } public enum TootPosted { /// Toot Posted - public static let title = L10n.tr("Localizable", "Common.Alerts.TootPosted.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.TootPosted.Title", fallback: "Toot Posted") } public enum TootSending { /// Sending toot - public static let title = L10n.tr("Localizable", "Common.Alerts.TootSending.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.TootSending.Title", fallback: "Sending toot") } public enum TweetDeleted { /// Tweet Deleted - public static let title = L10n.tr("Localizable", "Common.Alerts.TweetDeleted.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.TweetDeleted.Title", fallback: "Tweet Deleted") } public enum TweetFail { /// Your tweet has been saved to Drafts. - public static let draftSavedMessage = L10n.tr("Localizable", "Common.Alerts.TweetFail.DraftSavedMessage") + public static let draftSavedMessage = L10n.tr("Localizable", "Common.Alerts.TweetFail.DraftSavedMessage", fallback: "Your tweet has been saved to Drafts.") /// Please try again - public static let message = L10n.tr("Localizable", "Common.Alerts.TweetFail.Message") + public static let message = L10n.tr("Localizable", "Common.Alerts.TweetFail.Message", fallback: "Please try again") /// Failed to Tweet - public static let title = L10n.tr("Localizable", "Common.Alerts.TweetFail.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.TweetFail.Title", fallback: "Failed to Tweet") } public enum TweetPosted { /// Tweet Posted - public static let title = L10n.tr("Localizable", "Common.Alerts.TweetPosted.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.TweetPosted.Title", fallback: "Tweet Posted") } public enum TweetSending { /// Sending tweet - public static let title = L10n.tr("Localizable", "Common.Alerts.TweetSending.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.TweetSending.Title", fallback: "Sending tweet") } public enum TweetSent { /// Tweet Sent - public static let title = L10n.tr("Localizable", "Common.Alerts.TweetSent.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.TweetSent.Title", fallback: "Tweet Sent") } public enum UnblockUserConfirm { /// Do you want to unblock %@? public static func title(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Alerts.UnblockUserConfirm.Title", String(describing: p1)) + return L10n.tr("Localizable", "Common.Alerts.UnblockUserConfirm.Title", String(describing: p1), fallback: "Do you want to unblock %@?") } } public enum UnblockUserSuccess { /// %@ has been unblocked public static func title(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Alerts.UnblockUserSuccess.Title", String(describing: p1)) + return L10n.tr("Localizable", "Common.Alerts.UnblockUserSuccess.Title", String(describing: p1), fallback: "%@ has been unblocked") } } public enum UnfollowUser { /// Unfollow user %@? public static func message(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Alerts.UnfollowUser.Message", String(describing: p1)) + return L10n.tr("Localizable", "Common.Alerts.UnfollowUser.Message", String(describing: p1), fallback: "Unfollow user %@?") } } public enum UnfollowingSuccess { /// Unfollowing Succeeded - public static let title = L10n.tr("Localizable", "Common.Alerts.UnfollowingSuccess.Title") + public static let title = L10n.tr("Localizable", "Common.Alerts.UnfollowingSuccess.Title", fallback: "Unfollowing Succeeded") } public enum UnmuteUserConfirm { /// Do you want to unmute %@? public static func title(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Alerts.UnmuteUserConfirm.Title", String(describing: p1)) + return L10n.tr("Localizable", "Common.Alerts.UnmuteUserConfirm.Title", String(describing: p1), fallback: "Do you want to unmute %@?") } } public enum UnmuteUserSuccess { /// %@ has been unmuted public static func title(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Alerts.UnmuteUserSuccess.Title", String(describing: p1)) + return L10n.tr("Localizable", "Common.Alerts.UnmuteUserSuccess.Title", String(describing: p1), fallback: "%@ has been unmuted") } } } public enum Controls { public enum Actions { /// Add - public static let add = L10n.tr("Localizable", "Common.Controls.Actions.Add") + public static let add = L10n.tr("Localizable", "Common.Controls.Actions.Add", fallback: "Add") /// Browse - public static let browse = L10n.tr("Localizable", "Common.Controls.Actions.Browse") + public static let browse = L10n.tr("Localizable", "Common.Controls.Actions.Browse", fallback: "Browse") /// Cancel - public static let cancel = L10n.tr("Localizable", "Common.Controls.Actions.Cancel") + public static let cancel = L10n.tr("Localizable", "Common.Controls.Actions.Cancel", fallback: "Cancel") /// Clear - public static let clear = L10n.tr("Localizable", "Common.Controls.Actions.Clear") + public static let clear = L10n.tr("Localizable", "Common.Controls.Actions.Clear", fallback: "Clear") /// Confirm - public static let confirm = L10n.tr("Localizable", "Common.Controls.Actions.Confirm") + public static let confirm = L10n.tr("Localizable", "Common.Controls.Actions.Confirm", fallback: "Confirm") /// Copy - public static let copy = L10n.tr("Localizable", "Common.Controls.Actions.Copy") + public static let copy = L10n.tr("Localizable", "Common.Controls.Actions.Copy", fallback: "Copy") /// Delete - public static let delete = L10n.tr("Localizable", "Common.Controls.Actions.Delete") + public static let delete = L10n.tr("Localizable", "Common.Controls.Actions.Delete", fallback: "Delete") /// Done - public static let done = L10n.tr("Localizable", "Common.Controls.Actions.Done") + public static let done = L10n.tr("Localizable", "Common.Controls.Actions.Done", fallback: "Done") /// Edit - public static let edit = L10n.tr("Localizable", "Common.Controls.Actions.Edit") + public static let edit = L10n.tr("Localizable", "Common.Controls.Actions.Edit", fallback: "Edit") /// OK - public static let ok = L10n.tr("Localizable", "Common.Controls.Actions.Ok") + public static let ok = L10n.tr("Localizable", "Common.Controls.Actions.Ok", fallback: "OK") /// Open in Safari - public static let openInSafari = L10n.tr("Localizable", "Common.Controls.Actions.OpenInSafari") + public static let openInSafari = L10n.tr("Localizable", "Common.Controls.Actions.OpenInSafari", fallback: "Open in Safari") /// Preview - public static let preview = L10n.tr("Localizable", "Common.Controls.Actions.Preview") + public static let preview = L10n.tr("Localizable", "Common.Controls.Actions.Preview", fallback: "Preview") /// Remove - public static let remove = L10n.tr("Localizable", "Common.Controls.Actions.Remove") + public static let remove = L10n.tr("Localizable", "Common.Controls.Actions.Remove", fallback: "Remove") /// Save - public static let save = L10n.tr("Localizable", "Common.Controls.Actions.Save") + public static let save = L10n.tr("Localizable", "Common.Controls.Actions.Save", fallback: "Save") /// Save photo - public static let savePhoto = L10n.tr("Localizable", "Common.Controls.Actions.SavePhoto") + public static let savePhoto = L10n.tr("Localizable", "Common.Controls.Actions.SavePhoto", fallback: "Save photo") /// Share - public static let share = L10n.tr("Localizable", "Common.Controls.Actions.Share") + public static let share = L10n.tr("Localizable", "Common.Controls.Actions.Share", fallback: "Share") /// Share link - public static let shareLink = L10n.tr("Localizable", "Common.Controls.Actions.ShareLink") + public static let shareLink = L10n.tr("Localizable", "Common.Controls.Actions.ShareLink", fallback: "Share link") /// Share media - public static let shareMedia = L10n.tr("Localizable", "Common.Controls.Actions.ShareMedia") + public static let shareMedia = L10n.tr("Localizable", "Common.Controls.Actions.ShareMedia", fallback: "Share media") /// Sign in - public static let signIn = L10n.tr("Localizable", "Common.Controls.Actions.SignIn") + public static let signIn = L10n.tr("Localizable", "Common.Controls.Actions.SignIn", fallback: "Sign in") /// Sign out - public static let signOut = L10n.tr("Localizable", "Common.Controls.Actions.SignOut") + public static let signOut = L10n.tr("Localizable", "Common.Controls.Actions.SignOut", fallback: "Sign out") /// Take photo - public static let takePhoto = L10n.tr("Localizable", "Common.Controls.Actions.TakePhoto") + public static let takePhoto = L10n.tr("Localizable", "Common.Controls.Actions.TakePhoto", fallback: "Take photo") /// Yes - public static let yes = L10n.tr("Localizable", "Common.Controls.Actions.Yes") + public static let yes = L10n.tr("Localizable", "Common.Controls.Actions.Yes", fallback: "Yes") public enum ShareMediaMenu { /// Link - public static let link = L10n.tr("Localizable", "Common.Controls.Actions.ShareMediaMenu.Link") + public static let link = L10n.tr("Localizable", "Common.Controls.Actions.ShareMediaMenu.Link", fallback: "Link") /// Media - public static let media = L10n.tr("Localizable", "Common.Controls.Actions.ShareMediaMenu.Media") + public static let media = L10n.tr("Localizable", "Common.Controls.Actions.ShareMediaMenu.Media", fallback: "Media") } } public enum Friendship { /// Block %@ public static func blockUser(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Friendship.BlockUser", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Friendship.BlockUser", String(describing: p1), fallback: "Block %@") } /// Do you want to report and block %@ public static func doYouWantToReportAndBlockUser(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Friendship.DoYouWantToReportAnd BlockUser", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Friendship.DoYouWantToReportAndBlockUser", String(describing: p1), fallback: "Do you want to report and block %@") } /// Do you want to report %@ public static func doYouWantToReportUser(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Friendship.DoYouWantToReportUser", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Friendship.DoYouWantToReportUser", String(describing: p1), fallback: "Do you want to report %@") } /// follower - public static let follower = L10n.tr("Localizable", "Common.Controls.Friendship.Follower") + public static let follower = L10n.tr("Localizable", "Common.Controls.Friendship.Follower", fallback: "follower") /// followers - public static let followers = L10n.tr("Localizable", "Common.Controls.Friendship.Followers") + public static let followers = L10n.tr("Localizable", "Common.Controls.Friendship.Followers", fallback: "followers") /// Follows you - public static let followsYou = L10n.tr("Localizable", "Common.Controls.Friendship.FollowsYou") + public static let followsYou = L10n.tr("Localizable", "Common.Controls.Friendship.FollowsYou", fallback: "Follows you") /// Mute %@ public static func muteUser(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Friendship.MuteUser", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Friendship.MuteUser", String(describing: p1), fallback: "Mute %@") } /// %@ is following you public static func userIsFollowingYou(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Friendship.UserIsFollowingYou", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Friendship.UserIsFollowingYou", String(describing: p1), fallback: "%@ is following you") } /// %@ is not following you public static func userIsNotFollowingYou(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Friendship.UserIsNotFollowingYou", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Friendship.UserIsNotFollowingYou", String(describing: p1), fallback: "%@ is not following you") } public enum Actions { /// Block - public static let block = L10n.tr("Localizable", "Common.Controls.Friendship.Actions.Block") + public static let block = L10n.tr("Localizable", "Common.Controls.Friendship.Actions.Block", fallback: "Block") /// Blocked - public static let blocked = L10n.tr("Localizable", "Common.Controls.Friendship.Actions.Blocked") + public static let blocked = L10n.tr("Localizable", "Common.Controls.Friendship.Actions.Blocked", fallback: "Blocked") /// Follow - public static let follow = L10n.tr("Localizable", "Common.Controls.Friendship.Actions.Follow") + public static let follow = L10n.tr("Localizable", "Common.Controls.Friendship.Actions.Follow", fallback: "Follow") /// Following - public static let following = L10n.tr("Localizable", "Common.Controls.Friendship.Actions.Following") + public static let following = L10n.tr("Localizable", "Common.Controls.Friendship.Actions.Following", fallback: "Following") /// Mute - public static let mute = L10n.tr("Localizable", "Common.Controls.Friendship.Actions.Mute") + public static let mute = L10n.tr("Localizable", "Common.Controls.Friendship.Actions.Mute", fallback: "Mute") /// Pending - public static let pending = L10n.tr("Localizable", "Common.Controls.Friendship.Actions.Pending") + public static let pending = L10n.tr("Localizable", "Common.Controls.Friendship.Actions.Pending", fallback: "Pending") /// Report - public static let report = L10n.tr("Localizable", "Common.Controls.Friendship.Actions.Report") + public static let report = L10n.tr("Localizable", "Common.Controls.Friendship.Actions.Report", fallback: "Report") /// Report and Block - public static let reportAndBlock = L10n.tr("Localizable", "Common.Controls.Friendship.Actions.ReportAndBlock") + public static let reportAndBlock = L10n.tr("Localizable", "Common.Controls.Friendship.Actions.ReportAndBlock", fallback: "Report and Block") /// Request - public static let request = L10n.tr("Localizable", "Common.Controls.Friendship.Actions.Request") + public static let request = L10n.tr("Localizable", "Common.Controls.Friendship.Actions.Request", fallback: "Request") /// Unblock - public static let unblock = L10n.tr("Localizable", "Common.Controls.Friendship.Actions.Unblock") + public static let unblock = L10n.tr("Localizable", "Common.Controls.Friendship.Actions.Unblock", fallback: "Unblock") /// Unfollow - public static let unfollow = L10n.tr("Localizable", "Common.Controls.Friendship.Actions.Unfollow") + public static let unfollow = L10n.tr("Localizable", "Common.Controls.Friendship.Actions.Unfollow", fallback: "Unfollow") /// Unmute - public static let unmute = L10n.tr("Localizable", "Common.Controls.Friendship.Actions.Unmute") + public static let unmute = L10n.tr("Localizable", "Common.Controls.Friendship.Actions.Unmute", fallback: "Unmute") } } public enum Ios { /// Photo Library - public static let photoLibrary = L10n.tr("Localizable", "Common.Controls.Ios.PhotoLibrary") + public static let photoLibrary = L10n.tr("Localizable", "Common.Controls.Ios.PhotoLibrary", fallback: "Photo Library") } public enum List { /// No results - public static let noResults = L10n.tr("Localizable", "Common.Controls.List.NoResults") + public static let noResults = L10n.tr("Localizable", "Common.Controls.List.NoResults", fallback: "No results") } public enum ProfileDashboard { /// Followers - public static let followers = L10n.tr("Localizable", "Common.Controls.ProfileDashboard.Followers") + public static let followers = L10n.tr("Localizable", "Common.Controls.ProfileDashboard.Followers", fallback: "Followers") /// Following - public static let following = L10n.tr("Localizable", "Common.Controls.ProfileDashboard.Following") + public static let following = L10n.tr("Localizable", "Common.Controls.ProfileDashboard.Following", fallback: "Following") /// Listed - public static let listed = L10n.tr("Localizable", "Common.Controls.ProfileDashboard.Listed") + public static let listed = L10n.tr("Localizable", "Common.Controls.ProfileDashboard.Listed", fallback: "Listed") } public enum Status { /// Media - public static let media = L10n.tr("Localizable", "Common.Controls.Status.Media") + public static let media = L10n.tr("Localizable", "Common.Controls.Status.Media", fallback: "Media") /// %@ boosted public static func userBoosted(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Status.UserBoosted", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Status.UserBoosted", String(describing: p1), fallback: "%@ boosted") } /// %@ retweeted public static func userRetweeted(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Status.UserRetweeted", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Status.UserRetweeted", String(describing: p1), fallback: "%@ retweeted") } /// You boosted - public static let youBoosted = L10n.tr("Localizable", "Common.Controls.Status.YouBoosted") + public static let youBoosted = L10n.tr("Localizable", "Common.Controls.Status.YouBoosted", fallback: "You boosted") /// You retweeted - public static let youRetweeted = L10n.tr("Localizable", "Common.Controls.Status.YouRetweeted") + public static let youRetweeted = L10n.tr("Localizable", "Common.Controls.Status.YouRetweeted", fallback: "You retweeted") public enum Actions { /// Bookmark - public static let bookmark = L10n.tr("Localizable", "Common.Controls.Status.Actions.Bookmark") + public static let bookmark = L10n.tr("Localizable", "Common.Controls.Status.Actions.Bookmark", fallback: "Bookmark") /// Boost - public static let boost = L10n.tr("Localizable", "Common.Controls.Status.Actions.Boost") + public static let boost = L10n.tr("Localizable", "Common.Controls.Status.Actions.Boost", fallback: "Boost") /// Copy link - public static let copyLink = L10n.tr("Localizable", "Common.Controls.Status.Actions.CopyLink") + public static let copyLink = L10n.tr("Localizable", "Common.Controls.Status.Actions.CopyLink", fallback: "Copy link") /// Copy text - public static let copyText = L10n.tr("Localizable", "Common.Controls.Status.Actions.CopyText") + public static let copyText = L10n.tr("Localizable", "Common.Controls.Status.Actions.CopyText", fallback: "Copy text") /// Delete tweet - public static let deleteTweet = L10n.tr("Localizable", "Common.Controls.Status.Actions.DeleteTweet") + public static let deleteTweet = L10n.tr("Localizable", "Common.Controls.Status.Actions.DeleteTweet", fallback: "Delete tweet") /// Pin on Profile - public static let pinOnProfile = L10n.tr("Localizable", "Common.Controls.Status.Actions.PinOnProfile") + public static let pinOnProfile = L10n.tr("Localizable", "Common.Controls.Status.Actions.PinOnProfile", fallback: "Pin on Profile") /// Quote - public static let quote = L10n.tr("Localizable", "Common.Controls.Status.Actions.Quote") + public static let quote = L10n.tr("Localizable", "Common.Controls.Status.Actions.Quote", fallback: "Quote") + /// Reply + public static let reply = L10n.tr("Localizable", "Common.Controls.Status.Actions.Reply", fallback: "Reply") /// Retweet - public static let retweet = L10n.tr("Localizable", "Common.Controls.Status.Actions.Retweet") + public static let retweet = L10n.tr("Localizable", "Common.Controls.Status.Actions.Retweet", fallback: "Retweet") /// Save media - public static let saveMedia = L10n.tr("Localizable", "Common.Controls.Status.Actions.SaveMedia") + public static let saveMedia = L10n.tr("Localizable", "Common.Controls.Status.Actions.SaveMedia", fallback: "Save media") /// Share - public static let share = L10n.tr("Localizable", "Common.Controls.Status.Actions.Share") + public static let share = L10n.tr("Localizable", "Common.Controls.Status.Actions.Share", fallback: "Share") /// Share content - public static let shareContent = L10n.tr("Localizable", "Common.Controls.Status.Actions.ShareContent") + public static let shareContent = L10n.tr("Localizable", "Common.Controls.Status.Actions.ShareContent", fallback: "Share content") /// Share link - public static let shareLink = L10n.tr("Localizable", "Common.Controls.Status.Actions.ShareLink") + public static let shareLink = L10n.tr("Localizable", "Common.Controls.Status.Actions.ShareLink", fallback: "Share link") /// Translate - public static let translate = L10n.tr("Localizable", "Common.Controls.Status.Actions.Translate") + public static let translate = L10n.tr("Localizable", "Common.Controls.Status.Actions.Translate", fallback: "Translate") /// Unpin from Profile - public static let unpinFromProfile = L10n.tr("Localizable", "Common.Controls.Status.Actions.UnpinFromProfile") + public static let unpinFromProfile = L10n.tr("Localizable", "Common.Controls.Status.Actions.UnpinFromProfile", fallback: "Unpin from Profile") /// Vote - public static let vote = L10n.tr("Localizable", "Common.Controls.Status.Actions.Vote") + public static let vote = L10n.tr("Localizable", "Common.Controls.Status.Actions.Vote", fallback: "Vote") } public enum Poll { /// Closed - public static let expired = L10n.tr("Localizable", "Common.Controls.Status.Poll.Expired") + public static let expired = L10n.tr("Localizable", "Common.Controls.Status.Poll.Expired", fallback: "Closed") /// %@ people public static func totalPeople(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Status.Poll.TotalPeople", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Status.Poll.TotalPeople", String(describing: p1), fallback: "%@ people") } /// %@ person public static func totalPerson(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Status.Poll.TotalPerson", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Status.Poll.TotalPerson", String(describing: p1), fallback: "%@ person") } /// %@ vote public static func totalVote(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Status.Poll.TotalVote", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Status.Poll.TotalVote", String(describing: p1), fallback: "%@ vote") } /// %@ votes public static func totalVotes(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Status.Poll.TotalVotes", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Status.Poll.TotalVotes", String(describing: p1), fallback: "%@ votes") } } public enum ReplySettings { /// People %@ follows or mentioned can reply. public static func peopleUserFollowsOrMentionedCanReply(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Status.ReplySettings.PeopleUserFollowsOrMentionedCanReply", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Status.ReplySettings.PeopleUserFollowsOrMentionedCanReply", String(describing: p1), fallback: "People %@ follows or mentioned can reply.") } /// People %@ mentioned can reply. public static func peopleUserMentionedCanReply(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Controls.Status.ReplySettings.PeopleUserMentionedCanReply", String(describing: p1)) + return L10n.tr("Localizable", "Common.Controls.Status.ReplySettings.PeopleUserMentionedCanReply", String(describing: p1), fallback: "People %@ mentioned can reply.") } } public enum Thread { /// Show this thread - public static let show = L10n.tr("Localizable", "Common.Controls.Status.Thread.Show") + public static let show = L10n.tr("Localizable", "Common.Controls.Status.Thread.Show", fallback: "Show this thread") } } public enum Timeline { /// Load More - public static let loadMore = L10n.tr("Localizable", "Common.Controls.Timeline.LoadMore") + public static let loadMore = L10n.tr("Localizable", "Common.Controls.Timeline.LoadMore", fallback: "Load More") } public enum User { public enum Actions { /// Add/remove from Lists - public static let addRemoveFromLists = L10n.tr("Localizable", "Common.Controls.User.Actions.AddRemoveFromLists") + public static let addRemoveFromLists = L10n.tr("Localizable", "Common.Controls.User.Actions.AddRemoveFromLists", fallback: "Add/remove from Lists") /// View Listed - public static let viewListed = L10n.tr("Localizable", "Common.Controls.User.Actions.ViewListed") + public static let viewListed = L10n.tr("Localizable", "Common.Controls.User.Actions.ViewListed", fallback: "View Listed") /// View Lists - public static let viewLists = L10n.tr("Localizable", "Common.Controls.User.Actions.ViewLists") + public static let viewLists = L10n.tr("Localizable", "Common.Controls.User.Actions.ViewLists", fallback: "View Lists") } } } @@ -771,1011 +771,1020 @@ public enum L10n { public enum Like { /// %@ likes public static func multiple(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Countable.Like.Multiple", String(describing: p1)) + return L10n.tr("Localizable", "Common.Countable.Like.Multiple", String(describing: p1), fallback: "%@ likes") } /// %@ like public static func single(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Countable.Like.Single", String(describing: p1)) + return L10n.tr("Localizable", "Common.Countable.Like.Single", String(describing: p1), fallback: "%@ like") } } public enum List { /// %@ lists public static func multiple(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Countable.List.Multiple", String(describing: p1)) + return L10n.tr("Localizable", "Common.Countable.List.Multiple", String(describing: p1), fallback: "%@ lists") } /// %@ list public static func single(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Countable.List.Single", String(describing: p1)) + return L10n.tr("Localizable", "Common.Countable.List.Single", String(describing: p1), fallback: "%@ list") } } public enum Member { /// %@ members public static func multiple(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Countable.Member.Multiple", String(describing: p1)) + return L10n.tr("Localizable", "Common.Countable.Member.Multiple", String(describing: p1), fallback: "%@ members") } /// %@ member public static func single(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Countable.Member.Single", String(describing: p1)) + return L10n.tr("Localizable", "Common.Countable.Member.Single", String(describing: p1), fallback: "%@ member") } } public enum Photo { /// %@ photos public static func multiple(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Countable.Photo.Multiple", String(describing: p1)) + return L10n.tr("Localizable", "Common.Countable.Photo.Multiple", String(describing: p1), fallback: "%@ photos") } /// %@ photo public static func single(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Countable.Photo.Single", String(describing: p1)) + return L10n.tr("Localizable", "Common.Countable.Photo.Single", String(describing: p1), fallback: "%@ photo") } } public enum Quote { /// %@ quotes public static func mutiple(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Countable.Quote.Mutiple", String(describing: p1)) + return L10n.tr("Localizable", "Common.Countable.Quote.Mutiple", String(describing: p1), fallback: "%@ quotes") } /// %@ quote public static func single(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Countable.Quote.Single", String(describing: p1)) + return L10n.tr("Localizable", "Common.Countable.Quote.Single", String(describing: p1), fallback: "%@ quote") } } public enum Reply { /// %@ replies public static func mutiple(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Countable.Reply.Mutiple", String(describing: p1)) + return L10n.tr("Localizable", "Common.Countable.Reply.Mutiple", String(describing: p1), fallback: "%@ replies") } /// %@ reply public static func single(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Countable.Reply.Single", String(describing: p1)) + return L10n.tr("Localizable", "Common.Countable.Reply.Single", String(describing: p1), fallback: "%@ reply") } } public enum Retweet { /// %@ retweets public static func mutiple(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Countable.Retweet.Mutiple", String(describing: p1)) + return L10n.tr("Localizable", "Common.Countable.Retweet.Mutiple", String(describing: p1), fallback: "%@ retweets") } /// %@ retweet public static func single(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Countable.Retweet.Single", String(describing: p1)) + return L10n.tr("Localizable", "Common.Countable.Retweet.Single", String(describing: p1), fallback: "%@ retweet") } } public enum Tweet { /// %@ tweets public static func multiple(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Countable.Tweet.Multiple", String(describing: p1)) + return L10n.tr("Localizable", "Common.Countable.Tweet.Multiple", String(describing: p1), fallback: "%@ tweets") } /// %@ tweet public static func single(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Countable.Tweet.Single", String(describing: p1)) + return L10n.tr("Localizable", "Common.Countable.Tweet.Single", String(describing: p1), fallback: "%@ tweet") } } } public enum Notification { /// %@ favourited your toot public static func favourite(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Notification.Favourite", String(describing: p1)) + return L10n.tr("Localizable", "Common.Notification.Favourite", String(describing: p1), fallback: "%@ favourited your toot") } /// %@ followed you public static func follow(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Notification.Follow", String(describing: p1)) + return L10n.tr("Localizable", "Common.Notification.Follow", String(describing: p1), fallback: "%@ followed you") } /// %@ has requested to follow you public static func followRequest(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Notification.FollowRequest", String(describing: p1)) + return L10n.tr("Localizable", "Common.Notification.FollowRequest", String(describing: p1), fallback: "%@ has requested to follow you") } /// %@ mentions you public static func mentions(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Notification.Mentions", String(describing: p1)) + return L10n.tr("Localizable", "Common.Notification.Mentions", String(describing: p1), fallback: "%@ mentions you") } /// Your poll has ended - public static let ownPoll = L10n.tr("Localizable", "Common.Notification.OwnPoll") + public static let ownPoll = L10n.tr("Localizable", "Common.Notification.OwnPoll", fallback: "Your poll has ended") /// A poll you have voted in has ended - public static let poll = L10n.tr("Localizable", "Common.Notification.Poll") + public static let poll = L10n.tr("Localizable", "Common.Notification.Poll", fallback: "A poll you have voted in has ended") /// %@ boosted your toot public static func reblog(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Notification.Reblog", String(describing: p1)) + return L10n.tr("Localizable", "Common.Notification.Reblog", String(describing: p1), fallback: "%@ boosted your toot") } /// %@ just posted public static func status(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Notification.Status", String(describing: p1)) + return L10n.tr("Localizable", "Common.Notification.Status", String(describing: p1), fallback: "%@ just posted") } public enum FollowRequestAction { /// Approve - public static let approve = L10n.tr("Localizable", "Common.Notification.FollowRequestAction.Approve") + public static let approve = L10n.tr("Localizable", "Common.Notification.FollowRequestAction.Approve", fallback: "Approve") /// Deny - public static let deny = L10n.tr("Localizable", "Common.Notification.FollowRequestAction.Deny") + public static let deny = L10n.tr("Localizable", "Common.Notification.FollowRequestAction.Deny", fallback: "Deny") } public enum FollowRequestResponse { /// Follow Request Approved - public static let followRequestApproved = L10n.tr("Localizable", "Common.Notification.FollowRequestResponse.FollowRequestApproved") + public static let followRequestApproved = L10n.tr("Localizable", "Common.Notification.FollowRequestResponse.FollowRequestApproved", fallback: "Follow Request Approved") /// Follow Request Denied - public static let followRequestDenied = L10n.tr("Localizable", "Common.Notification.FollowRequestResponse.FollowRequestDenied") + public static let followRequestDenied = L10n.tr("Localizable", "Common.Notification.FollowRequestResponse.FollowRequestDenied", fallback: "Follow Request Denied") } public enum Messages { /// %@ sent you a message public static func content(_ p1: Any) -> String { - return L10n.tr("Localizable", "Common.Notification.Messages.Content", String(describing: p1)) + return L10n.tr("Localizable", "Common.Notification.Messages.Content", String(describing: p1), fallback: "%@ sent you a message") } /// New direct message - public static let title = L10n.tr("Localizable", "Common.Notification.Messages.Title") + public static let title = L10n.tr("Localizable", "Common.Notification.Messages.Title", fallback: "New direct message") } } public enum NotificationChannel { public enum BackgroundProgresses { /// Background progresses - public static let name = L10n.tr("Localizable", "Common.NotificationChannel.BackgroundProgresses.Name") + public static let name = L10n.tr("Localizable", "Common.NotificationChannel.BackgroundProgresses.Name", fallback: "Background progresses") } public enum ContentInteractions { /// Interactions like mentions and retweets - public static let description = L10n.tr("Localizable", "Common.NotificationChannel.ContentInteractions.Description") + public static let description = L10n.tr("Localizable", "Common.NotificationChannel.ContentInteractions.Description", fallback: "Interactions like mentions and retweets") /// Interactions - public static let name = L10n.tr("Localizable", "Common.NotificationChannel.ContentInteractions.Name") + public static let name = L10n.tr("Localizable", "Common.NotificationChannel.ContentInteractions.Name", fallback: "Interactions") } public enum ContentMessages { /// Direct messages - public static let description = L10n.tr("Localizable", "Common.NotificationChannel.ContentMessages.Description") + public static let description = L10n.tr("Localizable", "Common.NotificationChannel.ContentMessages.Description", fallback: "Direct messages") /// Messages - public static let name = L10n.tr("Localizable", "Common.NotificationChannel.ContentMessages.Name") + public static let name = L10n.tr("Localizable", "Common.NotificationChannel.ContentMessages.Name", fallback: "Messages") } } } - public enum Scene { public enum Authentication { /// Authentication - public static let title = L10n.tr("Localizable", "Scene.Authentication.Title") + public static let title = L10n.tr("Localizable", "Scene.Authentication.Title", fallback: "Authentication") } public enum Bookmark { /// Bookmark - public static let title = L10n.tr("Localizable", "Scene.Bookmark.Title") + public static let title = L10n.tr("Localizable", "Scene.Bookmark.Title", fallback: "Bookmark") } public enum Compose { /// , - public static let and = L10n.tr("Localizable", "Scene.Compose.And") + public static let and = L10n.tr("Localizable", "Scene.Compose.And", fallback: ", ") /// Write your warning here - public static let cwPlaceholder = L10n.tr("Localizable", "Scene.Compose.CwPlaceholder") + public static let cwPlaceholder = L10n.tr("Localizable", "Scene.Compose.CwPlaceholder", fallback: "Write your warning here") /// and - public static let lastEnd = L10n.tr("Localizable", "Scene.Compose.LastEnd") + public static let lastEnd = L10n.tr("Localizable", "Scene.Compose.LastEnd", fallback: " and ") /// Others in this conversation: - public static let othersInThisConversation = L10n.tr("Localizable", "Scene.Compose.OthersInThisConversation") + public static let othersInThisConversation = L10n.tr("Localizable", "Scene.Compose.OthersInThisConversation", fallback: "Others in this conversation:") /// What’s happening? - public static let placeholder = L10n.tr("Localizable", "Scene.Compose.Placeholder") + public static let placeholder = L10n.tr("Localizable", "Scene.Compose.Placeholder", fallback: "What’s happening?") /// Replying to - public static let replyingTo = L10n.tr("Localizable", "Scene.Compose.ReplyingTo") + public static let replyingTo = L10n.tr("Localizable", "Scene.Compose.ReplyingTo", fallback: "Replying to") /// Reply to … - public static let replyTo = L10n.tr("Localizable", "Scene.Compose.ReplyTo") + public static let replyTo = L10n.tr("Localizable", "Scene.Compose.ReplyTo", fallback: "Reply to …") public enum Media { /// Preview - public static let preview = L10n.tr("Localizable", "Scene.Compose.Media.Preview") + public static let preview = L10n.tr("Localizable", "Scene.Compose.Media.Preview", fallback: "Preview") /// Remove - public static let remove = L10n.tr("Localizable", "Scene.Compose.Media.Remove") + public static let remove = L10n.tr("Localizable", "Scene.Compose.Media.Remove", fallback: "Remove") public enum Caption { /// Add Caption - public static let add = L10n.tr("Localizable", "Scene.Compose.Media.Caption.Add") + public static let add = L10n.tr("Localizable", "Scene.Compose.Media.Caption.Add", fallback: "Add Caption") /// Add a description for this image - public static let addADescriptionForThisImage = L10n.tr("Localizable", "Scene.Compose.Media.Caption.AddADescriptionForThisImage") + public static let addADescriptionForThisImage = L10n.tr("Localizable", "Scene.Compose.Media.Caption.AddADescriptionForThisImage", fallback: "Add a description for this image") /// Remove Caption - public static let remove = L10n.tr("Localizable", "Scene.Compose.Media.Caption.Remove") + public static let remove = L10n.tr("Localizable", "Scene.Compose.Media.Caption.Remove", fallback: "Remove Caption") /// Update Caption - public static let update = L10n.tr("Localizable", "Scene.Compose.Media.Caption.Update") + public static let update = L10n.tr("Localizable", "Scene.Compose.Media.Caption.Update", fallback: "Update Caption") } } public enum ReplySettings { /// Everyone can reply - public static let everyoneCanReply = L10n.tr("Localizable", "Scene.Compose.ReplySettings.EveryoneCanReply") + public static let everyoneCanReply = L10n.tr("Localizable", "Scene.Compose.ReplySettings.EveryoneCanReply", fallback: "Everyone can reply") /// Only people you mention can reply - public static let onlyPeopleYouMentionCanReply = L10n.tr("Localizable", "Scene.Compose.ReplySettings.OnlyPeopleYouMentionCanReply") + public static let onlyPeopleYouMentionCanReply = L10n.tr("Localizable", "Scene.Compose.ReplySettings.OnlyPeopleYouMentionCanReply", fallback: "Only people you mention can reply") /// People you follow can reply - public static let peopleYouFollowCanReply = L10n.tr("Localizable", "Scene.Compose.ReplySettings.PeopleYouFollowCanReply") + public static let peopleYouFollowCanReply = L10n.tr("Localizable", "Scene.Compose.ReplySettings.PeopleYouFollowCanReply", fallback: "People you follow can reply") } public enum SaveDraft { /// Save draft - public static let action = L10n.tr("Localizable", "Scene.Compose.SaveDraft.Action") + public static let action = L10n.tr("Localizable", "Scene.Compose.SaveDraft.Action", fallback: "Save draft") /// Save draft? - public static let message = L10n.tr("Localizable", "Scene.Compose.SaveDraft.Message") + public static let message = L10n.tr("Localizable", "Scene.Compose.SaveDraft.Message", fallback: "Save draft?") } public enum Title { /// Compose - public static let compose = L10n.tr("Localizable", "Scene.Compose.Title.Compose") + public static let compose = L10n.tr("Localizable", "Scene.Compose.Title.Compose", fallback: "Compose") /// Quote - public static let quote = L10n.tr("Localizable", "Scene.Compose.Title.Quote") + public static let quote = L10n.tr("Localizable", "Scene.Compose.Title.Quote", fallback: "Quote") /// Reply - public static let reply = L10n.tr("Localizable", "Scene.Compose.Title.Reply") + public static let reply = L10n.tr("Localizable", "Scene.Compose.Title.Reply", fallback: "Reply") } public enum Visibility { /// Direct - public static let direct = L10n.tr("Localizable", "Scene.Compose.Visibility.Direct") + public static let direct = L10n.tr("Localizable", "Scene.Compose.Visibility.Direct", fallback: "Direct") /// Private - public static let `private` = L10n.tr("Localizable", "Scene.Compose.Visibility.Private") + public static let `private` = L10n.tr("Localizable", "Scene.Compose.Visibility.Private", fallback: "Private") /// Public - public static let `public` = L10n.tr("Localizable", "Scene.Compose.Visibility.Public") + public static let `public` = L10n.tr("Localizable", "Scene.Compose.Visibility.Public", fallback: "Public") /// Unlisted - public static let unlisted = L10n.tr("Localizable", "Scene.Compose.Visibility.Unlisted") + public static let unlisted = L10n.tr("Localizable", "Scene.Compose.Visibility.Unlisted", fallback: "Unlisted") } public enum VisibilityDescription { /// Visible for mentioned users only - public static let direct = L10n.tr("Localizable", "Scene.Compose.VisibilityDescription.Direct") + public static let direct = L10n.tr("Localizable", "Scene.Compose.VisibilityDescription.Direct", fallback: "Visible for mentioned users only") /// Visible for followers only - public static let `private` = L10n.tr("Localizable", "Scene.Compose.VisibilityDescription.Private") + public static let `private` = L10n.tr("Localizable", "Scene.Compose.VisibilityDescription.Private", fallback: "Visible for followers only") /// Visible for all, shown in public timelines - public static let `public` = L10n.tr("Localizable", "Scene.Compose.VisibilityDescription.Public") + public static let `public` = L10n.tr("Localizable", "Scene.Compose.VisibilityDescription.Public", fallback: "Visible for all, shown in public timelines") /// Visible for all, but not in public timelines - public static let unlisted = L10n.tr("Localizable", "Scene.Compose.VisibilityDescription.Unlisted") + public static let unlisted = L10n.tr("Localizable", "Scene.Compose.VisibilityDescription.Unlisted", fallback: "Visible for all, but not in public timelines") } public enum Vote { /// Multiple choice - public static let multiple = L10n.tr("Localizable", "Scene.Compose.Vote.Multiple") + public static let multiple = L10n.tr("Localizable", "Scene.Compose.Vote.Multiple", fallback: "Multiple choice") /// Choice %d public static func placeholderIndex(_ p1: Int) -> String { - return L10n.tr("Localizable", "Scene.Compose.Vote.PlaceholderIndex", p1) + return L10n.tr("Localizable", "Scene.Compose.Vote.PlaceholderIndex", p1, fallback: "Choice %d") } public enum Expiration { /// 1 day - public static let _1Day = L10n.tr("Localizable", "Scene.Compose.Vote.Expiration.1Day") + public static let _1Day = L10n.tr("Localizable", "Scene.Compose.Vote.Expiration.1Day", fallback: "1 day") /// 1 hour - public static let _1Hour = L10n.tr("Localizable", "Scene.Compose.Vote.Expiration.1Hour") + public static let _1Hour = L10n.tr("Localizable", "Scene.Compose.Vote.Expiration.1Hour", fallback: "1 hour") /// 30 minutes - public static let _30Min = L10n.tr("Localizable", "Scene.Compose.Vote.Expiration.30Min") + public static let _30Min = L10n.tr("Localizable", "Scene.Compose.Vote.Expiration.30Min", fallback: "30 minutes") /// 3 days - public static let _3Day = L10n.tr("Localizable", "Scene.Compose.Vote.Expiration.3Day") + public static let _3Day = L10n.tr("Localizable", "Scene.Compose.Vote.Expiration.3Day", fallback: "3 days") /// 5 minutes - public static let _5Min = L10n.tr("Localizable", "Scene.Compose.Vote.Expiration.5Min") + public static let _5Min = L10n.tr("Localizable", "Scene.Compose.Vote.Expiration.5Min", fallback: "5 minutes") /// 6 hours - public static let _6Hour = L10n.tr("Localizable", "Scene.Compose.Vote.Expiration.6Hour") + public static let _6Hour = L10n.tr("Localizable", "Scene.Compose.Vote.Expiration.6Hour", fallback: "6 hours") /// 7 days - public static let _7Day = L10n.tr("Localizable", "Scene.Compose.Vote.Expiration.7Day") + public static let _7Day = L10n.tr("Localizable", "Scene.Compose.Vote.Expiration.7Day", fallback: "7 days") } } } public enum ComposeHashtagSearch { /// Search hashtag - public static let searchPlaceholder = L10n.tr("Localizable", "Scene.ComposeHashtagSearch.SearchPlaceholder") + public static let searchPlaceholder = L10n.tr("Localizable", "Scene.ComposeHashtagSearch.SearchPlaceholder", fallback: "Search hashtag") } public enum ComposeUserSearch { /// Search users - public static let searchPlaceholder = L10n.tr("Localizable", "Scene.ComposeUserSearch.SearchPlaceholder") + public static let searchPlaceholder = L10n.tr("Localizable", "Scene.ComposeUserSearch.SearchPlaceholder", fallback: "Search users") } public enum Drafts { /// Drafts - public static let title = L10n.tr("Localizable", "Scene.Drafts.Title") + public static let title = L10n.tr("Localizable", "Scene.Drafts.Title", fallback: "Drafts") public enum Actions { /// Delete draft - public static let deleteDraft = L10n.tr("Localizable", "Scene.Drafts.Actions.DeleteDraft") + public static let deleteDraft = L10n.tr("Localizable", "Scene.Drafts.Actions.DeleteDraft", fallback: "Delete draft") /// Edit draft - public static let editDraft = L10n.tr("Localizable", "Scene.Drafts.Actions.EditDraft") + public static let editDraft = L10n.tr("Localizable", "Scene.Drafts.Actions.EditDraft", fallback: "Edit draft") } } public enum Drawer { /// Manage accounts - public static let manageAccounts = L10n.tr("Localizable", "Scene.Drawer.ManageAccounts") + public static let manageAccounts = L10n.tr("Localizable", "Scene.Drawer.ManageAccounts", fallback: "Manage accounts") /// Sign in - public static let signIn = L10n.tr("Localizable", "Scene.Drawer.SignIn") + public static let signIn = L10n.tr("Localizable", "Scene.Drawer.SignIn", fallback: "Sign in") } public enum Federated { /// Federated - public static let title = L10n.tr("Localizable", "Scene.Federated.Title") + public static let title = L10n.tr("Localizable", "Scene.Federated.Title", fallback: "Federated") } public enum Followers { /// Followers - public static let title = L10n.tr("Localizable", "Scene.Followers.Title") + public static let title = L10n.tr("Localizable", "Scene.Followers.Title", fallback: "Followers") } public enum Following { /// Following - public static let title = L10n.tr("Localizable", "Scene.Following.Title") + public static let title = L10n.tr("Localizable", "Scene.Following.Title", fallback: "Following") } public enum History { /// Clear - public static let clear = L10n.tr("Localizable", "Scene.History.Clear") + public static let clear = L10n.tr("Localizable", "Scene.History.Clear", fallback: "Clear") /// History - public static let title = L10n.tr("Localizable", "Scene.History.Title") + public static let title = L10n.tr("Localizable", "Scene.History.Title", fallback: "History") public enum Scope { /// Toot - public static let toot = L10n.tr("Localizable", "Scene.History.Scope.Toot") + public static let toot = L10n.tr("Localizable", "Scene.History.Scope.Toot", fallback: "Toot") /// Tweet - public static let tweet = L10n.tr("Localizable", "Scene.History.Scope.Tweet") + public static let tweet = L10n.tr("Localizable", "Scene.History.Scope.Tweet", fallback: "Tweet") /// User - public static let user = L10n.tr("Localizable", "Scene.History.Scope.User") + public static let user = L10n.tr("Localizable", "Scene.History.Scope.User", fallback: "User") } } public enum Likes { /// Likes - public static let title = L10n.tr("Localizable", "Scene.Likes.Title") + public static let title = L10n.tr("Localizable", "Scene.Likes.Title", fallback: "Likes") } public enum Listed { /// Listed - public static let title = L10n.tr("Localizable", "Scene.Listed.Title") + public static let title = L10n.tr("Localizable", "Scene.Listed.Title", fallback: "Listed") } public enum Lists { /// Lists - public static let title = L10n.tr("Localizable", "Scene.Lists.Title") + public static let title = L10n.tr("Localizable", "Scene.Lists.Title", fallback: "Lists") public enum Icons { /// Create list - public static let create = L10n.tr("Localizable", "Scene.Lists.Icons.Create") + public static let create = L10n.tr("Localizable", "Scene.Lists.Icons.Create", fallback: "Create list") /// Private visibility - public static let `private` = L10n.tr("Localizable", "Scene.Lists.Icons.Private") + public static let `private` = L10n.tr("Localizable", "Scene.Lists.Icons.Private", fallback: "Private visibility") } public enum Tabs { /// MY LISTS - public static let created = L10n.tr("Localizable", "Scene.Lists.Tabs.Created") + public static let created = L10n.tr("Localizable", "Scene.Lists.Tabs.Created", fallback: "MY LISTS") /// SUBSCRIBED - public static let subscribed = L10n.tr("Localizable", "Scene.Lists.Tabs.Subscribed") + public static let subscribed = L10n.tr("Localizable", "Scene.Lists.Tabs.Subscribed", fallback: "SUBSCRIBED") } } public enum ListsDetails { /// Add Members - public static let addMembers = L10n.tr("Localizable", "Scene.ListsDetails.AddMembers") + public static let addMembers = L10n.tr("Localizable", "Scene.ListsDetails.AddMembers", fallback: "Add Members") /// Delete this list: %@ public static func deleteListConfirm(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.ListsDetails.DeleteListConfirm", String(describing: p1)) + return L10n.tr("Localizable", "Scene.ListsDetails.DeleteListConfirm", String(describing: p1), fallback: "Delete this list: %@") } /// Delete this list - public static let deleteListTitle = L10n.tr("Localizable", "Scene.ListsDetails.DeleteListTitle") + public static let deleteListTitle = L10n.tr("Localizable", "Scene.ListsDetails.DeleteListTitle", fallback: "Delete this list") /// No Members Found. - public static let noMembersFound = L10n.tr("Localizable", "Scene.ListsDetails.NoMembersFound") + public static let noMembersFound = L10n.tr("Localizable", "Scene.ListsDetails.NoMembersFound", fallback: "No Members Found.") /// Lists Details - public static let title = L10n.tr("Localizable", "Scene.ListsDetails.Title") + public static let title = L10n.tr("Localizable", "Scene.ListsDetails.Title", fallback: "Lists Details") public enum Descriptions { /// %d Members public static func multipleMembers(_ p1: Int) -> String { - return L10n.tr("Localizable", "Scene.ListsDetails.Descriptions.MultipleMembers", p1) + return L10n.tr("Localizable", "Scene.ListsDetails.Descriptions.MultipleMembers", p1, fallback: "%d Members") } /// %d Subscribers public static func multipleSubscribers(_ p1: Int) -> String { - return L10n.tr("Localizable", "Scene.ListsDetails.Descriptions.MultipleSubscribers", p1) + return L10n.tr("Localizable", "Scene.ListsDetails.Descriptions.MultipleSubscribers", p1, fallback: "%d Subscribers") } /// 1 Member - public static let singleMember = L10n.tr("Localizable", "Scene.ListsDetails.Descriptions.SingleMember") + public static let singleMember = L10n.tr("Localizable", "Scene.ListsDetails.Descriptions.SingleMember", fallback: "1 Member") /// 1 Subscriber - public static let singleSubscriber = L10n.tr("Localizable", "Scene.ListsDetails.Descriptions.SingleSubscriber") + public static let singleSubscriber = L10n.tr("Localizable", "Scene.ListsDetails.Descriptions.SingleSubscriber", fallback: "1 Subscriber") } public enum MenuActions { /// Add Member - public static let addMember = L10n.tr("Localizable", "Scene.ListsDetails.MenuActions.AddMember") + public static let addMember = L10n.tr("Localizable", "Scene.ListsDetails.MenuActions.AddMember", fallback: "Add Member") /// Delete List - public static let deleteList = L10n.tr("Localizable", "Scene.ListsDetails.MenuActions.DeleteList") + public static let deleteList = L10n.tr("Localizable", "Scene.ListsDetails.MenuActions.DeleteList", fallback: "Delete List") /// Edit List - public static let editList = L10n.tr("Localizable", "Scene.ListsDetails.MenuActions.EditList") + public static let editList = L10n.tr("Localizable", "Scene.ListsDetails.MenuActions.EditList", fallback: "Edit List") /// Follow - public static let follow = L10n.tr("Localizable", "Scene.ListsDetails.MenuActions.Follow") + public static let follow = L10n.tr("Localizable", "Scene.ListsDetails.MenuActions.Follow", fallback: "Follow") /// Rename List - public static let renameList = L10n.tr("Localizable", "Scene.ListsDetails.MenuActions.RenameList") + public static let renameList = L10n.tr("Localizable", "Scene.ListsDetails.MenuActions.RenameList", fallback: "Rename List") /// Unfollow - public static let unfollow = L10n.tr("Localizable", "Scene.ListsDetails.MenuActions.Unfollow") + public static let unfollow = L10n.tr("Localizable", "Scene.ListsDetails.MenuActions.Unfollow", fallback: "Unfollow") } public enum Tabs { /// List Members - public static let members = L10n.tr("Localizable", "Scene.ListsDetails.Tabs.Members") + public static let members = L10n.tr("Localizable", "Scene.ListsDetails.Tabs.Members", fallback: "List Members") /// Subscribers - public static let subscriber = L10n.tr("Localizable", "Scene.ListsDetails.Tabs.Subscriber") + public static let subscriber = L10n.tr("Localizable", "Scene.ListsDetails.Tabs.Subscriber", fallback: "Subscribers") } } public enum ListsModify { /// Description - public static let description = L10n.tr("Localizable", "Scene.ListsModify.Description") + public static let description = L10n.tr("Localizable", "Scene.ListsModify.Description", fallback: "Description") /// Name - public static let name = L10n.tr("Localizable", "Scene.ListsModify.Name") + public static let name = L10n.tr("Localizable", "Scene.ListsModify.Name", fallback: "Name") /// Private - public static let `private` = L10n.tr("Localizable", "Scene.ListsModify.Private") + public static let `private` = L10n.tr("Localizable", "Scene.ListsModify.Private", fallback: "Private") public enum Create { /// New List - public static let title = L10n.tr("Localizable", "Scene.ListsModify.Create.Title") + public static let title = L10n.tr("Localizable", "Scene.ListsModify.Create.Title", fallback: "New List") } public enum Dialog { /// Create a list - public static let create = L10n.tr("Localizable", "Scene.ListsModify.Dialog.Create") + public static let create = L10n.tr("Localizable", "Scene.ListsModify.Dialog.Create", fallback: "Create a list") /// Rename the list - public static let edit = L10n.tr("Localizable", "Scene.ListsModify.Dialog.Edit") + public static let edit = L10n.tr("Localizable", "Scene.ListsModify.Dialog.Edit", fallback: "Rename the list") } public enum Edit { /// Edit List - public static let title = L10n.tr("Localizable", "Scene.ListsModify.Edit.Title") + public static let title = L10n.tr("Localizable", "Scene.ListsModify.Edit.Title", fallback: "Edit List") } } public enum ListsUsers { public enum Add { /// Search people - public static let search = L10n.tr("Localizable", "Scene.ListsUsers.Add.Search") + public static let search = L10n.tr("Localizable", "Scene.ListsUsers.Add.Search", fallback: "Search people") /// Search within people you follow - public static let searchWithinPeopleYouFollow = L10n.tr("Localizable", "Scene.ListsUsers.Add.SearchWithinPeopleYouFollow") + public static let searchWithinPeopleYouFollow = L10n.tr("Localizable", "Scene.ListsUsers.Add.SearchWithinPeopleYouFollow", fallback: "Search within people you follow") /// Add Member - public static let title = L10n.tr("Localizable", "Scene.ListsUsers.Add.Title") + public static let title = L10n.tr("Localizable", "Scene.ListsUsers.Add.Title", fallback: "Add Member") } public enum MenuActions { /// Add - public static let add = L10n.tr("Localizable", "Scene.ListsUsers.MenuActions.Add") + public static let add = L10n.tr("Localizable", "Scene.ListsUsers.MenuActions.Add", fallback: "Add") /// Remove - public static let remove = L10n.tr("Localizable", "Scene.ListsUsers.MenuActions.Remove") + public static let remove = L10n.tr("Localizable", "Scene.ListsUsers.MenuActions.Remove", fallback: "Remove") } } public enum Local { /// Local - public static let title = L10n.tr("Localizable", "Scene.Local.Title") + public static let title = L10n.tr("Localizable", "Scene.Local.Title", fallback: "Local") } public enum ManageAccounts { /// Delete account - public static let deleteAccount = L10n.tr("Localizable", "Scene.ManageAccounts.DeleteAccount") + public static let deleteAccount = L10n.tr("Localizable", "Scene.ManageAccounts.DeleteAccount", fallback: "Delete account") /// Accounts - public static let title = L10n.tr("Localizable", "Scene.ManageAccounts.Title") + public static let title = L10n.tr("Localizable", "Scene.ManageAccounts.Title", fallback: "Accounts") } public enum Mentions { /// Mentions - public static let title = L10n.tr("Localizable", "Scene.Mentions.Title") + public static let title = L10n.tr("Localizable", "Scene.Mentions.Title", fallback: "Mentions") } public enum Messages { /// Messages - public static let title = L10n.tr("Localizable", "Scene.Messages.Title") + public static let title = L10n.tr("Localizable", "Scene.Messages.Title", fallback: "Messages") public enum Action { /// Copy message text - public static let copyText = L10n.tr("Localizable", "Scene.Messages.Action.CopyText") + public static let copyText = L10n.tr("Localizable", "Scene.Messages.Action.CopyText", fallback: "Copy message text") /// Delete message for you - public static let delete = L10n.tr("Localizable", "Scene.Messages.Action.Delete") + public static let delete = L10n.tr("Localizable", "Scene.Messages.Action.Delete", fallback: "Delete message for you") } public enum Error { /// The Current account does not support direct messages - public static let notSupported = L10n.tr("Localizable", "Scene.Messages.Error.NotSupported") + public static let notSupported = L10n.tr("Localizable", "Scene.Messages.Error.NotSupported", fallback: "The Current account does not support direct messages") } public enum Expanded { /// [Photo] - public static let photo = L10n.tr("Localizable", "Scene.Messages.Expanded.Photo") + public static let photo = L10n.tr("Localizable", "Scene.Messages.Expanded.Photo", fallback: "[Photo]") } public enum Icon { /// Send message failed - public static let failed = L10n.tr("Localizable", "Scene.Messages.Icon.Failed") + public static let failed = L10n.tr("Localizable", "Scene.Messages.Icon.Failed", fallback: "Send message failed") } public enum NewConversation { /// Search people - public static let search = L10n.tr("Localizable", "Scene.Messages.NewConversation.Search") + public static let search = L10n.tr("Localizable", "Scene.Messages.NewConversation.Search", fallback: "Search people") /// Find people - public static let title = L10n.tr("Localizable", "Scene.Messages.NewConversation.Title") + public static let title = L10n.tr("Localizable", "Scene.Messages.NewConversation.Title", fallback: "Find people") } } public enum Notification { /// Notification - public static let title = L10n.tr("Localizable", "Scene.Notification.Title") + public static let title = L10n.tr("Localizable", "Scene.Notification.Title", fallback: "Notification") public enum Tabs { /// All - public static let all = L10n.tr("Localizable", "Scene.Notification.Tabs.All") + public static let all = L10n.tr("Localizable", "Scene.Notification.Tabs.All", fallback: "All") /// Mentions - public static let mentions = L10n.tr("Localizable", "Scene.Notification.Tabs.Mentions") + public static let mentions = L10n.tr("Localizable", "Scene.Notification.Tabs.Mentions", fallback: "Mentions") } } public enum Profile { /// Hide reply - public static let hideReply = L10n.tr("Localizable", "Scene.Profile.HideReply") + public static let hideReply = L10n.tr("Localizable", "Scene.Profile.HideReply", fallback: "Hide reply") /// Me - public static let title = L10n.tr("Localizable", "Scene.Profile.Title") + public static let title = L10n.tr("Localizable", "Scene.Profile.Title", fallback: "Me") public enum Fields { /// Joined in %@ public static func joinedInDate(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Profile.Fields.JoinedInDate", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Profile.Fields.JoinedInDate", String(describing: p1), fallback: "Joined in %@") } } public enum Filter { /// All tweets - public static let all = L10n.tr("Localizable", "Scene.Profile.Filter.All") + public static let all = L10n.tr("Localizable", "Scene.Profile.Filter.All", fallback: "All tweets") /// Exclude replies - public static let excludeReplies = L10n.tr("Localizable", "Scene.Profile.Filter.ExcludeReplies") + public static let excludeReplies = L10n.tr("Localizable", "Scene.Profile.Filter.ExcludeReplies", fallback: "Exclude replies") } public enum PermissionDeniedProfileBlocked { /// You have been blocked from viewing this user’s profile. - public static let message = L10n.tr("Localizable", "Scene.Profile.PermissionDeniedProfileBlocked.Message") + public static let message = L10n.tr("Localizable", "Scene.Profile.PermissionDeniedProfileBlocked.Message", fallback: "You have been blocked from viewing this user’s profile.") /// Permission Denied - public static let title = L10n.tr("Localizable", "Scene.Profile.PermissionDeniedProfileBlocked.Title") + public static let title = L10n.tr("Localizable", "Scene.Profile.PermissionDeniedProfileBlocked.Title", fallback: "Permission Denied") } } public enum Search { /// Saved Search - public static let savedSearch = L10n.tr("Localizable", "Scene.Search.SavedSearch") + public static let savedSearch = L10n.tr("Localizable", "Scene.Search.SavedSearch", fallback: "Saved Search") /// Show less - public static let showLess = L10n.tr("Localizable", "Scene.Search.ShowLess") + public static let showLess = L10n.tr("Localizable", "Scene.Search.ShowLess", fallback: "Show less") /// Show more - public static let showMore = L10n.tr("Localizable", "Scene.Search.ShowMore") + public static let showMore = L10n.tr("Localizable", "Scene.Search.ShowMore", fallback: "Show more") /// Search - public static let title = L10n.tr("Localizable", "Scene.Search.Title") + public static let title = L10n.tr("Localizable", "Scene.Search.Title", fallback: "Search") public enum SearchBar { /// Search tweets or users - public static let placeholder = L10n.tr("Localizable", "Scene.Search.SearchBar.Placeholder") + public static let placeholder = L10n.tr("Localizable", "Scene.Search.SearchBar.Placeholder", fallback: "Search tweets or users") } public enum Tabs { /// Hashtag - public static let hashtag = L10n.tr("Localizable", "Scene.Search.Tabs.Hashtag") + public static let hashtag = L10n.tr("Localizable", "Scene.Search.Tabs.Hashtag", fallback: "Hashtag") /// Media - public static let media = L10n.tr("Localizable", "Scene.Search.Tabs.Media") + public static let media = L10n.tr("Localizable", "Scene.Search.Tabs.Media", fallback: "Media") /// People - public static let people = L10n.tr("Localizable", "Scene.Search.Tabs.People") + public static let people = L10n.tr("Localizable", "Scene.Search.Tabs.People", fallback: "People") /// Toots - public static let toots = L10n.tr("Localizable", "Scene.Search.Tabs.Toots") + public static let toots = L10n.tr("Localizable", "Scene.Search.Tabs.Toots", fallback: "Toots") /// Tweets - public static let tweets = L10n.tr("Localizable", "Scene.Search.Tabs.Tweets") + public static let tweets = L10n.tr("Localizable", "Scene.Search.Tabs.Tweets", fallback: "Tweets") /// Users - public static let users = L10n.tr("Localizable", "Scene.Search.Tabs.Users") + public static let users = L10n.tr("Localizable", "Scene.Search.Tabs.Users", fallback: "Users") } } public enum Settings { /// Settings - public static let title = L10n.tr("Localizable", "Scene.Settings.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Title", fallback: "Settings") public enum About { /// Next generation of Twidere for Android 5.0+. /// Still in early stage. - public static let description = L10n.tr("Localizable", "Scene.Settings.About.Description") + public static let description = L10n.tr("Localizable", "Scene.Settings.About.Description", fallback: "Next generation of Twidere for Android 5.0+. \nStill in early stage.") /// License - public static let license = L10n.tr("Localizable", "Scene.Settings.About.License") + public static let license = L10n.tr("Localizable", "Scene.Settings.About.License", fallback: "License") /// About - public static let title = L10n.tr("Localizable", "Scene.Settings.About.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.About.Title", fallback: "About") /// Ver %@ public static func version(_ p1: Any) -> String { - return L10n.tr("Localizable", "Scene.Settings.About.Version", String(describing: p1)) + return L10n.tr("Localizable", "Scene.Settings.About.Version", String(describing: p1), fallback: "Ver %@") } public enum Logo { /// About page background logo - public static let background = L10n.tr("Localizable", "Scene.Settings.About.Logo.Background") + public static let background = L10n.tr("Localizable", "Scene.Settings.About.Logo.Background", fallback: "About page background logo") /// About page background logo shadow - public static let backgroundShadow = L10n.tr("Localizable", "Scene.Settings.About.Logo.BackgroundShadow") + public static let backgroundShadow = L10n.tr("Localizable", "Scene.Settings.About.Logo.BackgroundShadow", fallback: "About page background logo shadow") } } public enum Account { /// Blocked People - public static let blockedPeople = L10n.tr("Localizable", "Scene.Settings.Account.BlockedPeople") + public static let blockedPeople = L10n.tr("Localizable", "Scene.Settings.Account.BlockedPeople", fallback: "Blocked People") /// Mute and Block - public static let muteAndBlock = L10n.tr("Localizable", "Scene.Settings.Account.MuteAndBlock") + public static let muteAndBlock = L10n.tr("Localizable", "Scene.Settings.Account.MuteAndBlock", fallback: "Mute and Block") /// Muted People - public static let mutedPeople = L10n.tr("Localizable", "Scene.Settings.Account.MutedPeople") + public static let mutedPeople = L10n.tr("Localizable", "Scene.Settings.Account.MutedPeople", fallback: "Muted People") /// Account - public static let title = L10n.tr("Localizable", "Scene.Settings.Account.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Account.Title", fallback: "Account") } public enum Appearance { /// AMOLED optimized mode - public static let amoledOptimizedMode = L10n.tr("Localizable", "Scene.Settings.Appearance.AmoledOptimizedMode") + public static let amoledOptimizedMode = L10n.tr("Localizable", "Scene.Settings.Appearance.AmoledOptimizedMode", fallback: "AMOLED optimized mode") /// App Icon - public static let appIcon = L10n.tr("Localizable", "Scene.Settings.Appearance.AppIcon") + public static let appIcon = L10n.tr("Localizable", "Scene.Settings.Appearance.AppIcon", fallback: "App Icon") /// Highlight color - public static let highlightColor = L10n.tr("Localizable", "Scene.Settings.Appearance.HighlightColor") + public static let highlightColor = L10n.tr("Localizable", "Scene.Settings.Appearance.HighlightColor", fallback: "Highlight color") /// Pick color - public static let pickColor = L10n.tr("Localizable", "Scene.Settings.Appearance.PickColor") + public static let pickColor = L10n.tr("Localizable", "Scene.Settings.Appearance.PickColor", fallback: "Pick color") /// Appearance - public static let title = L10n.tr("Localizable", "Scene.Settings.Appearance.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Appearance.Title", fallback: "Appearance") public enum ScrollingTimeline { /// Hide app bar when scrolling - public static let appBar = L10n.tr("Localizable", "Scene.Settings.Appearance.ScrollingTimeline.AppBar") + public static let appBar = L10n.tr("Localizable", "Scene.Settings.Appearance.ScrollingTimeline.AppBar", fallback: "Hide app bar when scrolling") /// Hide FAB when scrolling - public static let fab = L10n.tr("Localizable", "Scene.Settings.Appearance.ScrollingTimeline.Fab") + public static let fab = L10n.tr("Localizable", "Scene.Settings.Appearance.ScrollingTimeline.Fab", fallback: "Hide FAB when scrolling") /// Hide tab bar when scrolling - public static let tabBar = L10n.tr("Localizable", "Scene.Settings.Appearance.ScrollingTimeline.TabBar") + public static let tabBar = L10n.tr("Localizable", "Scene.Settings.Appearance.ScrollingTimeline.TabBar", fallback: "Hide tab bar when scrolling") } public enum SectionHeader { /// Scrolling timeline - public static let scrollingTimeline = L10n.tr("Localizable", "Scene.Settings.Appearance.SectionHeader.ScrollingTimeline") + public static let scrollingTimeline = L10n.tr("Localizable", "Scene.Settings.Appearance.SectionHeader.ScrollingTimeline", fallback: "Scrolling timeline") /// Tab position - public static let tabPosition = L10n.tr("Localizable", "Scene.Settings.Appearance.SectionHeader.TabPosition") + public static let tabPosition = L10n.tr("Localizable", "Scene.Settings.Appearance.SectionHeader.TabPosition", fallback: "Tab position") /// Theme - public static let theme = L10n.tr("Localizable", "Scene.Settings.Appearance.SectionHeader.Theme") + public static let theme = L10n.tr("Localizable", "Scene.Settings.Appearance.SectionHeader.Theme", fallback: "Theme") /// Translation - public static let translation = L10n.tr("Localizable", "Scene.Settings.Appearance.SectionHeader.Translation") + public static let translation = L10n.tr("Localizable", "Scene.Settings.Appearance.SectionHeader.Translation", fallback: "Translation") } public enum TabPosition { /// Bottom - public static let bottom = L10n.tr("Localizable", "Scene.Settings.Appearance.TabPosition.Bottom") + public static let bottom = L10n.tr("Localizable", "Scene.Settings.Appearance.TabPosition.Bottom", fallback: "Bottom") /// Top - public static let top = L10n.tr("Localizable", "Scene.Settings.Appearance.TabPosition.Top") + public static let top = L10n.tr("Localizable", "Scene.Settings.Appearance.TabPosition.Top", fallback: "Top") } public enum Theme { /// Auto - public static let auto = L10n.tr("Localizable", "Scene.Settings.Appearance.Theme.Auto") + public static let auto = L10n.tr("Localizable", "Scene.Settings.Appearance.Theme.Auto", fallback: "Auto") /// Dark - public static let dark = L10n.tr("Localizable", "Scene.Settings.Appearance.Theme.Dark") + public static let dark = L10n.tr("Localizable", "Scene.Settings.Appearance.Theme.Dark", fallback: "Dark") /// Light - public static let light = L10n.tr("Localizable", "Scene.Settings.Appearance.Theme.Light") + public static let light = L10n.tr("Localizable", "Scene.Settings.Appearance.Theme.Light", fallback: "Light") } public enum Translation { /// Always - public static let always = L10n.tr("Localizable", "Scene.Settings.Appearance.Translation.Always") + public static let always = L10n.tr("Localizable", "Scene.Settings.Appearance.Translation.Always", fallback: "Always") /// Auto - public static let auto = L10n.tr("Localizable", "Scene.Settings.Appearance.Translation.Auto") + public static let auto = L10n.tr("Localizable", "Scene.Settings.Appearance.Translation.Auto", fallback: "Auto") /// Off - public static let off = L10n.tr("Localizable", "Scene.Settings.Appearance.Translation.Off") + public static let off = L10n.tr("Localizable", "Scene.Settings.Appearance.Translation.Off", fallback: "Off") /// Service - public static let service = L10n.tr("Localizable", "Scene.Settings.Appearance.Translation.Service") + public static let service = L10n.tr("Localizable", "Scene.Settings.Appearance.Translation.Service", fallback: "Service") /// Translate button - public static let translateButton = L10n.tr("Localizable", "Scene.Settings.Appearance.Translation.TranslateButton") + public static let translateButton = L10n.tr("Localizable", "Scene.Settings.Appearance.Translation.TranslateButton", fallback: "Translate button") } } public enum Behaviors { /// Behaviors - public static let title = L10n.tr("Localizable", "Scene.Settings.Behaviors.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Behaviors.Title", fallback: "Behaviors") public enum HistorySection { /// Enable History Record - public static let enableHistoryRecord = L10n.tr("Localizable", "Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord") + public static let enableHistoryRecord = L10n.tr("Localizable", "Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord", fallback: "Enable History Record") /// History - public static let history = L10n.tr("Localizable", "Scene.Settings.Behaviors.HistorySection.History") + public static let history = L10n.tr("Localizable", "Scene.Settings.Behaviors.HistorySection.History", fallback: "History") } public enum TabBarSection { /// Show tab bar labels - public static let showTabBarLabels = L10n.tr("Localizable", "Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels") + public static let showTabBarLabels = L10n.tr("Localizable", "Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels", fallback: "Show tab bar labels") /// Tab Bar - public static let tabBar = L10n.tr("Localizable", "Scene.Settings.Behaviors.TabBarSection.TabBar") + public static let tabBar = L10n.tr("Localizable", "Scene.Settings.Behaviors.TabBarSection.TabBar", fallback: "Tab Bar") /// Tap tab bar scroll to top - public static let tapTabBarScrollToTop = L10n.tr("Localizable", "Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop") + public static let tapTabBarScrollToTop = L10n.tr("Localizable", "Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop", fallback: "Tap tab bar scroll to top") } public enum TimelineRefreshingSection { /// Automatically refresh timeline - public static let automaticallyRefreshTimeline = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline") + public static let automaticallyRefreshTimeline = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline", fallback: "Automatically refresh timeline") /// Refresh interval - public static let refreshInterval = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval") + public static let refreshInterval = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval", fallback: "Refresh interval") /// Reset to top - public static let resetToTop = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop") + public static let resetToTop = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop", fallback: "Reset to top") /// Timeline Refreshing - public static let timelineRefreshing = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing") + public static let timelineRefreshing = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing", fallback: "Timeline Refreshing") public enum RefreshIntervalOption { /// 120 seconds - public static let _120Seconds = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds") + public static let _120Seconds = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds", fallback: "120 seconds") /// 300 seconds - public static let _300Seconds = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds") + public static let _300Seconds = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds", fallback: "300 seconds") /// 30 seconds - public static let _30Seconds = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds") + public static let _30Seconds = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds", fallback: "30 seconds") /// 60 seconds - public static let _60Seconds = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds") + public static let _60Seconds = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds", fallback: "60 seconds") } public enum ResetToTopOption { /// Double Tap - public static let doubleTap = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap") + public static let doubleTap = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap", fallback: "Double Tap") /// Single Tap - public static let singleTap = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap") + public static let singleTap = L10n.tr("Localizable", "Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap", fallback: "Single Tap") } } } public enum Display { /// Display - public static let title = L10n.tr("Localizable", "Scene.Settings.Display.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Display.Title", fallback: "Display") /// Url previews - public static let urlPreview = L10n.tr("Localizable", "Scene.Settings.Display.UrlPreview") + public static let urlPreview = L10n.tr("Localizable", "Scene.Settings.Display.UrlPreview", fallback: "Url previews") public enum DateFormat { /// Absolute - public static let absolute = L10n.tr("Localizable", "Scene.Settings.Display.DateFormat.Absolute") + public static let absolute = L10n.tr("Localizable", "Scene.Settings.Display.DateFormat.Absolute", fallback: "Absolute") /// Relative - public static let relative = L10n.tr("Localizable", "Scene.Settings.Display.DateFormat.Relative") + public static let relative = L10n.tr("Localizable", "Scene.Settings.Display.DateFormat.Relative", fallback: "Relative") } public enum Media { /// Always - public static let always = L10n.tr("Localizable", "Scene.Settings.Display.Media.Always") + public static let always = L10n.tr("Localizable", "Scene.Settings.Display.Media.Always", fallback: "Always") /// Automatic - public static let automatic = L10n.tr("Localizable", "Scene.Settings.Display.Media.Automatic") + public static let automatic = L10n.tr("Localizable", "Scene.Settings.Display.Media.Automatic", fallback: "Automatic") /// Auto playback - public static let autoPlayback = L10n.tr("Localizable", "Scene.Settings.Display.Media.AutoPlayback") + public static let autoPlayback = L10n.tr("Localizable", "Scene.Settings.Display.Media.AutoPlayback", fallback: "Auto playback") /// Media previews - public static let mediaPreviews = L10n.tr("Localizable", "Scene.Settings.Display.Media.MediaPreviews") + public static let mediaPreviews = L10n.tr("Localizable", "Scene.Settings.Display.Media.MediaPreviews", fallback: "Media previews") /// Mute by default - public static let muteByDefault = L10n.tr("Localizable", "Scene.Settings.Display.Media.MuteByDefault") + public static let muteByDefault = L10n.tr("Localizable", "Scene.Settings.Display.Media.MuteByDefault", fallback: "Mute by default") /// Off - public static let off = L10n.tr("Localizable", "Scene.Settings.Display.Media.Off") + public static let off = L10n.tr("Localizable", "Scene.Settings.Display.Media.Off", fallback: "Off") } public enum Preview { /// Thanks for using @TwidereProject! - public static let thankForUsingTwidereX = L10n.tr("Localizable", "Scene.Settings.Display.Preview.ThankForUsingTwidereX") + public static let thankForUsingTwidereX = L10n.tr("Localizable", "Scene.Settings.Display.Preview.ThankForUsingTwidereX", fallback: "Thanks for using @TwidereProject!") } public enum SectionHeader { /// Avatar - public static let avatar = L10n.tr("Localizable", "Scene.Settings.Display.SectionHeader.Avatar") + public static let avatar = L10n.tr("Localizable", "Scene.Settings.Display.SectionHeader.Avatar", fallback: "Avatar") /// Date Format - public static let dateFormat = L10n.tr("Localizable", "Scene.Settings.Display.SectionHeader.DateFormat") + public static let dateFormat = L10n.tr("Localizable", "Scene.Settings.Display.SectionHeader.DateFormat", fallback: "Date Format") /// Media - public static let media = L10n.tr("Localizable", "Scene.Settings.Display.SectionHeader.Media") + public static let media = L10n.tr("Localizable", "Scene.Settings.Display.SectionHeader.Media", fallback: "Media") /// Preview - public static let preview = L10n.tr("Localizable", "Scene.Settings.Display.SectionHeader.Preview") + public static let preview = L10n.tr("Localizable", "Scene.Settings.Display.SectionHeader.Preview", fallback: "Preview") /// Text - public static let text = L10n.tr("Localizable", "Scene.Settings.Display.SectionHeader.Text") + public static let text = L10n.tr("Localizable", "Scene.Settings.Display.SectionHeader.Text", fallback: "Text") } public enum Text { /// Avatar Style - public static let avatarStyle = L10n.tr("Localizable", "Scene.Settings.Display.Text.AvatarStyle") + public static let avatarStyle = L10n.tr("Localizable", "Scene.Settings.Display.Text.AvatarStyle", fallback: "Avatar Style") /// Circle - public static let circle = L10n.tr("Localizable", "Scene.Settings.Display.Text.Circle") + public static let circle = L10n.tr("Localizable", "Scene.Settings.Display.Text.Circle", fallback: "Circle") /// Rounded Square - public static let roundedSquare = L10n.tr("Localizable", "Scene.Settings.Display.Text.RoundedSquare") + public static let roundedSquare = L10n.tr("Localizable", "Scene.Settings.Display.Text.RoundedSquare", fallback: "Rounded Square") /// Use the system font size - public static let useTheSystemFontSize = L10n.tr("Localizable", "Scene.Settings.Display.Text.UseTheSystemFontSize") + public static let useTheSystemFontSize = L10n.tr("Localizable", "Scene.Settings.Display.Text.UseTheSystemFontSize", fallback: "Use the system font size") } } public enum Layout { /// Layout - public static let title = L10n.tr("Localizable", "Scene.Settings.Layout.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Layout.Title", fallback: "Layout") public enum Actions { /// Drawer actions - public static let drawer = L10n.tr("Localizable", "Scene.Settings.Layout.Actions.Drawer") + public static let drawer = L10n.tr("Localizable", "Scene.Settings.Layout.Actions.Drawer", fallback: "Drawer actions") /// Tabbar actions - public static let tabbar = L10n.tr("Localizable", "Scene.Settings.Layout.Actions.Tabbar") + public static let tabbar = L10n.tr("Localizable", "Scene.Settings.Layout.Actions.Tabbar", fallback: "Tabbar actions") } public enum Desc { /// Choose and arrange up to 5 actions that will appear on the tabbar (The local and federal timelines will only be displayed in Mastodon.) - public static let content = L10n.tr("Localizable", "Scene.Settings.Layout.Desc.Content") + public static let content = L10n.tr("Localizable", "Scene.Settings.Layout.Desc.Content", fallback: "Choose and arrange up to 5 actions that will appear on the tabbar (The local and federal timelines will only be displayed in Mastodon.)") /// Custom Layout - public static let title = L10n.tr("Localizable", "Scene.Settings.Layout.Desc.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Layout.Desc.Title", fallback: "Custom Layout") } } public enum Misc { /// Misc - public static let title = L10n.tr("Localizable", "Scene.Settings.Misc.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Misc.Title", fallback: "Misc") public enum Nitter { /// Third-party Twitter data provider - public static let title = L10n.tr("Localizable", "Scene.Settings.Misc.Nitter.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Misc.Nitter.Title", fallback: "Third-party Twitter data provider") public enum Dialog { public enum Information { /// Due to the limitation of Twitter API, some data might not be able to fetch from Twitter, you can use a third-party data provider to provide these data. Twidere does not take any responsibility for them. - public static let content = L10n.tr("Localizable", "Scene.Settings.Misc.Nitter.Dialog.Information.Content") + public static let content = L10n.tr("Localizable", "Scene.Settings.Misc.Nitter.Dialog.Information.Content", fallback: "Due to the limitation of Twitter API, some data might not be able to fetch from Twitter, you can use a third-party data provider to provide these data. Twidere does not take any responsibility for them.") /// Third party Twitter data provider - public static let title = L10n.tr("Localizable", "Scene.Settings.Misc.Nitter.Dialog.Information.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Misc.Nitter.Dialog.Information.Title", fallback: "Third party Twitter data provider") } public enum Usage { /// - Twitter status threading - public static let content = L10n.tr("Localizable", "Scene.Settings.Misc.Nitter.Dialog.Usage.Content") + public static let content = L10n.tr("Localizable", "Scene.Settings.Misc.Nitter.Dialog.Usage.Content", fallback: "- Twitter status threading") /// Project URL - public static let projectButton = L10n.tr("Localizable", "Scene.Settings.Misc.Nitter.Dialog.Usage.ProjectButton") + public static let projectButton = L10n.tr("Localizable", "Scene.Settings.Misc.Nitter.Dialog.Usage.ProjectButton", fallback: "Project URL") /// Using Third-party data provider in - public static let title = L10n.tr("Localizable", "Scene.Settings.Misc.Nitter.Dialog.Usage.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Misc.Nitter.Dialog.Usage.Title", fallback: "Using Third-party data provider in") } } public enum Input { /// Alternative Twitter front-end focused on privacy. - public static let description = L10n.tr("Localizable", "Scene.Settings.Misc.Nitter.Input.Description") + public static let description = L10n.tr("Localizable", "Scene.Settings.Misc.Nitter.Input.Description", fallback: "Alternative Twitter front-end focused on privacy.") /// Nitter instance URL is invalid, e.g. https://nitter.net - public static let invalid = L10n.tr("Localizable", "Scene.Settings.Misc.Nitter.Input.Invalid") + public static let invalid = L10n.tr("Localizable", "Scene.Settings.Misc.Nitter.Input.Invalid", fallback: "Nitter instance URL is invalid, e.g. https://nitter.net") /// Nitter Instance - public static let placeholder = L10n.tr("Localizable", "Scene.Settings.Misc.Nitter.Input.Placeholder") + public static let placeholder = L10n.tr("Localizable", "Scene.Settings.Misc.Nitter.Input.Placeholder", fallback: "Nitter Instance") /// Instance URL - public static let value = L10n.tr("Localizable", "Scene.Settings.Misc.Nitter.Input.Value") + public static let value = L10n.tr("Localizable", "Scene.Settings.Misc.Nitter.Input.Value", fallback: "Instance URL") } } public enum Proxy { /// Password - public static let password = L10n.tr("Localizable", "Scene.Settings.Misc.Proxy.Password") + public static let password = L10n.tr("Localizable", "Scene.Settings.Misc.Proxy.Password", fallback: "Password") /// Server - public static let server = L10n.tr("Localizable", "Scene.Settings.Misc.Proxy.Server") + public static let server = L10n.tr("Localizable", "Scene.Settings.Misc.Proxy.Server", fallback: "Server") /// Proxy settings - public static let title = L10n.tr("Localizable", "Scene.Settings.Misc.Proxy.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Misc.Proxy.Title", fallback: "Proxy settings") /// Username - public static let username = L10n.tr("Localizable", "Scene.Settings.Misc.Proxy.Username") + public static let username = L10n.tr("Localizable", "Scene.Settings.Misc.Proxy.Username", fallback: "Username") public enum Enable { /// Use proxy for all network requests - public static let description = L10n.tr("Localizable", "Scene.Settings.Misc.Proxy.Enable.Description") + public static let description = L10n.tr("Localizable", "Scene.Settings.Misc.Proxy.Enable.Description", fallback: "Use proxy for all network requests") /// Proxy - public static let title = L10n.tr("Localizable", "Scene.Settings.Misc.Proxy.Enable.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Misc.Proxy.Enable.Title", fallback: "Proxy") } public enum Port { /// Proxy server port must be numbers - public static let error = L10n.tr("Localizable", "Scene.Settings.Misc.Proxy.Port.Error") + public static let error = L10n.tr("Localizable", "Scene.Settings.Misc.Proxy.Port.Error", fallback: "Proxy server port must be numbers") /// Port - public static let title = L10n.tr("Localizable", "Scene.Settings.Misc.Proxy.Port.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Misc.Proxy.Port.Title", fallback: "Port") } public enum `Type` { /// HTTP - public static let http = L10n.tr("Localizable", "Scene.Settings.Misc.Proxy.Type.Http") + public static let http = L10n.tr("Localizable", "Scene.Settings.Misc.Proxy.Type.Http", fallback: "HTTP") /// Reverse - public static let reverse = L10n.tr("Localizable", "Scene.Settings.Misc.Proxy.Type.Reverse") + public static let reverse = L10n.tr("Localizable", "Scene.Settings.Misc.Proxy.Type.Reverse", fallback: "Reverse") /// SOCKS - public static let socks = L10n.tr("Localizable", "Scene.Settings.Misc.Proxy.Type.Socks") + public static let socks = L10n.tr("Localizable", "Scene.Settings.Misc.Proxy.Type.Socks", fallback: "SOCKS") /// Proxy type - public static let title = L10n.tr("Localizable", "Scene.Settings.Misc.Proxy.Type.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Misc.Proxy.Type.Title", fallback: "Proxy type") } } } public enum Notification { /// Accounts - public static let accounts = L10n.tr("Localizable", "Scene.Settings.Notification.Accounts") + public static let accounts = L10n.tr("Localizable", "Scene.Settings.Notification.Accounts", fallback: "Accounts") /// Show Notification - public static let notificationSwitch = L10n.tr("Localizable", "Scene.Settings.Notification.NotificationSwitch") + public static let notificationSwitch = L10n.tr("Localizable", "Scene.Settings.Notification.NotificationSwitch", fallback: "Show Notification") /// Push Notification - public static let pushNotification = L10n.tr("Localizable", "Scene.Settings.Notification.PushNotification") + public static let pushNotification = L10n.tr("Localizable", "Scene.Settings.Notification.PushNotification", fallback: "Push Notification") /// Notification - public static let title = L10n.tr("Localizable", "Scene.Settings.Notification.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Notification.Title", fallback: "Notification") public enum Mastodon { /// Favorite - public static let favorite = L10n.tr("Localizable", "Scene.Settings.Notification.Mastodon.Favorite") + public static let favorite = L10n.tr("Localizable", "Scene.Settings.Notification.Mastodon.Favorite", fallback: "Favorite") /// Mention - public static let mention = L10n.tr("Localizable", "Scene.Settings.Notification.Mastodon.Mention") + public static let mention = L10n.tr("Localizable", "Scene.Settings.Notification.Mastodon.Mention", fallback: "Mention") /// New Follow - public static let newFollow = L10n.tr("Localizable", "Scene.Settings.Notification.Mastodon.NewFollow") + public static let newFollow = L10n.tr("Localizable", "Scene.Settings.Notification.Mastodon.NewFollow", fallback: "New Follow") /// poll - public static let poll = L10n.tr("Localizable", "Scene.Settings.Notification.Mastodon.Poll") + public static let poll = L10n.tr("Localizable", "Scene.Settings.Notification.Mastodon.Poll", fallback: "poll") /// Reblog - public static let reblog = L10n.tr("Localizable", "Scene.Settings.Notification.Mastodon.Reblog") + public static let reblog = L10n.tr("Localizable", "Scene.Settings.Notification.Mastodon.Reblog", fallback: "Reblog") + } + } + public enum PrivacyAndSafety { + /// Always show sensitive media + public static let alwaysShowSensitiveMedia = L10n.tr("Localizable", "Scene.Settings.PrivacyAndSafety.AlwaysShowSensitiveMedia", fallback: "Always show sensitive media") + /// Privacy and safety + public static let title = L10n.tr("Localizable", "Scene.Settings.PrivacyAndSafety.Title", fallback: "Privacy and safety") + public enum SectionHeader { + /// Mute and Block + public static let muteAndBlock = L10n.tr("Localizable", "Scene.Settings.PrivacyAndSafety.SectionHeader.MuteAndBlock", fallback: "Mute and Block") + /// Sensitive Info + public static let sensitive = L10n.tr("Localizable", "Scene.Settings.PrivacyAndSafety.SectionHeader.Sensitive", fallback: "Sensitive Info") } } public enum SectionHeader { /// About - public static let about = L10n.tr("Localizable", "Scene.Settings.SectionHeader.About") + public static let about = L10n.tr("Localizable", "Scene.Settings.SectionHeader.About", fallback: "About") /// Account - public static let account = L10n.tr("Localizable", "Scene.Settings.SectionHeader.Account") + public static let account = L10n.tr("Localizable", "Scene.Settings.SectionHeader.Account", fallback: "Account") /// General - public static let general = L10n.tr("Localizable", "Scene.Settings.SectionHeader.General") + public static let general = L10n.tr("Localizable", "Scene.Settings.SectionHeader.General", fallback: "General") } public enum Storage { /// Storage - public static let title = L10n.tr("Localizable", "Scene.Settings.Storage.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Storage.Title", fallback: "Storage") public enum All { /// Delete all Twidere X cache. Your account credentials will not be lost. - public static let subTitle = L10n.tr("Localizable", "Scene.Settings.Storage.All.SubTitle") + public static let subTitle = L10n.tr("Localizable", "Scene.Settings.Storage.All.SubTitle", fallback: "Delete all Twidere X cache. Your account credentials will not be lost.") /// Clear all cache - public static let title = L10n.tr("Localizable", "Scene.Settings.Storage.All.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Storage.All.Title", fallback: "Clear all cache") } public enum Media { /// Clear stored media cache. - public static let subTitle = L10n.tr("Localizable", "Scene.Settings.Storage.Media.SubTitle") + public static let subTitle = L10n.tr("Localizable", "Scene.Settings.Storage.Media.SubTitle", fallback: "Clear stored media cache.") /// Clear media cache - public static let title = L10n.tr("Localizable", "Scene.Settings.Storage.Media.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Storage.Media.Title", fallback: "Clear media cache") } public enum Search { /// Clear search history - public static let title = L10n.tr("Localizable", "Scene.Settings.Storage.Search.Title") + public static let title = L10n.tr("Localizable", "Scene.Settings.Storage.Search.Title", fallback: "Clear search history") } } } public enum SignIn { /// Hello! /// Sign in to Get Started. - public static let helloSignInToGetStarted = L10n.tr("Localizable", "Scene.SignIn.HelloSignInToGetStarted") + public static let helloSignInToGetStarted = L10n.tr("Localizable", "Scene.SignIn.HelloSignInToGetStarted", fallback: "Hello!\nSign in to Get Started.") /// Sign in with Mastodon - public static let signInWithMastodon = L10n.tr("Localizable", "Scene.SignIn.SignInWithMastodon") + public static let signInWithMastodon = L10n.tr("Localizable", "Scene.SignIn.SignInWithMastodon", fallback: "Sign in with Mastodon") /// Sign in with Twitter - public static let signInWithTwitter = L10n.tr("Localizable", "Scene.SignIn.SignInWithTwitter") + public static let signInWithTwitter = L10n.tr("Localizable", "Scene.SignIn.SignInWithTwitter", fallback: "Sign in with Twitter") public enum TwitterOptions { /// Sign in with Custom Twitter Key - public static let signInWithCustomTwitterKey = L10n.tr("Localizable", "Scene.SignIn.TwitterOptions.SignInWithCustomTwitterKey") + public static let signInWithCustomTwitterKey = L10n.tr("Localizable", "Scene.SignIn.TwitterOptions.SignInWithCustomTwitterKey", fallback: "Sign in with Custom Twitter Key") /// Twitter API v2 access is required. - public static let twitterApiV2AccessIsRequired = L10n.tr("Localizable", "Scene.SignIn.TwitterOptions.TwitterApiV2AccessIsRequired") + public static let twitterApiV2AccessIsRequired = L10n.tr("Localizable", "Scene.SignIn.TwitterOptions.TwitterApiV2AccessIsRequired", fallback: "Twitter API v2 access is required.") } } public enum Status { /// Tweet - public static let title = L10n.tr("Localizable", "Scene.Status.Title") + public static let title = L10n.tr("Localizable", "Scene.Status.Title", fallback: "Tweet") /// Toot - public static let titleMastodon = L10n.tr("Localizable", "Scene.Status.TitleMastodon") + public static let titleMastodon = L10n.tr("Localizable", "Scene.Status.TitleMastodon", fallback: "Toot") public enum Like { /// %d Likes public static func multiple(_ p1: Int) -> String { - return L10n.tr("Localizable", "Scene.Status.Like.Multiple", p1) + return L10n.tr("Localizable", "Scene.Status.Like.Multiple", p1, fallback: "%d Likes") } /// 1 Like - public static let single = L10n.tr("Localizable", "Scene.Status.Like.Single") + public static let single = L10n.tr("Localizable", "Scene.Status.Like.Single", fallback: "1 Like") } public enum Quote { /// %d Quotes public static func mutiple(_ p1: Int) -> String { - return L10n.tr("Localizable", "Scene.Status.Quote.Mutiple", p1) + return L10n.tr("Localizable", "Scene.Status.Quote.Mutiple", p1, fallback: "%d Quotes") } /// 1 Quote - public static let single = L10n.tr("Localizable", "Scene.Status.Quote.Single") + public static let single = L10n.tr("Localizable", "Scene.Status.Quote.Single", fallback: "1 Quote") } public enum Reply { /// %d Replies public static func mutiple(_ p1: Int) -> String { - return L10n.tr("Localizable", "Scene.Status.Reply.Mutiple", p1) + return L10n.tr("Localizable", "Scene.Status.Reply.Mutiple", p1, fallback: "%d Replies") } /// 1 Reply - public static let single = L10n.tr("Localizable", "Scene.Status.Reply.Single") + public static let single = L10n.tr("Localizable", "Scene.Status.Reply.Single", fallback: "1 Reply") } public enum Retweet { /// %d Retweets public static func mutiple(_ p1: Int) -> String { - return L10n.tr("Localizable", "Scene.Status.Retweet.Mutiple", p1) + return L10n.tr("Localizable", "Scene.Status.Retweet.Mutiple", p1, fallback: "%d Retweets") } /// 1 Retweet - public static let single = L10n.tr("Localizable", "Scene.Status.Retweet.Single") + public static let single = L10n.tr("Localizable", "Scene.Status.Retweet.Single", fallback: "1 Retweet") } } public enum Timeline { /// Timeline - public static let title = L10n.tr("Localizable", "Scene.Timeline.Title") + public static let title = L10n.tr("Localizable", "Scene.Timeline.Title", fallback: "Timeline") } public enum Trends { /// %d people talking public static func accounts(_ p1: Int) -> String { - return L10n.tr("Localizable", "Scene.Trends.Accounts", p1) + return L10n.tr("Localizable", "Scene.Trends.Accounts", p1, fallback: "%d people talking") } /// Trending Now - public static let now = L10n.tr("Localizable", "Scene.Trends.Now") + public static let now = L10n.tr("Localizable", "Scene.Trends.Now", fallback: "Trending Now") /// Trends - public static let title = L10n.tr("Localizable", "Scene.Trends.Title") + public static let title = L10n.tr("Localizable", "Scene.Trends.Title", fallback: "Trends") /// Trends Location - public static let trendsLocation = L10n.tr("Localizable", "Scene.Trends.TrendsLocation") + public static let trendsLocation = L10n.tr("Localizable", "Scene.Trends.TrendsLocation", fallback: "Trends Location") /// Trends - Worldwide - public static let worldWide = L10n.tr("Localizable", "Scene.Trends.WorldWide") + public static let worldWide = L10n.tr("Localizable", "Scene.Trends.WorldWide", fallback: "Trends - Worldwide") /// Worldwide - public static let worldWideWithoutPrefix = L10n.tr("Localizable", "Scene.Trends.WorldWideWithoutPrefix") + public static let worldWideWithoutPrefix = L10n.tr("Localizable", "Scene.Trends.WorldWideWithoutPrefix", fallback: "Worldwide") } } - public enum Count { /// Plural format key: "Input limit remains %#@character_count@" public static func inputLimitRemains(_ p1: Int) -> String { - return L10n.tr("Localizable", "count.input_limit_remains", p1) + return L10n.tr("Localizable", "count.input_limit_remains", p1, fallback: "Plural format key: \"Input limit remains %#@character_count@\"") } /// Plural format key: "%#@like_count@" public static func like(_ p1: Int) -> String { - return L10n.tr("Localizable", "count.like", p1) + return L10n.tr("Localizable", "count.like", p1, fallback: "Plural format key: \"%#@like_count@\"") } /// Plural format key: "%#@count_media@" public static func media(_ p1: Int) -> String { - return L10n.tr("Localizable", "count.media", p1) + return L10n.tr("Localizable", "count.media", p1, fallback: "Plural format key: \"%#@count_media@\"") } /// Plural format key: "%#@count_notification@" public static func notification(_ p1: Int) -> String { - return L10n.tr("Localizable", "count.notification", p1) + return L10n.tr("Localizable", "count.notification", p1, fallback: "Plural format key: \"%#@count_notification@\"") } /// Plural format key: "%#@count_people@" public static func people(_ p1: Int) -> String { - return L10n.tr("Localizable", "count.people", p1) + return L10n.tr("Localizable", "count.people", p1, fallback: "Plural format key: \"%#@count_people@\"") } /// Plural format key: "%#@post_count@" public static func post(_ p1: Int) -> String { - return L10n.tr("Localizable", "count.post", p1) + return L10n.tr("Localizable", "count.post", p1, fallback: "Plural format key: \"%#@post_count@\"") } /// Plural format key: "%#@quote_count@" public static func quote(_ p1: Int) -> String { - return L10n.tr("Localizable", "count.quote", p1) + return L10n.tr("Localizable", "count.quote", p1, fallback: "Plural format key: \"%#@quote_count@\"") } /// Plural format key: "%#@reblog_count@" public static func reblog(_ p1: Int) -> String { - return L10n.tr("Localizable", "count.reblog", p1) + return L10n.tr("Localizable", "count.reblog", p1, fallback: "Plural format key: \"%#@reblog_count@\"") } /// Plural format key: "%#@reply_count@" public static func reply(_ p1: Int) -> String { - return L10n.tr("Localizable", "count.reply", p1) + return L10n.tr("Localizable", "count.reply", p1, fallback: "Plural format key: \"%#@reply_count@\"") } /// Plural format key: "%#@retweet_count@" public static func retweet(_ p1: Int) -> String { - return L10n.tr("Localizable", "count.retweet", p1) + return L10n.tr("Localizable", "count.retweet", p1, fallback: "Plural format key: \"%#@retweet_count@\"") } /// Plural format key: "%#@count_vote@" public static func vote(_ p1: Int) -> String { - return L10n.tr("Localizable", "count.vote", p1) + return L10n.tr("Localizable", "count.vote", p1, fallback: "Plural format key: \"%#@count_vote@\"") } public enum MetricFormatted { /// Plural format key: "%@ %#@post_count@" public static func post(_ p1: Any, _ p2: Int) -> String { - return L10n.tr("Localizable", "count.metric_formatted.post", String(describing: p1), p2) + return L10n.tr("Localizable", "count.metric_formatted.post", String(describing: p1), p2, fallback: "Plural format key: \"%@ %#@post_count@\"") } } public enum People { /// Plural format key: "%#@count_people_talking@" public static func talking(_ p1: Int) -> String { - return L10n.tr("Localizable", "count.people.talking", p1) + return L10n.tr("Localizable", "count.people.talking", p1, fallback: "Plural format key: \"%#@count_people_talking@\"") } } } - public enum Date { public enum Day { /// Plural format key: "%#@count_day_left@" public static func `left`(_ p1: Int) -> String { - return L10n.tr("Localizable", "date.day.left", p1) + return L10n.tr("Localizable", "date.day.left", p1, fallback: "Plural format key: \"%#@count_day_left@\"") } } public enum Hour { /// Plural format key: "%#@count_hour_left@" public static func `left`(_ p1: Int) -> String { - return L10n.tr("Localizable", "date.hour.left", p1) + return L10n.tr("Localizable", "date.hour.left", p1, fallback: "Plural format key: \"%#@count_hour_left@\"") } } public enum Minute { /// Plural format key: "%#@count_minute_left@" public static func `left`(_ p1: Int) -> String { - return L10n.tr("Localizable", "date.minute.left", p1) + return L10n.tr("Localizable", "date.minute.left", p1, fallback: "Plural format key: \"%#@count_minute_left@\"") } } public enum Month { /// Plural format key: "%#@count_month_left@" public static func `left`(_ p1: Int) -> String { - return L10n.tr("Localizable", "date.month.left", p1) + return L10n.tr("Localizable", "date.month.left", p1, fallback: "Plural format key: \"%#@count_month_left@\"") } } public enum Second { /// Plural format key: "%#@count_second_left@" public static func `left`(_ p1: Int) -> String { - return L10n.tr("Localizable", "date.second.left", p1) + return L10n.tr("Localizable", "date.second.left", p1, fallback: "Plural format key: \"%#@count_second_left@\"") } } public enum Year { /// Plural format key: "%#@count_year_left@" public static func `left`(_ p1: Int) -> String { - return L10n.tr("Localizable", "date.year.left", p1) + return L10n.tr("Localizable", "date.year.left", p1, fallback: "Plural format key: \"%#@count_year_left@\"") } } } @@ -1786,8 +1795,8 @@ public enum L10n { // MARK: - Implementation Details extension L10n { - private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { - let format = Bundle.module.localizedString(forKey: key, value: nil, table: table) + private static func tr(_ table: String, _ key: String, _ args: CVarArg..., fallback value: String) -> String { + let format = Bundle.module.localizedString(forKey: key, value: value, table: table) return String(format: format, locale: Locale.current, arguments: args) } } diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings index 08da8f12..7661db40 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings @@ -128,7 +128,7 @@ "Common.Alerts.PhotoSaveFail.Title" = "تعذر حفظ الصورة"; "Common.Alerts.PhotoSaved.Title" = "حُفظت الصورة"; "Common.Alerts.PostFailInvalidPoll.Message" = "Poll has empty field. Please fulfill the field then try again"; -"Common.Alerts.PostFailInvalidPoll.Title" = "Failed to Publish"; +"Common.Alerts.PostFailInvalidPoll.Title" = "تعذر النشر"; "Common.Alerts.RateLimitExceeded.Message" = "وصلت إلى حد استخدام واجهة برمجة تطبيقات تويتر"; "Common.Alerts.RateLimitExceeded.Title" = "اجتزت الحد المسموح به"; "Common.Alerts.ReportAndBlockUserSuccess.Title" = "أُبلغ عن %@ وحجب بسبب الازعاج"; @@ -160,7 +160,7 @@ "Common.Controls.Actions.Add" = "أضف"; "Common.Controls.Actions.Browse" = "تصفح"; "Common.Controls.Actions.Cancel" = "ألغِ"; -"Common.Controls.Actions.Clear" = "Clear"; +"Common.Controls.Actions.Clear" = "مسح"; "Common.Controls.Actions.Confirm" = "أكِّد"; "Common.Controls.Actions.Copy" = "انسخ"; "Common.Controls.Actions.Delete" = "حذف"; @@ -194,8 +194,8 @@ "Common.Controls.Friendship.Actions.Unfollow" = "الغ متابعته"; "Common.Controls.Friendship.Actions.Unmute" = "ألغ الكتم"; "Common.Controls.Friendship.BlockUser" = "احجب %@"; -"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Do you want to report and block %@"; -"Common.Controls.Friendship.DoYouWantToReportUser" = "Do you want to report %@"; +"Common.Controls.Friendship.DoYouWantToReportAndBlockUser" = "Do you want to report and block %@"; +"Common.Controls.Friendship.DoYouWantToReportUser" = "هل تريد أن تبلغ عن %@"; "Common.Controls.Friendship.Follower" = "متابِع"; "Common.Controls.Friendship.Followers" = "متابِعون"; "Common.Controls.Friendship.FollowsYou" = "يتابعك"; @@ -214,6 +214,7 @@ "Common.Controls.Status.Actions.DeleteTweet" = "احذف التغريدة"; "Common.Controls.Status.Actions.PinOnProfile" = "ثبته في اللاحة"; "Common.Controls.Status.Actions.Quote" = "اقتبس"; +"Common.Controls.Status.Actions.Reply" = "رد"; "Common.Controls.Status.Actions.Retweet" = "أعد تغريده"; "Common.Controls.Status.Actions.SaveMedia" = "احفظ الوسيط"; "Common.Controls.Status.Actions.Share" = "شاركه"; @@ -228,8 +229,8 @@ "Common.Controls.Status.Poll.TotalPerson" = "%@ شخص"; "Common.Controls.Status.Poll.TotalVote" = "%@ صوت"; "Common.Controls.Status.Poll.TotalVotes" = "%@ صوت"; -"Common.Controls.Status.ReplySettings.PeopleUserFollowsOrMentionedCanReply" = "People %@ follows or mentioned can reply."; -"Common.Controls.Status.ReplySettings.PeopleUserMentionedCanReply" = "People %@ mentioned can reply."; +"Common.Controls.Status.ReplySettings.PeopleUserFollowsOrMentionedCanReply" = "يمكن للأشخاص المتابعين أو المشار إليهم %@ الرد."; +"Common.Controls.Status.ReplySettings.PeopleUserMentionedCanReply" = "يمكن للأشخاص المشار إليهم %@ الرد."; "Common.Controls.Status.Thread.Show" = "اظهر هذا النقاش"; "Common.Controls.Status.UserBoosted" = "رقى %@"; "Common.Controls.Status.UserRetweeted" = "%@ أعاد التغريد"; @@ -258,10 +259,10 @@ "Common.Notification.Favourite" = "أُعجِب %@ بتبويقك"; "Common.Notification.Follow" = "تابعك %@"; "Common.Notification.FollowRequest" = "%@ يطلب متابعتك"; -"Common.Notification.FollowRequestAction.Approve" = "Approve"; -"Common.Notification.FollowRequestAction.Deny" = "Deny"; -"Common.Notification.FollowRequestResponse.FollowRequestApproved" = "Follow Request Approved"; -"Common.Notification.FollowRequestResponse.FollowRequestDenied" = "Follow Request Denied"; +"Common.Notification.FollowRequestAction.Approve" = "موافقة"; +"Common.Notification.FollowRequestAction.Deny" = "رفض"; +"Common.Notification.FollowRequestResponse.FollowRequestApproved" = "الموافقة على طلب المتابعة"; +"Common.Notification.FollowRequestResponse.FollowRequestDenied" = "رفض طلب المتابعة"; "Common.Notification.Mentions" = "ذكرك %@"; "Common.Notification.Messages.Content" = "ارسل %@ لك رسالة"; "Common.Notification.Messages.Title" = "رسالة مباشرة جديدة"; @@ -280,16 +281,16 @@ "Scene.Compose.CwPlaceholder" = "اكتب التحذير"; "Scene.Compose.LastEnd" = " و "; "Scene.Compose.Media.Caption.Add" = "Add Caption"; -"Scene.Compose.Media.Caption.AddADescriptionForThisImage" = "Add a description for this image"; +"Scene.Compose.Media.Caption.AddADescriptionForThisImage" = "اضف وصف لهذه الصورة"; "Scene.Compose.Media.Caption.Remove" = "Remove Caption"; "Scene.Compose.Media.Caption.Update" = "Update Caption"; "Scene.Compose.Media.Preview" = "معاينة"; "Scene.Compose.Media.Remove" = "أزِل"; "Scene.Compose.OthersInThisConversation" = "آخرون في هذه المحادثة:"; "Scene.Compose.Placeholder" = "ماذا يحدث ؟"; -"Scene.Compose.ReplySettings.EveryoneCanReply" = "Everyone can reply"; -"Scene.Compose.ReplySettings.OnlyPeopleYouMentionCanReply" = "Only people you mention can reply"; -"Scene.Compose.ReplySettings.PeopleYouFollowCanReply" = "People you follow can reply"; +"Scene.Compose.ReplySettings.EveryoneCanReply" = "الجميع يمكنهم الرد"; +"Scene.Compose.ReplySettings.OnlyPeopleYouMentionCanReply" = "فقط الأشخاص الذين أشرت إليهم يمكنهم الرد"; +"Scene.Compose.ReplySettings.PeopleYouFollowCanReply" = "الأشخاص الذين تتابعهم يمكنهم الرد"; "Scene.Compose.ReplyTo" = "رد على …"; "Scene.Compose.ReplyingTo" = "رد على"; "Scene.Compose.SaveDraft.Action" = "احفظ المسودة"; @@ -324,10 +325,10 @@ "Scene.Federated.Title" = "الشبكة الموحدة"; "Scene.Followers.Title" = "المتابِعون"; "Scene.Following.Title" = "المتابَعون"; -"Scene.History.Clear" = "Clear"; +"Scene.History.Clear" = "مسح"; "Scene.History.Scope.Toot" = "تبويقة"; "Scene.History.Scope.Tweet" = "تغريدة"; -"Scene.History.Scope.User" = "User"; +"Scene.History.Scope.User" = "المستخدم"; "Scene.History.Title" = "التأريخ"; "Scene.Likes.Title" = "الإعجابات"; "Scene.Listed.Title" = "مدرج"; @@ -406,7 +407,7 @@ "Scene.Settings.About.Title" = "حول"; "Scene.Settings.About.Version" = "النسخة %@"; "Scene.Settings.Account.BlockedPeople" = "Blocked People"; -"Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; +"Scene.Settings.Account.MuteAndBlock" = "كتم و حظر"; "Scene.Settings.Account.MutedPeople" = "Muted People"; "Scene.Settings.Account.Title" = "الحساب"; "Scene.Settings.Appearance.AmoledOptimizedMode" = "الوضع المحسّن لـ AMOLED"; @@ -431,22 +432,22 @@ "Scene.Settings.Appearance.Translation.Off" = "إيقاف"; "Scene.Settings.Appearance.Translation.Service" = "الخدمة"; "Scene.Settings.Appearance.Translation.TranslateButton" = "زر الترجمة"; -"Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord" = "Enable History Record"; +"Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord" = "تمكين سجل التاريخ"; "Scene.Settings.Behaviors.HistorySection.History" = "التأريخ"; -"Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels" = "Show tab bar labels"; -"Scene.Settings.Behaviors.TabBarSection.TabBar" = "Tab Bar"; -"Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop" = "Tap tab bar scroll to top"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline" = "Automatically refresh timeline"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval" = "Refresh interval"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds" = "120 seconds"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds" = "300 seconds"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds" = "30 seconds"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds" = "60 seconds"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop" = "Reset to top"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap" = "Double Tap"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap" = "Single Tap"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing" = "Timeline Refreshing"; -"Scene.Settings.Behaviors.Title" = "Behaviors"; +"Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels" = "عرض تسميات شريط التبويب"; +"Scene.Settings.Behaviors.TabBarSection.TabBar" = "شريط التبويب"; +"Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop" = "انقر على شريط التبويب لتمرير إلى الأعلى"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline" = "تحديث الخط الزمني تلقائياً"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval" = "الفاصل الزمني للتحديث"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds" = "120 ثانية"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds" = "300 ثانية"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds" = "30 ثانية"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds" = "60 ثانية"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop" = "إعادة التعيين إلى الأعلى"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap" = "نقر مزدوج"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap" = "نقرة واحدة"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing" = "تحديث الخط الزمني"; +"Scene.Settings.Behaviors.Title" = "السلوكيات"; "Scene.Settings.Display.DateFormat.Absolute" = "مُطلق"; "Scene.Settings.Display.DateFormat.Relative" = "نسبي"; "Scene.Settings.Display.Media.Always" = "دائماً"; @@ -456,7 +457,7 @@ "Scene.Settings.Display.Media.MuteByDefault" = "مكتوم افتراضيًا"; "Scene.Settings.Display.Media.Off" = "إيقاف"; "Scene.Settings.Display.Preview.ThankForUsingTwidereX" = "شكرا على استخدام @TwidereProject!"; -"Scene.Settings.Display.SectionHeader.Avatar" = "Avatar"; +"Scene.Settings.Display.SectionHeader.Avatar" = "الصورة الرمزية"; "Scene.Settings.Display.SectionHeader.DateFormat" = "صيغة التاريخ"; "Scene.Settings.Display.SectionHeader.Media" = "الوسائط"; "Scene.Settings.Display.SectionHeader.Preview" = "معاينة"; @@ -496,14 +497,18 @@ "Scene.Settings.Misc.Proxy.Username" = "اسم المستخدم"; "Scene.Settings.Misc.Title" = "متفرقات"; "Scene.Settings.Notification.Accounts" = "الحسابات"; -"Scene.Settings.Notification.Mastodon.Favorite" = "Favorite"; -"Scene.Settings.Notification.Mastodon.Mention" = "Mention"; -"Scene.Settings.Notification.Mastodon.NewFollow" = "New Follow"; -"Scene.Settings.Notification.Mastodon.Poll" = "poll"; -"Scene.Settings.Notification.Mastodon.Reblog" = "Reblog"; +"Scene.Settings.Notification.Mastodon.Favorite" = "المفضلة"; +"Scene.Settings.Notification.Mastodon.Mention" = "إشارة"; +"Scene.Settings.Notification.Mastodon.NewFollow" = "متابع جديد"; +"Scene.Settings.Notification.Mastodon.Poll" = "تصويت"; +"Scene.Settings.Notification.Mastodon.Reblog" = "معاد تدوينه"; "Scene.Settings.Notification.NotificationSwitch" = "أظهر الإشعارات"; -"Scene.Settings.Notification.PushNotification" = "Push Notification"; +"Scene.Settings.Notification.PushNotification" = "إرسال الإشعارات"; "Scene.Settings.Notification.Title" = "التنبيهات"; +"Scene.Settings.PrivacyAndSafety.AlwaysShowSensitiveMedia" = "Always show sensitive media"; +"Scene.Settings.PrivacyAndSafety.SectionHeader.MuteAndBlock" = "كتم و حظر"; +"Scene.Settings.PrivacyAndSafety.SectionHeader.Sensitive" = "Sensitive Info"; +"Scene.Settings.PrivacyAndSafety.Title" = "Privacy and safety"; "Scene.Settings.SectionHeader.About" = "حول"; "Scene.Settings.SectionHeader.Account" = "الحساب"; "Scene.Settings.SectionHeader.General" = "عامّ"; @@ -536,4 +541,4 @@ "Scene.Trends.Title" = "الشائع"; "Scene.Trends.TrendsLocation" = "Trends Location"; "Scene.Trends.WorldWide" = "الشائع - عالميا"; -"Scene.Trends.WorldWideWithoutPrefix" = "Worldwide"; \ No newline at end of file +"Scene.Trends.WorldWideWithoutPrefix" = "عالمي"; \ No newline at end of file diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings index 11ce2c25..06d80d2f 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings @@ -8,21 +8,21 @@ "Accessibility.Common.Logo.Twitter" = "Logo de Twitter"; "Accessibility.Common.More" = "Més"; "Accessibility.Common.NetworkImage" = "Imatge de xarxa"; -"Accessibility.Common.Status.Actions.HideContent" = "Hide content"; -"Accessibility.Common.Status.Actions.HideMedia" = "Hide media"; +"Accessibility.Common.Status.Actions.HideContent" = "Amaga el contingut"; +"Accessibility.Common.Status.Actions.HideMedia" = "Amaga multimèdia"; "Accessibility.Common.Status.Actions.Like" = "M'agrada"; "Accessibility.Common.Status.Actions.Menu" = "Menú"; "Accessibility.Common.Status.Actions.Reply" = "Resposta"; "Accessibility.Common.Status.Actions.Retweet" = "Repiular"; -"Accessibility.Common.Status.Actions.RevealContent" = "Reveal content"; -"Accessibility.Common.Status.Actions.RevealMedia" = "Reveal media"; -"Accessibility.Common.Status.AuthorAvatar" = "Author avatar"; -"Accessibility.Common.Status.Boosted" = "Boosted"; -"Accessibility.Common.Status.ContentWarning" = "Content Warning"; -"Accessibility.Common.Status.Liked" = "Liked"; +"Accessibility.Common.Status.Actions.RevealContent" = "Revela el contingut"; +"Accessibility.Common.Status.Actions.RevealMedia" = "Revela multimèdia"; +"Accessibility.Common.Status.AuthorAvatar" = "Avatar de l'autor"; +"Accessibility.Common.Status.Boosted" = "Impulsat"; +"Accessibility.Common.Status.ContentWarning" = "Avís de contingut"; +"Accessibility.Common.Status.Liked" = "Agradat"; "Accessibility.Common.Status.Location" = "Ubicació"; "Accessibility.Common.Status.Media" = "Multimèdia"; -"Accessibility.Common.Status.PollOptionOrdinalPrefix" = "Poll option"; +"Accessibility.Common.Status.PollOptionOrdinalPrefix" = "Opció de votació"; "Accessibility.Common.Status.Retweeted" = "Repiulades"; "Accessibility.Common.Video.Play" = "Reprodueix el vídeo"; "Accessibility.Scene.Compose.AddMention" = "Afegeix menció"; @@ -30,54 +30,54 @@ "Accessibility.Scene.Compose.Image" = "Afegeix imatge"; "Accessibility.Scene.Compose.Location.Disable" = "Desactivar la ubicació"; "Accessibility.Scene.Compose.Location.Enable" = "Habilita la ubicació"; -"Accessibility.Scene.Compose.MediaInsert.Camera" = "Take Photo"; -"Accessibility.Scene.Compose.MediaInsert.Gif" = "Add GIF"; -"Accessibility.Scene.Compose.MediaInsert.Library" = "Browse Library"; -"Accessibility.Scene.Compose.MediaInsert.RecordVideo" = "Record Video"; +"Accessibility.Scene.Compose.MediaInsert.Camera" = "Fes una foto"; +"Accessibility.Scene.Compose.MediaInsert.Gif" = "Afegiu un GIF"; +"Accessibility.Scene.Compose.MediaInsert.Library" = "Navegueu per la biblioteca"; +"Accessibility.Scene.Compose.MediaInsert.RecordVideo" = "Enregistra un vídeo"; "Accessibility.Scene.Compose.Send" = "Enviar"; -"Accessibility.Scene.Compose.Thread" = "Thread mode"; -"Accessibility.Scene.Gif.Search" = "Search GIF"; +"Accessibility.Scene.Compose.Thread" = "Mode de fil"; +"Accessibility.Scene.Gif.Search" = "Cerca un GIF"; "Accessibility.Scene.Gif.Title" = "GIPHY"; "Accessibility.Scene.Home.Compose" = "Redacta"; "Accessibility.Scene.Home.Drawer.AccountDropdown" = "Desplegable del compte"; "Accessibility.Scene.Home.Menu" = "Menú"; "Accessibility.Scene.ManageAccounts.Add" = "Afegir"; -"Accessibility.Scene.ManageAccounts.CurrentSignInUser" = "Current sign-in user: %@"; +"Accessibility.Scene.ManageAccounts.CurrentSignInUser" = "Usuari actual: %@"; "Accessibility.Scene.Search.History" = "Historial"; "Accessibility.Scene.Search.Save" = "Desa"; "Accessibility.Scene.Settings.Display.FontSize" = "Mida de la lletra"; -"Accessibility.Scene.SignIn.PleaseEnterMastodonDomainToSignIn" = "Please enter Mastodon domain to sign-in"; -"Accessibility.Scene.SignIn.TwitterClientAuthenticationKeySetting" = "Twitter client authentication key setting"; +"Accessibility.Scene.SignIn.PleaseEnterMastodonDomainToSignIn" = "Si us plau, introduïu el domini Mastodon per connectar-se"; +"Accessibility.Scene.SignIn.TwitterClientAuthenticationKeySetting" = "Ajustus de la clau d'autentificació del client de Twitter"; "Accessibility.Scene.Timeline.LoadGap" = "Carrega"; "Accessibility.Scene.User.Location" = "Ubicació"; "Accessibility.Scene.User.Tab.Favourite" = "Favorit"; "Accessibility.Scene.User.Tab.Media" = "Multimèdia"; "Accessibility.Scene.User.Tab.Status" = "Estats"; "Accessibility.Scene.User.Website" = "Lloc web"; -"Accessibility.VoiceOver.DoubleTapAndHoldToDisplayMenu" = "Double tap and hold to display menu"; -"Accessibility.VoiceOver.DoubleTapAndHoldToOpenTheAccountsPanel" = "Double tap and hold to open the accounts panel"; -"Accessibility.VoiceOver.DoubleTapToOpenProfile" = "Double tap to open profile"; -"Accessibility.VoiceOver.Selected" = "Selected"; +"Accessibility.VoiceOver.DoubleTapAndHoldToDisplayMenu" = "Feu doble toc i manteniu premut per mostrar el menú"; +"Accessibility.VoiceOver.DoubleTapAndHoldToOpenTheAccountsPanel" = "Feu doble toc i manteniu premut per el taulell de comptes"; +"Accessibility.VoiceOver.DoubleTapToOpenProfile" = "Feu doble toc per obrir el perfil"; +"Accessibility.VoiceOver.Selected" = "Seleccionats"; "Common.Alerts.AccountSuspended.Message" = "Twitter suspèn comptes que violen %@"; "Common.Alerts.AccountSuspended.Title" = "Compte suspès"; "Common.Alerts.AccountSuspended.TwitterRules" = "Normes de Twitter"; "Common.Alerts.AccountTemporarilyLocked.Message" = "Obre Twitter per desblocar"; "Common.Alerts.AccountTemporarilyLocked.Title" = "Compte blocat temporalment"; -"Common.Alerts.BlockUserConfirm.Title" = "Do you want to block %@?"; +"Common.Alerts.BlockUserConfirm.Title" = "Voleu blocar a %@?"; "Common.Alerts.BlockUserSuccess.Title" = "%@ ha estat blocat"; "Common.Alerts.CancelFollowRequest.Message" = "Canceŀlar la petició de seguiment a %@?"; -"Common.Alerts.DeleteTootConfirm.Message" = "Do you want to delete this toot?"; -"Common.Alerts.DeleteTootConfirm.Title" = "Delete Toot"; -"Common.Alerts.DeleteTweetConfirm.Message" = "Do you want to delete this tweet?"; -"Common.Alerts.DeleteTweetConfirm.Title" = "Delete Tweet"; +"Common.Alerts.DeleteTootConfirm.Message" = "Voleu suprimir aquest Tut?"; +"Common.Alerts.DeleteTootConfirm.Title" = "Esborra Tut"; +"Common.Alerts.DeleteTweetConfirm.Message" = "Voleu suprimir aquesta piulada?"; +"Common.Alerts.DeleteTweetConfirm.Title" = "Esborra piulada"; "Common.Alerts.FailedToAddListMember.Message" = "Si us plau, torna-ho a provar"; -"Common.Alerts.FailedToAddListMember.Title" = "Failed to Add List Member"; +"Common.Alerts.FailedToAddListMember.Title" = "No s'ha pogut afegir un membre a la llista"; "Common.Alerts.FailedToBlockUser.Message" = "Si us plau, torna-ho a provar"; "Common.Alerts.FailedToBlockUser.Title" = "Error al blocar %@"; "Common.Alerts.FailedToDeleteList.Message" = "Si us plau, torna-ho a provar"; -"Common.Alerts.FailedToDeleteList.Title" = "Failed to Delete List"; +"Common.Alerts.FailedToDeleteList.Title" = "No s'ha pogut esborrar la llista"; "Common.Alerts.FailedToDeleteToot.Message" = "Si us plau, torna-ho a provar"; -"Common.Alerts.FailedToDeleteToot.Title" = "Failed to Delete Toot"; +"Common.Alerts.FailedToDeleteToot.Title" = "No s'ha pogut esborrar el Tut"; "Common.Alerts.FailedToDeleteTweet.Message" = "Si us plau, torna-ho a provar"; "Common.Alerts.FailedToDeleteTweet.Title" = "Error al esborrar piulada"; "Common.Alerts.FailedToFollowing.Message" = "Si us plau, torna-ho a provar"; @@ -91,13 +91,13 @@ "Common.Alerts.FailedToMuteUser.Message" = "Si us plau, torna-ho a provar"; "Common.Alerts.FailedToMuteUser.Title" = "Error al silenciar %@"; "Common.Alerts.FailedToRemoveListMember.Message" = "Si us plau, torna-ho a provar"; -"Common.Alerts.FailedToRemoveListMember.Title" = "Failed to Remove List Member"; +"Common.Alerts.FailedToRemoveListMember.Title" = "No s'ha pogut eliminar un membre de la llista"; "Common.Alerts.FailedToReportAndBlockUser.Message" = "Si us plau, torna-ho a provar"; "Common.Alerts.FailedToReportAndBlockUser.Title" = "Error al denunciar i blocar %@"; "Common.Alerts.FailedToReportUser.Message" = "Si us plau, torna-ho a provar"; "Common.Alerts.FailedToReportUser.Title" = "Error al denunciar %@"; -"Common.Alerts.FailedToSendMessage.Message" = "Failed to send message"; -"Common.Alerts.FailedToSendMessage.Title" = "Sending message"; +"Common.Alerts.FailedToSendMessage.Message" = "No s'ha pogut enviar el missatge"; +"Common.Alerts.FailedToSendMessage.Title" = "S'està enviant el missatge"; "Common.Alerts.FailedToUnblockUser.Message" = "Si us plau, torna-ho a provar"; "Common.Alerts.FailedToUnblockUser.Title" = "Error al desblocar %@"; "Common.Alerts.FailedToUnfollowing.Message" = "Si us plau, torna-ho a provar"; @@ -107,63 +107,63 @@ "Common.Alerts.FollowingRequestSent.Title" = "Petició de seguiment enviada"; "Common.Alerts.FollowingSuccess.Title" = "Seguiment realitzat"; "Common.Alerts.ListDeleted.Message" = "Si us plau, torna-ho a provar"; -"Common.Alerts.ListDeleted.Title" = "List Deleted"; -"Common.Alerts.ListMemberRemoved.Title" = "List Member Removed"; +"Common.Alerts.ListDeleted.Title" = "Llista esborrada"; +"Common.Alerts.ListMemberRemoved.Title" = "Membre de la llista eliminat"; "Common.Alerts.MediaSaveFail.Message" = "Si us plau, torna-ho a provar"; "Common.Alerts.MediaSaveFail.Title" = "No s'ha pogut desar el mitjà"; "Common.Alerts.MediaSaved.Title" = "S'ha desat el mitjà"; "Common.Alerts.MediaSaving.Title" = "S'està desant el mitjà"; -"Common.Alerts.MediaSharing.Title" = "Media will be shared after download is completed"; -"Common.Alerts.MuteUserConfirm.Title" = "Do you want to mute %@?"; +"Common.Alerts.MediaSharing.Title" = "Mèdia serà compartida després de que finalitzi la descàrrega"; +"Common.Alerts.MuteUserConfirm.Title" = "Voleu silenciar a %@?"; "Common.Alerts.MuteUserSuccess.Title" = "%@ ha estat silenciat"; "Common.Alerts.NoTweetsFound.Title" = "No s'han trobat piulades"; "Common.Alerts.PermissionDeniedFriendshipBlocked.Message" = "Has estat blocat de seguir aquest compte a petició de l'usuari"; "Common.Alerts.PermissionDeniedFriendshipBlocked.Title" = "Permís denegat"; "Common.Alerts.PermissionDeniedNotAuthorized.Message" = "Ho sento, no estàs autoritzat"; "Common.Alerts.PermissionDeniedNotAuthorized.Title" = "Permís denegat"; -"Common.Alerts.PhotoCopied.Title" = "Photo Copied"; +"Common.Alerts.PhotoCopied.Title" = "Fotografia copiada"; "Common.Alerts.PhotoCopyFail.Message" = "Si us plau, torna-ho a provar"; -"Common.Alerts.PhotoCopyFail.Title" = "Failed to Copy Photo"; +"Common.Alerts.PhotoCopyFail.Title" = "No s'ha pogut copiar la fotografia"; "Common.Alerts.PhotoSaveFail.Message" = "Si us plau, torna-ho a provar"; "Common.Alerts.PhotoSaveFail.Title" = "Error al desar la imatge"; "Common.Alerts.PhotoSaved.Title" = "Imatge desada"; -"Common.Alerts.PostFailInvalidPoll.Message" = "Poll has empty field. Please fulfill the field then try again"; -"Common.Alerts.PostFailInvalidPoll.Title" = "Failed to Publish"; +"Common.Alerts.PostFailInvalidPoll.Message" = "L'enquesta té camps buits. Si us plau, empleneu-los tots i torneu a provar"; +"Common.Alerts.PostFailInvalidPoll.Title" = "No s'ha pogut publicar"; "Common.Alerts.RateLimitExceeded.Message" = "S'ha arribat al límit d'ús de la API de Twitter"; "Common.Alerts.RateLimitExceeded.Title" = "Taxa d'ús excedida"; "Common.Alerts.ReportAndBlockUserSuccess.Title" = "%@ ha estat denunciat per publicitat abusiva i blocat"; "Common.Alerts.ReportUserSuccess.Title" = "%@ ha estat denunciat per publicitat abusiva"; -"Common.Alerts.RequestThrottle.Message" = "Operation too frequent. Please try again later"; -"Common.Alerts.RequestThrottle.Title" = "Request Throttle"; -"Common.Alerts.SignOutUserConfirm.Message" = "Do you want to sign out?"; -"Common.Alerts.SignOutUserConfirm.Title" = "Sign out"; +"Common.Alerts.RequestThrottle.Message" = "Operació massa freqüent. Si us plau, torneu-ho a provar més tard"; +"Common.Alerts.RequestThrottle.Title" = "Regula"; +"Common.Alerts.SignOutUserConfirm.Message" = "Voleu finalitzar la sessió?"; +"Common.Alerts.SignOutUserConfirm.Title" = "Finalitza la sessió"; "Common.Alerts.TooManyRequests.Title" = "Massa soŀlicituds"; -"Common.Alerts.TootDeleted.Title" = "Toot Deleted"; -"Common.Alerts.TootFail.DraftSavedMessage" = "Your toot has been saved to Drafts."; +"Common.Alerts.TootDeleted.Title" = "Tut esborrat"; +"Common.Alerts.TootFail.DraftSavedMessage" = "El vostre Tut s'ha desat a esborranys."; "Common.Alerts.TootFail.Message" = "Si us plau, torna-ho a provar"; -"Common.Alerts.TootFail.Title" = "Failed to Toot"; -"Common.Alerts.TootPosted.Title" = "Toot Posted"; -"Common.Alerts.TootSending.Title" = "Sending toot"; +"Common.Alerts.TootFail.Title" = "No s'ha pogut fer Tut"; +"Common.Alerts.TootPosted.Title" = "Tut publicat"; +"Common.Alerts.TootSending.Title" = "Enviant Tut"; "Common.Alerts.TweetDeleted.Title" = "Piulada esborrada"; -"Common.Alerts.TweetFail.DraftSavedMessage" = "Your tweet has been saved to Drafts."; +"Common.Alerts.TweetFail.DraftSavedMessage" = "La vostra piulada s'ha desat a esborranys."; "Common.Alerts.TweetFail.Message" = "Si us plau, torna-ho a provar"; "Common.Alerts.TweetFail.Title" = "Error al piular"; -"Common.Alerts.TweetPosted.Title" = "Tweet Posted"; +"Common.Alerts.TweetPosted.Title" = "Piulada publicada"; "Common.Alerts.TweetSending.Title" = "S'està enviant la piulada"; "Common.Alerts.TweetSent.Title" = "Piulada enviada"; -"Common.Alerts.UnblockUserConfirm.Title" = "Do you want to unblock %@?"; +"Common.Alerts.UnblockUserConfirm.Title" = "Voleu desblocar a %@?"; "Common.Alerts.UnblockUserSuccess.Title" = "%@ ha estat desblocat"; "Common.Alerts.UnfollowUser.Message" = "Deixar de seguir l'usuari %@?"; "Common.Alerts.UnfollowingSuccess.Title" = "S'ha deixat de seguir"; -"Common.Alerts.UnmuteUserConfirm.Title" = "Do you want to unmute %@?"; +"Common.Alerts.UnmuteUserConfirm.Title" = "Voleu dessilenciar a %@?"; "Common.Alerts.UnmuteUserSuccess.Title" = "%@ ha estat silenciat"; "Common.Controls.Actions.Add" = "Afegir"; -"Common.Controls.Actions.Browse" = "Browse"; +"Common.Controls.Actions.Browse" = "Mostra"; "Common.Controls.Actions.Cancel" = "Canceŀla"; -"Common.Controls.Actions.Clear" = "Clear"; +"Common.Controls.Actions.Clear" = "Neteja"; "Common.Controls.Actions.Confirm" = "Confirma"; -"Common.Controls.Actions.Copy" = "Copy"; -"Common.Controls.Actions.Delete" = "Delete"; +"Common.Controls.Actions.Copy" = "Copia"; +"Common.Controls.Actions.Delete" = "Esborra "; "Common.Controls.Actions.Done" = "Fet"; "Common.Controls.Actions.Edit" = "Edita"; "Common.Controls.Actions.Ok" = "D'acord"; @@ -172,30 +172,30 @@ "Common.Controls.Actions.Remove" = "Elimina"; "Common.Controls.Actions.Save" = "Desa"; "Common.Controls.Actions.SavePhoto" = "Desa la foto"; -"Common.Controls.Actions.Share" = "Share"; +"Common.Controls.Actions.Share" = "Comparteix"; "Common.Controls.Actions.ShareLink" = "Comparteix l'enllaç"; -"Common.Controls.Actions.ShareMedia" = "Share media"; -"Common.Controls.Actions.ShareMediaMenu.Link" = "Link"; +"Common.Controls.Actions.ShareMedia" = "Comparteix multimèdia"; +"Common.Controls.Actions.ShareMediaMenu.Link" = "Enllaç"; "Common.Controls.Actions.ShareMediaMenu.Media" = "Multimèdia"; "Common.Controls.Actions.SignIn" = "Iniciar sessió"; -"Common.Controls.Actions.SignOut" = "Sign out"; +"Common.Controls.Actions.SignOut" = "Tanca sessió"; "Common.Controls.Actions.TakePhoto" = "Fes una foto"; "Common.Controls.Actions.Yes" = "Sí"; "Common.Controls.Friendship.Actions.Block" = "Bloca"; -"Common.Controls.Friendship.Actions.Blocked" = "Blocked"; +"Common.Controls.Friendship.Actions.Blocked" = "Blocat"; "Common.Controls.Friendship.Actions.Follow" = "Segueix"; "Common.Controls.Friendship.Actions.Following" = "Seguint"; "Common.Controls.Friendship.Actions.Mute" = "Silencia"; "Common.Controls.Friendship.Actions.Pending" = "Pendents"; "Common.Controls.Friendship.Actions.Report" = "Denuncia"; "Common.Controls.Friendship.Actions.ReportAndBlock" = "Denuncia i bloca"; -"Common.Controls.Friendship.Actions.Request" = "Request"; +"Common.Controls.Friendship.Actions.Request" = "Sol·licita"; "Common.Controls.Friendship.Actions.Unblock" = "Desbloca"; "Common.Controls.Friendship.Actions.Unfollow" = "Deixa de seguir"; "Common.Controls.Friendship.Actions.Unmute" = "Deixar de silenciar"; "Common.Controls.Friendship.BlockUser" = "Bloca %@"; -"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Do you want to report and block %@"; -"Common.Controls.Friendship.DoYouWantToReportUser" = "Do you want to report %@"; +"Common.Controls.Friendship.DoYouWantToReportAndBlockUser" = "Do you want to report and block %@"; +"Common.Controls.Friendship.DoYouWantToReportUser" = "Voleu informar de %@"; "Common.Controls.Friendship.Follower" = "seguidor"; "Common.Controls.Friendship.Followers" = "seguidors"; "Common.Controls.Friendship.FollowsYou" = "Et segueix"; @@ -203,24 +203,25 @@ "Common.Controls.Friendship.UserIsFollowingYou" = "%@ et segueix"; "Common.Controls.Friendship.UserIsNotFollowingYou" = "%@ no et segueix"; "Common.Controls.Ios.PhotoLibrary" = "Galeria d'imatges"; -"Common.Controls.List.NoResults" = "No results"; +"Common.Controls.List.NoResults" = "Cap resultat"; "Common.Controls.ProfileDashboard.Followers" = "Seguidors"; "Common.Controls.ProfileDashboard.Following" = "Seguint"; "Common.Controls.ProfileDashboard.Listed" = "Llistat"; "Common.Controls.Status.Actions.Bookmark" = "Marcador"; -"Common.Controls.Status.Actions.Boost" = "Boost"; +"Common.Controls.Status.Actions.Boost" = "Impulsa"; "Common.Controls.Status.Actions.CopyLink" = "Copia l'enllaç"; "Common.Controls.Status.Actions.CopyText" = "Copia el text"; "Common.Controls.Status.Actions.DeleteTweet" = "Esborra la piulada"; -"Common.Controls.Status.Actions.PinOnProfile" = "Pin on Profile"; +"Common.Controls.Status.Actions.PinOnProfile" = "Fixa en el perfil"; "Common.Controls.Status.Actions.Quote" = "Cita"; +"Common.Controls.Status.Actions.Reply" = "Resposta"; "Common.Controls.Status.Actions.Retweet" = "Repiular"; -"Common.Controls.Status.Actions.SaveMedia" = "Save media"; -"Common.Controls.Status.Actions.Share" = "Share"; -"Common.Controls.Status.Actions.ShareContent" = "Share content"; +"Common.Controls.Status.Actions.SaveMedia" = "Desa multimèdia"; +"Common.Controls.Status.Actions.Share" = "Comparteix"; +"Common.Controls.Status.Actions.ShareContent" = "Comparteix el contingut"; "Common.Controls.Status.Actions.ShareLink" = "Comparteix l'enllaç"; -"Common.Controls.Status.Actions.Translate" = "Translate"; -"Common.Controls.Status.Actions.UnpinFromProfile" = "Unpin from Profile"; +"Common.Controls.Status.Actions.Translate" = "Tradueix"; +"Common.Controls.Status.Actions.UnpinFromProfile" = "Treu del perfil"; "Common.Controls.Status.Actions.Vote" = "Votació"; "Common.Controls.Status.Media" = "Multimèdia"; "Common.Controls.Status.Poll.Expired" = "Tancada"; @@ -228,17 +229,17 @@ "Common.Controls.Status.Poll.TotalPerson" = "%@ persona"; "Common.Controls.Status.Poll.TotalVote" = "%@ vots"; "Common.Controls.Status.Poll.TotalVotes" = "%@ vots"; -"Common.Controls.Status.ReplySettings.PeopleUserFollowsOrMentionedCanReply" = "People %@ follows or mentioned can reply."; -"Common.Controls.Status.ReplySettings.PeopleUserMentionedCanReply" = "People %@ mentioned can reply."; -"Common.Controls.Status.Thread.Show" = "Show this thread"; -"Common.Controls.Status.UserBoosted" = "%@ boosted"; +"Common.Controls.Status.ReplySettings.PeopleUserFollowsOrMentionedCanReply" = "Gent %@ que et segueix o menciona pot respondre."; +"Common.Controls.Status.ReplySettings.PeopleUserMentionedCanReply" = "Gent %@ mencionada por respondre."; +"Common.Controls.Status.Thread.Show" = "Mostra aquest fil"; +"Common.Controls.Status.UserBoosted" = "%@ impulsat"; "Common.Controls.Status.UserRetweeted" = "%@ repiulades"; -"Common.Controls.Status.YouBoosted" = "You boosted"; -"Common.Controls.Status.YouRetweeted" = "You retweeted"; +"Common.Controls.Status.YouBoosted" = "Heu impulsat"; +"Common.Controls.Status.YouRetweeted" = "Heu repiulat"; "Common.Controls.Timeline.LoadMore" = "Carrega'n més"; -"Common.Controls.User.Actions.AddRemoveFromLists" = "Add/remove from Lists"; -"Common.Controls.User.Actions.ViewListed" = "View Listed"; -"Common.Controls.User.Actions.ViewLists" = "View Lists"; +"Common.Controls.User.Actions.AddRemoveFromLists" = "Afegeix/elimina de les llistes"; +"Common.Controls.User.Actions.ViewListed" = "Mostra en llistes"; +"Common.Controls.User.Actions.ViewLists" = "Mostra les llistes"; "Common.Countable.Like.Multiple" = "%@ agradaments"; "Common.Countable.Like.Single" = "%@ agradament"; "Common.Countable.List.Multiple" = "%@ llistes"; @@ -257,39 +258,39 @@ "Common.Countable.Tweet.Single" = "%@ piulada"; "Common.Notification.Favourite" = "%@ han impulsat el vostre tut"; "Common.Notification.Follow" = "%@ et segueix"; -"Common.Notification.FollowRequest" = "%@ has requested to follow you"; -"Common.Notification.FollowRequestAction.Approve" = "Approve"; -"Common.Notification.FollowRequestAction.Deny" = "Deny"; -"Common.Notification.FollowRequestResponse.FollowRequestApproved" = "Follow Request Approved"; -"Common.Notification.FollowRequestResponse.FollowRequestDenied" = "Follow Request Denied"; +"Common.Notification.FollowRequest" = "%@ ha sol·licitat seguir-te"; +"Common.Notification.FollowRequestAction.Approve" = "Aprova"; +"Common.Notification.FollowRequestAction.Deny" = "Refusa"; +"Common.Notification.FollowRequestResponse.FollowRequestApproved" = "Sol·licitud de seguiment aprovada"; +"Common.Notification.FollowRequestResponse.FollowRequestDenied" = "Sol·licitud de seguiment refusada"; "Common.Notification.Mentions" = "%@ t'ha mencionat"; -"Common.Notification.Messages.Content" = "%@ sent you a message"; -"Common.Notification.Messages.Title" = "New direct message"; +"Common.Notification.Messages.Content" = "%@ us ha enviat un missatge"; +"Common.Notification.Messages.Title" = "Nou missatge directe"; "Common.Notification.OwnPoll" = "La vostra enquesta ha finalitzar"; "Common.Notification.Poll" = "Una votació en que has participat ha finalitzat"; "Common.Notification.Reblog" = "%@ han impulsat el vostre tut"; -"Common.Notification.Status" = "%@ just posted"; -"Common.NotificationChannel.BackgroundProgresses.Name" = "Background progresses"; +"Common.Notification.Status" = "%@ acaba de publicar"; +"Common.NotificationChannel.BackgroundProgresses.Name" = "En curs en segon pla"; "Common.NotificationChannel.ContentInteractions.Description" = "Interaccions com mencions i repiulades"; "Common.NotificationChannel.ContentInteractions.Name" = "Interaccions"; -"Common.NotificationChannel.ContentMessages.Description" = "Direct messages"; +"Common.NotificationChannel.ContentMessages.Description" = "Missatges directes"; "Common.NotificationChannel.ContentMessages.Name" = "Missatges"; "Scene.Authentication.Title" = "Autenticació"; "Scene.Bookmark.Title" = "Marcador"; "Scene.Compose.And" = ", "; "Scene.Compose.CwPlaceholder" = "Escriviu la vostra advertència aquí"; "Scene.Compose.LastEnd" = " i "; -"Scene.Compose.Media.Caption.Add" = "Add Caption"; -"Scene.Compose.Media.Caption.AddADescriptionForThisImage" = "Add a description for this image"; -"Scene.Compose.Media.Caption.Remove" = "Remove Caption"; -"Scene.Compose.Media.Caption.Update" = "Update Caption"; +"Scene.Compose.Media.Caption.Add" = "Afegeix llegenda"; +"Scene.Compose.Media.Caption.AddADescriptionForThisImage" = "Afegeix descripció d'aquesta imatge"; +"Scene.Compose.Media.Caption.Remove" = "Esborra llegenda"; +"Scene.Compose.Media.Caption.Update" = "Actualitza llegenda"; "Scene.Compose.Media.Preview" = "Previsualitza"; "Scene.Compose.Media.Remove" = "Elimina"; "Scene.Compose.OthersInThisConversation" = "Altres en aquesta conversa:"; "Scene.Compose.Placeholder" = "Què està passant?"; -"Scene.Compose.ReplySettings.EveryoneCanReply" = "Everyone can reply"; -"Scene.Compose.ReplySettings.OnlyPeopleYouMentionCanReply" = "Only people you mention can reply"; -"Scene.Compose.ReplySettings.PeopleYouFollowCanReply" = "People you follow can reply"; +"Scene.Compose.ReplySettings.EveryoneCanReply" = "Tothom pot respondre"; +"Scene.Compose.ReplySettings.OnlyPeopleYouMentionCanReply" = "Només la gent que menciones pot respondre"; +"Scene.Compose.ReplySettings.PeopleYouFollowCanReply" = "La gent que seguiu pot respondre"; "Scene.Compose.ReplyTo" = "Resposta a …"; "Scene.Compose.ReplyingTo" = "Responent a"; "Scene.Compose.SaveDraft.Action" = "Desa l'esborrany"; @@ -301,10 +302,10 @@ "Scene.Compose.Visibility.Private" = "Privada"; "Scene.Compose.Visibility.Public" = "Pública"; "Scene.Compose.Visibility.Unlisted" = "Cap llista"; -"Scene.Compose.VisibilityDescription.Direct" = "Visible for mentioned users only"; -"Scene.Compose.VisibilityDescription.Private" = "Visible for followers only"; -"Scene.Compose.VisibilityDescription.Public" = "Visible for all, shown in public timelines"; -"Scene.Compose.VisibilityDescription.Unlisted" = "Visible for all, but not in public timelines"; +"Scene.Compose.VisibilityDescription.Direct" = "Visible només per als usuaris esmentats"; +"Scene.Compose.VisibilityDescription.Private" = "Visible només per als seguidors"; +"Scene.Compose.VisibilityDescription.Public" = "Visible per tothom, mostra a les cronologies públiques"; +"Scene.Compose.VisibilityDescription.Unlisted" = "Visible per tothom, però no en cronologies públiques"; "Scene.Compose.Vote.Expiration.1Day" = "1 dia"; "Scene.Compose.Vote.Expiration.1Hour" = "1 hora"; "Scene.Compose.Vote.Expiration.30Min" = "30 minuts"; @@ -313,7 +314,7 @@ "Scene.Compose.Vote.Expiration.6Hour" = "6 hores"; "Scene.Compose.Vote.Expiration.7Day" = "7 dies"; "Scene.Compose.Vote.Multiple" = "Elecció múltiple"; -"Scene.Compose.Vote.PlaceholderIndex" = "Choice %d"; +"Scene.Compose.Vote.PlaceholderIndex" = "Elecció d"; "Scene.ComposeHashtagSearch.SearchPlaceholder" = "Cerca etiquetes"; "Scene.ComposeUserSearch.SearchPlaceholder" = "Cerca usuaris"; "Scene.Drafts.Actions.DeleteDraft" = "Suprimeix l'esborrany"; @@ -321,13 +322,13 @@ "Scene.Drafts.Title" = "Esborranys"; "Scene.Drawer.ManageAccounts" = "Gestiona els comptes"; "Scene.Drawer.SignIn" = "Iniciar sessió"; -"Scene.Federated.Title" = "Federated"; +"Scene.Federated.Title" = "Federat"; "Scene.Followers.Title" = "Seguidors"; "Scene.Following.Title" = "Seguint"; -"Scene.History.Clear" = "Clear"; -"Scene.History.Scope.Toot" = "Toot"; +"Scene.History.Clear" = "Neteja"; +"Scene.History.Scope.Toot" = "Tut"; "Scene.History.Scope.Tweet" = "Piulada"; -"Scene.History.Scope.User" = "User"; +"Scene.History.Scope.User" = "Usuari"; "Scene.History.Title" = "Historial"; "Scene.Likes.Title" = "M'agrada"; "Scene.Listed.Title" = "Llistat"; @@ -338,7 +339,7 @@ "Scene.Lists.Title" = "Llistes"; "Scene.ListsDetails.AddMembers" = "Afegir membres"; "Scene.ListsDetails.DeleteListConfirm" = "Suprimeix aquesta llista: %@"; -"Scene.ListsDetails.DeleteListTitle" = "Delete this list"; +"Scene.ListsDetails.DeleteListTitle" = "Elimina aquesta llista"; "Scene.ListsDetails.Descriptions.MultipleMembers" = "%d Membres"; "Scene.ListsDetails.Descriptions.MultipleSubscribers" = "%d subscriptors"; "Scene.ListsDetails.Descriptions.SingleMember" = "1 Membre"; @@ -361,7 +362,7 @@ "Scene.ListsModify.Name" = "Nom"; "Scene.ListsModify.Private" = "Privada"; "Scene.ListsUsers.Add.Search" = "Cerca persones"; -"Scene.ListsUsers.Add.SearchWithinPeopleYouFollow" = "Search within people you follow"; +"Scene.ListsUsers.Add.SearchWithinPeopleYouFollow" = "Cerca entre les persones que segueixes"; "Scene.ListsUsers.Add.Title" = "Afegeix membre"; "Scene.ListsUsers.MenuActions.Add" = "Afegir"; "Scene.ListsUsers.MenuActions.Remove" = "Elimina"; @@ -369,18 +370,18 @@ "Scene.ManageAccounts.DeleteAccount" = "Esborra el compte"; "Scene.ManageAccounts.Title" = "Comptes"; "Scene.Mentions.Title" = "Mencions"; -"Scene.Messages.Action.CopyText" = "Copy message text"; -"Scene.Messages.Action.Delete" = "Delete message for you"; -"Scene.Messages.Error.NotSupported" = "The Current account does not support direct messages"; -"Scene.Messages.Expanded.Photo" = "[Photo]"; -"Scene.Messages.Icon.Failed" = "Send message failed"; +"Scene.Messages.Action.CopyText" = "Copia el text del missatge"; +"Scene.Messages.Action.Delete" = "Esborra el missatge per tu"; +"Scene.Messages.Error.NotSupported" = "El compte actual no permet missatges directes"; +"Scene.Messages.Expanded.Photo" = "Sense Foto"; +"Scene.Messages.Icon.Failed" = "No s'ha pogut enviar el missatge"; "Scene.Messages.NewConversation.Search" = "Cerca persones"; -"Scene.Messages.NewConversation.Title" = "Find people"; +"Scene.Messages.NewConversation.Title" = "Troba Gent"; "Scene.Messages.Title" = "Missatges"; "Scene.Notification.Tabs.All" = "Tot"; "Scene.Notification.Tabs.Mentions" = "Mencions"; "Scene.Notification.Title" = "Notificació"; -"Scene.Profile.Fields.JoinedInDate" = "Joined in %@"; +"Scene.Profile.Fields.JoinedInDate" = "Unit a %@"; "Scene.Profile.Filter.All" = "Totes les piulades"; "Scene.Profile.Filter.ExcludeReplies" = "Amaga les respostes"; "Scene.Profile.HideReply" = "Amaga la resposta"; @@ -389,12 +390,12 @@ "Scene.Profile.Title" = "Jo"; "Scene.Search.SavedSearch" = "Cerca desada"; "Scene.Search.SearchBar.Placeholder" = "Cerca piulades o usuaris"; -"Scene.Search.ShowLess" = "Show less"; -"Scene.Search.ShowMore" = "Show more"; +"Scene.Search.ShowLess" = "Mostra menys"; +"Scene.Search.ShowMore" = "Mostra més"; "Scene.Search.Tabs.Hashtag" = "Etiqueta"; "Scene.Search.Tabs.Media" = "Multimèdia"; -"Scene.Search.Tabs.People" = "People"; -"Scene.Search.Tabs.Toots" = "Toots"; +"Scene.Search.Tabs.People" = "Persones"; +"Scene.Search.Tabs.Toots" = "Tuts"; "Scene.Search.Tabs.Tweets" = "Piulades"; "Scene.Search.Tabs.Users" = "Usuaris"; "Scene.Search.Title" = "Cerca"; @@ -405,21 +406,21 @@ Encara en una fase inicial."; "Scene.Settings.About.Logo.BackgroundShadow" = "Quant al ombrejat del logo del fons de la pàgina"; "Scene.Settings.About.Title" = "Quant a"; "Scene.Settings.About.Version" = "Versió %@"; -"Scene.Settings.Account.BlockedPeople" = "Blocked People"; -"Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; -"Scene.Settings.Account.MutedPeople" = "Muted People"; -"Scene.Settings.Account.Title" = "Account"; -"Scene.Settings.Appearance.AmoledOptimizedMode" = "AMOLED optimized mode"; -"Scene.Settings.Appearance.AppIcon" = "App Icon"; +"Scene.Settings.Account.BlockedPeople" = "Persones blocades"; +"Scene.Settings.Account.MuteAndBlock" = "Silencia i bloca"; +"Scene.Settings.Account.MutedPeople" = "Persones silenciades"; +"Scene.Settings.Account.Title" = "Compte"; +"Scene.Settings.Appearance.AmoledOptimizedMode" = "Mode optimitzat AMOLED"; +"Scene.Settings.Appearance.AppIcon" = "Icona de l'aplicació"; "Scene.Settings.Appearance.HighlightColor" = "Color de realçament"; "Scene.Settings.Appearance.PickColor" = "Tria un color"; "Scene.Settings.Appearance.ScrollingTimeline.AppBar" = "Amaga la barra de l'aplicació al desplaçar-te"; -"Scene.Settings.Appearance.ScrollingTimeline.Fab" = "Hide FAB when scrolling"; +"Scene.Settings.Appearance.ScrollingTimeline.Fab" = "Amaga FAB al desplaçar-se"; "Scene.Settings.Appearance.ScrollingTimeline.TabBar" = "Amaga la barra de pestanyes al desplaçar-te"; -"Scene.Settings.Appearance.SectionHeader.ScrollingTimeline" = "Línia de temps"; +"Scene.Settings.Appearance.SectionHeader.ScrollingTimeline" = "Cronologia"; "Scene.Settings.Appearance.SectionHeader.TabPosition" = "Posició de la pestanya"; "Scene.Settings.Appearance.SectionHeader.Theme" = "Tema"; -"Scene.Settings.Appearance.SectionHeader.Translation" = "Translation"; +"Scene.Settings.Appearance.SectionHeader.Translation" = "Traducció"; "Scene.Settings.Appearance.TabPosition.Bottom" = "Baix"; "Scene.Settings.Appearance.TabPosition.Top" = "Dalt"; "Scene.Settings.Appearance.Theme.Auto" = "Automàtic"; @@ -429,31 +430,31 @@ Encara en una fase inicial."; "Scene.Settings.Appearance.Translation.Always" = "Sempre"; "Scene.Settings.Appearance.Translation.Auto" = "Automàtic"; "Scene.Settings.Appearance.Translation.Off" = "Desactivada"; -"Scene.Settings.Appearance.Translation.Service" = "Service"; -"Scene.Settings.Appearance.Translation.TranslateButton" = "Translate button"; -"Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord" = "Enable History Record"; +"Scene.Settings.Appearance.Translation.Service" = "Servei "; +"Scene.Settings.Appearance.Translation.TranslateButton" = "Botó de traducció"; +"Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord" = "Habilita el registre de l'historial"; "Scene.Settings.Behaviors.HistorySection.History" = "Historial"; -"Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels" = "Show tab bar labels"; -"Scene.Settings.Behaviors.TabBarSection.TabBar" = "Tab Bar"; -"Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop" = "Tap tab bar scroll to top"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline" = "Automatically refresh timeline"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval" = "Refresh interval"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds" = "120 seconds"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds" = "300 seconds"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds" = "30 seconds"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds" = "60 seconds"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop" = "Reset to top"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap" = "Double Tap"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap" = "Single Tap"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing" = "Timeline Refreshing"; -"Scene.Settings.Behaviors.Title" = "Behaviors"; +"Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels" = "Mostra etiquetes a la barra"; +"Scene.Settings.Behaviors.TabBarSection.TabBar" = "Barra"; +"Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop" = "Feu un toc a la barra per anar a dalt"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline" = "Actualitza automàticament la cronologia"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval" = "Interval d'actualització"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds" = "120 segons"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds" = "300 segons"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds" = "30 segons"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds" = "60 segons"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop" = "Restableix a dalt"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap" = "Doble toc"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap" = "Toc únic"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing" = "Actualització de la cronologia"; +"Scene.Settings.Behaviors.Title" = "Comportaments"; "Scene.Settings.Display.DateFormat.Absolute" = "Absoluta"; "Scene.Settings.Display.DateFormat.Relative" = "Relativa"; "Scene.Settings.Display.Media.Always" = "Sempre"; "Scene.Settings.Display.Media.AutoPlayback" = "Reproducció automàtica"; "Scene.Settings.Display.Media.Automatic" = "Automàtic"; "Scene.Settings.Display.Media.MediaPreviews" = "Previsualitzacions dels mitjans"; -"Scene.Settings.Display.Media.MuteByDefault" = "Mute by default"; +"Scene.Settings.Display.Media.MuteByDefault" = "Silenciat per defecte"; "Scene.Settings.Display.Media.Off" = "Desactivada"; "Scene.Settings.Display.Preview.ThankForUsingTwidereX" = "Gràcies per utilitzar @TwidereProject!"; "Scene.Settings.Display.SectionHeader.Avatar" = "Avatar"; @@ -467,52 +468,56 @@ Encara en una fase inicial."; "Scene.Settings.Display.Text.UseTheSystemFontSize" = "Empra la mida de la lletra del sistema"; "Scene.Settings.Display.Title" = "Visualització"; "Scene.Settings.Display.UrlPreview" = "Previsualització d'Url"; -"Scene.Settings.Layout.Actions.Drawer" = "Drawer actions"; -"Scene.Settings.Layout.Actions.Tabbar" = "Tabbar actions"; -"Scene.Settings.Layout.Desc.Content" = "Choose and arrange up to 5 actions that will appear on the tabbar (The local and federal timelines will only be displayed in Mastodon.)"; -"Scene.Settings.Layout.Desc.Title" = "Custom Layout"; -"Scene.Settings.Layout.Title" = "Layout"; -"Scene.Settings.Misc.Nitter.Dialog.Information.Content" = "Due to the limitation of Twitter API, some data might not be able to fetch from Twitter, you can use a third-party data provider to provide these data. Twidere does not take any responsibility for them."; -"Scene.Settings.Misc.Nitter.Dialog.Information.Title" = "Third party Twitter data provider"; -"Scene.Settings.Misc.Nitter.Dialog.Usage.Content" = "- Twitter status threading"; +"Scene.Settings.Layout.Actions.Drawer" = "Accions de la caixa"; +"Scene.Settings.Layout.Actions.Tabbar" = "Accions de la barra"; +"Scene.Settings.Layout.Desc.Content" = "Trieu i ordeneu fins a 5 accions que es mostraran a la barra (les cronologies locals i federades només ho faran a Mastodon)"; +"Scene.Settings.Layout.Desc.Title" = "Disposició personalitzada"; +"Scene.Settings.Layout.Title" = "Disposició"; +"Scene.Settings.Misc.Nitter.Dialog.Information.Content" = "Degut a les limitacions de l'API de Twitter, algunes dades no es poden obtenir de Twitter, podeu emprar un tercer proveïdor per aconseguir aquestes dades. Twidere no assumeix cap responsabilitat en el seu nom."; +"Scene.Settings.Misc.Nitter.Dialog.Information.Title" = "Proveïdors de dades de tercers per Twitter"; +"Scene.Settings.Misc.Nitter.Dialog.Usage.Content" = "- Estat de fils de Twitter"; "Scene.Settings.Misc.Nitter.Dialog.Usage.ProjectButton" = "URL del projecte"; -"Scene.Settings.Misc.Nitter.Dialog.Usage.Title" = "Using Third-party data provider in"; -"Scene.Settings.Misc.Nitter.Input.Description" = "Alternative Twitter front-end focused on privacy."; -"Scene.Settings.Misc.Nitter.Input.Invalid" = "Nitter instance URL is invalid, e.g. https://nitter.net"; -"Scene.Settings.Misc.Nitter.Input.Placeholder" = "Nitter Instance"; -"Scene.Settings.Misc.Nitter.Input.Value" = "Instance URL"; -"Scene.Settings.Misc.Nitter.Title" = "Third-party Twitter data provider"; -"Scene.Settings.Misc.Proxy.Enable.Description" = "Use proxy for all network requests"; -"Scene.Settings.Misc.Proxy.Enable.Title" = "Proxy"; -"Scene.Settings.Misc.Proxy.Password" = "Password"; -"Scene.Settings.Misc.Proxy.Port.Error" = "Proxy server port must be numbers"; +"Scene.Settings.Misc.Nitter.Dialog.Usage.Title" = "Emprant proveïdors de dades de tercers a"; +"Scene.Settings.Misc.Nitter.Input.Description" = "Client alternatiu de Twitter centrat en la privacitat."; +"Scene.Settings.Misc.Nitter.Input.Invalid" = "La URL de la instància de Nitter no és vàlida, e.g.: https://nitter.net"; +"Scene.Settings.Misc.Nitter.Input.Placeholder" = "Instància de Nitter"; +"Scene.Settings.Misc.Nitter.Input.Value" = "URL de la instància"; +"Scene.Settings.Misc.Nitter.Title" = "Proveïdors de dades de tercers per Twuitter"; +"Scene.Settings.Misc.Proxy.Enable.Description" = "Empra aquest intermediari per totes les sol·licituds de xarxa"; +"Scene.Settings.Misc.Proxy.Enable.Title" = "Servidor intermedi"; +"Scene.Settings.Misc.Proxy.Password" = "Contrasenya"; +"Scene.Settings.Misc.Proxy.Port.Error" = "El port del servidor intermediari ha de ser numèric"; "Scene.Settings.Misc.Proxy.Port.Title" = "Port"; -"Scene.Settings.Misc.Proxy.Server" = "Server"; -"Scene.Settings.Misc.Proxy.Title" = "Proxy settings"; +"Scene.Settings.Misc.Proxy.Server" = "Servidor "; +"Scene.Settings.Misc.Proxy.Title" = "Configuració del servidor intermediari"; "Scene.Settings.Misc.Proxy.Type.Http" = "HTTP"; -"Scene.Settings.Misc.Proxy.Type.Reverse" = "Reverse"; +"Scene.Settings.Misc.Proxy.Type.Reverse" = "Inverteix"; "Scene.Settings.Misc.Proxy.Type.Socks" = "SOCKS"; -"Scene.Settings.Misc.Proxy.Type.Title" = "Proxy type"; -"Scene.Settings.Misc.Proxy.Username" = "Username"; -"Scene.Settings.Misc.Title" = "Misc"; +"Scene.Settings.Misc.Proxy.Type.Title" = "Tipus de servidor intermediari"; +"Scene.Settings.Misc.Proxy.Username" = "Nom d'usuari"; +"Scene.Settings.Misc.Title" = "Misceŀlània"; "Scene.Settings.Notification.Accounts" = "Comptes"; -"Scene.Settings.Notification.Mastodon.Favorite" = "Favorite"; -"Scene.Settings.Notification.Mastodon.Mention" = "Mention"; -"Scene.Settings.Notification.Mastodon.NewFollow" = "New Follow"; -"Scene.Settings.Notification.Mastodon.Poll" = "poll"; -"Scene.Settings.Notification.Mastodon.Reblog" = "Reblog"; -"Scene.Settings.Notification.NotificationSwitch" = "Show Notification"; -"Scene.Settings.Notification.PushNotification" = "Push Notification"; +"Scene.Settings.Notification.Mastodon.Favorite" = "Favorit"; +"Scene.Settings.Notification.Mastodon.Mention" = "Menció"; +"Scene.Settings.Notification.Mastodon.NewFollow" = "Nou seguidor"; +"Scene.Settings.Notification.Mastodon.Poll" = "enquesta"; +"Scene.Settings.Notification.Mastodon.Reblog" = "Rebloga"; +"Scene.Settings.Notification.NotificationSwitch" = "Mostra notificació"; +"Scene.Settings.Notification.PushNotification" = "Notificació «push»"; "Scene.Settings.Notification.Title" = "Notificació"; +"Scene.Settings.PrivacyAndSafety.AlwaysShowSensitiveMedia" = "Always show sensitive media"; +"Scene.Settings.PrivacyAndSafety.SectionHeader.MuteAndBlock" = "Silencia i bloca"; +"Scene.Settings.PrivacyAndSafety.SectionHeader.Sensitive" = "Sensitive Info"; +"Scene.Settings.PrivacyAndSafety.Title" = "Privacy and safety"; "Scene.Settings.SectionHeader.About" = "Quant a"; -"Scene.Settings.SectionHeader.Account" = "Account"; +"Scene.Settings.SectionHeader.Account" = "Compte"; "Scene.Settings.SectionHeader.General" = "General"; -"Scene.Settings.Storage.All.SubTitle" = "Delete all Twidere X cache. Your account credentials will not be lost."; -"Scene.Settings.Storage.All.Title" = "Clear all cache"; -"Scene.Settings.Storage.Media.SubTitle" = "Clear stored media cache."; -"Scene.Settings.Storage.Media.Title" = "Clear media cache"; -"Scene.Settings.Storage.Search.Title" = "Clear search history"; -"Scene.Settings.Storage.Title" = "Storage"; +"Scene.Settings.Storage.All.SubTitle" = "Esborra tota la memòria cau de Twidere X. Les vostres credencials del compte no es perdran."; +"Scene.Settings.Storage.All.Title" = "Esborra tota la memòria cau"; +"Scene.Settings.Storage.Media.SubTitle" = "Esborra la memòria cau de fitxers multimèdia."; +"Scene.Settings.Storage.Media.Title" = "Esborra la memòria cau multimèdia"; +"Scene.Settings.Storage.Search.Title" = "Esborra l'historial de cerca"; +"Scene.Settings.Storage.Title" = "Emmagatzematge"; "Scene.Settings.Title" = "Configuració"; "Scene.SignIn.HelloSignInToGetStarted" = "Hola! Inicia sessió per començar."; @@ -529,11 +534,11 @@ Inicia sessió per començar."; "Scene.Status.Retweet.Mutiple" = "%d repiulades"; "Scene.Status.Retweet.Single" = "1 repiulada"; "Scene.Status.Title" = "Piulada"; -"Scene.Status.TitleMastodon" = "Toot"; -"Scene.Timeline.Title" = "Línia de temps"; -"Scene.Trends.Accounts" = "%d people talking"; -"Scene.Trends.Now" = "Trending Now"; +"Scene.Status.TitleMastodon" = "Tut"; +"Scene.Timeline.Title" = "Cronologia"; +"Scene.Trends.Accounts" = "%d persones estan parlant"; +"Scene.Trends.Now" = "Ara és tendència"; "Scene.Trends.Title" = "Tendències"; -"Scene.Trends.TrendsLocation" = "Trends Location"; -"Scene.Trends.WorldWide" = "Trends - Worldwide"; -"Scene.Trends.WorldWideWithoutPrefix" = "Worldwide"; \ No newline at end of file +"Scene.Trends.TrendsLocation" = "Ubicació de les tendències"; +"Scene.Trends.WorldWide" = "Tendències - Mundials"; +"Scene.Trends.WorldWideWithoutPrefix" = "Mundial"; \ No newline at end of file diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings index 2878035b..2f3490ca 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings @@ -194,7 +194,7 @@ "Common.Controls.Friendship.Actions.Unfollow" = "Entfolgen"; "Common.Controls.Friendship.Actions.Unmute" = "Stummschaltung aufheben"; "Common.Controls.Friendship.BlockUser" = "%@ blockieren"; -"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Do you want to report and block %@"; +"Common.Controls.Friendship.DoYouWantToReportAndBlockUser" = "Do you want to report and block %@"; "Common.Controls.Friendship.DoYouWantToReportUser" = "Do you want to report %@"; "Common.Controls.Friendship.Follower" = "folgender"; "Common.Controls.Friendship.Followers" = "folgende"; @@ -214,6 +214,7 @@ "Common.Controls.Status.Actions.DeleteTweet" = "Tweet löschen"; "Common.Controls.Status.Actions.PinOnProfile" = "Profil anheften"; "Common.Controls.Status.Actions.Quote" = "Zitieren"; +"Common.Controls.Status.Actions.Reply" = "Antworten"; "Common.Controls.Status.Actions.Retweet" = "Retweet"; "Common.Controls.Status.Actions.SaveMedia" = "Datei speichern"; "Common.Controls.Status.Actions.Share" = "Teilen"; @@ -504,6 +505,10 @@ Still in early stage."; "Scene.Settings.Notification.NotificationSwitch" = "Benachrichtigung anzeigen"; "Scene.Settings.Notification.PushNotification" = "Push Notification"; "Scene.Settings.Notification.Title" = "Benachrichtigung"; +"Scene.Settings.PrivacyAndSafety.AlwaysShowSensitiveMedia" = "Always show sensitive media"; +"Scene.Settings.PrivacyAndSafety.SectionHeader.MuteAndBlock" = "Mute and Block"; +"Scene.Settings.PrivacyAndSafety.SectionHeader.Sensitive" = "Sensitive Info"; +"Scene.Settings.PrivacyAndSafety.Title" = "Privacy and safety"; "Scene.Settings.SectionHeader.About" = "Über"; "Scene.Settings.SectionHeader.Account" = "Konto"; "Scene.Settings.SectionHeader.General" = "Allgemein"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings index 964a9fd6..8c2d8557 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings @@ -194,7 +194,7 @@ "Common.Controls.Friendship.Actions.Unfollow" = "Unfollow"; "Common.Controls.Friendship.Actions.Unmute" = "Unmute"; "Common.Controls.Friendship.BlockUser" = "Block %@"; -"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Do you want to report and block %@"; +"Common.Controls.Friendship.DoYouWantToReportAndBlockUser" = "Do you want to report and block %@"; "Common.Controls.Friendship.DoYouWantToReportUser" = "Do you want to report %@"; "Common.Controls.Friendship.Follower" = "follower"; "Common.Controls.Friendship.Followers" = "followers"; @@ -214,6 +214,7 @@ "Common.Controls.Status.Actions.DeleteTweet" = "Delete tweet"; "Common.Controls.Status.Actions.PinOnProfile" = "Pin on Profile"; "Common.Controls.Status.Actions.Quote" = "Quote"; +"Common.Controls.Status.Actions.Reply" = "Reply"; "Common.Controls.Status.Actions.Retweet" = "Retweet"; "Common.Controls.Status.Actions.SaveMedia" = "Save media"; "Common.Controls.Status.Actions.Share" = "Share"; @@ -504,6 +505,10 @@ Still in early stage."; "Scene.Settings.Notification.NotificationSwitch" = "Show Notification"; "Scene.Settings.Notification.PushNotification" = "Push Notification"; "Scene.Settings.Notification.Title" = "Notification"; +"Scene.Settings.PrivacyAndSafety.AlwaysShowSensitiveMedia" = "Always show sensitive media"; +"Scene.Settings.PrivacyAndSafety.SectionHeader.MuteAndBlock" = "Mute and Block"; +"Scene.Settings.PrivacyAndSafety.SectionHeader.Sensitive" = "Sensitive Info"; +"Scene.Settings.PrivacyAndSafety.Title" = "Privacy and safety"; "Scene.Settings.SectionHeader.About" = "About"; "Scene.Settings.SectionHeader.Account" = "Account"; "Scene.Settings.SectionHeader.General" = "General"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings index 1bbc4c1a..14546fd4 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings @@ -128,13 +128,13 @@ "Common.Alerts.PhotoSaveFail.Title" = "Error al guardar la foto"; "Common.Alerts.PhotoSaved.Title" = "Foto guardada"; "Common.Alerts.PostFailInvalidPoll.Message" = "Poll has empty field. Please fulfill the field then try again"; -"Common.Alerts.PostFailInvalidPoll.Title" = "Failed to Publish"; +"Common.Alerts.PostFailInvalidPoll.Title" = "Fallo al publicar"; "Common.Alerts.RateLimitExceeded.Message" = "Límite de uso de la API de Twitter alcanzado"; "Common.Alerts.RateLimitExceeded.Title" = "Límite de transferencia excedido"; "Common.Alerts.ReportAndBlockUserSuccess.Title" = "%@ ha sido reportado por spam y bloqueado"; "Common.Alerts.ReportUserSuccess.Title" = "%@ ha sido reportado por spam"; "Common.Alerts.RequestThrottle.Message" = "Operation too frequent. Please try again later"; -"Common.Alerts.RequestThrottle.Title" = "Request Throttle"; +"Common.Alerts.RequestThrottle.Title" = "Reducir aceleración"; "Common.Alerts.SignOutUserConfirm.Message" = "¿Desea cerrar sesión?"; "Common.Alerts.SignOutUserConfirm.Title" = "Cerrar sesión"; "Common.Alerts.TooManyRequests.Title" = "Demasiadas solicitudes"; @@ -160,7 +160,7 @@ "Common.Controls.Actions.Add" = "Añadir"; "Common.Controls.Actions.Browse" = "Navegar"; "Common.Controls.Actions.Cancel" = "Cancelar"; -"Common.Controls.Actions.Clear" = "Clear"; +"Common.Controls.Actions.Clear" = "Eliminar"; "Common.Controls.Actions.Confirm" = "Confirmar"; "Common.Controls.Actions.Copy" = "Copiar"; "Common.Controls.Actions.Delete" = "Eliminar"; @@ -194,7 +194,7 @@ "Common.Controls.Friendship.Actions.Unfollow" = "Dejar de seguir"; "Common.Controls.Friendship.Actions.Unmute" = "No Silenciar"; "Common.Controls.Friendship.BlockUser" = "Bloquear a %@"; -"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Do you want to report and block %@"; +"Common.Controls.Friendship.DoYouWantToReportAndBlockUser" = "Do you want to report and block %@"; "Common.Controls.Friendship.DoYouWantToReportUser" = "Do you want to report %@"; "Common.Controls.Friendship.Follower" = "seguidor"; "Common.Controls.Friendship.Followers" = "seguidores"; @@ -214,6 +214,7 @@ "Common.Controls.Status.Actions.DeleteTweet" = "Eliminar tweet"; "Common.Controls.Status.Actions.PinOnProfile" = "Fijar en el perfil"; "Common.Controls.Status.Actions.Quote" = "Cita"; +"Common.Controls.Status.Actions.Reply" = "Responder"; "Common.Controls.Status.Actions.Retweet" = "Retwittear"; "Common.Controls.Status.Actions.SaveMedia" = "Guardar multimedia"; "Common.Controls.Status.Actions.Share" = "Compartir"; @@ -228,8 +229,8 @@ "Common.Controls.Status.Poll.TotalPerson" = "%@ persona"; "Common.Controls.Status.Poll.TotalVote" = "%@ voto"; "Common.Controls.Status.Poll.TotalVotes" = "%@ votos"; -"Common.Controls.Status.ReplySettings.PeopleUserFollowsOrMentionedCanReply" = "People %@ follows or mentioned can reply."; -"Common.Controls.Status.ReplySettings.PeopleUserMentionedCanReply" = "People %@ mentioned can reply."; +"Common.Controls.Status.ReplySettings.PeopleUserFollowsOrMentionedCanReply" = "Las personas que %@ siguen o mencionadas pueden responder."; +"Common.Controls.Status.ReplySettings.PeopleUserMentionedCanReply" = "Los usuarios %@ mencionados pueden responder."; "Common.Controls.Status.Thread.Show" = "Mostrar este hilo de conversación"; "Common.Controls.Status.UserBoosted" = "%@ retooteó"; "Common.Controls.Status.UserRetweeted" = "%@ ha retwitteado"; @@ -258,8 +259,8 @@ "Common.Notification.Favourite" = "%@ ha marcado como favorito tu toot"; "Common.Notification.Follow" = "%@ te ha seguido"; "Common.Notification.FollowRequest" = "%@ ha solicitado seguirte"; -"Common.Notification.FollowRequestAction.Approve" = "Approve"; -"Common.Notification.FollowRequestAction.Deny" = "Deny"; +"Common.Notification.FollowRequestAction.Approve" = "Aprobar"; +"Common.Notification.FollowRequestAction.Deny" = "Denegar"; "Common.Notification.FollowRequestResponse.FollowRequestApproved" = "Follow Request Approved"; "Common.Notification.FollowRequestResponse.FollowRequestDenied" = "Follow Request Denied"; "Common.Notification.Mentions" = "%@ te ha mencionado"; @@ -303,8 +304,8 @@ "Scene.Compose.Visibility.Unlisted" = "No listado"; "Scene.Compose.VisibilityDescription.Direct" = "Visible sólo para usuarios mencionados"; "Scene.Compose.VisibilityDescription.Private" = "Visible sólo para seguidores"; -"Scene.Compose.VisibilityDescription.Public" = "Visible para todos, se muestra en cronologías de inicio públicas"; -"Scene.Compose.VisibilityDescription.Unlisted" = "Visible para todos, pero no se muestra en cronologías de inicio públicas"; +"Scene.Compose.VisibilityDescription.Public" = "Visible para todos, se muestra en cronologías públicas"; +"Scene.Compose.VisibilityDescription.Unlisted" = "Visible para todos, pero no se muestra en cronologías públicas"; "Scene.Compose.Vote.Expiration.1Day" = "1 día"; "Scene.Compose.Vote.Expiration.1Hour" = "1 hora"; "Scene.Compose.Vote.Expiration.30Min" = "30 minutos"; @@ -324,7 +325,7 @@ "Scene.Federated.Title" = "Federada"; "Scene.Followers.Title" = "Seguidores"; "Scene.Following.Title" = "Siguiendo"; -"Scene.History.Clear" = "Clear"; +"Scene.History.Clear" = "Eliminar"; "Scene.History.Scope.Toot" = "Toot"; "Scene.History.Scope.Tweet" = "Tweet"; "Scene.History.Scope.User" = "User"; @@ -416,7 +417,7 @@ Todavía en fase temprana."; "Scene.Settings.Appearance.ScrollingTimeline.AppBar" = "Ocultar barra de aplicación al desplazar"; "Scene.Settings.Appearance.ScrollingTimeline.Fab" = "Ocultar botón de acción flotante al desplazar"; "Scene.Settings.Appearance.ScrollingTimeline.TabBar" = "Ocultar barra de pestañas al desplazar"; -"Scene.Settings.Appearance.SectionHeader.ScrollingTimeline" = "Cronología de inicio desplazable"; +"Scene.Settings.Appearance.SectionHeader.ScrollingTimeline" = "Línea de tiempo desplazable"; "Scene.Settings.Appearance.SectionHeader.TabPosition" = "Posición de la pestaña"; "Scene.Settings.Appearance.SectionHeader.Theme" = "Tema"; "Scene.Settings.Appearance.SectionHeader.Translation" = "Traducción"; @@ -504,6 +505,10 @@ Todavía en fase temprana."; "Scene.Settings.Notification.NotificationSwitch" = "Mostrar notificación"; "Scene.Settings.Notification.PushNotification" = "Push Notification"; "Scene.Settings.Notification.Title" = "Notificación"; +"Scene.Settings.PrivacyAndSafety.AlwaysShowSensitiveMedia" = "Always show sensitive media"; +"Scene.Settings.PrivacyAndSafety.SectionHeader.MuteAndBlock" = "Mute and Block"; +"Scene.Settings.PrivacyAndSafety.SectionHeader.Sensitive" = "Sensitive Info"; +"Scene.Settings.PrivacyAndSafety.Title" = "Privacy and safety"; "Scene.Settings.SectionHeader.About" = "Acerca de"; "Scene.Settings.SectionHeader.Account" = "Cuenta"; "Scene.Settings.SectionHeader.General" = "General"; @@ -530,7 +535,7 @@ Inicie sesión para empezar."; "Scene.Status.Retweet.Single" = "1 Retweet"; "Scene.Status.Title" = "Tweet"; "Scene.Status.TitleMastodon" = "Toot"; -"Scene.Timeline.Title" = "Cronología de inicio"; +"Scene.Timeline.Title" = "Inicio"; "Scene.Trends.Accounts" = "%d personas hablando"; "Scene.Trends.Now" = "Tendencias en este momento"; "Scene.Trends.Title" = "Tendencias"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.stringsdict index b4cf04f6..9fcf85d6 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.stringsdict @@ -13,9 +13,9 @@ NSStringFormatValueTypeKey ld one - 1 notification + 1 notificación other - %ld notifications + %Id notificaciones count.media diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings index 9c81387c..e519e8e6 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings @@ -194,7 +194,7 @@ "Common.Controls.Friendship.Actions.Unfollow" = "Utzi jarraitzeari"; "Common.Controls.Friendship.Actions.Unmute" = "Desmututu"; "Common.Controls.Friendship.BlockUser" = "%@ blokeatu"; -"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Do you want to report and block %@"; +"Common.Controls.Friendship.DoYouWantToReportAndBlockUser" = "Do you want to report and block %@"; "Common.Controls.Friendship.DoYouWantToReportUser" = "Do you want to report %@"; "Common.Controls.Friendship.Follower" = "jarratzaile"; "Common.Controls.Friendship.Followers" = "jarratzaileak"; @@ -214,6 +214,7 @@ "Common.Controls.Status.Actions.DeleteTweet" = "Txioa ezabatu"; "Common.Controls.Status.Actions.PinOnProfile" = "Profilean finkatu"; "Common.Controls.Status.Actions.Quote" = "Aipatu"; +"Common.Controls.Status.Actions.Reply" = "Erantzun"; "Common.Controls.Status.Actions.Retweet" = "Bertxiotu"; "Common.Controls.Status.Actions.SaveMedia" = "Save media"; "Common.Controls.Status.Actions.Share" = "Partekatu"; @@ -504,6 +505,10 @@ Oraindik hasierako fasean dago."; "Scene.Settings.Notification.NotificationSwitch" = "Erakutsi jakinarazpena"; "Scene.Settings.Notification.PushNotification" = "Push Notification"; "Scene.Settings.Notification.Title" = "Jakinarazpena"; +"Scene.Settings.PrivacyAndSafety.AlwaysShowSensitiveMedia" = "Always show sensitive media"; +"Scene.Settings.PrivacyAndSafety.SectionHeader.MuteAndBlock" = "Mute and Block"; +"Scene.Settings.PrivacyAndSafety.SectionHeader.Sensitive" = "Sensitive Info"; +"Scene.Settings.PrivacyAndSafety.Title" = "Privacy and safety"; "Scene.Settings.SectionHeader.About" = "Honi buruz"; "Scene.Settings.SectionHeader.Account" = "Account"; "Scene.Settings.SectionHeader.General" = "Orokorra"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings index 0bb78b34..4f582e8b 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings @@ -133,8 +133,8 @@ "Common.Alerts.RateLimitExceeded.Title" = "Taxa de uso superada"; "Common.Alerts.ReportAndBlockUserSuccess.Title" = "Denunciaches e bloqueaches a %@ por spam"; "Common.Alerts.ReportUserSuccess.Title" = "Denunciaches a %@ por spam"; -"Common.Alerts.RequestThrottle.Message" = "Operation too frequent. Please try again later"; -"Common.Alerts.RequestThrottle.Title" = "Request Throttle"; +"Common.Alerts.RequestThrottle.Message" = "Operación moi frecuente. Téntao máis tarde, por favor"; +"Common.Alerts.RequestThrottle.Title" = "Solicitude de aceleración"; "Common.Alerts.SignOutUserConfirm.Message" = "Queres pechar a sesión?"; "Common.Alerts.SignOutUserConfirm.Title" = "Pechar sesión"; "Common.Alerts.TooManyRequests.Title" = "Demasiadas peticións"; @@ -194,7 +194,7 @@ "Common.Controls.Friendship.Actions.Unfollow" = "Deixar de seguir"; "Common.Controls.Friendship.Actions.Unmute" = "Non acalar"; "Common.Controls.Friendship.BlockUser" = "Bloquear a %@"; -"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Queres denunciar e bloquear a %@"; +"Common.Controls.Friendship.DoYouWantToReportAndBlockUser" = "Do you want to report and block %@"; "Common.Controls.Friendship.DoYouWantToReportUser" = "Queres denunciar a %@"; "Common.Controls.Friendship.Follower" = "seguidora"; "Common.Controls.Friendship.Followers" = "seguidores"; @@ -214,6 +214,7 @@ "Common.Controls.Status.Actions.DeleteTweet" = "Borrar chío"; "Common.Controls.Status.Actions.PinOnProfile" = "Fixar no perfil"; "Common.Controls.Status.Actions.Quote" = "Cita"; +"Common.Controls.Status.Actions.Reply" = "Responder"; "Common.Controls.Status.Actions.Retweet" = "Rechouchío"; "Common.Controls.Status.Actions.SaveMedia" = "Gardar multimedia"; "Common.Controls.Status.Actions.Share" = "Compartir"; @@ -287,7 +288,7 @@ "Scene.Compose.Media.Remove" = "Eliminar"; "Scene.Compose.OthersInThisConversation" = "Máis persoas nesta conversa:"; "Scene.Compose.Placeholder" = "Que está a pasar?"; -"Scene.Compose.ReplySettings.EveryoneCanReply" = "Everyone can reply"; +"Scene.Compose.ReplySettings.EveryoneCanReply" = "Todos poden responder"; "Scene.Compose.ReplySettings.OnlyPeopleYouMentionCanReply" = "Só as persoas que segues poden responder"; "Scene.Compose.ReplySettings.PeopleYouFollowCanReply" = "As persoas que segues poden responder"; "Scene.Compose.ReplyTo" = "Responder a …"; @@ -327,7 +328,7 @@ "Scene.History.Clear" = "Limpar"; "Scene.History.Scope.Toot" = "Toot"; "Scene.History.Scope.Tweet" = "Chío"; -"Scene.History.Scope.User" = "User"; +"Scene.History.Scope.User" = "Usuaria"; "Scene.History.Title" = "Historial"; "Scene.Likes.Title" = "Favoritos"; "Scene.Listed.Title" = "Listado"; @@ -431,22 +432,22 @@ Aínda en fase previa."; "Scene.Settings.Appearance.Translation.Off" = "Apagada"; "Scene.Settings.Appearance.Translation.Service" = "Servizo"; "Scene.Settings.Appearance.Translation.TranslateButton" = "Botón traducir"; -"Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord" = "Enable History Record"; +"Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord" = "Activar rexistro do historial"; "Scene.Settings.Behaviors.HistorySection.History" = "Historial"; -"Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels" = "Show tab bar labels"; -"Scene.Settings.Behaviors.TabBarSection.TabBar" = "Tab Bar"; -"Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop" = "Tap tab bar scroll to top"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline" = "Automatically refresh timeline"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval" = "Refresh interval"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds" = "120 seconds"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds" = "300 seconds"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds" = "30 seconds"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds" = "60 seconds"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop" = "Reset to top"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap" = "Double Tap"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap" = "Single Tap"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing" = "Timeline Refreshing"; -"Scene.Settings.Behaviors.Title" = "Behaviors"; +"Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels" = "Amosar etiquetas na barra de lapelas"; +"Scene.Settings.Behaviors.TabBarSection.TabBar" = "Barra de lapelas"; +"Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop" = "Arrolar arriba na barra de lapelas"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline" = "Recarga automática da cronoloxía"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval" = "Intervalo de recarga"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds" = "120 segundos"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds" = "300 segundos"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds" = "30 segundos"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds" = "60 segundos"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop" = "Restablecer"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap" = "Toque duplo"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap" = "Un toque"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing" = "Recargar cronoloxía"; +"Scene.Settings.Behaviors.Title" = "Comportamentos"; "Scene.Settings.Display.DateFormat.Absolute" = "Absoluta"; "Scene.Settings.Display.DateFormat.Relative" = "Relativa"; "Scene.Settings.Display.Media.Always" = "Sempre"; @@ -504,6 +505,10 @@ Aínda en fase previa."; "Scene.Settings.Notification.NotificationSwitch" = "Amosar notificación"; "Scene.Settings.Notification.PushNotification" = "Notificacións emerxentes"; "Scene.Settings.Notification.Title" = "Notificación"; +"Scene.Settings.PrivacyAndSafety.AlwaysShowSensitiveMedia" = "Always show sensitive media"; +"Scene.Settings.PrivacyAndSafety.SectionHeader.MuteAndBlock" = "Silenciar e bloquear"; +"Scene.Settings.PrivacyAndSafety.SectionHeader.Sensitive" = "Sensitive Info"; +"Scene.Settings.PrivacyAndSafety.Title" = "Privacy and safety"; "Scene.Settings.SectionHeader.About" = "Sobre"; "Scene.Settings.SectionHeader.Account" = "Conta"; "Scene.Settings.SectionHeader.General" = "Xeral"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings index 9c880cb2..79694e3e 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings @@ -133,8 +133,8 @@ "Common.Alerts.RateLimitExceeded.Title" = "レート制限を超えました"; "Common.Alerts.ReportAndBlockUserSuccess.Title" = "%@ はスパムとして報告されブロックされました"; "Common.Alerts.ReportUserSuccess.Title" = "%@ はスパムとして報告されました"; -"Common.Alerts.RequestThrottle.Message" = "Operation too frequent. Please try again later"; -"Common.Alerts.RequestThrottle.Title" = "Request Throttle"; +"Common.Alerts.RequestThrottle.Message" = "操作が頻繁すぎます。しばらくしてからもう一度お試しください"; +"Common.Alerts.RequestThrottle.Title" = "スロットルを要求する"; "Common.Alerts.SignOutUserConfirm.Message" = "サインアウトしますか?"; "Common.Alerts.SignOutUserConfirm.Title" = "サインアウト"; "Common.Alerts.TooManyRequests.Title" = "リクエストが多すぎます"; @@ -194,8 +194,8 @@ "Common.Controls.Friendship.Actions.Unfollow" = "フォロー解除"; "Common.Controls.Friendship.Actions.Unmute" = "ミュート解除"; "Common.Controls.Friendship.BlockUser" = "%@ をブロック"; -"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Do you want to report and block %@"; -"Common.Controls.Friendship.DoYouWantToReportUser" = "Do you want to report %@"; +"Common.Controls.Friendship.DoYouWantToReportAndBlockUser" = "Do you want to report and block %@"; +"Common.Controls.Friendship.DoYouWantToReportUser" = "%@ を報告しますか?"; "Common.Controls.Friendship.Follower" = "フォロワー"; "Common.Controls.Friendship.Followers" = "フォロワー"; "Common.Controls.Friendship.FollowsYou" = "あなたをフォローしています"; @@ -214,6 +214,7 @@ "Common.Controls.Status.Actions.DeleteTweet" = "ツイートを削除"; "Common.Controls.Status.Actions.PinOnProfile" = "プロフィールに固定表示"; "Common.Controls.Status.Actions.Quote" = "引用"; +"Common.Controls.Status.Actions.Reply" = "返信"; "Common.Controls.Status.Actions.Retweet" = "リツイート"; "Common.Controls.Status.Actions.SaveMedia" = "メディアを保存"; "Common.Controls.Status.Actions.Share" = "共有"; @@ -287,7 +288,7 @@ "Scene.Compose.Media.Remove" = "削除"; "Scene.Compose.OthersInThisConversation" = "この会話の他の人:"; "Scene.Compose.Placeholder" = "いまどうしてる?"; -"Scene.Compose.ReplySettings.EveryoneCanReply" = "Everyone can reply"; +"Scene.Compose.ReplySettings.EveryoneCanReply" = "誰でもリプライすることができます。"; "Scene.Compose.ReplySettings.OnlyPeopleYouMentionCanReply" = "返信できるのはあなたがメンションした人のみです"; "Scene.Compose.ReplySettings.PeopleYouFollowCanReply" = "フォローしている人が返信できます"; "Scene.Compose.ReplyTo" = "返信 ..."; @@ -327,7 +328,7 @@ "Scene.History.Clear" = "削除"; "Scene.History.Scope.Toot" = "トゥート"; "Scene.History.Scope.Tweet" = "ツイート"; -"Scene.History.Scope.User" = "User"; +"Scene.History.Scope.User" = "ユーザー"; "Scene.History.Title" = "履歴"; "Scene.Likes.Title" = "いいね"; "Scene.Listed.Title" = "リスト"; @@ -405,10 +406,10 @@ "Scene.Settings.About.Logo.BackgroundShadow" = "情報ページの背景のロゴの影"; "Scene.Settings.About.Title" = "情報"; "Scene.Settings.About.Version" = "Ver %@"; -"Scene.Settings.Account.BlockedPeople" = "Blocked People"; -"Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; -"Scene.Settings.Account.MutedPeople" = "Muted People"; -"Scene.Settings.Account.Title" = "Account"; +"Scene.Settings.Account.BlockedPeople" = "ブロックされた人"; +"Scene.Settings.Account.MuteAndBlock" = "ミュートとブロック"; +"Scene.Settings.Account.MutedPeople" = "ミュート中のユーザー"; +"Scene.Settings.Account.Title" = "アカウント"; "Scene.Settings.Appearance.AmoledOptimizedMode" = "AMOLED 最適化モード"; "Scene.Settings.Appearance.AppIcon" = "アプリアイコン"; "Scene.Settings.Appearance.HighlightColor" = "ハイライト色"; @@ -431,22 +432,22 @@ "Scene.Settings.Appearance.Translation.Off" = "オフ"; "Scene.Settings.Appearance.Translation.Service" = "サービス"; "Scene.Settings.Appearance.Translation.TranslateButton" = "翻訳ボタン"; -"Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord" = "Enable History Record"; +"Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord" = "履歴レコードを有効にする"; "Scene.Settings.Behaviors.HistorySection.History" = "履歴"; -"Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels" = "Show tab bar labels"; -"Scene.Settings.Behaviors.TabBarSection.TabBar" = "Tab Bar"; -"Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop" = "Tap tab bar scroll to top"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline" = "Automatically refresh timeline"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval" = "Refresh interval"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds" = "120 seconds"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds" = "300 seconds"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds" = "30 seconds"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds" = "60 seconds"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop" = "Reset to top"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap" = "Double Tap"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap" = "Single Tap"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing" = "Timeline Refreshing"; -"Scene.Settings.Behaviors.Title" = "Behaviors"; +"Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels" = "タブバーのラベルを表示"; +"Scene.Settings.Behaviors.TabBarSection.TabBar" = "タブバー"; +"Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop" = "タブバーをタップして上にスクロールする"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline" = "タイムラインを自動的に更新"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval" = "更新間隔"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds" = "120秒"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds" = "300秒"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds" = "30秒"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds" = "60秒"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop" = "トップにリセット"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap" = "ダブルタップ"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap" = "シングルタップ"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing" = "タイムライン更新中"; +"Scene.Settings.Behaviors.Title" = "動作"; "Scene.Settings.Display.DateFormat.Absolute" = "絶対"; "Scene.Settings.Display.DateFormat.Relative" = "相対"; "Scene.Settings.Display.Media.Always" = "常に"; @@ -456,7 +457,7 @@ "Scene.Settings.Display.Media.MuteByDefault" = "デフォルトでミュート"; "Scene.Settings.Display.Media.Off" = "オフ"; "Scene.Settings.Display.Preview.ThankForUsingTwidereX" = "@TwidereProject をご利用いただきありがとうございます!"; -"Scene.Settings.Display.SectionHeader.Avatar" = "Avatar"; +"Scene.Settings.Display.SectionHeader.Avatar" = "アバター"; "Scene.Settings.Display.SectionHeader.DateFormat" = "日付フォーマット"; "Scene.Settings.Display.SectionHeader.Media" = "メディア"; "Scene.Settings.Display.SectionHeader.Preview" = "プレビュー"; @@ -497,16 +498,20 @@ "Scene.Settings.Misc.Proxy.Username" = "ユーザー名"; "Scene.Settings.Misc.Title" = "その他"; "Scene.Settings.Notification.Accounts" = "アカウント"; -"Scene.Settings.Notification.Mastodon.Favorite" = "Favorite"; -"Scene.Settings.Notification.Mastodon.Mention" = "Mention"; -"Scene.Settings.Notification.Mastodon.NewFollow" = "New Follow"; -"Scene.Settings.Notification.Mastodon.Poll" = "poll"; -"Scene.Settings.Notification.Mastodon.Reblog" = "Reblog"; +"Scene.Settings.Notification.Mastodon.Favorite" = "いいね"; +"Scene.Settings.Notification.Mastodon.Mention" = "メンション"; +"Scene.Settings.Notification.Mastodon.NewFollow" = "新しいフォロー"; +"Scene.Settings.Notification.Mastodon.Poll" = "投票"; +"Scene.Settings.Notification.Mastodon.Reblog" = "リブログ"; "Scene.Settings.Notification.NotificationSwitch" = "通知を表示"; -"Scene.Settings.Notification.PushNotification" = "Push Notification"; +"Scene.Settings.Notification.PushNotification" = "プッシュ通知"; "Scene.Settings.Notification.Title" = "通知"; +"Scene.Settings.PrivacyAndSafety.AlwaysShowSensitiveMedia" = "Always show sensitive media"; +"Scene.Settings.PrivacyAndSafety.SectionHeader.MuteAndBlock" = "ミュートとブロック"; +"Scene.Settings.PrivacyAndSafety.SectionHeader.Sensitive" = "Sensitive Info"; +"Scene.Settings.PrivacyAndSafety.Title" = "Privacy and safety"; "Scene.Settings.SectionHeader.About" = "情報"; -"Scene.Settings.SectionHeader.Account" = "Account"; +"Scene.Settings.SectionHeader.Account" = "アカウント"; "Scene.Settings.SectionHeader.General" = "一般"; "Scene.Settings.Storage.All.SubTitle" = "すべての Twidere X のキャッシュを削除します。アカウントの情報は失われません。"; "Scene.Settings.Storage.All.Title" = "すべてのキャッシュを削除"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.stringsdict index 26c99e7a..5ad032b0 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.stringsdict @@ -13,7 +13,7 @@ NSStringFormatValueTypeKey ld other - %ld notifications + %ld件の通知 count.media @@ -47,7 +47,7 @@ count.metric_formatted.post NSStringLocalizedFormatKey - %@ %#@post_count@ + %@%#@post_count@ post_count NSStringFormatSpecTypeKey @@ -55,7 +55,7 @@ NSStringFormatValueTypeKey ld other - posts + 投稿 count.post @@ -181,7 +181,7 @@ NSStringFormatValueTypeKey ld other - %ld people talking + %ld人がこの話題について話しています date.year.left diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings index 23724451..1b8dbbfb 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings @@ -194,7 +194,7 @@ "Common.Controls.Friendship.Actions.Unfollow" = "팔로우 끊기"; "Common.Controls.Friendship.Actions.Unmute" = "숨기기 풀기"; "Common.Controls.Friendship.BlockUser" = "%@ 차단하기"; -"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Do you want to report and block %@"; +"Common.Controls.Friendship.DoYouWantToReportAndBlockUser" = "Do you want to report and block %@"; "Common.Controls.Friendship.DoYouWantToReportUser" = "Do you want to report %@"; "Common.Controls.Friendship.Follower" = "팔로워"; "Common.Controls.Friendship.Followers" = "팔로워"; @@ -214,6 +214,7 @@ "Common.Controls.Status.Actions.DeleteTweet" = "트윗 지우기"; "Common.Controls.Status.Actions.PinOnProfile" = "프로필에 고정"; "Common.Controls.Status.Actions.Quote" = "인용"; +"Common.Controls.Status.Actions.Reply" = "답글"; "Common.Controls.Status.Actions.Retweet" = "리트윗"; "Common.Controls.Status.Actions.SaveMedia" = "Save media"; "Common.Controls.Status.Actions.Share" = "공유"; @@ -504,6 +505,10 @@ "Scene.Settings.Notification.NotificationSwitch" = "알림 보이기"; "Scene.Settings.Notification.PushNotification" = "Push Notification"; "Scene.Settings.Notification.Title" = "알림"; +"Scene.Settings.PrivacyAndSafety.AlwaysShowSensitiveMedia" = "Always show sensitive media"; +"Scene.Settings.PrivacyAndSafety.SectionHeader.MuteAndBlock" = "Mute and Block"; +"Scene.Settings.PrivacyAndSafety.SectionHeader.Sensitive" = "Sensitive Info"; +"Scene.Settings.PrivacyAndSafety.Title" = "Privacy and safety"; "Scene.Settings.SectionHeader.About" = "정보"; "Scene.Settings.SectionHeader.Account" = "계정"; "Scene.Settings.SectionHeader.General" = "일반"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings index baf55acd..d92cb0a2 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings @@ -133,8 +133,8 @@ "Common.Alerts.RateLimitExceeded.Title" = "Limite de Acesso Excedido"; "Common.Alerts.ReportAndBlockUserSuccess.Title" = "%@ foi reportado por spam e bloqueado"; "Common.Alerts.ReportUserSuccess.Title" = "%@ foi reportado por spam"; -"Common.Alerts.RequestThrottle.Message" = "Operation too frequent. Please try again later"; -"Common.Alerts.RequestThrottle.Title" = "Request Throttle"; +"Common.Alerts.RequestThrottle.Message" = "Operação muito frequente. Por favor, tente novamente mais tarde"; +"Common.Alerts.RequestThrottle.Title" = "Solicitar Acelerador"; "Common.Alerts.SignOutUserConfirm.Message" = "Você quer encerrar esta sessão?"; "Common.Alerts.SignOutUserConfirm.Title" = "Encerrar sessão"; "Common.Alerts.TooManyRequests.Title" = "Muitos Pedidos"; @@ -194,7 +194,7 @@ "Common.Controls.Friendship.Actions.Unfollow" = "Deixar de seguir"; "Common.Controls.Friendship.Actions.Unmute" = "Dessilenciar"; "Common.Controls.Friendship.BlockUser" = "Bloquear %@"; -"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Deseja reportar e bloquear %@"; +"Common.Controls.Friendship.DoYouWantToReportAndBlockUser" = "Do you want to report and block %@"; "Common.Controls.Friendship.DoYouWantToReportUser" = "Você quer reportar %@"; "Common.Controls.Friendship.Follower" = "seguidor"; "Common.Controls.Friendship.Followers" = "seguidores"; @@ -214,6 +214,7 @@ "Common.Controls.Status.Actions.DeleteTweet" = "Excluir tweet"; "Common.Controls.Status.Actions.PinOnProfile" = "Fixar no Perfil"; "Common.Controls.Status.Actions.Quote" = "Citação"; +"Common.Controls.Status.Actions.Reply" = "Responder"; "Common.Controls.Status.Actions.Retweet" = "Retweet"; "Common.Controls.Status.Actions.SaveMedia" = "Salvar mídia"; "Common.Controls.Status.Actions.Share" = "Compartilhar"; @@ -287,7 +288,7 @@ "Scene.Compose.Media.Remove" = "Remover"; "Scene.Compose.OthersInThisConversation" = "Outros nesta conversa:"; "Scene.Compose.Placeholder" = "O que está acontecendo?"; -"Scene.Compose.ReplySettings.EveryoneCanReply" = "Everyone can reply"; +"Scene.Compose.ReplySettings.EveryoneCanReply" = "Todos podem responder"; "Scene.Compose.ReplySettings.OnlyPeopleYouMentionCanReply" = "Apenas as pessoas mencionadas podem responder"; "Scene.Compose.ReplySettings.PeopleYouFollowCanReply" = "As pessoas que você segue podem responder"; "Scene.Compose.ReplyTo" = "Resposta para …"; @@ -327,7 +328,7 @@ "Scene.History.Clear" = "Limpar"; "Scene.History.Scope.Toot" = "Toot"; "Scene.History.Scope.Tweet" = "Tweet"; -"Scene.History.Scope.User" = "User"; +"Scene.History.Scope.User" = "Usuário"; "Scene.History.Title" = "Histórico"; "Scene.Likes.Title" = "Curtidas"; "Scene.Listed.Title" = "Listado"; @@ -431,22 +432,22 @@ Ainda na fase inicial."; "Scene.Settings.Appearance.Translation.Off" = "Desligado"; "Scene.Settings.Appearance.Translation.Service" = "Serviço"; "Scene.Settings.Appearance.Translation.TranslateButton" = "Botão de tradução"; -"Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord" = "Enable History Record"; +"Scene.Settings.Behaviors.HistorySection.EnableHistoryRecord" = "Ativar Registro de Histórico"; "Scene.Settings.Behaviors.HistorySection.History" = "Histórico"; -"Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels" = "Show tab bar labels"; -"Scene.Settings.Behaviors.TabBarSection.TabBar" = "Tab Bar"; -"Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop" = "Tap tab bar scroll to top"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline" = "Automatically refresh timeline"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval" = "Refresh interval"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds" = "120 seconds"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds" = "300 seconds"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds" = "30 seconds"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds" = "60 seconds"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop" = "Reset to top"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap" = "Double Tap"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap" = "Single Tap"; -"Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing" = "Timeline Refreshing"; -"Scene.Settings.Behaviors.Title" = "Behaviors"; +"Scene.Settings.Behaviors.TabBarSection.ShowTabBarLabels" = "Mostrar rótulos da barra de abas"; +"Scene.Settings.Behaviors.TabBarSection.TabBar" = "Barra de Abas"; +"Scene.Settings.Behaviors.TabBarSection.TapTabBarScrollToTop" = "Toque na barra de abas para rolar até o topo"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.AutomaticallyRefreshTimeline" = "Atualizar linha do tempo automaticamente"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshInterval" = "Intervalo de atualização"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.120Seconds" = "120 segundos"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.300Seconds" = "300 segundos"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.30Seconds" = "30 segundos"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.RefreshIntervalOption.60Seconds" = "60 segundos"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTop" = "Redefinir para o topo"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.DoubleTap" = "Toque Duplo"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.ResetToTopOption.SingleTap" = "Toque Único"; +"Scene.Settings.Behaviors.TimelineRefreshingSection.TimelineRefreshing" = "Atualização da Linha do Tempo"; +"Scene.Settings.Behaviors.Title" = "Comportamentos"; "Scene.Settings.Display.DateFormat.Absolute" = "Absoluto"; "Scene.Settings.Display.DateFormat.Relative" = "Relativo"; "Scene.Settings.Display.Media.Always" = "Sempre"; @@ -504,6 +505,10 @@ Ainda na fase inicial."; "Scene.Settings.Notification.NotificationSwitch" = "Mostrar Notificação"; "Scene.Settings.Notification.PushNotification" = "Notificação Push"; "Scene.Settings.Notification.Title" = "Notificação"; +"Scene.Settings.PrivacyAndSafety.AlwaysShowSensitiveMedia" = "Always show sensitive media"; +"Scene.Settings.PrivacyAndSafety.SectionHeader.MuteAndBlock" = "Silenciar e Bloquear"; +"Scene.Settings.PrivacyAndSafety.SectionHeader.Sensitive" = "Sensitive Info"; +"Scene.Settings.PrivacyAndSafety.Title" = "Privacy and safety"; "Scene.Settings.SectionHeader.About" = "Sobre"; "Scene.Settings.SectionHeader.Account" = "Conta"; "Scene.Settings.SectionHeader.General" = "Geral"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings index cececc80..875e51a7 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings @@ -133,8 +133,8 @@ "Common.Alerts.RateLimitExceeded.Title" = "Kullanım limiti aşıldı"; "Common.Alerts.ReportAndBlockUserSuccess.Title" = "%@ spam için şikayet edildi ve engellendi"; "Common.Alerts.ReportUserSuccess.Title" = "%@ spam için şikayet edildi"; -"Common.Alerts.RequestThrottle.Message" = "Operation too frequent. Please try again later"; -"Common.Alerts.RequestThrottle.Title" = "Request Throttle"; +"Common.Alerts.RequestThrottle.Message" = "İşlem çok sık oldu. Lütfen daha sonra tekrar deneyin"; +"Common.Alerts.RequestThrottle.Title" = "Talep kısıtlaması"; "Common.Alerts.SignOutUserConfirm.Message" = "Çıkış yapmak istiyor musunuz?"; "Common.Alerts.SignOutUserConfirm.Title" = "Çıkış Yap"; "Common.Alerts.TooManyRequests.Title" = "Çok Fazla İstek"; @@ -194,7 +194,7 @@ "Common.Controls.Friendship.Actions.Unfollow" = "Takibi bırak"; "Common.Controls.Friendship.Actions.Unmute" = "Sessizden çıkar"; "Common.Controls.Friendship.BlockUser" = "%@ Engelle"; -"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "%@ kullanıcısını bildirmek ve engellemek istiyor musunuz"; +"Common.Controls.Friendship.DoYouWantToReportAndBlockUser" = "%@ kullanıcısını bildirmek ve engellemek istiyor musunuz"; "Common.Controls.Friendship.DoYouWantToReportUser" = "%@ kullanıcısını bildirmek istiyor musunuz"; "Common.Controls.Friendship.Follower" = "takipçi"; "Common.Controls.Friendship.Followers" = "takipçiler"; @@ -214,6 +214,7 @@ "Common.Controls.Status.Actions.DeleteTweet" = "Tweeti sil"; "Common.Controls.Status.Actions.PinOnProfile" = "Profilde Sabitle"; "Common.Controls.Status.Actions.Quote" = "Alıntı"; +"Common.Controls.Status.Actions.Reply" = "Yanıt"; "Common.Controls.Status.Actions.Retweet" = "Retweetle"; "Common.Controls.Status.Actions.SaveMedia" = "Medyayı Kaydet"; "Common.Controls.Status.Actions.Share" = "Paylaş"; @@ -504,6 +505,10 @@ Hala erken aşamada."; "Scene.Settings.Notification.NotificationSwitch" = "Bildirim göster"; "Scene.Settings.Notification.PushNotification" = "Anlık bildirim"; "Scene.Settings.Notification.Title" = "Bildirim"; +"Scene.Settings.PrivacyAndSafety.AlwaysShowSensitiveMedia" = "Hassas medyayı her zaman göster"; +"Scene.Settings.PrivacyAndSafety.SectionHeader.MuteAndBlock" = "Sustur ve Engelle"; +"Scene.Settings.PrivacyAndSafety.SectionHeader.Sensitive" = "Hassas Bilgi"; +"Scene.Settings.PrivacyAndSafety.Title" = "Gizlilik ve güvenlik"; "Scene.Settings.SectionHeader.About" = "Hakkında"; "Scene.Settings.SectionHeader.Account" = "Hesabım"; "Scene.Settings.SectionHeader.General" = "Genel"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings index 8632282f..f7fab321 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings @@ -194,7 +194,7 @@ "Common.Controls.Friendship.Actions.Unfollow" = "取消关注"; "Common.Controls.Friendship.Actions.Unmute" = "解除静音"; "Common.Controls.Friendship.BlockUser" = "屏蔽 %@"; -"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "是否要举报并屏蔽 %@"; +"Common.Controls.Friendship.DoYouWantToReportAndBlockUser" = "是否要举报并屏蔽 %@"; "Common.Controls.Friendship.DoYouWantToReportUser" = "是否要举报 %@"; "Common.Controls.Friendship.Follower" = "关注者"; "Common.Controls.Friendship.Followers" = "关注者"; @@ -214,6 +214,7 @@ "Common.Controls.Status.Actions.DeleteTweet" = "删除推文"; "Common.Controls.Status.Actions.PinOnProfile" = "在个人资料页面置顶"; "Common.Controls.Status.Actions.Quote" = "引用"; +"Common.Controls.Status.Actions.Reply" = "回复"; "Common.Controls.Status.Actions.Retweet" = "转推"; "Common.Controls.Status.Actions.SaveMedia" = "保存媒体"; "Common.Controls.Status.Actions.Share" = "分享"; @@ -504,6 +505,10 @@ "Scene.Settings.Notification.NotificationSwitch" = "显示通知"; "Scene.Settings.Notification.PushNotification" = "推送通知"; "Scene.Settings.Notification.Title" = "通知"; +"Scene.Settings.PrivacyAndSafety.AlwaysShowSensitiveMedia" = "始终显示敏感媒体内容"; +"Scene.Settings.PrivacyAndSafety.SectionHeader.MuteAndBlock" = "静音和屏蔽"; +"Scene.Settings.PrivacyAndSafety.SectionHeader.Sensitive" = "敏感内容"; +"Scene.Settings.PrivacyAndSafety.Title" = "隐私和安全"; "Scene.Settings.SectionHeader.About" = "关于"; "Scene.Settings.SectionHeader.Account" = "账号"; "Scene.Settings.SectionHeader.General" = "通用"; diff --git a/TwidereXIntent/ar.lproj/Intents.strings b/TwidereXIntent/ar.lproj/Intents.strings index f3ebaae0..f6b8247a 100644 --- a/TwidereXIntent/ar.lproj/Intents.strings +++ b/TwidereXIntent/ar.lproj/Intents.strings @@ -24,7 +24,7 @@ "Fn8AQn" = "الرابط"; -"G5v3xr" = "Set active account"; +"G5v3xr" = "تعيين حساب نشط"; "GILN5g" = "هُناك ${count} خِيار مُطابق لِـ\"${accounts}\"."; diff --git a/TwidereXIntent/ca.lproj/Intents.strings b/TwidereXIntent/ca.lproj/Intents.strings index 28fc3329..29012bae 100644 --- a/TwidereXIntent/ca.lproj/Intents.strings +++ b/TwidereXIntent/ca.lproj/Intents.strings @@ -10,7 +10,7 @@ "1iPE2d-jEAHra" = "Just to confirm, you wanted ‘Unlisted’?"; -"8WWS78" = "Username"; +"8WWS78" = "Nom d'usuari"; "9OV1Ic" = "Posts"; diff --git a/TwidereXIntent/ja.lproj/Intents.strings b/TwidereXIntent/ja.lproj/Intents.strings index 7918fbf6..f682aa6a 100644 --- a/TwidereXIntent/ja.lproj/Intents.strings +++ b/TwidereXIntent/ja.lproj/Intents.strings @@ -1,83 +1,83 @@ -"0e8JfP" = "Just to confirm, you wanted ‘${account}’?"; +"0e8JfP" = "「${account}」で間違いないですか?"; -"1iPE2d-BS59z4" = "Just to confirm, you wanted ‘Public’?"; +"1iPE2d-BS59z4" = "「パブリック」で間違いないですか?"; -"1iPE2d-aV5Ezl" = "Just to confirm, you wanted ‘Default’?"; +"1iPE2d-aV5Ezl" = "「デフォルト」で間違いないですか?"; -"1iPE2d-aV8f0g" = "Just to confirm, you wanted ‘Private’?"; +"1iPE2d-aV8f0g" = "「プライベート」で間違いないですか?"; -"1iPE2d-aeaw8w" = "Just to confirm, you wanted ‘Direct’?"; +"1iPE2d-aeaw8w" = "「ダイレクト」で間違いないですか?"; -"1iPE2d-jEAHra" = "Just to confirm, you wanted ‘Unlisted’?"; +"1iPE2d-jEAHra" = "「リストから削除」で間違いないですか?"; "8WWS78" = "ユーザー名"; -"9OV1Ic" = "Posts"; +"9OV1Ic" = "投稿"; -"ApmQKm" = "Switch Account"; +"ApmQKm" = "アカウントの切り替え"; "BS59z4" = "公開"; "Ciuk7c" = "名前"; -"DBgjXT" = "Account"; +"DBgjXT" = "アカウント"; "Fn8AQn" = "URL"; -"G5v3xr" = "Set active account"; +"G5v3xr" = "アクティブなアカウントを設定"; -"GILN5g" = "There are ${count} options matching ‘${accounts}’."; +"GILN5g" = "${accounts} にマッチする${count} 個のオプションがあります。"; "N4GVJI" = "${errorDescription}"; -"QSA5ql" = "Account"; +"QSA5ql" = "アカウント"; -"QWcTQP" = "Switch to ${account}"; +"QWcTQP" = "${account}に変更"; -"SnCJhk" = "Publish Post"; +"SnCJhk" = "投稿する"; -"SqRrlA" = "Just to confirm, you wanted ‘${accounts}’?"; +"SqRrlA" = "「${accounts}」で間違いないですか?"; -"SrV2FP" = "Error Description"; +"SrV2FP" = "エラーの説明"; -"TzDamL" = "Toot Visibility"; +"TzDamL" = "公開範囲"; "WVP0I1" = "${errorDescription}"; -"Xs6a35" = "Toot Visibility"; +"Xs6a35" = "公開範囲"; -"YNUo2I" = "There are ${count} options matching ‘${account}’."; +"YNUo2I" = "${account} にマッチする${count} 個のオプションがあります。"; "Yb4tzx" = "アカウント"; -"aV5Ezl" = "Default"; +"aV5Ezl" = "デフォルト"; "aV8f0g" = "フォロワー限定"; "aeaw8w" = "ダイレクト"; -"ch7ADX" = "Compose new post"; +"ch7ADX" = "投稿の新規作成"; -"f1YNIs" = "Post Content"; +"f1YNIs" = "投稿内容"; "jEAHra" = "未収載"; -"kWl9zU" = "Post ${content}with ${accounts}"; +"kWl9zU" = "${accounts}で${content}を投稿"; -"noeHVX" = "Post"; +"noeHVX" = "投稿"; -"vbgnbQ" = "Post ${content}with ${accounts}"; +"vbgnbQ" = "${accounts}で${content}を投稿"; -"xeqcBa-BS59z4" = "There are ${count} options matching ‘Public’."; +"xeqcBa-BS59z4" = "「パブリック」にマッチするオプションが${count}個あります。"; -"xeqcBa-aV5Ezl" = "There are ${count} options matching ‘Default’."; +"xeqcBa-aV5Ezl" = " 「デフォルト」にマッチする${count} 個のオプションがあります。"; -"xeqcBa-aV8f0g" = "There are ${count} options matching ‘Private’."; +"xeqcBa-aV8f0g" = "「プライベート」にマッチするオプションが${count}個あります。"; -"xeqcBa-aeaw8w" = "There are ${count} options matching ‘Direct’."; +"xeqcBa-aeaw8w" = "「ダイレクト」にマッチするオプションが${count}個あります。"; -"xeqcBa-jEAHra" = "There are ${count} options matching ‘Unlisted’."; +"xeqcBa-jEAHra" = "「リストから削除」にマッチするオプションが${count}個あります。"; -"z3DAP7" = "Switch to ${account}"; +"z3DAP7" = "${account}に変更"; -"zlMGvn" = "Content"; +"zlMGvn" = "コンテンツ"; From e8a61d5de3353479cc0c65a731582777a1aded47 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 17 Mar 2023 16:10:10 +0800 Subject: [PATCH 045/128] feat: add post GIF support --- .../Extension/NSItemProvider.swift | 48 +++++++++++++++++++ .../Service/APIService/APIService+Media.swift | 2 +- .../TwidereUI/Content/StatusView.swift | 15 ++++-- .../Attachment/AttachmentView.swift | 11 +++++ .../AttachmentViewModel+Upload.swift | 33 +++++++++---- .../Attachment/AttachmentViewModel.swift | 31 ++++++++++-- .../TwitterSDK/API/Twitter+API+Media.swift | 29 ++--------- .../Provider/DataSourceFacade+Banner.swift | 2 +- .../Scene/Compose/ComposeViewController.swift | 2 + .../Root/MainTab/MainTabBarController.swift | 2 +- 10 files changed, 130 insertions(+), 45 deletions(-) diff --git a/TwidereSDK/Sources/TwidereCommon/Extension/NSItemProvider.swift b/TwidereSDK/Sources/TwidereCommon/Extension/NSItemProvider.swift index c6fbff4f..a9ec58a4 100644 --- a/TwidereSDK/Sources/TwidereCommon/Extension/NSItemProvider.swift +++ b/TwidereSDK/Sources/TwidereCommon/Extension/NSItemProvider.swift @@ -99,6 +99,54 @@ extension NSItemProvider { } +extension NSItemProvider { + + public struct GIFLoadResult { + public let data: Data + public let url: URL + public let sizeInBytes: UInt64 + } + + public func loadGIFData() async throws -> GIFLoadResult? { + try await withCheckedThrowingContinuation { continuation in + loadFileRepresentation(forTypeIdentifier: UTType.gif.identifier) { url, error in + if let error = error { + continuation.resume(with: .failure(error)) + return + } + + guard let url = url, + let attribute = try? FileManager.default.attributesOfItem(atPath: url.path), + let sizeInBytes = attribute[.size] as? UInt64 + else { + continuation.resume(with: .success(nil)) + assertionFailure() + return + } + + do { + let fileURL = try FileManager.default.createTemporaryFileURL( + filename: UUID().uuidString, + pathExtension: url.pathExtension + ) + try FileManager.default.copyItem(at: url, to: fileURL) + let data = try Data(contentsOf: fileURL) + let result = GIFLoadResult( + data: data, + url: fileURL, + sizeInBytes: sizeInBytes + ) + + continuation.resume(with: .success(result)) + } catch { + continuation.resume(with: .failure(error)) + } + } // end loadFileRepresentation + } // end try await withCheckedThrowingContinuation + } // end func + +} + extension NSItemProvider { public struct VideoLoadResult { diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Media.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Media.swift index caed87c7..ca4ad683 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Media.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Media.swift @@ -60,7 +60,7 @@ extension APIService { public func twitterMediaStatus( mediaID: String, twitterAuthenticationContext: TwitterAuthenticationContext - ) async throws -> Twitter.Response.Content { + ) async throws -> Twitter.Response.Content { let authorization = twitterAuthenticationContext.authorization let query = Twitter.API.Media.StatusQuery(mediaID: mediaID) return try await Twitter.API.Media.status( diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index db5be7d2..428c2b5a 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -149,11 +149,16 @@ public struct StatusView: View { } } // end VStack .overlay(alignment: .bottom) { - VStack(spacing: .zero) { - Spacer() - Divider() - Color.clear - .frame(height: 1) + switch viewModel.kind { + case .timeline, .repost, .conversationRoot, .conversationThread: + VStack(spacing: .zero) { + Spacer() + Divider() + Color.clear + .frame(height: 1) + } + default: + EmptyView() } } } // end HStack diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Attachment/AttachmentView.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Attachment/AttachmentView.swift index 6d836d0f..9547ac5a 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Attachment/AttachmentView.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Attachment/AttachmentView.swift @@ -106,6 +106,7 @@ public struct AttachmentView: View { let canAddCaption: Bool = { switch viewModel.output { case .image: return true + case .gif: return false case .video: return false case .none: return false } @@ -150,6 +151,11 @@ public struct AttachmentView: View { Image(uiImage: image) .resizable() .aspectRatio(contentMode: .fill) + case .gif: + let image = viewModel.thumbnail ?? .placeholder(color: .systemGray3) + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fill) case .video(let url, _): let player = AVPlayer(url: url) VideoPlayer(player: player) @@ -209,6 +215,11 @@ public struct AttachmentView: View { Image(uiImage: image) .resizable() .aspectRatio(contentMode: .fill) + case .gif: + let image = viewModel.thumbnail ?? .placeholder(color: .systemGray3) + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fill) case .video(let url, _): let player = AVPlayer(url: url) VideoPlayer(player: player) diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Upload.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Upload.swift index f760bd7d..b0ada45d 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Upload.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Upload.swift @@ -171,8 +171,11 @@ extension AttachmentViewModel { type: imageData.kf.imageFormat == .PNG ? UTType.png : UTType.jpeg ) -// case .gif(let url): -// fatalError() + case .gif(_, let url): + return SliceResult( + url: url, + type: .gif + ) case .video(let url, _): return SliceResult( url: url, @@ -251,12 +254,24 @@ extension AttachmentViewModel { chunkIndex += 1 } + var isFinalizing = false var isFinalized = false repeat { - let mediaFinalizedResponse = try await context.apiService.TwitterMediaFinalize( - mediaID: mediaID, - twitterAuthenticationContext: twitterAuthenticationContext - ) + let mediaFinalizedResponse: Twitter.Response.Content = try await { + if !isFinalizing { + let result = try await context.apiService.TwitterMediaFinalize( + mediaID: mediaID, + twitterAuthenticationContext: twitterAuthenticationContext + ) + isFinalizing = true + return result + } else { + return try await context.apiService.twitterMediaStatus( + mediaID: mediaID, + twitterAuthenticationContext: twitterAuthenticationContext + ) + } + }() guard let processingInfo = mediaFinalizedResponse.value.processingInfo else { isFinalized = true @@ -265,14 +280,14 @@ extension AttachmentViewModel { if let checkAfterSecs = processingInfo.checkAfterSecs { let checkAfterSeconds = UInt64(checkAfterSecs) - AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): finalize status pending. check after \(checkAfterSecs)s") + AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): mediaID: \(mediaID) - finalize status pending. check after \(checkAfterSecs)s") assert(!Thread.isMainThread) try? await Task.sleep(nanoseconds: checkAfterSeconds * .second) // 1s * checkAfterSeconds continue } else { - AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): finalize success") + AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): mediaID: \(mediaID) - finalize success") isFinalized = true } } while !isFinalized @@ -404,6 +419,8 @@ extension AttachmentViewModel.Output { case .png: return .png(data) case .jpg: return .jpeg(data) } + case .gif(let data, _): + return .gif(data) case .video(let url, _): return .other(url, fileExtension: url.pathExtension, mimeType: "video/mp4") } diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift index dc01102c..fd374a5d 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift @@ -46,6 +46,8 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable switch output { case .image(let data, _): return UIImage(data: data) + case .gif(let data, _): + return UIImage(data: data) case .video(let url, _): return AttachmentViewModel.createThumbnailForVideo(url: url) case .none: @@ -58,8 +60,10 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable deinit { switch output { case .image: - // FIXME: + // FIXME: any cleanup? break + case .gif(_, let url): + try? FileManager.default.removeItem(at: url) case .video(let url, _): try? FileManager.default.removeItem(at: url) case nil : @@ -78,7 +82,7 @@ extension AttachmentViewModel { public enum Output { case image(Data, imageKind: ImageKind) - // case gif(Data) + case gif(Data, URL) case video(URL, mimeType: String) // assert use file for video only public enum ImageKind { @@ -89,6 +93,7 @@ extension AttachmentViewModel { public var twitterMediaCategory: TwitterMediaCategory { switch self { case .image: return .image + case .gif: return .GIF case .video: return .amplifyVideo } } @@ -193,7 +198,12 @@ extension AttachmentViewModel { } private static func load(itemProvider: NSItemProvider) async throws -> Output { - if itemProvider.isImage() { + if itemProvider.isGIF() { + guard let result = try await itemProvider.loadGIFData() else { + throw AttachmentError.invalidAttachmentType + } + return .gif(result.data, result.url) + } else if itemProvider.isImage() { guard let result = try await itemProvider.loadImageData() else { throw AttachmentError.invalidAttachmentType } @@ -353,6 +363,14 @@ extension AttachmentViewModel: NSItemProviderWriting { default: completionHandler(nil, nil) } + case .gif(let data, _): + switch typeIdentifier { + case UTType.gif.identifier: + loadingProgress.completedUnitCount = 100 + completionHandler(data, nil) + default: + completionHandler(nil, nil) + } case .video(let url, _): switch typeIdentifier { case UTType.png.identifier: @@ -384,6 +402,13 @@ extension AttachmentViewModel: NSItemProviderWriting { } extension NSItemProvider { + fileprivate func isGIF() -> Bool { + return hasRepresentationConforming( + toTypeIdentifier: UTType.gif.identifier, + fileOptions: [] + ) + } + fileprivate func isImage() -> Bool { return hasRepresentationConforming( toTypeIdentifier: UTType.image.identifier, diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Media.swift b/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Media.swift index 64fd2d0b..7d338721 100644 --- a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Media.swift +++ b/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Media.swift @@ -186,7 +186,7 @@ extension Twitter.API.Media { public struct FinalizeResponse: Codable { public let mediaIDString: String - public let size: Int + public let size: Int? public let expiresAfterSecs: Int public let processingInfo: ProcessingInfo? // server return it when media needs processing @@ -216,7 +216,7 @@ extension Twitter.API.Media { session: URLSession, query: StatusQuery, authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { + ) async throws -> Twitter.Response.Content { let request = Twitter.API.request( url: uploadEndpointURL, method: .GET, @@ -224,7 +224,7 @@ extension Twitter.API.Media { authorization: authorization ) let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: StatusResponse.self, from: data, response: response) + let value = try Twitter.API.decode(type: FinalizeResponse.self, from: data, response: response) return Twitter.Response.Content(value: value, response: response) } @@ -249,27 +249,4 @@ extension Twitter.API.Media { var body: Data? { nil } } - public struct StatusResponse: Codable { - public let mediaIDString: String - public let expiresAfterSecs: Int - public let processingInfo: ProcessingInfo - - public enum CodingKeys: String, CodingKey { - case mediaIDString = "media_id_string" - case expiresAfterSecs = "expires_after_secs" - case processingInfo = "processing_info" - } - - public struct ProcessingInfo: Codable { - public let state: String // pending, in_progress, failed, succeeded - public let checkAfterSecs: Int? - - - public enum CodingKeys: String, CodingKey { - case state - case checkAfterSecs = "check_after_secs" - } - } - } - } diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Banner.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Banner.swift index 1d9767c6..f8c9bf05 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Banner.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Banner.swift @@ -56,7 +56,7 @@ extension DataSourceFacade { config.interactiveHide = true let bannerView = NotificationBannerView() bannerView.configure(style: .warning) - bannerView.titleLabel.text = "Forbidden" + bannerView.titleLabel.text = "Forbidden" // TODO: i18n bannerView.messageLabel.text = "Application token expired. Please sign in the app again to reactive." bannerView.messageLabel.numberOfLines = 0 bannerView.actionButtonTapHandler = { [weak dependency] _ in diff --git a/TwidereX/Scene/Compose/ComposeViewController.swift b/TwidereX/Scene/Compose/ComposeViewController.swift index 7c41a959..26329bb4 100644 --- a/TwidereX/Scene/Compose/ComposeViewController.swift +++ b/TwidereX/Scene/Compose/ComposeViewController.swift @@ -114,6 +114,8 @@ extension ComposeViewController: ComposeContentViewControllerDelegate { switch attachmentViewModel.output { case .image(let data, _): _item = UIImage(data: data).flatMap { .image(.init(image: $0)) } + case .gif(let data, _): + _item = UIImage(data: data).flatMap { .image(.init(image: $0)) } case .video(let url, _): let playerViewController = AVPlayerViewController() playerViewController.player = AVPlayer(url: url) diff --git a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift index a5db5d55..f5b8c011 100644 --- a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift +++ b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift @@ -144,7 +144,7 @@ extension MainTabBarController { return } - if let error = error as? Twitter.API.Error.ResponseError { + if let error = error as? Twitter.API.Error.ResponseError, case .accountIsTemporarilyLocked = error.twitterAPIError { Task { @MainActor in DataSourceFacade.presentForbiddenBanner( error: error, From bc49f0f53d3485d3d28c7770d3f39712f8713d13 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 17 Mar 2023 18:12:37 +0800 Subject: [PATCH 046/128] feat: add status menu action handler --- .../Generated/Strings.swift | 10 +++ .../Resources/ar.lproj/Localizable.strings | 5 ++ .../Resources/ca.lproj/Localizable.strings | 5 ++ .../Resources/de.lproj/Localizable.strings | 5 ++ .../Resources/en.lproj/Localizable.strings | 5 ++ .../Resources/es.lproj/Localizable.strings | 5 ++ .../Resources/eu.lproj/Localizable.strings | 5 ++ .../Resources/gl.lproj/Localizable.strings | 5 ++ .../Resources/ja.lproj/Localizable.strings | 5 ++ .../Resources/ko.lproj/Localizable.strings | 5 ++ .../Resources/pt-BR.lproj/Localizable.strings | 5 ++ .../Resources/tr.lproj/Localizable.strings | 5 ++ .../zh-Hans.lproj/Localizable.strings | 5 ++ .../TwidereUI/Content/StatusToolbarView.swift | 66 +++++++++++----- .../Content/StatusView+ViewModel.swift | 4 - .../TwidereUI/Content/StatusView.swift | 27 ++++--- .../WrapperViewRepresentable.swift | 24 ++++++ .../Provider/DataSourceFacade+Share.swift | 13 +--- .../Provider/DataSourceFacade+Status.swift | 78 +++++++++++++++---- ...ider+StatusViewTableViewCellDelegate.swift | 2 + .../StatusViewTableViewCellDelegate.swift | 6 +- 21 files changed, 228 insertions(+), 62 deletions(-) create mode 100644 TwidereSDK/Sources/TwidereUI/UIViewRepresentable/WrapperViewRepresentable.swift diff --git a/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift b/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift index be26567f..ac520b3a 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift +++ b/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift @@ -694,12 +694,16 @@ public enum L10n { public static let copyText = L10n.tr("Localizable", "Common.Controls.Status.Actions.CopyText", fallback: "Copy text") /// Delete tweet public static let deleteTweet = L10n.tr("Localizable", "Common.Controls.Status.Actions.DeleteTweet", fallback: "Delete tweet") + /// Like + public static let like = L10n.tr("Localizable", "Common.Controls.Status.Actions.Like", fallback: "Like") /// Pin on Profile public static let pinOnProfile = L10n.tr("Localizable", "Common.Controls.Status.Actions.PinOnProfile", fallback: "Pin on Profile") /// Quote public static let quote = L10n.tr("Localizable", "Common.Controls.Status.Actions.Quote", fallback: "Quote") /// Reply public static let reply = L10n.tr("Localizable", "Common.Controls.Status.Actions.Reply", fallback: "Reply") + /// Repost + public static let repost = L10n.tr("Localizable", "Common.Controls.Status.Actions.Repost", fallback: "Repost") /// Retweet public static let retweet = L10n.tr("Localizable", "Common.Controls.Status.Actions.Retweet", fallback: "Retweet") /// Save media @@ -712,6 +716,12 @@ public enum L10n { public static let shareLink = L10n.tr("Localizable", "Common.Controls.Status.Actions.ShareLink", fallback: "Share link") /// Translate public static let translate = L10n.tr("Localizable", "Common.Controls.Status.Actions.Translate", fallback: "Translate") + /// Undo Boost + public static let undoBoost = L10n.tr("Localizable", "Common.Controls.Status.Actions.UndoBoost", fallback: "Undo Boost") + /// Undo Repost + public static let undoRepost = L10n.tr("Localizable", "Common.Controls.Status.Actions.UndoRepost", fallback: "Undo Repost") + /// Undo Retweet + public static let undoRetweet = L10n.tr("Localizable", "Common.Controls.Status.Actions.UndoRetweet", fallback: "Undo Retweet") /// Unpin from Profile public static let unpinFromProfile = L10n.tr("Localizable", "Common.Controls.Status.Actions.UnpinFromProfile", fallback: "Unpin from Profile") /// Vote diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings index 7661db40..e4ebd371 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings @@ -212,15 +212,20 @@ "Common.Controls.Status.Actions.CopyLink" = "انسخ الرابط"; "Common.Controls.Status.Actions.CopyText" = "انسخ النص"; "Common.Controls.Status.Actions.DeleteTweet" = "احذف التغريدة"; +"Common.Controls.Status.Actions.Like" = "أعجبني"; "Common.Controls.Status.Actions.PinOnProfile" = "ثبته في اللاحة"; "Common.Controls.Status.Actions.Quote" = "اقتبس"; "Common.Controls.Status.Actions.Reply" = "رد"; +"Common.Controls.Status.Actions.Repost" = "Repost"; "Common.Controls.Status.Actions.Retweet" = "أعد تغريده"; "Common.Controls.Status.Actions.SaveMedia" = "احفظ الوسيط"; "Common.Controls.Status.Actions.Share" = "شاركه"; "Common.Controls.Status.Actions.ShareContent" = "شارك المحتوى"; "Common.Controls.Status.Actions.ShareLink" = "شارك الرابط"; "Common.Controls.Status.Actions.Translate" = "ترجمه"; +"Common.Controls.Status.Actions.UndoBoost" = "Undo Boost"; +"Common.Controls.Status.Actions.UndoRepost" = "Undo Repost"; +"Common.Controls.Status.Actions.UndoRetweet" = "Undo Retweet"; "Common.Controls.Status.Actions.UnpinFromProfile" = "ألغ تثبيتها من اللاحة"; "Common.Controls.Status.Actions.Vote" = "صوّت"; "Common.Controls.Status.Media" = "الوسائط"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings index 06d80d2f..a8454527 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings @@ -212,15 +212,20 @@ "Common.Controls.Status.Actions.CopyLink" = "Copia l'enllaç"; "Common.Controls.Status.Actions.CopyText" = "Copia el text"; "Common.Controls.Status.Actions.DeleteTweet" = "Esborra la piulada"; +"Common.Controls.Status.Actions.Like" = "M'agrada"; "Common.Controls.Status.Actions.PinOnProfile" = "Fixa en el perfil"; "Common.Controls.Status.Actions.Quote" = "Cita"; "Common.Controls.Status.Actions.Reply" = "Resposta"; +"Common.Controls.Status.Actions.Repost" = "Repost"; "Common.Controls.Status.Actions.Retweet" = "Repiular"; "Common.Controls.Status.Actions.SaveMedia" = "Desa multimèdia"; "Common.Controls.Status.Actions.Share" = "Comparteix"; "Common.Controls.Status.Actions.ShareContent" = "Comparteix el contingut"; "Common.Controls.Status.Actions.ShareLink" = "Comparteix l'enllaç"; "Common.Controls.Status.Actions.Translate" = "Tradueix"; +"Common.Controls.Status.Actions.UndoBoost" = "Undo Boost"; +"Common.Controls.Status.Actions.UndoRepost" = "Undo Repost"; +"Common.Controls.Status.Actions.UndoRetweet" = "Undo Retweet"; "Common.Controls.Status.Actions.UnpinFromProfile" = "Treu del perfil"; "Common.Controls.Status.Actions.Vote" = "Votació"; "Common.Controls.Status.Media" = "Multimèdia"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings index 2f3490ca..5f590576 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings @@ -212,15 +212,20 @@ "Common.Controls.Status.Actions.CopyLink" = "Link kopieren"; "Common.Controls.Status.Actions.CopyText" = "Text kopieren"; "Common.Controls.Status.Actions.DeleteTweet" = "Tweet löschen"; +"Common.Controls.Status.Actions.Like" = "Gefällt mir"; "Common.Controls.Status.Actions.PinOnProfile" = "Profil anheften"; "Common.Controls.Status.Actions.Quote" = "Zitieren"; "Common.Controls.Status.Actions.Reply" = "Antworten"; +"Common.Controls.Status.Actions.Repost" = "Repost"; "Common.Controls.Status.Actions.Retweet" = "Retweet"; "Common.Controls.Status.Actions.SaveMedia" = "Datei speichern"; "Common.Controls.Status.Actions.Share" = "Teilen"; "Common.Controls.Status.Actions.ShareContent" = "Inhalt teilen"; "Common.Controls.Status.Actions.ShareLink" = "Link teilen"; "Common.Controls.Status.Actions.Translate" = "Übersetzen"; +"Common.Controls.Status.Actions.UndoBoost" = "Undo Boost"; +"Common.Controls.Status.Actions.UndoRepost" = "Undo Repost"; +"Common.Controls.Status.Actions.UndoRetweet" = "Undo Retweet"; "Common.Controls.Status.Actions.UnpinFromProfile" = "Vom Profil lösen"; "Common.Controls.Status.Actions.Vote" = "Abstimmung"; "Common.Controls.Status.Media" = "Medien"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings index 8c2d8557..3e7e8a05 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings @@ -212,15 +212,20 @@ "Common.Controls.Status.Actions.CopyLink" = "Copy link"; "Common.Controls.Status.Actions.CopyText" = "Copy text"; "Common.Controls.Status.Actions.DeleteTweet" = "Delete tweet"; +"Common.Controls.Status.Actions.Like" = "Like"; "Common.Controls.Status.Actions.PinOnProfile" = "Pin on Profile"; "Common.Controls.Status.Actions.Quote" = "Quote"; "Common.Controls.Status.Actions.Reply" = "Reply"; +"Common.Controls.Status.Actions.Repost" = "Repost"; "Common.Controls.Status.Actions.Retweet" = "Retweet"; "Common.Controls.Status.Actions.SaveMedia" = "Save media"; "Common.Controls.Status.Actions.Share" = "Share"; "Common.Controls.Status.Actions.ShareContent" = "Share content"; "Common.Controls.Status.Actions.ShareLink" = "Share link"; "Common.Controls.Status.Actions.Translate" = "Translate"; +"Common.Controls.Status.Actions.UndoBoost" = "Undo Boost"; +"Common.Controls.Status.Actions.UndoRepost" = "Undo Repost"; +"Common.Controls.Status.Actions.UndoRetweet" = "Undo Retweet"; "Common.Controls.Status.Actions.UnpinFromProfile" = "Unpin from Profile"; "Common.Controls.Status.Actions.Vote" = "Vote"; "Common.Controls.Status.Media" = "Media"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings index 14546fd4..d4d5287f 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings @@ -212,15 +212,20 @@ "Common.Controls.Status.Actions.CopyLink" = "Copiar enlace"; "Common.Controls.Status.Actions.CopyText" = "Copiar texto"; "Common.Controls.Status.Actions.DeleteTweet" = "Eliminar tweet"; +"Common.Controls.Status.Actions.Like" = "Me gusta"; "Common.Controls.Status.Actions.PinOnProfile" = "Fijar en el perfil"; "Common.Controls.Status.Actions.Quote" = "Cita"; "Common.Controls.Status.Actions.Reply" = "Responder"; +"Common.Controls.Status.Actions.Repost" = "Repost"; "Common.Controls.Status.Actions.Retweet" = "Retwittear"; "Common.Controls.Status.Actions.SaveMedia" = "Guardar multimedia"; "Common.Controls.Status.Actions.Share" = "Compartir"; "Common.Controls.Status.Actions.ShareContent" = "Compartir contenido"; "Common.Controls.Status.Actions.ShareLink" = "Compartir enlace"; "Common.Controls.Status.Actions.Translate" = "Traducir"; +"Common.Controls.Status.Actions.UndoBoost" = "Undo Boost"; +"Common.Controls.Status.Actions.UndoRepost" = "Undo Repost"; +"Common.Controls.Status.Actions.UndoRetweet" = "Undo Retweet"; "Common.Controls.Status.Actions.UnpinFromProfile" = "Desfijar del perfil"; "Common.Controls.Status.Actions.Vote" = "Votar"; "Common.Controls.Status.Media" = "Multimedia"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings index e519e8e6..977d3c09 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings @@ -212,15 +212,20 @@ "Common.Controls.Status.Actions.CopyLink" = "Kopiatu esteka"; "Common.Controls.Status.Actions.CopyText" = "Kopiatu testua"; "Common.Controls.Status.Actions.DeleteTweet" = "Txioa ezabatu"; +"Common.Controls.Status.Actions.Like" = "Atsegin"; "Common.Controls.Status.Actions.PinOnProfile" = "Profilean finkatu"; "Common.Controls.Status.Actions.Quote" = "Aipatu"; "Common.Controls.Status.Actions.Reply" = "Erantzun"; +"Common.Controls.Status.Actions.Repost" = "Repost"; "Common.Controls.Status.Actions.Retweet" = "Bertxiotu"; "Common.Controls.Status.Actions.SaveMedia" = "Save media"; "Common.Controls.Status.Actions.Share" = "Partekatu"; "Common.Controls.Status.Actions.ShareContent" = "Edukia partekatu"; "Common.Controls.Status.Actions.ShareLink" = "Partekatu esteka"; "Common.Controls.Status.Actions.Translate" = "Itzuli"; +"Common.Controls.Status.Actions.UndoBoost" = "Undo Boost"; +"Common.Controls.Status.Actions.UndoRepost" = "Undo Repost"; +"Common.Controls.Status.Actions.UndoRetweet" = "Undo Retweet"; "Common.Controls.Status.Actions.UnpinFromProfile" = "Profilatik finkatuta zegoena kendu"; "Common.Controls.Status.Actions.Vote" = "Botoa eman"; "Common.Controls.Status.Media" = "Multimedia"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings index 4f582e8b..730c7590 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings @@ -212,15 +212,20 @@ "Common.Controls.Status.Actions.CopyLink" = "Copiar ligazón"; "Common.Controls.Status.Actions.CopyText" = "Copiar texto"; "Common.Controls.Status.Actions.DeleteTweet" = "Borrar chío"; +"Common.Controls.Status.Actions.Like" = "Favorito"; "Common.Controls.Status.Actions.PinOnProfile" = "Fixar no perfil"; "Common.Controls.Status.Actions.Quote" = "Cita"; "Common.Controls.Status.Actions.Reply" = "Responder"; +"Common.Controls.Status.Actions.Repost" = "Repost"; "Common.Controls.Status.Actions.Retweet" = "Rechouchío"; "Common.Controls.Status.Actions.SaveMedia" = "Gardar multimedia"; "Common.Controls.Status.Actions.Share" = "Compartir"; "Common.Controls.Status.Actions.ShareContent" = "Compartir contido"; "Common.Controls.Status.Actions.ShareLink" = "Compartir ligazón"; "Common.Controls.Status.Actions.Translate" = "Traducir"; +"Common.Controls.Status.Actions.UndoBoost" = "Undo Boost"; +"Common.Controls.Status.Actions.UndoRepost" = "Undo Repost"; +"Common.Controls.Status.Actions.UndoRetweet" = "Undo Retweet"; "Common.Controls.Status.Actions.UnpinFromProfile" = "Retirar do perfil"; "Common.Controls.Status.Actions.Vote" = "Votar"; "Common.Controls.Status.Media" = "Multimedia"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings index 79694e3e..fac12b1b 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings @@ -212,15 +212,20 @@ "Common.Controls.Status.Actions.CopyLink" = "リンクをコピー"; "Common.Controls.Status.Actions.CopyText" = "テキストをコピー"; "Common.Controls.Status.Actions.DeleteTweet" = "ツイートを削除"; +"Common.Controls.Status.Actions.Like" = "いいね"; "Common.Controls.Status.Actions.PinOnProfile" = "プロフィールに固定表示"; "Common.Controls.Status.Actions.Quote" = "引用"; "Common.Controls.Status.Actions.Reply" = "返信"; +"Common.Controls.Status.Actions.Repost" = "Repost"; "Common.Controls.Status.Actions.Retweet" = "リツイート"; "Common.Controls.Status.Actions.SaveMedia" = "メディアを保存"; "Common.Controls.Status.Actions.Share" = "共有"; "Common.Controls.Status.Actions.ShareContent" = "コンテンツを共有"; "Common.Controls.Status.Actions.ShareLink" = "リンクを共有"; "Common.Controls.Status.Actions.Translate" = "翻訳"; +"Common.Controls.Status.Actions.UndoBoost" = "Undo Boost"; +"Common.Controls.Status.Actions.UndoRepost" = "Undo Repost"; +"Common.Controls.Status.Actions.UndoRetweet" = "Undo Retweet"; "Common.Controls.Status.Actions.UnpinFromProfile" = "プロフィールへの固定を解除"; "Common.Controls.Status.Actions.Vote" = "投票"; "Common.Controls.Status.Media" = "メディア"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings index 1b8dbbfb..642ab517 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings @@ -212,15 +212,20 @@ "Common.Controls.Status.Actions.CopyLink" = "링크 복사하기"; "Common.Controls.Status.Actions.CopyText" = "글 복사하기"; "Common.Controls.Status.Actions.DeleteTweet" = "트윗 지우기"; +"Common.Controls.Status.Actions.Like" = "좋아요"; "Common.Controls.Status.Actions.PinOnProfile" = "프로필에 고정"; "Common.Controls.Status.Actions.Quote" = "인용"; "Common.Controls.Status.Actions.Reply" = "답글"; +"Common.Controls.Status.Actions.Repost" = "Repost"; "Common.Controls.Status.Actions.Retweet" = "리트윗"; "Common.Controls.Status.Actions.SaveMedia" = "Save media"; "Common.Controls.Status.Actions.Share" = "공유"; "Common.Controls.Status.Actions.ShareContent" = "Share content"; "Common.Controls.Status.Actions.ShareLink" = "링크 공유하기"; "Common.Controls.Status.Actions.Translate" = "번역"; +"Common.Controls.Status.Actions.UndoBoost" = "Undo Boost"; +"Common.Controls.Status.Actions.UndoRepost" = "Undo Repost"; +"Common.Controls.Status.Actions.UndoRetweet" = "Undo Retweet"; "Common.Controls.Status.Actions.UnpinFromProfile" = "프로필에서 고정 풀기"; "Common.Controls.Status.Actions.Vote" = "투표"; "Common.Controls.Status.Media" = "미디어"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings index d92cb0a2..7cf9ca13 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings @@ -212,15 +212,20 @@ "Common.Controls.Status.Actions.CopyLink" = "Copiar link"; "Common.Controls.Status.Actions.CopyText" = "Copiar texto"; "Common.Controls.Status.Actions.DeleteTweet" = "Excluir tweet"; +"Common.Controls.Status.Actions.Like" = "Curtir"; "Common.Controls.Status.Actions.PinOnProfile" = "Fixar no Perfil"; "Common.Controls.Status.Actions.Quote" = "Citação"; "Common.Controls.Status.Actions.Reply" = "Responder"; +"Common.Controls.Status.Actions.Repost" = "Repost"; "Common.Controls.Status.Actions.Retweet" = "Retweet"; "Common.Controls.Status.Actions.SaveMedia" = "Salvar mídia"; "Common.Controls.Status.Actions.Share" = "Compartilhar"; "Common.Controls.Status.Actions.ShareContent" = "Compartilhar conteúdo"; "Common.Controls.Status.Actions.ShareLink" = "Compartilhar link"; "Common.Controls.Status.Actions.Translate" = "Traduzir"; +"Common.Controls.Status.Actions.UndoBoost" = "Undo Boost"; +"Common.Controls.Status.Actions.UndoRepost" = "Undo Repost"; +"Common.Controls.Status.Actions.UndoRetweet" = "Undo Retweet"; "Common.Controls.Status.Actions.UnpinFromProfile" = "Desafixar do Perfil"; "Common.Controls.Status.Actions.Vote" = "Votar"; "Common.Controls.Status.Media" = "Mídia"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings index 875e51a7..3f8c7bd6 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings @@ -212,15 +212,20 @@ "Common.Controls.Status.Actions.CopyLink" = "Linki kopyala"; "Common.Controls.Status.Actions.CopyText" = "Metni kopyala"; "Common.Controls.Status.Actions.DeleteTweet" = "Tweeti sil"; +"Common.Controls.Status.Actions.Like" = "Beğen"; "Common.Controls.Status.Actions.PinOnProfile" = "Profilde Sabitle"; "Common.Controls.Status.Actions.Quote" = "Alıntı"; "Common.Controls.Status.Actions.Reply" = "Yanıt"; +"Common.Controls.Status.Actions.Repost" = "Repost"; "Common.Controls.Status.Actions.Retweet" = "Retweetle"; "Common.Controls.Status.Actions.SaveMedia" = "Medyayı Kaydet"; "Common.Controls.Status.Actions.Share" = "Paylaş"; "Common.Controls.Status.Actions.ShareContent" = "İçeriği paylaş"; "Common.Controls.Status.Actions.ShareLink" = "Linki paylaş"; "Common.Controls.Status.Actions.Translate" = "Çevir"; +"Common.Controls.Status.Actions.UndoBoost" = "Undo Boost"; +"Common.Controls.Status.Actions.UndoRepost" = "Undo Repost"; +"Common.Controls.Status.Actions.UndoRetweet" = "Undo Retweet"; "Common.Controls.Status.Actions.UnpinFromProfile" = "Profilden sabitlemeyi kaldır"; "Common.Controls.Status.Actions.Vote" = "Oy ver"; "Common.Controls.Status.Media" = "Medya"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings index f7fab321..d8ea84d2 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings @@ -212,15 +212,20 @@ "Common.Controls.Status.Actions.CopyLink" = "复制链接"; "Common.Controls.Status.Actions.CopyText" = "复制文本"; "Common.Controls.Status.Actions.DeleteTweet" = "删除推文"; +"Common.Controls.Status.Actions.Like" = "喜欢"; "Common.Controls.Status.Actions.PinOnProfile" = "在个人资料页面置顶"; "Common.Controls.Status.Actions.Quote" = "引用"; "Common.Controls.Status.Actions.Reply" = "回复"; +"Common.Controls.Status.Actions.Repost" = "Repost"; "Common.Controls.Status.Actions.Retweet" = "转推"; "Common.Controls.Status.Actions.SaveMedia" = "保存媒体"; "Common.Controls.Status.Actions.Share" = "分享"; "Common.Controls.Status.Actions.ShareContent" = "分享内容"; "Common.Controls.Status.Actions.ShareLink" = "分享链接"; "Common.Controls.Status.Actions.Translate" = "翻译"; +"Common.Controls.Status.Actions.UndoBoost" = "Undo Boost"; +"Common.Controls.Status.Actions.UndoRepost" = "Undo Repost"; +"Common.Controls.Status.Actions.UndoRetweet" = "Undo Retweet"; "Common.Controls.Status.Actions.UnpinFromProfile" = "在个人资料页面取消置顶"; "Common.Controls.Status.Actions.Vote" = "投票"; "Common.Controls.Status.Media" = "媒体"; diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift index d9a78c37..9c7069a3 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift @@ -33,6 +33,9 @@ public struct StatusToolbarView: View { } likeButton shareMenu + .background( + WrapperViewRepresentable(view: viewModel.menuButtonBackgroundView) + ) } // end HStack } // end body @@ -108,29 +111,28 @@ extension StatusToolbarView { } public var shareMenu: some View { - Button { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): share") + Menu { ForEach(menuActions, id: \.self) { action in - Button { + Button(role: action.isDestructive ? .destructive : nil) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(String(describing: action))") handler(action) } label: { Label { - + Text(action.text) } icon: { - + Image(uiImage: action.icon) } } // end Button } // end ForEach } label: { HStack { let image: UIImage = { - // switch viewModel.kind { - // case .conversationRoot: +// switch viewModel.kind { +// case .conversationRoot: return Asset.Editing.ellipsisMini.image.withRenderingMode(.alwaysTemplate) - // default: - // return Asset.Editing.ellipsisMini.image.withRenderingMode(.alwaysTemplate) - // } +// default: +// return Asset.Editing.ellipsisMini.image.withRenderingMode(.alwaysTemplate) +// } }() Image(uiImage: image) .foregroundColor(.secondary) @@ -143,6 +145,8 @@ extension StatusToolbarView { extension StatusToolbarView { public class ViewModel: ObservableObject { + public let menuButtonBackgroundView = UIView() + // input @Published var platform: Platform = .none @Published var replyCount: Int? @@ -169,18 +173,42 @@ extension StatusToolbarView { case shareLink case saveMedia case translate + case delete public var text: String { switch self { - case .reply: return "" - case .repost: return "" - case .quote: return "" - case .like: return "" - case .copyText: return "" - case .copyLink: return "" - case .shareLink: return "" - case .saveMedia: return "" - case .translate: return "" + case .reply: return L10n.Common.Controls.Status.Actions.reply + case .repost: return L10n.Common.Controls.Status.Actions.repost + case .quote: return L10n.Common.Controls.Status.Actions.quote + case .like: return L10n.Common.Controls.Status.Actions.like + case .copyText: return L10n.Common.Controls.Status.Actions.copyText + case .copyLink: return L10n.Common.Controls.Status.Actions.copyLink + case .shareLink: return L10n.Common.Controls.Status.Actions.shareLink + case .saveMedia: return L10n.Common.Controls.Status.Actions.saveMedia + case .translate: return L10n.Common.Controls.Status.Actions.translate + case .delete: return L10n.Common.Controls.Actions.delete + } + } + + public var icon: UIImage { + switch self { + case .reply: return Asset.Arrows.arrowTurnUpLeft.image + case .repost: return Asset.Media.repeat.image + case .quote: return Asset.TextFormatting.textQuote.image + case .like: return Asset.Health.heartFill.image + case .copyText: return UIImage(systemName: "doc.on.doc")! + case .copyLink: return UIImage(systemName: "link")! + case .shareLink: return UIImage(systemName: "square.and.arrow.up")! + case .saveMedia: return UIImage(systemName: "square.and.arrow.down")! + case .translate: return UIImage(systemName: "character.bubble")! + case .delete: return UIImage(systemName: "minus.circle")! + } + } + + public var isDestructive: Bool { + switch self { + case .delete: return true + default: return false } } } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index 9053bf6e..e54dd421 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -174,8 +174,6 @@ extension StatusView { // toolbar public let toolbarViewModel = StatusToolbarView.ViewModel() - // toolbar - share menu context - @Published public var statusLink: URL? public var canDelete: Bool { guard let authContext = self.authContext else { return false } guard let authorUserIdentifier = self.authorUserIdentifier else { return false } @@ -1124,7 +1122,6 @@ extension StatusView.ViewModel { } else { // do nothing } - statusLink = status.statusURL } // end init } @@ -1266,6 +1263,5 @@ extension StatusView.ViewModel { } else { // do nothing } - statusLink = URL(string: status.url ?? status.uri) } } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index 428c2b5a..87d55b17 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -39,7 +39,7 @@ public protocol StatusViewDelegate: AnyObject { // // func statusView(_ statusView: StatusView, quoteStatusViewDidPressed quoteStatusView: StatusView) // - func statusView(_ viewModel: StatusView.ViewModel, statusToolbarButtonDidPressed action: StatusToolbarView.Action) + func statusView(_ viewModel: StatusView.ViewModel, statusToolbarViewModel: StatusToolbarView.ViewModel, statusToolbarButtonDidPressed action: StatusToolbarView.Action) // func statusView(_ statusView: StatusView, statusToolbar: StatusToolbar, menuActionDidPressed action: StatusToolbar.MenuAction, menuButton button: UIButton) // // func statusView(_ statusView: StatusView, translateButtonDidPressed button: UIButton) @@ -330,22 +330,27 @@ extension StatusView { var actions: [StatusToolbarView.Action] = [] // copyText actions.append(.copyText) - // copyLink, shareLink - if viewModel.statusLink != nil { - actions.append(.copyLink) - actions.append(.shareLink) - } + // copyLink + actions.append(.copyLink) + // shareLink + actions.append(.shareLink) // save media if !viewModel.mediaViewModels.isEmpty { - actions.append(.copyText) + actions.append(.saveMedia) + } + // translate + actions.append(.translate) + if viewModel.canDelete { + actions.append(.delete) } - // - actions.append(.copyText) - actions.append(.copyText) return actions }(), handler: { action in - viewModel.delegate?.statusView(viewModel, statusToolbarButtonDidPressed: action) + viewModel.delegate?.statusView( + viewModel, + statusToolbarViewModel: viewModel.toolbarViewModel, + statusToolbarButtonDidPressed: action + ) } ) .frame(height: 48) diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/WrapperViewRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/WrapperViewRepresentable.swift new file mode 100644 index 00000000..4f52d319 --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/WrapperViewRepresentable.swift @@ -0,0 +1,24 @@ +// +// WrapperViewRepresentable.swift +// +// +// Created by MainasuK on 2023/3/17. +// + +import UIKit +import SwiftUI +import TwidereCore + +public struct WrapperViewRepresentable: UIViewRepresentable { + + public let view: UIView + + public func makeUIView(context: Context) -> UIView { + return view + } + + public func updateUIView(_ view: UIView, context: Context) { + // do nothing + } + +} diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Share.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Share.swift index cbb8a42a..c1d5adb3 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Share.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Share.swift @@ -14,7 +14,7 @@ extension DataSourceFacade { public static func responseToStatusShareAction( provider: DataSourceProvider, status: StatusRecord, - button: UIButton + sourceView: UIView ) async { let activityViewController = await createActivityViewController( provider: provider, @@ -23,7 +23,7 @@ extension DataSourceFacade { provider.coordinator.present( scene: .activityViewController( activityViewController: activityViewController, - sourceView: button + sourceView: sourceView ), from: provider, transition: .activityViewControllerPresent(animated: true, completion: nil) @@ -40,13 +40,8 @@ extension DataSourceFacade { ) async -> UIActivityViewController { var activityItems: [Any] = await provider.context.managedObjectContext.perform { guard let object = status.object(in: provider.context.managedObjectContext) else { return [] } - switch object { - case .twitter(let status): - return [status.statusURL] - case .mastodon(let status): - let url = status.url ?? status.uri - return [URL(string: url)].compactMap { $0 } as [Any] - } + guard let url = object.statusURL else { return [] } + return [url] as [Any] } var applicationActivities: [UIActivity] = [ SafariActivity(sceneCoordinator: provider.coordinator), // open URL diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift index ab729f05..1fc0669c 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift @@ -15,6 +15,7 @@ extension DataSourceFacade { static func responseToStatusToolbar( provider: DataSourceProvider & AuthContextProvider, viewModel: StatusView.ViewModel, + statusToolbarViewModel: StatusToolbarView.ViewModel, status: StatusRecord, action: StatusToolbarView.Action ) async { @@ -111,28 +112,73 @@ extension DataSourceFacade { let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) impactFeedbackGenerator.impactOccurred() - guard let link = viewModel.statusLink?.absoluteString else { return } + let _link: String? = await provider.context.managedObjectContext.perform { + guard let object = status.object(in: provider.context.managedObjectContext) else { return nil } + guard let url = object.statusURL else { return nil } + return url.absoluteString + } + guard let link = _link else { return } UIPasteboard.general.string = link case .shareLink: let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) impactFeedbackGenerator.impactOccurred() - guard let link = viewModel.statusLink?.absoluteString else { return } - UIPasteboard.general.string = link + await DataSourceFacade.responseToStatusShareAction( + provider: provider, + status: status, + sourceView: statusToolbarViewModel.menuButtonBackgroundView + ) case .saveMedia: - break - case .translate: - break + let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) + let notificationFeedbackGenerator = UINotificationFeedbackGenerator() - // media menu button trigger this + let mediaViewModels = viewModel.mediaViewModels + do { + impactFeedbackGenerator.impactOccurred() + for mediaViewModel in mediaViewModels { + guard let url = mediaViewModel.downloadURL else { + assertionFailure() + continue + } + try await provider.context.photoLibraryService.save( + source: .remote(url: url), + resourceType: { + switch mediaViewModel.mediaKind { + case .video: return .video + case .animatedGIF: return .video + case .photo: return .photo + } + }() + ) + } + provider.context.photoLibraryService.presentSuccessNotification(title: L10n.Common.Alerts.PhotoSaved.title) + notificationFeedbackGenerator.notificationOccurred(.success) + } catch { + provider.context.photoLibraryService.presentFailureNotification( + error: error, + title: L10n.Common.Alerts.PhotoSaveFail.title, + message: L10n.Common.Alerts.PhotoSaveFail.message + ) + notificationFeedbackGenerator.notificationOccurred(.error) + } + case .translate: let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) impactFeedbackGenerator.impactOccurred() - -// await DataSourceFacade.responseToStatusShareAction( -// provider: provider, -// status: status, -// button: sender -// ) + do { + try await DataSourceFacade.responseToStatusTranslate( + provider: provider, + status: status + ) + } catch { + assertionFailure(error.localizedDescription) + } + case .delete: + await DataSourceFacade.responseToRemoveStatusAction( + provider: provider, + target: .status, + status: status, + authenticationContext: provider.authContext.authenticationContext + ) } // end switch action } } @@ -236,7 +282,7 @@ extension DataSourceFacade { target: StatusTarget, status: StatusRecord, authenticationContext: AuthenticationContext - ) async throws { + ) async { let _redirectRecord = await DataSourceFacade.status( managedObjectContext: provider.context.managedObjectContext, status: status, @@ -244,7 +290,7 @@ extension DataSourceFacade { ) guard let redirectRecord = _redirectRecord else { return } - try await responseToRemoveStatusAction( + await responseToRemoveStatusAction( provider: provider, status: redirectRecord, authenticationContext: authenticationContext @@ -256,7 +302,7 @@ extension DataSourceFacade { provider: DataSourceProvider, status: StatusRecord, authenticationContext: AuthenticationContext - ) async throws { + ) async { let title: String = { switch status { case .twitter: return L10n.Common.Alerts.DeleteTweetConfirm.title diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift index b34aa9db..b65d5ec8 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift @@ -324,6 +324,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC func tableViewCell( _ cell: UITableViewCell, viewModel: StatusView.ViewModel, + statusToolbarViewModel: StatusToolbarView.ViewModel, statusToolbarButtonDidPressed action: StatusToolbarView.Action ) { Task { @@ -334,6 +335,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC await DataSourceFacade.responseToStatusToolbar( provider: self, viewModel: viewModel, + statusToolbarViewModel: statusToolbarViewModel, status: status, action: action ) diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift index 7fd8fcf1..4d05936a 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift @@ -28,7 +28,7 @@ protocol StatusViewTableViewCellDelegate: AutoGenerateProtocolDelegate { func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, toggleContentDisplay isReveal: Bool) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, toggleContentWarningOverlayDisplay isReveal: Bool) - func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, statusToolbarButtonDidPressed action: StatusToolbarView.Action) + func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, statusToolbarViewModel: StatusToolbarView.ViewModel, statusToolbarButtonDidPressed action: StatusToolbarView.Action) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, viewHeightDidChange: Void) // sourcery:end } @@ -49,8 +49,8 @@ extension StatusViewDelegate where Self: StatusViewContainerTableViewCell { delegate?.tableViewCell(self, viewModel: viewModel, toggleContentWarningOverlayDisplay: isReveal) } - func statusView(_ viewModel: StatusView.ViewModel, statusToolbarButtonDidPressed action: StatusToolbarView.Action) { - delegate?.tableViewCell(self, viewModel: viewModel, statusToolbarButtonDidPressed: action) + func statusView(_ viewModel: StatusView.ViewModel, statusToolbarViewModel: StatusToolbarView.ViewModel, statusToolbarButtonDidPressed action: StatusToolbarView.Action) { + delegate?.tableViewCell(self, viewModel: viewModel, statusToolbarViewModel: statusToolbarViewModel, statusToolbarButtonDidPressed: action) } func statusView(_ viewModel: StatusView.ViewModel, viewHeightDidChange: Void) { From c020c98c15cfd64a122fa01fba6dd94b8a513717 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 17 Mar 2023 18:25:55 +0800 Subject: [PATCH 047/128] feat: update undo repost menu label --- .../Media/repeat-off.imageset/Contents.json | 15 +++++++++++++++ .../Media/repeat-off.imageset/repeat-off.pdf | Bin 0 -> 4804 bytes .../Sources/TwidereAsset/Generated/Assets.swift | 1 + .../TwidereUI/Content/StatusToolbarView.swift | 6 ++++-- 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat-off.imageset/Contents.json create mode 100644 TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat-off.imageset/repeat-off.pdf diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat-off.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat-off.imageset/Contents.json new file mode 100644 index 00000000..99c347ac --- /dev/null +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat-off.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "repeat-off.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat-off.imageset/repeat-off.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat-off.imageset/repeat-off.pdf new file mode 100644 index 0000000000000000000000000000000000000000..de99140deb455760ed761802178ef6186aba9a9b GIT binary patch literal 4804 zcmb7I$&TDM5WVXwcp1PxSj~L_3VZU;`j-mgfp#BM2z zbr@(~vo^9`Rf#vR?_NDqedszRq&WQgTPMWx=i3kg4yKZ$i{Q0;U?!S2{Zr`;3ZBE@Eo$6(Ngsd;m_>F(F^|#a~sZFqAv+H=-{N=RS zckq1Ftb3V)*Iw)zQ1#9_W0in8=d_MaK%lH+g^X-q?RpjKXH|@_#Z6<8!EN`U)4hpa z8Ycq0R9=#Ex=%_)t$?W@y|%*iS}JK0kYG$i+@_aCOD#q0wM*VBv4LFlMoJAVL}#rR zy4NWMr$y>zu-;q57J^CMVNH_GV-3k&`#_%gx^0lH+va*}Lr{+N70lIDvG+D5B}Idb zYc?HYq3$+oLfl0Y+hb(2-~)N4cxkM{I|73YN=EFFL1-$8ZKsMZLt7c5lGca`ioI1< zLtqk(eUvsBjZIlFl5Lw}CJ3eKVi-5QQQk$U*S$(kJN&MDACXx3ZNhZYF<~7`!<5R| z>=U8YrwNnnIvr<`mNiC5FL;za<<+inl-1$@#^@Qcb3?YV!||9t(F3Az*e#TgA&5)S z(Q$SqdsuUa8G)Uw7*eL>#kQf&BFNG>?<(D3$d`(;_=TMmeR2xSa8U*)1ro{XU?tdd z%?s@Ij6YkH1f0EwWQ0I;N{gxH_&!EyB+AR&3(_{0CiaZ>h0~jvkkfly${@n!mCVQO zB)x<+!q^wi5XJcfORi>FOC)9&Qv~K0GjB<2K5f>^DqxIVXfk8c%Q=)|UEudzCz*QJ z?Yp18xTnbf-&3fF>DVR=xxgiHLV-T-M3VUIj zne$Jzl2vd4#Rgx1K3GQulryYZ39Xh|lra`{)k!ZyMB4%wYz)ePK`BHHkaVJB*a*m~ z=pr`6^)6^<6Cmx-x@0M~N=lPKnG9MSZJhT~qCt7EDp9Wo6t(uAprnGL_P^vx&Nd5D zMinFKkPb4rR6O+PeW14Otx+MMHY1W`ExJCvz?>u~8smLHD|65QK4&vN=vX|oK~XD( z31vZj^3XaZQNIm}2%@xrRJ1x#Tc&kau4E6 z(A<(6B?iU`8J%OfO@}$JID&Fp+BPG)_AM?x7<+2dp8G(a@k%aeyU79%25f=n0+!=~ zwSlhD|5W=t$Jiiax4V!Vqhud)4&apXb6H0iZAdo}d=Z@LP|M^N1#2(#W`^f-4-ZuE zpmGE`Fdh>pg4Ezn=zR(xZicQ1kHntyF;|Rar31AB4WE%FpoY5Xox{Az&J60tRP5DkYSbLKBnV_BEXxnZbOW(5EdZ-_pq>lrmF5 z6*LKlm=x+D2<)7Td1tPv41II|Mz_baW!5&|DvZ(iFs{3F=ik6ax=C*)0@+z$JcG>T z?E-7J6JJ;%#WR=QO)HzMW)5=pqu8);Q-K(3-1)-Z%_UUk8i4Bj-`|65R+omKI|JsQ zGQVq;ryor6@OcRv_jM93pLV5O0d@FvGmz$5s!|_A^g)uwTIy17(O4`sFa{@;Iemye z81|8eb#0z7#)3MI9q3jVI>~ifm$NxT*$(o*iXuzPIEZ?WFOTBS1nKHFw2h)53BItg zIS`$@LLM%KlU=;_8%1HRmhyhKmu;rNyK#@mSQ+W)!eP8ks88ZfJKaApFExmnA2GdEP4~m; z@bS1APJ-@6?niaEhy8ij!y;XbRPgos2u!#UYOYTR;%k-FcaINnRZs-?qT0DVe1xo; z`F{e^Xz~}DD8LgNoC3dHpV!;N`%BSJ$KhQkMd(Vb^jCQ2a5VvVeDHan-^89F%z&4W z)hP7W5ZcQXWI@&|$mvP&d|W?nhhs;l_w60F0 literal 0 HcmV?d00001 diff --git a/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift b/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift index ee6fa613..d244bd3b 100644 --- a/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift +++ b/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift @@ -148,6 +148,7 @@ public enum Asset { public static let altRectangle = ImageAsset(name: "Media/alt.rectangle") public static let gifRectangle = ImageAsset(name: "Media/gif.rectangle") public static let playerRectangle = ImageAsset(name: "Media/player.rectangle") + public static let repeatOff = ImageAsset(name: "Media/repeat-off") public static let `repeat` = ImageAsset(name: "Media/repeat") public static let repeatMini = ImageAsset(name: "Media/repeat.mini") } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift index 9c7069a3..614fa354 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift @@ -76,9 +76,11 @@ extension StatusToolbarView { handler(.repost) } label: { Label { - Text(L10n.Common.Controls.Status.Actions.retweet) + let text = viewModel.isReposted ? L10n.Common.Controls.Status.Actions.undoRetweet : L10n.Common.Controls.Status.Actions.retweet + Text(text) } icon: { - Image(uiImage: Asset.Media.repeat.image.withRenderingMode(.alwaysTemplate)) + let image = viewModel.isReposted ? Asset.Media.repeatOff.image.withRenderingMode(.alwaysTemplate) : Asset.Media.repeat.image.withRenderingMode(.alwaysTemplate) + Image(uiImage: image) } } // quote From 875cfd14415796100070463c13fcb21b8ecbdb5a Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 20 Mar 2023 18:49:06 +0800 Subject: [PATCH 048/128] feat: improve context menu preview open transition animation --- .../Extension/UIViewController.swift | 47 ++++++++++++++++ .../Container/MediaGridContainerView.swift | 18 ++++--- .../Content/MediaView+ViewModel.swift | 2 +- .../Sources/TwidereUI/Content/MediaView.swift | 1 + .../TwidereUI/Content/StatusView.swift | 6 ++- .../ContextMenuInteractionRepresentable.swift | 54 +++++++++++++++++-- TwidereX/Coordinator/SceneCoordinator.swift | 6 +-- TwidereX/Extension/UIViewController.swift | 39 -------------- .../Provider/DataSourceFacade+Media.swift | 33 +++++++----- ...ider+StatusViewTableViewCellDelegate.swift | 21 ++++++++ .../Scene/Compose/ComposeViewController.swift | 2 +- .../NotificationViewController.swift | 2 +- .../Scene/Profile/ProfileViewController.swift | 2 +- .../StatusViewTableViewCellDelegate.swift | 5 ++ .../Base/Common/TimelineViewController.swift | 2 +- .../DrawerSidebarTransitionController.swift | 2 +- .../MediaPreviewTransitionItem.swift | 27 +++++----- 17 files changed, 184 insertions(+), 85 deletions(-) create mode 100644 TwidereSDK/Sources/TwidereCommon/Extension/UIViewController.swift diff --git a/TwidereSDK/Sources/TwidereCommon/Extension/UIViewController.swift b/TwidereSDK/Sources/TwidereCommon/Extension/UIViewController.swift new file mode 100644 index 00000000..aaa66b0e --- /dev/null +++ b/TwidereSDK/Sources/TwidereCommon/Extension/UIViewController.swift @@ -0,0 +1,47 @@ +// +// UIViewController.swift +// +// +// Created by MainasuK on 2023/3/17. +// + +import UIKit + +extension UIViewController { + + /// Returns the top most view controller from given view controller's stack. + public var topMost: UIViewController? { + // presented view controller + if let presentedViewController = presentedViewController { + return presentedViewController.topMost + } + + // UITabBarController + if let tabBarController = self as? UITabBarController, + let selectedViewController = tabBarController.selectedViewController { + return selectedViewController.topMost + } + + // UINavigationController + if let navigationController = self as? UINavigationController, + let visibleViewController = navigationController.visibleViewController { + return visibleViewController.topMost + } + + // UIPageController + if let pageViewController = self as? UIPageViewController, + pageViewController.viewControllers?.count == 1 { + return pageViewController.viewControllers?.first?.topMost ?? self + } + + // child view controller + for subview in self.view?.subviews ?? [] { + if let childViewController = subview.next as? UIViewController { + return childViewController.topMost + } + } + + return self + } + +} diff --git a/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift b/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift index 003e0228..90968aa7 100644 --- a/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift +++ b/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift @@ -21,6 +21,7 @@ public struct MediaGridContainerView: View { public let idealHeight: CGFloat // ideal height for grid exclude single media public let previewAction: (MediaView.ViewModel) -> Void + public let previewActionWithContext: (MediaView.ViewModel, ContextMenuInteractionPreviewActionContext) -> Void public var body: some View { VStack { @@ -250,8 +251,8 @@ extension MediaGridContainerView { } ] return UIMenu(title: "", image: nil, identifier: nil, options: [], children: children) - }, previewAction: { - previewAction(viewModels[index]) + }, previewActionWithContext: { context in + previewActionWithContext(viewModels[index], context) }) } // end func @@ -370,6 +371,9 @@ struct MediaGridContainerView_Previews: PreviewProvider { idealHeight: 280, previewAction: { _ in // do nothing + }, + previewActionWithContext: { _, _ in + // do nothing } ) .frame(width: 300) @@ -385,12 +389,12 @@ extension View { func contextMenu( contextMenuContentPreviewProvider: @escaping UIContextMenuContentPreviewProvider, contextMenuActionProvider: @escaping UIContextMenuActionProvider, - previewAction: @escaping () -> Void + previewActionWithContext: @escaping (ContextMenuInteractionPreviewActionContext) -> Void ) -> some View { modifier(ContextMenuViewModifier( contextMenuContentPreviewProvider: contextMenuContentPreviewProvider, contextMenuActionProvider: contextMenuActionProvider, - previewAction: previewAction + previewActionWithContext: previewActionWithContext )) } } @@ -398,7 +402,7 @@ extension View { struct ContextMenuViewModifier: ViewModifier { let contextMenuContentPreviewProvider: UIContextMenuContentPreviewProvider let contextMenuActionProvider: UIContextMenuActionProvider - let previewAction: () -> Void + let previewActionWithContext: (ContextMenuInteractionPreviewActionContext) -> Void func body(content: Content) -> some View { ContextMenuInteractionRepresentable( @@ -406,8 +410,8 @@ struct ContextMenuViewModifier: ViewModifier { contextMenuActionProvider: contextMenuActionProvider ) { content - } previewAction: { - previewAction() + } previewActionWithContext: { context in + previewActionWithContext(context) } } } diff --git a/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift index 64133672..00278aac 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift @@ -35,7 +35,7 @@ extension MediaView { // video duration in MS public let durationMS: Int? - @Published public var inContextMenuPreviewing = false + @Published public var shouldHideForTransitioning = false // output public var durationText: String? diff --git a/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift b/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift index 3c5c84ed..444efbf0 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift @@ -43,6 +43,7 @@ public struct MediaView: View { EmptyView() } } + .opacity(viewModel.shouldHideForTransitioning ? 0 : 1) } } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index 87d55b17..035144fe 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -31,6 +31,7 @@ public protocol StatusViewDelegate: AnyObject { // media func statusView(_ viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel) + func statusView(_ viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel, previewActionContext: ContextMenuInteractionPreviewActionContext) func statusView(_ viewModel: StatusView.ViewModel, toggleContentWarningOverlayDisplay isReveal: Bool) // func statusView(_ statusView: StatusView, quoteStatusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int) // @@ -83,7 +84,7 @@ public struct StatusView: View { avatarButton .padding(.trailing, StatusView.hangingAvatarButtonTrailingSapcing) } - let contentSpacing: CGFloat = 0 + let contentSpacing: CGFloat = 4 VStack(spacing: contentSpacing) { // authorView authorView @@ -124,6 +125,9 @@ public struct StatusView: View { idealHeight: 280, previewAction: { mediaViewModel in viewModel.delegate?.statusView(viewModel, previewActionForMediaViewModel: mediaViewModel) + }, + previewActionWithContext: { mediaViewModel, context in + viewModel.delegate?.statusView(viewModel, previewActionForMediaViewModel: mediaViewModel, previewActionContext: context) } ) .clipShape(RoundedRectangle(cornerRadius: MediaGridContainerView.cornerRadius)) diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/ContextMenuInteractionRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/ContextMenuInteractionRepresentable.swift index b22a7bd2..5dfb49ca 100644 --- a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/ContextMenuInteractionRepresentable.swift +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/ContextMenuInteractionRepresentable.swift @@ -5,15 +5,17 @@ // Created by MainasuK on 2023/2/27. // +import os.log import UIKit import SwiftUI +import Combine struct ContextMenuInteractionRepresentable: UIViewRepresentable { let contextMenuContentPreviewProvider: UIContextMenuContentPreviewProvider let contextMenuActionProvider: UIContextMenuActionProvider @ViewBuilder var view: Content - let previewAction: () -> Void + let previewActionWithContext: (ContextMenuInteractionPreviewActionContext) -> Void func makeUIView(context: Context) -> UIView { let hostingController = UIHostingController(rootView: view) @@ -34,10 +36,18 @@ struct ContextMenuInteractionRepresentable: UIViewRepresentable { } class Coordinator: NSObject, UIContextMenuInteractionDelegate { + let logger = Logger(subsystem: "ContextMenuInteractionRepresentable", category: "Coordinator") + + var disposeBag = Set() + let representable: ContextMenuInteractionRepresentable var hostingViewController: UIHostingController? + var activePreviewActionContext: ContextMenuInteractionPreviewActionContext? + + @Published var previewViewFrameInWindow: CGRect = .zero + init(representable: ContextMenuInteractionRepresentable) { self.representable = representable } @@ -51,11 +61,21 @@ struct ContextMenuInteractionRepresentable: UIViewRepresentable { let parameters = UIPreviewParameters() parameters.backgroundColor = .clear parameters.visiblePath = UIBezierPath(roundedRect: hostingViewController.view.bounds, cornerRadius: MediaGridContainerView.cornerRadius) - return UITargetedPreview(view: hostingViewController.view, parameters: parameters) + let targetedPreview = UITargetedPreview(view: hostingViewController.view, parameters: parameters) + return targetedPreview + } + + func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configuration: UIContextMenuConfiguration, dismissalPreviewForItemWithIdentifier identifier: NSCopying) -> UITargetedPreview? { + return activePreviewActionContext?.dismissTargetedPreviewHandler() } func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { - representable.previewAction() + let context = ContextMenuInteractionPreviewActionContext( + interaction: interaction, + animator: animator + ) + activePreviewActionContext = context + representable.previewActionWithContext(context) } func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willDisplayMenuFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) { @@ -67,3 +87,31 @@ struct ContextMenuInteractionRepresentable: UIViewRepresentable { } } } + +public class ContextMenuInteractionPreviewActionContext { + public let interaction: UIContextMenuInteraction + public let animator: UIContextMenuInteractionCommitAnimating + public var dismissTargetedPreviewHandler: () -> UITargetedPreview? = { nil } + + public init(interaction: UIContextMenuInteraction, animator: UIContextMenuInteractionCommitAnimating) { + self.interaction = interaction + self.animator = animator + } +} + +extension ContextMenuInteractionPreviewActionContext { + public func platterClippingView() -> UIView? { + // iOS 16: pass + guard let window = interaction.view?.window, + let contextMenuContainerView = window.subviews.first(where: { !($0.gestureRecognizers ?? []).isEmpty }), + let contextMenuPlatterTransitionView = contextMenuContainerView.subviews.first(where: { !($0 is UIVisualEffectView) }), + let morphingPlatterView = contextMenuPlatterTransitionView.subviews.first(where: { ($0.gestureRecognizers ?? []).count == 1 }), + let platterClippingView = morphingPlatterView.subviews.last, platterClippingView.bounds != .zero + else { + assertionFailure("system API changes!") + return nil + } + + return platterClippingView + } +} diff --git a/TwidereX/Coordinator/SceneCoordinator.swift b/TwidereX/Coordinator/SceneCoordinator.swift index d14ba4ad..199746bb 100644 --- a/TwidereX/Coordinator/SceneCoordinator.swift +++ b/TwidereX/Coordinator/SceneCoordinator.swift @@ -43,7 +43,7 @@ extension SceneCoordinator { case show // push case showDetail // replace case modal(animated: Bool, completion: (() -> Void)? = nil) - case custom(transitioningDelegate: UIViewControllerTransitioningDelegate) + case custom(animated: Bool, transitioningDelegate: UIViewControllerTransitioningDelegate) case customPush case safariPresent(animated: Bool, completion: (() -> Void)? = nil) case activityViewControllerPresent(animated: Bool, completion: (() -> Void)? = nil) @@ -222,10 +222,10 @@ extension SceneCoordinator { } presentingViewController.present(modalNavigationController, animated: animated, completion: completion) - case .custom(let transitioningDelegate): + case .custom(let animated, let transitioningDelegate): viewController.modalPresentationStyle = .custom viewController.transitioningDelegate = transitioningDelegate - sender?.present(viewController, animated: true, completion: nil) + sender?.present(viewController, animated: animated, completion: nil) case .customPush: // set delegate in view controller diff --git a/TwidereX/Extension/UIViewController.swift b/TwidereX/Extension/UIViewController.swift index 2dd5c435..dac64a18 100644 --- a/TwidereX/Extension/UIViewController.swift +++ b/TwidereX/Extension/UIViewController.swift @@ -9,45 +9,6 @@ import UIKit import TwidereCore -extension UIViewController { - - /// Returns the top most view controller from given view controller's stack. - var topMost: UIViewController? { - // presented view controller - if let presentedViewController = presentedViewController { - return presentedViewController.topMost - } - - // UITabBarController - if let tabBarController = self as? UITabBarController, - let selectedViewController = tabBarController.selectedViewController { - return selectedViewController.topMost - } - - // UINavigationController - if let navigationController = self as? UINavigationController, - let visibleViewController = navigationController.visibleViewController { - return visibleViewController.topMost - } - - // UIPageController - if let pageViewController = self as? UIPageViewController, - pageViewController.viewControllers?.count == 1 { - return pageViewController.viewControllers?.first?.topMost ?? self - } - - // child view controller - for subview in self.view?.subviews ?? [] { - if let childViewController = subview.next as? UIViewController { - return childViewController.topMost - } - } - - return self - } - -} - extension UIViewController { func viewController(of type: T.Type) -> T? { diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift index 22295cbc..a494fe74 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift @@ -74,12 +74,10 @@ extension DataSourceFacade { provider: DataSourceProvider & AuthContextProvider & MediaPreviewableViewController, status: StatusRecord, statusViewModel: StatusView.ViewModel, - mediaViewModel: MediaView.ViewModel - ) async { -// let attachments: [AttachmentObject] = await provider.context.managedObjectContext.perform { -// guard let status = status.object(in: provider.context.managedObjectContext) else { return [] } -// return status.attachments -// } + mediaViewModel: MediaView.ViewModel, + previewActionContext: ContextMenuInteractionPreviewActionContext? = nil, + animated: Bool = true + ) { guard let index = statusViewModel.mediaViewModels.firstIndex(of: mediaViewModel) else { assertionFailure("invalid callback") return @@ -121,8 +119,15 @@ extension DataSourceFacade { // return // } + // note: + // previewActionContext will automatically dismiss with fade animation style + previewActionContext?.animator.preferredCommitStyle = .dismiss + let _initialFrame: CGRect? = { + guard let platterClippingView = previewActionContext?.platterClippingView() else { return nil } + return platterClippingView.convert(platterClippingView.frame, to: nil) + }() - await coordinateToMediaPreviewScene( + coordinateToMediaPreviewScene( provider: provider, status: status, mediaPreviewItem: .statusMedia(.init( @@ -138,7 +143,9 @@ extension DataSourceFacade { previewableViewController: provider ) - item.initialFrame = mediaViewModel.frameInWindow + // use the contextMenu previewView frame if possible + // so that the transition will continue from the previewView position + item.initialFrame = _initialFrame ?? mediaViewModel.frameInWindow let thumbnail = mediaViewModel.thumbnail item.image = thumbnail @@ -153,7 +160,8 @@ extension DataSourceFacade { item.sourceImageViewCornerRadius = MediaGridContainerView.cornerRadius return item - }() + }(), + animated: animated ) // end coordinateToMediaPreviewScene } @@ -162,8 +170,9 @@ extension DataSourceFacade { provider: DataSourceProvider & AuthContextProvider & MediaPreviewableViewController, status: StatusRecord, mediaPreviewItem: MediaPreviewViewModel.Item, - mediaPreviewTransitionItem: MediaPreviewTransitionItem - ) async { + mediaPreviewTransitionItem: MediaPreviewTransitionItem, + animated: Bool + ) { let mediaPreviewViewModel = MediaPreviewViewModel( context: provider.context, authContext: provider.authContext, @@ -173,7 +182,7 @@ extension DataSourceFacade { provider.coordinator.present( scene: .mediaPreview(viewModel: mediaPreviewViewModel), from: provider, - transition: .custom(transitioningDelegate: provider.mediaPreviewTransitionController) + transition: .custom(animated: animated, transitioningDelegate: provider.mediaPreviewTransitionController) ) } diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift index b65d5ec8..85977dcb 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift @@ -172,6 +172,27 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC ) } // end Task } + + @MainActor + func tableViewCell( + _ cell: UITableViewCell, + viewModel: StatusView.ViewModel, + previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel, + previewActionContext: ContextMenuInteractionPreviewActionContext + ) { + guard let status = viewModel.status else { + assertionFailure() + return + } + + DataSourceFacade.coordinateToMediaPreviewScene( + provider: self, + status: status, + statusViewModel: viewModel, + mediaViewModel: mediaViewModel, + previewActionContext: previewActionContext + ) + } // func tableViewCell( // _ cell: UITableViewCell, diff --git a/TwidereX/Scene/Compose/ComposeViewController.swift b/TwidereX/Scene/Compose/ComposeViewController.swift index 26329bb4..c2be3a7f 100644 --- a/TwidereX/Scene/Compose/ComposeViewController.swift +++ b/TwidereX/Scene/Compose/ComposeViewController.swift @@ -151,7 +151,7 @@ extension ComposeViewController: ComposeContentViewControllerDelegate { coordinator.present( scene: .mediaPreview(viewModel: mediaPreviewViewModel), from: self, - transition: .custom(transitioningDelegate: mediaPreviewTransitionController) + transition: .custom(animated: true, transitioningDelegate: mediaPreviewTransitionController) ) } diff --git a/TwidereX/Scene/Notification/NotificationViewController.swift b/TwidereX/Scene/Notification/NotificationViewController.swift index 01a21f0e..8aae0660 100644 --- a/TwidereX/Scene/Notification/NotificationViewController.swift +++ b/TwidereX/Scene/Notification/NotificationViewController.swift @@ -133,7 +133,7 @@ extension NotificationViewController { @objc private func avatarButtonPressed(_ sender: UIButton) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") let drawerSidebarViewModel = DrawerSidebarViewModel(context: context, authContext: viewModel.authContext) - coordinator.present(scene: .drawerSidebar(viewModel: drawerSidebarViewModel), from: self, transition: .custom(transitioningDelegate: drawerSidebarTransitionController)) + coordinator.present(scene: .drawerSidebar(viewModel: drawerSidebarViewModel), from: self, transition: .custom(animated: true, transitioningDelegate: drawerSidebarTransitionController)) } } diff --git a/TwidereX/Scene/Profile/ProfileViewController.swift b/TwidereX/Scene/Profile/ProfileViewController.swift index b2445250..48bcbe7a 100644 --- a/TwidereX/Scene/Profile/ProfileViewController.swift +++ b/TwidereX/Scene/Profile/ProfileViewController.swift @@ -274,7 +274,7 @@ extension ProfileViewController { @objc private func avatarButtonPressed(_ sender: UIButton) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") let drawerSidebarViewModel = DrawerSidebarViewModel(context: context, authContext: authContext) - coordinator.present(scene: .drawerSidebar(viewModel: drawerSidebarViewModel), from: self, transition: .custom(transitioningDelegate: drawerSidebarTransitionController)) + coordinator.present(scene: .drawerSidebar(viewModel: drawerSidebarViewModel), from: self, transition: .custom(animated: true, transitioningDelegate: drawerSidebarTransitionController)) } @objc private func refreshControlValueChanged(_ sender: UIRefreshControl) { diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift index 4d05936a..f6a37227 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift @@ -27,6 +27,7 @@ protocol StatusViewTableViewCellDelegate: AutoGenerateProtocolDelegate { // sourcery:inline:StatusViewTableViewCellDelegate.AutoGenerateProtocolDelegate func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, toggleContentDisplay isReveal: Bool) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel) + func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel, previewActionContext: ContextMenuInteractionPreviewActionContext) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, toggleContentWarningOverlayDisplay isReveal: Bool) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, statusToolbarViewModel: StatusToolbarView.ViewModel, statusToolbarButtonDidPressed action: StatusToolbarView.Action) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, viewHeightDidChange: Void) @@ -45,6 +46,10 @@ extension StatusViewDelegate where Self: StatusViewContainerTableViewCell { delegate?.tableViewCell(self, viewModel: viewModel, previewActionForMediaViewModel: mediaViewModel) } + func statusView(_ viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel, previewActionContext: ContextMenuInteractionPreviewActionContext) { + delegate?.tableViewCell(self, viewModel: viewModel, previewActionForMediaViewModel: mediaViewModel, previewActionContext: previewActionContext) + } + func statusView(_ viewModel: StatusView.ViewModel, toggleContentWarningOverlayDisplay isReveal: Bool) { delegate?.tableViewCell(self, viewModel: viewModel, toggleContentWarningOverlayDisplay: isReveal) } diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift index 2e4e2356..b27a6799 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift @@ -200,7 +200,7 @@ extension TimelineViewController { @objc private func avatarButtonPressed(_ sender: UIButton) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") let drawerSidebarViewModel = DrawerSidebarViewModel(context: context, authContext: authContext) - coordinator.present(scene: .drawerSidebar(viewModel: drawerSidebarViewModel), from: self, transition: .custom(transitioningDelegate: drawerSidebarTransitionController)) + coordinator.present(scene: .drawerSidebar(viewModel: drawerSidebarViewModel), from: self, transition: .custom(animated: true, transitioningDelegate: drawerSidebarTransitionController)) } @objc private func refreshControlValueChanged(_ sender: UIRefreshControl) { diff --git a/TwidereX/Scene/Transition/DrawerSidebar/DrawerSidebarTransitionController.swift b/TwidereX/Scene/Transition/DrawerSidebar/DrawerSidebarTransitionController.swift index 40fef328..f5dd5020 100644 --- a/TwidereX/Scene/Transition/DrawerSidebar/DrawerSidebarTransitionController.swift +++ b/TwidereX/Scene/Transition/DrawerSidebar/DrawerSidebarTransitionController.swift @@ -152,7 +152,7 @@ extension DrawerSidebarTransitionController { hostViewController.coordinator.present( scene: .drawerSidebar(viewModel: drawerSidebarViewModel), from: hostViewController, - transition: .custom(transitioningDelegate: self) + transition: .custom(animated: true, transitioningDelegate: self) ) case .dismiss: diff --git a/TwidereX/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift b/TwidereX/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift index fbd5d623..4fcdf2b8 100644 --- a/TwidereX/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift +++ b/TwidereX/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift @@ -17,8 +17,7 @@ class MediaPreviewTransitionItem: Identifiable { // source var image: UIImage? var aspectRatio: CGSize? - var initialFrame: CGRect? = nil - var sourceImageView: UIImageView? + var initialFrame: CGRect? = nil // start frame for .push animation var sourceImageViewCornerRadius: CGFloat? // target @@ -55,18 +54,18 @@ extension MediaPreviewTransitionItem { position: UIViewAnimatingPosition, index: Int? ) { -// let alpha: CGFloat = position == .end ? 1 : 0 -// switch self { -// case .none: -// break -// case .attachment(let mediaView): -// mediaView.alpha = alpha -// case .attachments(let mediaGridContainerView): -// if let index = index { -// mediaGridContainerView.setAlpha(0, index: index) -// } else { -// mediaGridContainerView.setAlpha(alpha) -// } + switch self { + case .none: + break + case .mediaView(let viewModel): + viewModel.shouldHideForTransitioning = position != .end + case .profileAvatar(let view): + // TODO: + break + case .profileBanner(let view): + // TODO: + break + } // case .profileAvatar(let profileHeaderView): // profileHeaderView.avatarView.avatarButton.alpha = alpha // case .profileBanner: From d9f7451abcb3dacfc5227a1e6384efb9324df43b Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 23 Mar 2023 21:02:31 +0800 Subject: [PATCH 049/128] feat: complete post poll UI and UX --- .../API/Error/Mastodon+API+Error.swift | 18 + .../Sources/TwidereCore/Error/AppError.swift | 14 +- .../Protocol/TextStyleConfigurable.swift | 12 +- .../MediaGridContainerView+ViewModel.swift | 57 -- .../PollOptionView+Configuration.swift | 18 +- .../Content/PollOptionView+ViewModel.swift | 478 ++++++------- .../TwidereUI/Content/PollOptionView.swift | 663 ++++++++++++------ .../Sources/TwidereUI/Content/PollView.swift | 282 ++++++++ .../Content/StatusView+ViewModel.swift | 32 +- .../TwidereUI/Content/StatusView.swift | 36 +- .../TwidereUI/Extension/UITableView.swift | 1 + .../TwidereUI/Shape/CheckmarkView.swift | 82 +++ .../Poll/PollOptionTableViewCell.swift | 65 -- .../TouchBlockingViewRepresentable.swift | 21 + .../Provider/DataSourceFacade+Banner.swift | 16 + .../Provider/DataSourceFacade+Poll.swift | 101 +-- .../Provider/DataSourceFacade+Status.swift | 1 + ...ider+StatusViewTableViewCellDelegate.swift | 62 ++ ...taSourceProvider+UITableViewDelegate.swift | 5 + .../TableViewCell/StatusTableViewCell.swift | 2 + .../StatusViewTableViewCellDelegate.swift | 15 + .../List/ListTimelineViewController.swift | 2 +- 22 files changed, 1307 insertions(+), 676 deletions(-) delete mode 100644 TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView+ViewModel.swift create mode 100644 TwidereSDK/Sources/TwidereUI/Content/PollView.swift create mode 100644 TwidereSDK/Sources/TwidereUI/Shape/CheckmarkView.swift delete mode 100644 TwidereSDK/Sources/TwidereUI/TableViewCell/Poll/PollOptionTableViewCell.swift create mode 100644 TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TouchBlockingViewRepresentable.swift diff --git a/TwidereSDK/Sources/MastodonSDK/API/Error/Mastodon+API+Error.swift b/TwidereSDK/Sources/MastodonSDK/API/Error/Mastodon+API+Error.swift index 94d063c4..14578821 100644 --- a/TwidereSDK/Sources/MastodonSDK/API/Error/Mastodon+API+Error.swift +++ b/TwidereSDK/Sources/MastodonSDK/API/Error/Mastodon+API+Error.swift @@ -34,3 +34,21 @@ extension Mastodon.API { } } + +extension Mastodon.API.Error: LocalizedError { + public var errorDescription: String? { + guard let mastodonError = mastodonError else { + return httpResponseStatus.reasonPhrase + } + + return mastodonError.errorDescription + } + + public var failureReason: String? { + guard let mastodonError = mastodonError else { + return nil + } + + return mastodonError.failureReason + } +} diff --git a/TwidereSDK/Sources/TwidereCore/Error/AppError.swift b/TwidereSDK/Sources/TwidereCore/Error/AppError.swift index 3772dff0..ded4d7e1 100644 --- a/TwidereSDK/Sources/TwidereCore/Error/AppError.swift +++ b/TwidereSDK/Sources/TwidereCore/Error/AppError.swift @@ -55,7 +55,7 @@ extension AppError.ErrorReason: LocalizedError { public var errorDescription: String? { switch self { case .internal(let reason): - return "Internal" + return "Internal. \(reason)" case .twitterInternalError(let error): return error.errorDescription case .authenticationMissing: @@ -71,11 +71,7 @@ extension AppError.ErrorReason: LocalizedError { return twitterAPIError.errorDescription case .mastodonResponseError(let error): - guard let mastodonError = error.mastodonError else { - return error.httpResponseStatus.reasonPhrase - } - - return mastodonError.errorDescription + return error.errorDescription } } @@ -98,11 +94,7 @@ extension AppError.ErrorReason: LocalizedError { return twitterAPIError.failureReason case .mastodonResponseError(let error): - guard let mastodonError = error.mastodonError else { - return nil - } - - return mastodonError.failureReason + return error.failureReason } } diff --git a/TwidereSDK/Sources/TwidereCore/Protocol/TextStyleConfigurable.swift b/TwidereSDK/Sources/TwidereCore/Protocol/TextStyleConfigurable.swift index 8522392e..d7efd15a 100644 --- a/TwidereSDK/Sources/TwidereCore/Protocol/TextStyleConfigurable.swift +++ b/TwidereSDK/Sources/TwidereCore/Protocol/TextStyleConfigurable.swift @@ -30,6 +30,7 @@ public enum TextStyle { case pollOptionTitle case pollOptionPercentage case pollVoteDescription + case pollVoteButton case userAuthorUsername case userDescription case profileAuthorName @@ -76,6 +77,7 @@ extension TextStyle { case .pollOptionTitle: return 1 case .pollOptionPercentage: return 1 case .pollVoteDescription: return 1 + case .pollVoteButton: return 1 case .userAuthorName: return 1 case .userAuthorUsername: return 1 case .userDescription: return 1 @@ -117,11 +119,13 @@ extension TextStyle { case .statusMetrics: return .preferredFont(forTextStyle: .footnote) case .pollOptionTitle: - return .systemFont(ofSize: 15, weight: .regular) + return UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 14)) case .pollOptionPercentage: - return .systemFont(ofSize: 12, weight: .regular) + return UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 14)) case .pollVoteDescription: - return .systemFont(ofSize: 14, weight: .regular) + return UIFontMetrics(forTextStyle: .callout).scaledFont(for: .systemFont(ofSize: 14)) + case .pollVoteButton: + return UIFontMetrics(forTextStyle: .callout).scaledFont(for: .systemFont(ofSize: 14, weight: .medium)) case .userAuthorName: return .preferredFont(forTextStyle: .headline) case .userAuthorUsername: @@ -187,6 +191,8 @@ extension TextStyle { return .secondaryLabel case .pollVoteDescription: return .secondaryLabel + case .pollVoteButton: + return .tintColor case .userAuthorUsername: return .secondaryLabel case .userDescription: diff --git a/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView+ViewModel.swift deleted file mode 100644 index 5861ad09..00000000 --- a/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView+ViewModel.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// MediaGridContainerView+ViewModel.swift -// -// -// Created by MainasuK on 2021-12-14. -// - -import UIKit -import Combine - -extension MediaGridContainerView { - public class ViewModel { - var disposeBag = Set() - - @Published public var isSensitiveToggleButtonDisplay: Bool = false - @Published public var isContentWarningOverlayDisplay: Bool? = nil - } -} - -extension MediaGridContainerView.ViewModel { - -// func resetContentWarningOverlay() { -// isContentWarningOverlayDisplay = nil -// } -// -// func bind(view: MediaGridContainerView) { -// $isSensitiveToggleButtonDisplay -// .sink { isDisplay in -// view.sensitiveToggleButtonBlurVisualEffectView.isHidden = !isDisplay -// } -// .store(in: &disposeBag) -// $isContentWarningOverlayDisplay -// .sink { isDisplay in -// assert(Thread.isMainThread) -// guard let isDisplay = isDisplay else { return } -// let withAnimation = self.isContentWarningOverlayDisplay != nil -// view.configureOverlayDisplay(isDisplay: isDisplay, animated: withAnimation) -// } -// .store(in: &disposeBag) -// } - -} - -extension MediaGridContainerView { -// func configureOverlayDisplay(isDisplay: Bool, animated: Bool) { -// if animated { -// UIView.animate(withDuration: 0.33, delay: 0, options: .curveEaseInOut) { -// self.contentWarningOverlayView.blurVisualEffectView.alpha = isDisplay ? 1 : 0 -// } -// } else { -// contentWarningOverlayView.blurVisualEffectView.alpha = isDisplay ? 1 : 0 -// } -// -// contentWarningOverlayView.isUserInteractionEnabled = isDisplay -// contentWarningOverlayView.tapGestureRecognizer.isEnabled = isDisplay -// } -} diff --git a/TwidereSDK/Sources/TwidereUI/Content/PollOptionView+Configuration.swift b/TwidereSDK/Sources/TwidereUI/Content/PollOptionView+Configuration.swift index b2b9a86e..dd8a3b93 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/PollOptionView+Configuration.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/PollOptionView+Configuration.swift @@ -18,12 +18,12 @@ import TwidereCore // public typealias ConfigurationContext = StatusView.ConfigurationContext //} -extension PollOptionView { - - public func configure( - pollOption: PollOptionObject +//extension PollOptionView { +// +// public func configure( +// pollOption: PollOptionObject // configurationContext: ConfigurationContext - ) { +// ) { // switch pollOption { // case .twitter(let object): // configure( @@ -36,8 +36,8 @@ extension PollOptionView { // configurationContext: configurationContext // ) // } - } - +// } +// // public func configure( // pollOption option: TwitterPollOption, // configurationContext: ConfigurationContext @@ -170,5 +170,5 @@ extension PollOptionView { // .assign(to: \.percentage, on: viewModel) // .store(in: &disposeBag) // } - -} +// +//} diff --git a/TwidereSDK/Sources/TwidereUI/Content/PollOptionView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/PollOptionView+ViewModel.swift index 096b0497..d6b38524 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/PollOptionView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/PollOptionView+ViewModel.swift @@ -14,242 +14,242 @@ import TwitterMeta import MastodonMeta import TwidereCore -extension PollOptionView { - - static let percentageFormatter: NumberFormatter = { - let formatter = NumberFormatter() - formatter.numberStyle = .percent - formatter.maximumFractionDigits = 1 - formatter.minimumIntegerDigits = 1 - formatter.roundingMode = .down - return formatter - }() - - public final class ViewModel: ObservableObject { - var disposeBag = Set() - var observations = Set() - var objects = Set() - - @Published var authenticationContext: AuthenticationContext? - - @Published var style: PollOptionView.Style? - - @Published public var content: String = "" // for edit style - - @Published public var metaContent: MetaContent? // for plain style - @Published public var percentage: Double? - - @Published public var isExpire: Bool = false - @Published public var isMultiple: Bool = false - @Published public var isSelect: Bool? = false // nil for server not return selection array - @Published public var isPollVoted: Bool = false - @Published public var isMyPoll: Bool = false - - // output - @Published public var corner: Corner = .none - @Published public var stripProgressTinitColor: UIColor = .clear - @Published public var selectImageTintColor: UIColor = Asset.Colors.hightLight.color - @Published public var isReveal: Bool = false - - @Published public var groupedAccessibilityLabel = "" - - init() { - // corner - $isMultiple - .map { $0 ? .radius(8) : .circle } - .assign(to: &$corner) - // stripProgressTinitColor - Publishers.CombineLatest3( - $style, - $isSelect, - $isReveal - ) - .map { style, isSelect, isReveal -> UIColor in - guard case .plain = style else { return .clear } - guard isReveal else { - return .clear - } - - if isSelect == true { - return Asset.Colors.hightLight.color.withAlphaComponent(0.75) - } else { - return Asset.Colors.hightLight.color.withAlphaComponent(0.20) - } - } - .assign(to: &$stripProgressTinitColor) - // selectImageTintColor - Publishers.CombineLatest( - $isSelect, - $isReveal - ) - .map { isSelect, isReveal in - guard let isSelect = isSelect else { - return .clear // none selection state - } - - if isReveal { - return isSelect ? .white : .clear - } else { - return Asset.Colors.hightLight.color - } - } - .assign(to: &$selectImageTintColor) - // isReveal - Publishers.CombineLatest3( - $isExpire, - $isPollVoted, - $isMyPoll - ) - .map { isExpire, isPollVoted, isMyPoll in - return isExpire || isPollVoted || isMyPoll - } - .assign(to: &$isReveal) - // groupedAccessibilityLabel - - Publishers.CombineLatest3( - $metaContent, - $percentage, - $isReveal - ) - .map { metaContent, percentage, isReveal -> String in - var strings: [String?] = [] - - metaContent.flatMap { strings.append($0.string) } - - if isReveal, - let percentage = percentage, - let string = PollOptionView.percentageFormatter.string(from: NSNumber(value: percentage)) - { - strings.append(string) - } - - return strings.compactMap { $0 }.joined(separator: ", ") - } - .assign(to: &$groupedAccessibilityLabel) - } - - public enum Corner: Hashable { - case none - case circle - case radius(CGFloat) - } - } -} - -extension PollOptionView.ViewModel { - public func bind(view: PollOptionView) { - // content - NotificationCenter.default - .publisher(for: UITextField.textDidChangeNotification, object: view.textField) - .receive(on: DispatchQueue.main) - .map { _ in view.textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" } - .assign(to: &$content) - // metaContent - $metaContent - .sink { metaContent in - guard let metaContent = metaContent else { - view.titleMetaLabel.reset() - return - } - view.titleMetaLabel.configure(content: metaContent) - } - .store(in: &disposeBag) - // percentage - Publishers.CombineLatest( - $isReveal, - $percentage - ) - .sink { isReveal, percentage in - guard isReveal else { - view.percentageMetaLabel.configure(content: PlaintextMetaContent(string: "")) - return - } - - let oldPercentage = self.percentage - - let animated = oldPercentage != nil && percentage != nil - view.stripProgressView.setProgress(percentage ?? 0, animated: animated) - - guard let percentage = percentage, - let string = PollOptionView.percentageFormatter.string(from: NSNumber(value: percentage)) - else { - view.percentageMetaLabel.configure(content: PlaintextMetaContent(string: "")) - return - } - - view.percentageMetaLabel.configure(content: PlaintextMetaContent(string: string)) - } - .store(in: &disposeBag) - // corner - $corner - .removeDuplicates() - .sink { _ in - view.setNeedsLayout() - } - .store(in: &disposeBag) - // backgroundColor - $stripProgressTinitColor - .map { $0 as UIColor? } - .assign(to: \.tintColor, on: view.stripProgressView) - .store(in: &disposeBag) - // selectionImageView - Publishers.CombineLatest4( - $style, - $isMultiple, - $isSelect, - $isReveal - ) - .map { style, isMultiple, isSelect, isReveal -> UIImage? in - guard case .plain = style else { return nil } - - func circle(isSelect: Bool) -> UIImage { - let image = isSelect ? Asset.Indices.checkmarkCircleFill.image : Asset.Indices.circle.image - return image.withRenderingMode(.alwaysTemplate) - } - - func square(isSelect: Bool) -> UIImage { - let image = isSelect ? Asset.Indices.checkmarkSquareFill.image : Asset.Indices.square.image - return image.withRenderingMode(.alwaysTemplate) - } - - func image(isMultiple: Bool, isSelect: Bool) -> UIImage { - return isMultiple ? square(isSelect: isSelect) : circle(isSelect: isSelect) - } - - if isReveal { - guard isSelect == true else { - // not display image when isReveal: - // - the server not return selection state - // - the user not select - return nil - } - return image(isMultiple: isMultiple, isSelect: true) - } else { - return image(isMultiple: isMultiple, isSelect: isSelect == true) - } - } - .sink { image in - view.selectionImageView.image = image - } - .store(in: &disposeBag) - // selectImageTintColor - $selectImageTintColor - .assign(to: \.tintColor, on: view.selectionImageView) - .store(in: &disposeBag) - // accessibility - $isSelect - .sink { isSelect in - if isSelect == true { - view.accessibilityTraits.insert(.selected) - } else { - view.accessibilityTraits.remove(.selected) - } - } - .store(in: &disposeBag) - $groupedAccessibilityLabel - .sink { groupedAccessibilityLabel in - view.accessibilityLabel = groupedAccessibilityLabel - } - .store(in: &disposeBag) - } -} +//extension PollOptionView { +// +// static let percentageFormatter: NumberFormatter = { +// let formatter = NumberFormatter() +// formatter.numberStyle = .percent +// formatter.maximumFractionDigits = 1 +// formatter.minimumIntegerDigits = 1 +// formatter.roundingMode = .down +// return formatter +// }() +// +// public final class ViewModel: ObservableObject { +// var disposeBag = Set() +// var observations = Set() +// var objects = Set() +// +// @Published var authenticationContext: AuthenticationContext? +// +// @Published var style: PollOptionView.Style? +// +// @Published public var content: String = "" // for edit style +// +// @Published public var metaContent: MetaContent? // for plain style +// @Published public var percentage: Double? +// +// @Published public var isExpire: Bool = false +// @Published public var isMultiple: Bool = false +// @Published public var isSelect: Bool? = false // nil for server not return selection array +// @Published public var isPollVoted: Bool = false +// @Published public var isMyPoll: Bool = false +// +// // output +// @Published public var corner: Corner = .none +// @Published public var stripProgressTinitColor: UIColor = .clear +// @Published public var selectImageTintColor: UIColor = Asset.Colors.hightLight.color +// @Published public var isReveal: Bool = false +// +// @Published public var groupedAccessibilityLabel = "" +// +// init() { +// // corner +// $isMultiple +// .map { $0 ? .radius(8) : .circle } +// .assign(to: &$corner) +// // stripProgressTinitColor +// Publishers.CombineLatest3( +// $style, +// $isSelect, +// $isReveal +// ) +// .map { style, isSelect, isReveal -> UIColor in +// guard case .plain = style else { return .clear } +// guard isReveal else { +// return .clear +// } +// +// if isSelect == true { +// return Asset.Colors.hightLight.color.withAlphaComponent(0.75) +// } else { +// return Asset.Colors.hightLight.color.withAlphaComponent(0.20) +// } +// } +// .assign(to: &$stripProgressTinitColor) +// // selectImageTintColor +// Publishers.CombineLatest( +// $isSelect, +// $isReveal +// ) +// .map { isSelect, isReveal in +// guard let isSelect = isSelect else { +// return .clear // none selection state +// } +// +// if isReveal { +// return isSelect ? .white : .clear +// } else { +// return Asset.Colors.hightLight.color +// } +// } +// .assign(to: &$selectImageTintColor) +// // isReveal +// Publishers.CombineLatest3( +// $isExpire, +// $isPollVoted, +// $isMyPoll +// ) +// .map { isExpire, isPollVoted, isMyPoll in +// return isExpire || isPollVoted || isMyPoll +// } +// .assign(to: &$isReveal) +// // groupedAccessibilityLabel +// +// Publishers.CombineLatest3( +// $metaContent, +// $percentage, +// $isReveal +// ) +// .map { metaContent, percentage, isReveal -> String in +// var strings: [String?] = [] +// +// metaContent.flatMap { strings.append($0.string) } +// +// if isReveal, +// let percentage = percentage, +// let string = PollOptionView.percentageFormatter.string(from: NSNumber(value: percentage)) +// { +// strings.append(string) +// } +// +// return strings.compactMap { $0 }.joined(separator: ", ") +// } +// .assign(to: &$groupedAccessibilityLabel) +// } +// +// public enum Corner: Hashable { +// case none +// case circle +// case radius(CGFloat) +// } +// } +//} +// +//extension PollOptionView.ViewModel { +// public func bind(view: PollOptionView) { +// // content +// NotificationCenter.default +// .publisher(for: UITextField.textDidChangeNotification, object: view.textField) +// .receive(on: DispatchQueue.main) +// .map { _ in view.textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" } +// .assign(to: &$content) +// // metaContent +// $metaContent +// .sink { metaContent in +// guard let metaContent = metaContent else { +// view.titleMetaLabel.reset() +// return +// } +// view.titleMetaLabel.configure(content: metaContent) +// } +// .store(in: &disposeBag) +// // percentage +// Publishers.CombineLatest( +// $isReveal, +// $percentage +// ) +// .sink { isReveal, percentage in +// guard isReveal else { +// view.percentageMetaLabel.configure(content: PlaintextMetaContent(string: "")) +// return +// } +// +// let oldPercentage = self.percentage +// +// let animated = oldPercentage != nil && percentage != nil +// view.stripProgressView.setProgress(percentage ?? 0, animated: animated) +// +// guard let percentage = percentage, +// let string = PollOptionView.percentageFormatter.string(from: NSNumber(value: percentage)) +// else { +// view.percentageMetaLabel.configure(content: PlaintextMetaContent(string: "")) +// return +// } +// +// view.percentageMetaLabel.configure(content: PlaintextMetaContent(string: string)) +// } +// .store(in: &disposeBag) +// // corner +// $corner +// .removeDuplicates() +// .sink { _ in +// view.setNeedsLayout() +// } +// .store(in: &disposeBag) +// // backgroundColor +// $stripProgressTinitColor +// .map { $0 as UIColor? } +// .assign(to: \.tintColor, on: view.stripProgressView) +// .store(in: &disposeBag) +// // selectionImageView +// Publishers.CombineLatest4( +// $style, +// $isMultiple, +// $isSelect, +// $isReveal +// ) +// .map { style, isMultiple, isSelect, isReveal -> UIImage? in +// guard case .plain = style else { return nil } +// +// func circle(isSelect: Bool) -> UIImage { +// let image = isSelect ? Asset.Indices.checkmarkCircleFill.image : Asset.Indices.circle.image +// return image.withRenderingMode(.alwaysTemplate) +// } +// +// func square(isSelect: Bool) -> UIImage { +// let image = isSelect ? Asset.Indices.checkmarkSquareFill.image : Asset.Indices.square.image +// return image.withRenderingMode(.alwaysTemplate) +// } +// +// func image(isMultiple: Bool, isSelect: Bool) -> UIImage { +// return isMultiple ? square(isSelect: isSelect) : circle(isSelect: isSelect) +// } +// +// if isReveal { +// guard isSelect == true else { +// // not display image when isReveal: +// // - the server not return selection state +// // - the user not select +// return nil +// } +// return image(isMultiple: isMultiple, isSelect: true) +// } else { +// return image(isMultiple: isMultiple, isSelect: isSelect == true) +// } +// } +// .sink { image in +// view.selectionImageView.image = image +// } +// .store(in: &disposeBag) +// // selectImageTintColor +// $selectImageTintColor +// .assign(to: \.tintColor, on: view.selectionImageView) +// .store(in: &disposeBag) +// // accessibility +// $isSelect +// .sink { isSelect in +// if isSelect == true { +// view.accessibilityTraits.insert(.selected) +// } else { +// view.accessibilityTraits.remove(.selected) +// } +// } +// .store(in: &disposeBag) +// $groupedAccessibilityLabel +// .sink { groupedAccessibilityLabel in +// view.accessibilityLabel = groupedAccessibilityLabel +// } +// .store(in: &disposeBag) +// } +//} diff --git a/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift b/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift index d28e5d74..69161d22 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift @@ -5,257 +5,460 @@ // Created by MainasuK on 2021-11-29. // -import UIKit +import os.log +import Foundation +import SwiftUI import Combine -import MetaTextKit -import MetaLabel -import TwidereLocalization -import UITextView_Placeholder -import TwidereCore - -public protocol PollOptionViewDelegate: AnyObject { - func pollOptionView(_ pollOptionView: PollOptionView, deleteBackwardResponseTextField textField: DeleteBackwardResponseTextField, textBeforeDelete: String?) -} +import MastodonMeta +import CoreDataStack -public final class PollOptionView: UIView { - - static let height: CGFloat = 36 - - public weak var delegate: PollOptionViewDelegate? - private(set) var style: Style? - - var disposeBag = Set() - public private(set) lazy var viewModel: ViewModel = { - let viewModel = ViewModel() - viewModel.bind(view: self) - return viewModel - }() - - let containerView = UIView() - - let stripProgressView = StripProgressView() - - let selectionImageView: UIImageView = { - let imageView = UIImageView() - return imageView - }() - - public let titleMetaLabel = MetaLabel(style: .pollOptionTitle) - - public let percentageMetaLabel = MetaLabel(style: .pollOptionPercentage) - - // TODO: MetaTextField? - public let textField: DeleteBackwardResponseTextField = { - let textField = DeleteBackwardResponseTextField() - textField.font = .systemFont(ofSize: 16, weight: .regular) - textField.textColor = .label - textField.text = "Choice" - textField.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight ? .left : .right - return textField - }() +public struct PollOptionView: View { - public func prepareForReuse() { - viewModel.objects.removeAll() - viewModel.percentage = nil - stripProgressView.setProgress(0, animated: false) - } - - public override init(frame: CGRect) { - super.init(frame: frame) - _init() - } - - public required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension PollOptionView { + @ObservedObject public var viewModel: ViewModel + public let selectAction: (ViewModel) -> Void - private func _init() { - textField.deleteBackwardDelegate = self - - // Accessibility - // hint: Poll option - accessibilityHint = L10n.Accessibility.Common.Status.pollOptionOrdinalPrefix + var bodyFont: UIFont { TextStyle.pollOptionTitle.font } + var rowHeight: CGFloat { + let height = abs(bodyFont.ascender) + abs(bodyFont.descender) + return max(markViewMinHeight + 2 * markViewPadding, height) } + var markViewMinHeight: CGFloat { 20.0 } + var markViewPadding: CGFloat { 4.0 } - public override func layoutSubviews() { - super.layoutSubviews() - - setupCorner() - } - - func setupCorner() { - switch viewModel.corner { - case .none: - containerView.layer.masksToBounds = false - stripProgressView.cornerRadius = 0 - case .radius(let radius): - containerView.layer.masksToBounds = true - guard radius < bounds.height / 2 else { - fallthrough - } - containerView.layer.cornerCurve = .continuous - containerView.layer.cornerRadius = radius - stripProgressView.cornerRadius = radius - case .circle: - let radius = bounds.height / 2 - containerView.layer.masksToBounds = true - containerView.layer.cornerCurve = .circular - containerView.layer.cornerRadius = radius - stripProgressView.cornerRadius = radius - } - } - - public func setup(style: Style) { - guard self.style == nil else { - assertionFailure("Should only setup once") - return - } - self.style = style - self.viewModel.style = style - style.layout(view: self) - } - - public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { - textField.layer.borderColor = UIColor.secondaryLabel.cgColor + var markView: some View { + GeometryReader { proxy in + let tintColor = viewModel.canSelect ? Asset.Colors.hightLight.color : .systemBackground + let dimension = proxy.size.width + CheckmarkView( + tintColor: tintColor, + borderWidth: ceil(dimension / 15), + cornerRadius: viewModel.isMulitpleChoice ? dimension / 6 : dimension / 2, + check: viewModel.isOptionVoted || viewModel.isSelected + ) } } -} - -extension PollOptionView { - public enum Style { - case plain - case edit - - func layout(view: PollOptionView) { - switch self { - case .plain: layoutPlain(view: view) - case .edit: layoutEdit(view: view) + public var body: some View { + Button { + selectAction(viewModel) + } label: { + let rowHeight = self.rowHeight + let rowCornerRadius: CGFloat = { + if viewModel.isMulitpleChoice { + return rowHeight / 6 + } else { + return rowHeight / 2 + } + }() + HStack(spacing: .zero) { + markView + .padding(markViewPadding) + .frame(width: rowHeight, height: rowHeight) + .opacity(viewModel.canSelect || viewModel.isOptionVoted ? 1 : 0) + LabelRepresentable( + metaContent: viewModel.content, + textStyle: .pollOptionTitle, + setupLabel: { label in + label.setContentHuggingPriority(.required, for: .horizontal) + label.setContentCompressionResistancePriority(.required, for: .horizontal) + } + ) + .fixedSize(horizontal: false, vertical: true) + .border(.red, width: 1) + Spacer() + Text(viewModel.percentageText) + .font(Font(bodyFont)) + .foregroundColor(.secondary) + .monospacedDigit() + .padding(.horizontal, 6) + .opacity(viewModel.isResultReveal ? 1 : 0) } + .frame(height: rowHeight) + .background( + GeometryReader { proxy in + ZStack(alignment: .leading) { + Color(uiColor: Asset.Colors.hightLight.color.withAlphaComponent(0.15)) + // note: + // Use offset method to keep the perfect circle shape on edges. + // So the edge of the bar with percenage likes 0.1 will display as circle + // but not rounded square + let alpha = viewModel.isOptionVoted ? 0.75 : 0.25 + let color = Asset.Colors.hightLight.color.withAlphaComponent(alpha) + let offsetX = proxy.size.width * (1 - viewModel.percentage) + Color(uiColor: color) + .cornerRadius(rowCornerRadius) + .offset(x: -offsetX) // tweak position + .animation(.easeInOut, value: viewModel.percentage) + .opacity(viewModel.isResultReveal ? 1 : 0) + } + .compositingGroup() + .cornerRadius(rowCornerRadius) // clip + } + ) } + .buttonStyle(.borderless) } } -extension PollOptionView.Style { - private func layoutPlain(view: PollOptionView) { - view.containerView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(view.containerView) - NSLayoutConstraint.activate([ - view.containerView.topAnchor.constraint(equalTo: view.topAnchor), - view.containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - view.containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - view.containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - view.containerView.backgroundColor = Asset.Colors.hightLight.color.withAlphaComponent(0.08) +extension PollOptionView { + public class ViewModel: ObservableObject, Identifiable { - view.stripProgressView.translatesAutoresizingMaskIntoConstraints = false - view.containerView.addSubview(view.stripProgressView) - NSLayoutConstraint.activate([ - view.stripProgressView.topAnchor.constraint(equalTo: view.containerView.topAnchor), - view.stripProgressView.leadingAnchor.constraint(equalTo: view.containerView.leadingAnchor), - view.stripProgressView.trailingAnchor.constraint(equalTo: view.containerView.trailingAnchor), - view.stripProgressView.bottomAnchor.constraint(equalTo: view.containerView.bottomAnchor), - ]) + public var id: Int { index } - view.selectionImageView.translatesAutoresizingMaskIntoConstraints = false - view.containerView.addSubview(view.selectionImageView) - NSLayoutConstraint.activate([ - view.selectionImageView.topAnchor.constraint(equalTo: view.containerView.topAnchor, constant: 6), - view.selectionImageView.leadingAnchor.constraint(equalTo: view.containerView.leadingAnchor, constant: 6), - view.containerView.bottomAnchor.constraint(equalTo: view.selectionImageView.bottomAnchor, constant: 6), - view.selectionImageView.widthAnchor.constraint(equalToConstant: 24).priority(.required - 1), - view.selectionImageView.heightAnchor.constraint(equalToConstant: 24).priority(.required - 1), - ]) + // input + private let authContext: AuthContext? + @MainActor private let pollOption: PollOptionObject - view.titleMetaLabel.translatesAutoresizingMaskIntoConstraints = false - view.containerView.addSubview(view.titleMetaLabel) - NSLayoutConstraint.activate([ - view.titleMetaLabel.leadingAnchor.constraint(equalTo: view.selectionImageView.trailingAnchor, constant: 4), - view.titleMetaLabel.centerYAnchor.constraint(equalTo: view.containerView.centerYAnchor), - ]) - view.titleMetaLabel.setContentHuggingPriority(.defaultLow - 10, for: .horizontal) + public let index: Int + public let content: MetaContent + public let isMulitpleChoice: Bool + public let isMyself: Bool - view.percentageMetaLabel.translatesAutoresizingMaskIntoConstraints = false - view.containerView.addSubview(view.percentageMetaLabel) - NSLayoutConstraint.activate([ - view.percentageMetaLabel.leadingAnchor.constraint(equalTo: view.titleMetaLabel.trailingAnchor, constant: 4), - view.containerView.trailingAnchor.constraint(equalTo: view.percentageMetaLabel.trailingAnchor, constant: 8), - view.percentageMetaLabel.centerYAnchor.constraint(equalTo: view.containerView.centerYAnchor), - ]) - view.percentageMetaLabel.setContentHuggingPriority(.required - 10, for: .horizontal) - view.percentageMetaLabel.setContentCompressionResistancePriority(.required - 2, for: .horizontal) + @Published public var isClosed = false + @Published public var totalVotes: Int = 0 + @Published public var votes: Int = 0 + @Published public var isOptionVoted = false + @Published public var isPollVoted = false + @Published public var isSelected: Bool = false - view.titleMetaLabel.isUserInteractionEnabled = false - view.percentageMetaLabel.isUserInteractionEnabled = false - } - - private func layoutEdit(view: PollOptionView) { - view.containerView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(view.containerView) - NSLayoutConstraint.activate([ - view.containerView.topAnchor.constraint(equalTo: view.topAnchor), - view.containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - view.containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - view.containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) + public var canSelect: Bool { + if isMyself { return false } + if isClosed { return false } + if case .twitter = pollOption { return false } + if isPollVoted || isOptionVoted { return false } + return true + } + public var isResultReveal: Bool { + return !canSelect + } - view.textField.translatesAutoresizingMaskIntoConstraints = false - view.containerView.addSubview(view.textField) - NSLayoutConstraint.activate([ - view.textField.topAnchor.constraint(equalTo: view.containerView.topAnchor), - view.textField.leadingAnchor.constraint(equalTo: view.containerView.leadingAnchor), - view.textField.trailingAnchor.constraint(equalTo: view.containerView.trailingAnchor), - view.textField.bottomAnchor.constraint(equalTo: view.containerView.bottomAnchor), - ]) + // output + private static let percentageFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.minimumFractionDigits = 0 + formatter.maximumFractionDigits = 2 + formatter.numberStyle = .percent + return formatter + }() + public var percentage: Double { + guard totalVotes > 0 else { return 0.0 } + return Double(votes) / Double(totalVotes) + } + public var percentageText: String { + let _text = Self.percentageFormatter.string(from: NSNumber(value: percentage)) ?? nil + return _text ?? "" + } - view.containerView.layer.masksToBounds = true - view.containerView.layer.cornerRadius = 6 - view.containerView.layer.cornerCurve = .continuous - view.containerView.layer.borderColor = UIColor.secondaryLabel.cgColor - view.containerView.layer.borderWidth = UIView.separatorLineHeight(of: view) - } - -} - -// MARK; - DeleteBackwardResponseTextFieldDelegate -extension PollOptionView: DeleteBackwardResponseTextFieldDelegate { - public func deleteBackwardResponseTextField(_ textField: DeleteBackwardResponseTextField, textBeforeDelete: String?) { - delegate?.pollOptionView(self, deleteBackwardResponseTextField: textField, textBeforeDelete: textBeforeDelete) - } -} - -#if DEBUG -import SwiftUI -struct PollOptionView_Preview: PreviewProvider { - static var previews: some View { - Group { - UIViewPreview(width: 400, height: 36) { - let pollOptionView = PollOptionView() - pollOptionView.setup(style: .edit) - return pollOptionView + public init( + authContext: AuthContext?, + pollOption: PollOptionObject, + isMyself: Bool + ) { + self.authContext = authContext + self.pollOption = pollOption + self.isMyself = isMyself + + assert(Thread.isMainThread) + switch pollOption { + case .twitter(let option): + index = Int(option.position) + content = PlaintextMetaContent(string: option.label) + isClosed = true // cannot vote for Twitter + isMulitpleChoice = false + isSelected = false + option.publisher(for: \.votes) + .map { Int($0) } + .assign(to: &$votes) + case .mastodon(let option): + index = Int(option.index) + content = { + do { + let content = MastodonContent(content: option.title, emojis: option.poll.status.emojis.asDictionary) + let metaContent = try MastodonMetaContent.convert(document: content) + return metaContent + } catch { + return PlaintextMetaContent(string: option.title) + } + }() + isMulitpleChoice = option.poll.multiple + option.poll.publisher(for: \.expired) + .assign(to: &$isClosed) + option.poll.publisher(for: \.votesCount) + .map { Int($0) } + .assign(to: &$totalVotes) + option.publisher(for: \.votesCount) + .map { Int($0) } + .assign(to: &$votes) + option.publisher(for: \.isSelected) + .assign(to: &$isSelected) } - .frame(width: 400, height: 36) - .padding(10) - .previewLayout(.sizeThatFits) - .previewDisplayName("Edit") - UIViewPreview(width: 400, height: 36) { - let pollOptionView = PollOptionView() - pollOptionView.setup(style: .plain) - return pollOptionView + + switch (authContext?.authenticationContext, pollOption) { + case (.twitter, .twitter): + break + case (.mastodon(let authenticationContext), .mastodon(let option)): + // bind isVoted + option.publisher(for: \.voteBy) + .map { voteBy in + voteBy.contains(where: { $0.id == authenticationContext.userID && $0.domain == authenticationContext.domain }) + } + .assign(to: &$isOptionVoted) + option.poll.publisher(for: \.voteBy) + .map { voteBy in + voteBy.contains(where: { $0.id == authenticationContext.userID && $0.domain == authenticationContext.domain }) + } + .assign(to: &$isPollVoted) + default: + break } - .frame(width: 400, height: 36) - .padding(10) - .previewLayout(.sizeThatFits) - .previewDisplayName("Plain") - } - } + } // end init + } // end class } -#endif + + +//public protocol PollOptionViewDelegate: AnyObject { +// func pollOptionView(_ pollOptionView: PollOptionView, deleteBackwardResponseTextField textField: DeleteBackwardResponseTextField, textBeforeDelete: String?) +//} +// +//public final class PollOptionView: UIView { +// +// static let height: CGFloat = 36 +// +// public weak var delegate: PollOptionViewDelegate? +// private(set) var style: Style? +// +// var disposeBag = Set() +// public private(set) lazy var viewModel: ViewModel = { +// let viewModel = ViewModel() +// viewModel.bind(view: self) +// return viewModel +// }() +// +// let containerView = UIView() +// +// let stripProgressView = StripProgressView() +// +// let selectionImageView: UIImageView = { +// let imageView = UIImageView() +// return imageView +// }() +// +// public let titleMetaLabel = MetaLabel(style: .pollOptionTitle) +// +// public let percentageMetaLabel = MetaLabel(style: .pollOptionPercentage) +// +// // TODO: MetaTextField? +// public let textField: DeleteBackwardResponseTextField = { +// let textField = DeleteBackwardResponseTextField() +// textField.font = .systemFont(ofSize: 16, weight: .regular) +// textField.textColor = .label +// textField.text = "Choice" +// textField.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight ? .left : .right +// return textField +// }() +// +// public func prepareForReuse() { +// viewModel.objects.removeAll() +// viewModel.percentage = nil +// stripProgressView.setProgress(0, animated: false) +// } +// +// public override init(frame: CGRect) { +// super.init(frame: frame) +// _init() +// } +// +// public required init?(coder: NSCoder) { +// super.init(coder: coder) +// _init() +// } +// +//} +// +//extension PollOptionView { +// +// private func _init() { +// textField.deleteBackwardDelegate = self +// +// // Accessibility +// // hint: Poll option +// accessibilityHint = L10n.Accessibility.Common.Status.pollOptionOrdinalPrefix +// } +// +// public override func layoutSubviews() { +// super.layoutSubviews() +// +// setupCorner() +// } +// +// func setupCorner() { +// switch viewModel.corner { +// case .none: +// containerView.layer.masksToBounds = false +// stripProgressView.cornerRadius = 0 +// case .radius(let radius): +// containerView.layer.masksToBounds = true +// guard radius < bounds.height / 2 else { +// fallthrough +// } +// containerView.layer.cornerCurve = .continuous +// containerView.layer.cornerRadius = radius +// stripProgressView.cornerRadius = radius +// case .circle: +// let radius = bounds.height / 2 +// containerView.layer.masksToBounds = true +// containerView.layer.cornerCurve = .circular +// containerView.layer.cornerRadius = radius +// stripProgressView.cornerRadius = radius +// } +// } +// +// public func setup(style: Style) { +// guard self.style == nil else { +// assertionFailure("Should only setup once") +// return +// } +// self.style = style +// self.viewModel.style = style +// style.layout(view: self) +// } +// +// public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { +// super.traitCollectionDidChange(previousTraitCollection) +// +// if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { +// textField.layer.borderColor = UIColor.secondaryLabel.cgColor +// } +// } +// +//} +// +//extension PollOptionView { +// public enum Style { +// case plain +// case edit +// +// func layout(view: PollOptionView) { +// switch self { +// case .plain: layoutPlain(view: view) +// case .edit: layoutEdit(view: view) +// } +// } +// } +//} +// +//extension PollOptionView.Style { +// private func layoutPlain(view: PollOptionView) { +// view.containerView.translatesAutoresizingMaskIntoConstraints = false +// view.addSubview(view.containerView) +// NSLayoutConstraint.activate([ +// view.containerView.topAnchor.constraint(equalTo: view.topAnchor), +// view.containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), +// view.containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), +// view.containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor), +// ]) +// view.containerView.backgroundColor = Asset.Colors.hightLight.color.withAlphaComponent(0.08) +// +// view.stripProgressView.translatesAutoresizingMaskIntoConstraints = false +// view.containerView.addSubview(view.stripProgressView) +// NSLayoutConstraint.activate([ +// view.stripProgressView.topAnchor.constraint(equalTo: view.containerView.topAnchor), +// view.stripProgressView.leadingAnchor.constraint(equalTo: view.containerView.leadingAnchor), +// view.stripProgressView.trailingAnchor.constraint(equalTo: view.containerView.trailingAnchor), +// view.stripProgressView.bottomAnchor.constraint(equalTo: view.containerView.bottomAnchor), +// ]) +// +// view.selectionImageView.translatesAutoresizingMaskIntoConstraints = false +// view.containerView.addSubview(view.selectionImageView) +// NSLayoutConstraint.activate([ +// view.selectionImageView.topAnchor.constraint(equalTo: view.containerView.topAnchor, constant: 6), +// view.selectionImageView.leadingAnchor.constraint(equalTo: view.containerView.leadingAnchor, constant: 6), +// view.containerView.bottomAnchor.constraint(equalTo: view.selectionImageView.bottomAnchor, constant: 6), +// view.selectionImageView.widthAnchor.constraint(equalToConstant: 24).priority(.required - 1), +// view.selectionImageView.heightAnchor.constraint(equalToConstant: 24).priority(.required - 1), +// ]) +// +// view.titleMetaLabel.translatesAutoresizingMaskIntoConstraints = false +// view.containerView.addSubview(view.titleMetaLabel) +// NSLayoutConstraint.activate([ +// view.titleMetaLabel.leadingAnchor.constraint(equalTo: view.selectionImageView.trailingAnchor, constant: 4), +// view.titleMetaLabel.centerYAnchor.constraint(equalTo: view.containerView.centerYAnchor), +// ]) +// view.titleMetaLabel.setContentHuggingPriority(.defaultLow - 10, for: .horizontal) +// +// view.percentageMetaLabel.translatesAutoresizingMaskIntoConstraints = false +// view.containerView.addSubview(view.percentageMetaLabel) +// NSLayoutConstraint.activate([ +// view.percentageMetaLabel.leadingAnchor.constraint(equalTo: view.titleMetaLabel.trailingAnchor, constant: 4), +// view.containerView.trailingAnchor.constraint(equalTo: view.percentageMetaLabel.trailingAnchor, constant: 8), +// view.percentageMetaLabel.centerYAnchor.constraint(equalTo: view.containerView.centerYAnchor), +// ]) +// view.percentageMetaLabel.setContentHuggingPriority(.required - 10, for: .horizontal) +// view.percentageMetaLabel.setContentCompressionResistancePriority(.required - 2, for: .horizontal) +// +// view.titleMetaLabel.isUserInteractionEnabled = false +// view.percentageMetaLabel.isUserInteractionEnabled = false +// } +// +// private func layoutEdit(view: PollOptionView) { +// view.containerView.translatesAutoresizingMaskIntoConstraints = false +// view.addSubview(view.containerView) +// NSLayoutConstraint.activate([ +// view.containerView.topAnchor.constraint(equalTo: view.topAnchor), +// view.containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), +// view.containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), +// view.containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor), +// ]) +// +// view.textField.translatesAutoresizingMaskIntoConstraints = false +// view.containerView.addSubview(view.textField) +// NSLayoutConstraint.activate([ +// view.textField.topAnchor.constraint(equalTo: view.containerView.topAnchor), +// view.textField.leadingAnchor.constraint(equalTo: view.containerView.leadingAnchor), +// view.textField.trailingAnchor.constraint(equalTo: view.containerView.trailingAnchor), +// view.textField.bottomAnchor.constraint(equalTo: view.containerView.bottomAnchor), +// ]) +// +// view.containerView.layer.masksToBounds = true +// view.containerView.layer.cornerRadius = 6 +// view.containerView.layer.cornerCurve = .continuous +// view.containerView.layer.borderColor = UIColor.secondaryLabel.cgColor +// view.containerView.layer.borderWidth = UIView.separatorLineHeight(of: view) +// } +// +//} +// +//// MARK; - DeleteBackwardResponseTextFieldDelegate +//extension PollOptionView: DeleteBackwardResponseTextFieldDelegate { +// public func deleteBackwardResponseTextField(_ textField: DeleteBackwardResponseTextField, textBeforeDelete: String?) { +// delegate?.pollOptionView(self, deleteBackwardResponseTextField: textField, textBeforeDelete: textBeforeDelete) +// } +//} +// +//#if DEBUG +//import SwiftUI +//struct PollOptionView_Preview: PreviewProvider { +// static var previews: some View { +// Group { +// UIViewPreview(width: 400, height: 36) { +// let pollOptionView = PollOptionView() +// pollOptionView.setup(style: .edit) +// return pollOptionView +// } +// .frame(width: 400, height: 36) +// .padding(10) +// .previewLayout(.sizeThatFits) +// .previewDisplayName("Edit") +// UIViewPreview(width: 400, height: 36) { +// let pollOptionView = PollOptionView() +// pollOptionView.setup(style: .plain) +// return pollOptionView +// } +// .frame(width: 400, height: 36) +// .padding(10) +// .previewLayout(.sizeThatFits) +// .previewDisplayName("Plain") +// } +// } +//} +//#endif diff --git a/TwidereSDK/Sources/TwidereUI/Content/PollView.swift b/TwidereSDK/Sources/TwidereUI/Content/PollView.swift new file mode 100644 index 00000000..0dec275c --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Content/PollView.swift @@ -0,0 +1,282 @@ +// +// PollView.swift +// +// +// Created by MainasuK on 2023/3/21. +// + +import os.log +import Foundation +import SwiftUI +import Combine +import MastodonMeta +import CoreDataStack + +public struct PollView: View { + + static let logger = Logger(subsystem: "PollView", category: "View") + var logger: Logger { PollView.logger } + + @ObservedObject public var viewModel: ViewModel + public let selectAction: (PollOptionView.ViewModel) -> Void + public let voteAction: (ViewModel) -> Void + + public var body: some View { + VStack(spacing: 12) { + ForEach(viewModel.options) { option in + PollOptionView(viewModel: option) { optionViewModel in + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(optionViewModel.index)") + selectAction(optionViewModel) + } +// VStack { +// HStack { +// Text(option.content) +// .font(.system(size: 16, weight: .regular)) +// .foregroundColor(Color(uiColor: Asset.Color.M3.Sys.onSurface.color)) +// Spacer() +// } +// HStack(alignment: .center) { +// GeometryReader { proxy in +// RoundedRectangle(cornerRadius: proxy.size.height / 2) +// .frame(width: proxy.size.width) +// .foregroundColor(Color(uiColor: Asset.Color.M3.Sys.surfaceVariant.color)) +// .overlay( +// HStack { +// let gradient = Gradient(stops: [ +// .init(color: Color(uiColor: UIColor(hex: 0xFF7575)), location: 0.0), +// .init(color: Color(uiColor: UIColor(hex: 0x8B54FF)), location: 1.0), +// ]) +// LinearGradient(gradient: gradient, startPoint: .leading, endPoint: .trailing) +// .foregroundColor(Color(uiColor: Asset.Color.Sys.primary.color)) +// .mask(alignment: .leading) { +// RoundedRectangle(cornerRadius: proxy.size.height / 2) +// .frame(width: proxy.size.width * (option.percentage ?? 0)) +// } +// } +// .frame(alignment: .leading) +// ) +// .clipShape( +// RoundedRectangle(cornerRadius: proxy.size.height / 2) +// ) +// } +// .frame(height: 12) +// Text("99.99%") // fixed width size +// .lineLimit(1) +// .font(.system(size: 14, weight: .regular)) +// .foregroundColor(.clear) +// .overlay( +// HStack(spacing: .zero) { +// Spacer() +// Text(option.percentageText) +// .lineLimit(1) +// .font(.system(size: 14, weight: .regular)) +// .foregroundColor(Color(uiColor: Asset.Color.secondary.color)) +// .fixedSize(horizontal: true, vertical: false) +// } +// ) +// } +// } + } // end ForEach + HStack { + Text(verbatim: viewModel.pollDescription) + .font(Font(TextStyle.pollVoteDescription.font)) + .lineLimit(TextStyle.pollVoteDescription.numberOfLines) + .foregroundColor(Color(uiColor: TextStyle.pollVoteDescription.textColor)) + Spacer() + if viewModel.isVoteButtonDisplay { + Button { + guard viewModel.isVoteButtonEnabled else { return } + guard !viewModel.isVoting else { return } + voteAction(viewModel) + } label: { + let textColor = viewModel.isVoteButtonEnabled ? TextStyle.pollVoteButton.textColor : .secondaryLabel + Text(L10n.Common.Controls.Status.Actions.vote) + .font(Font(TextStyle.pollVoteButton.font)) + .lineLimit(TextStyle.pollVoteButton.numberOfLines) + .foregroundColor(Color(uiColor: textColor)) + .opacity(viewModel.isVoting ? 0 : 1) + .overlay { + if viewModel.isVoting { + ProgressView() + .progressViewStyle(.circular) + .tint(.secondary) + } + } + } + .buttonStyle(.borderless) + } + } + } // end VStack + } + + var pollDescriptionLabelTintColor: Color { + let color = UIColor { traitCollection in + switch traitCollection.userInterfaceStyle { + // TODO: use new color palate + case .light: return UIColor(hex: 0xABABA9) + default: return UIColor(hex: 0x5A5E6C) + } + } + return Color(uiColor: color) + } + +} + +extension PollView { + public class ViewModel: ObservableObject { + + var disposeBag = Set() + + // input + private let authContext: AuthContext? + @MainActor private let poll: PollObject + + public let platform: Platform + public let endDate: Date? + public let isMyself: Bool + + @Published public var options: [PollOptionView.ViewModel] = [] + @Published public var isClosed = false + @Published public var isVoting = false + @Published public var isPollVoted = false + + // output + @Published public var votesCount = 0 + @Published public var isVoteButtonEnabled = true + public var isVoteButtonDisplay: Bool { + return !isMyself && !isClosed && !isPollVoted + } + + public var pollDescription: String { + var texts: [String] = [] + switch platform { + case .none: + return "" + case .twitter: + let peopleCount = votesCount + texts.append(L10n.Count.people(peopleCount)) + case .mastodon: + texts.append(L10n.Count.vote(votesCount)) + } + if isClosed { + texts.append(L10n.Common.Controls.Status.Poll.expired) + } else if let endDate = endDate { + let now = Date() + let timeInterval = endDate.timeIntervalSince(now) + if timeInterval > 0, let text = endDate.localizedTimeLeft { + texts.append(text) + } + } + return texts.joined(separator: " · ") + } + + @MainActor + public var needsUpdate: Bool { + return poll.needsUpdate + } + + public init( + authContext: AuthContext?, + poll: PollObject + ) { + self.authContext = authContext + self.poll = poll + let isMyself = { + switch authContext?.authenticationContext { + case .twitter(let authenticationContext): + guard case let .twitter(poll) = poll else { + assertionFailure() + return false + } + return authenticationContext.userID == poll.status.author.id + case .mastodon(let authenticationContext): + guard case let .mastodon(poll) = poll else { + assertionFailure() + return false + } + return authenticationContext.userID == poll.status.author.id && authenticationContext.domain == poll.status.author.domain + default: + return false + } + }() + self.isMyself = isMyself + + switch poll { + case .twitter(let poll): + platform = .twitter + options = poll.options + .sorted(by: { $0.position < $1.position }) + .map { + PollOptionView.ViewModel( + authContext: authContext, + pollOption: .twitter(object: $0), + isMyself: isMyself + ) + } + endDate = poll.endDatetime + isClosed = true // cannot vote for Twitter + case .mastodon(let poll): + platform = .mastodon + options = poll.options + .sorted(by: { $0.index < $1.index }) + .map { + PollOptionView.ViewModel( + authContext: authContext, + pollOption: .mastodon(object: $0), + isMyself: isMyself + ) + } + endDate = poll.expiresAt + poll.publisher(for: \.expired) + .assign(to: &$isClosed) + poll.publisher(for: \.isVoting) + .assign(to: &$isVoting) + if case let .mastodon(authenticationContext) = authContext?.authenticationContext { + poll.publisher(for: \.voteBy) + .map { voteBy in + voteBy.contains(where: { $0.id == authenticationContext.userID && $0.domain == authenticationContext.domain }) + } + .assign(to: &$isPollVoted) + } + } + + // collect votes into votesCount + Publishers.MergeMany(options.map { $0.$votes }) + .receive(on: DispatchQueue.main) + .compactMap { [weak self] _ in + guard let self = self else { return nil } + return self.options + .map { $0.votes } + .reduce(0, +) + } + .removeDuplicates() + .assign(to: \.votesCount, on: self) + .store(in: &disposeBag) + + // bind votesCount + $votesCount + .removeDuplicates() + .sink { [weak self] totalVotes in + guard let self = self else { return } + self.options.forEach { option in + option.totalVotes = totalVotes + } + } + .store(in: &disposeBag) + + // bind canVote + Publishers.MergeMany(options.map { $0.$isSelected }) + .receive(on: DispatchQueue.main) + .compactMap { [weak self] _ in + guard let self = self else { return nil } + return self.options + .map { $0.isSelected } + .contains(true) + } + .removeDuplicates() + .assign(to: \.isVoteButtonEnabled, on: self) + .store(in: &disposeBag) + } + } +} + diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index e54dd421..ff6d6126 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -59,14 +59,6 @@ extension StatusView { // return formatter // }() // -// var disposeBag = Set() -// var observations = Set() -// var objects = Set() - -// @Published public var platform: Platform = .none -// @Published public var authenticationContext: AuthenticationContext? // me -// @Published public var managedObjectContext: NSManagedObjectContext? -// // @Published public var header: Header = .none // // @Published public var userIdentifier: UserIdentifier? @@ -76,6 +68,7 @@ extension StatusView { // // @Published public var protected: Bool = false + // content @Published public var spoilerContent: MetaContent? @Published public var content: MetaContent = PlaintextMetaContent(string: "") @@ -86,6 +79,7 @@ extension StatusView { return isContentSensitive ? isContentSensitiveToggled : !isContentEmpty } + // language @Published public var language: String? @Published public private(set) var translateButtonPreference: UserDefaults.TranslateButtonPreference? public var isTranslateButtonDisplay: Bool { @@ -112,6 +106,7 @@ extension StatusView { } } + // media @Published public var mediaViewModels: [MediaView.ViewModel] = [] @Published public var isMediaSensitive: Bool = false @Published public var isMediaSensitiveToggled: Bool = false @@ -146,7 +141,11 @@ extension StatusView { // // @Published public var isRepost = false // @Published public var isRepostEnabled = true + + // poll + @Published public var pollViewModel: PollView.ViewModel? + // visibility @Published public var visibility: MastodonVisibility? var visibilityIconImage: UIImage? { switch visibility { @@ -170,6 +169,7 @@ extension StatusView { ////// // @Published public var groupedAccessibilityLabel = "" + // timestamp @Published public var timestampLabelViewModel: TimestampLabelView.ViewModel? // toolbar @@ -1095,6 +1095,14 @@ extension StatusView.ViewModel { // media mediaViewModels = MediaView.ViewModel.viewModels(from: status) + // poll + if let poll = status.poll { + self.pollViewModel = PollView.ViewModel( + authContext: authContext, + poll: .twitter(object: poll) + ) + } + // toolbar toolbarViewModel.platform = .twitter status.publisher(for: \.replyCount) @@ -1228,6 +1236,14 @@ extension StatusView.ViewModel { // media mediaViewModels = MediaView.ViewModel.viewModels(from: status) + // poll + if let poll = status.poll { + self.pollViewModel = PollView.ViewModel( + authContext: authContext, + poll: .mastodon(object: poll) + ) + } + // media content warning isMediaSensitive = status.isMediaSensitive isMediaSensitiveToggled = status.isMediaSensitiveToggled diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index 035144fe..12413817 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -35,9 +35,10 @@ public protocol StatusViewDelegate: AnyObject { func statusView(_ viewModel: StatusView.ViewModel, toggleContentWarningOverlayDisplay isReveal: Bool) // func statusView(_ statusView: StatusView, quoteStatusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int) // -// func statusView(_ statusView: StatusView, pollTableView tableView: UITableView, didSelectRowAt indexPath: IndexPath) -// func statusView(_ statusView: StatusView, pollVoteButtonDidPressed button: UIButton) -// + func statusView(_ viewModel: StatusView.ViewModel, pollVoteActionForViewModel pollViewModel: PollView.ViewModel) + func statusView(_ viewModel: StatusView.ViewModel, pollUpdateIfNeedsForViewModel pollViewModel: PollView.ViewModel) + func statusView(_ viewModel: StatusView.ViewModel, pollViewModel: PollView.ViewModel, pollOptionDidSelectForViewModel optionViewModel: PollOptionView.ViewModel) + // func statusView(_ statusView: StatusView, quoteStatusViewDidPressed quoteStatusView: StatusView) // func statusView(_ viewModel: StatusView.ViewModel, statusToolbarViewModel: StatusToolbarView.ViewModel, statusToolbarButtonDidPressed action: StatusToolbarView.Action) @@ -139,6 +140,21 @@ public struct StatusView: View { } .padding(.horizontal, viewModel.margin) } + // poll + if let pollViewModel = viewModel.pollViewModel { + PollView( + viewModel: pollViewModel, + selectAction: { optionViewModel in + viewModel.delegate?.statusView(viewModel, pollViewModel: pollViewModel, pollOptionDidSelectForViewModel: optionViewModel) + }, voteAction: { pollViewModel in + viewModel.delegate?.statusView(viewModel, pollVoteActionForViewModel: pollViewModel) + } + ) + .padding(.horizontal, viewModel.margin) + .onAppear { + viewModel.delegate?.statusView(viewModel, pollUpdateIfNeedsForViewModel: pollViewModel) + } + } // quote if let quoteViewModel = viewModel.quoteViewModel { StatusView(viewModel: quoteViewModel) @@ -219,7 +235,7 @@ extension StatusView { AnyLayout(VStackLayout(alignment: .leading, spacing: .zero)) infoLayout { let nameLayout = dynamicTypeSize < .accessibility1 ? - AnyLayout(HStackLayout(alignment: .firstTextBaseline, spacing: 6)) : + AnyLayout(HStackLayout(alignment: .bottom, spacing: 6)) : AnyLayout(VStackLayout(alignment: .leading, spacing: .zero)) nameLayout { // name @@ -231,6 +247,8 @@ extension StatusView { label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) } ) + .fixedSize(horizontal: false, vertical: true) + .layoutPriority(0.618) // username LabelRepresentable( metaContent: PlaintextMetaContent(string: "@" + viewModel.authorUsernme), @@ -240,6 +258,8 @@ extension StatusView { label.setContentCompressionResistancePriority(.defaultLow - 100, for: .horizontal) } ) + .fixedSize(horizontal: false, vertical: true) + .layoutPriority(0.382) } .frame(alignment: .leading) Spacer() @@ -278,7 +298,13 @@ extension StatusView { .onPreferenceChange(ViewHeightKey.self) { height in self.viewModel.authorAvatarDimension = height } - } + // add spacer to make the infoLayout leading align + if dynamicTypeSize < .accessibility1 { + // do nothing + } else { + Spacer() + } + } // end HStack } public var avatarButton: some View { diff --git a/TwidereSDK/Sources/TwidereUI/Extension/UITableView.swift b/TwidereSDK/Sources/TwidereUI/Extension/UITableView.swift index 411692be..fd1a4136 100644 --- a/TwidereSDK/Sources/TwidereUI/Extension/UITableView.swift +++ b/TwidereSDK/Sources/TwidereUI/Extension/UITableView.swift @@ -19,6 +19,7 @@ extension UITableView { extension UITableView { + // TODO: tweak cell selection background color public func deselectRow(with transitionCoordinator: UIViewControllerTransitionCoordinator?, animated: Bool) { guard let indexPathForSelectedRow = indexPathForSelectedRow else { return } diff --git a/TwidereSDK/Sources/TwidereUI/Shape/CheckmarkView.swift b/TwidereSDK/Sources/TwidereUI/Shape/CheckmarkView.swift new file mode 100644 index 00000000..341dfe68 --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Shape/CheckmarkView.swift @@ -0,0 +1,82 @@ +// +// CheckmarkView.swift +// +// +// Created by MainasuK on 2023/3/21. +// + +import Foundation +import SwiftUI + +public struct CheckmarkView: View { + + public let tintColor: UIColor + public let borderWidth: CGFloat + public let cornerRadius: CGFloat + public let check: Bool + + + public var body: some View { + ZStack { + Color(uiColor: tintColor) + if check { + CheckmarkShape() + .blendMode(.destinationOut) + } else { + Color(uiColor: tintColor) + .cornerRadius(cornerRadius - borderWidth) + .padding(borderWidth) + .blendMode(.destinationOut) + } + } + .compositingGroup() + .cornerRadius(cornerRadius) + } +} + +struct CheckmarkShape: Shape { + + func path(in rect: CGRect) -> Path { + let width = rect.width + let height = rect.height + let radius = width / 20 + + var path = Path() + let cos45 = cos(45.0 * CGFloat.pi / 180.0) + let sin45 = cos(45.0 * CGFloat.pi / 180.0) + let root2 = 2.squareRoot() + path.addArc(center: CGPoint(x: 9/20 * width, y: 12/20 * height), radius: radius, startAngle: Angle(degrees: 45), endAngle: Angle(degrees: 135), clockwise: false) + path.addLine(to: CGPoint(x: 7/20 * width - cos45 * radius, y: 10/20 * height + sin45 * radius)) + path.addArc(center: CGPoint(x: 7/20 * width, y: 10/20 * height), radius: radius, startAngle: Angle(degrees: 135), endAngle: Angle(degrees: 315), clockwise: false) + path.addLine(to: CGPoint(x: 9/20 * width, y: 12/20 * height - root2 * radius)) + path.addArc(center: CGPoint(x: 13/20 * width, y: 8/20 * height), radius: radius, startAngle: Angle(degrees: 225), endAngle: Angle(degrees: 405), clockwise: false) + path.closeSubpath() + return path + } +} + + +#if DEBUG +import SwiftUI +struct CheckmarkView_Preview: PreviewProvider { + + static var width: CGFloat = 100 + + static var previews: some View { + CheckmarkView( + tintColor: .systemBlue, + borderWidth: width / 18, + cornerRadius: width / 4, + check: true + ) + .previewLayout(.fixed(width: width, height: width)) + CheckmarkView( + tintColor: .systemBlue, + borderWidth: width / 18, + cornerRadius: width / 4, + check: false + ) + .previewLayout(.fixed(width: width, height: width)) + } +} +#endif diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/Poll/PollOptionTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/Poll/PollOptionTableViewCell.swift deleted file mode 100644 index 9e4078d9..00000000 --- a/TwidereSDK/Sources/TwidereUI/TableViewCell/Poll/PollOptionTableViewCell.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// PollOptionTableViewCell.swift -// -// -// Created by MainasuK on 2021-12-8. -// - -import UIKit - -public final class PollOptionTableViewCell: UITableViewCell { - - public static let margin: CGFloat = 4 - public static let height: CGFloat = 2 * margin + PollOptionView.height - - public let optionView = PollOptionView() - - public override func prepareForReuse() { - super.prepareForReuse() - - optionView.disposeBag.removeAll() - optionView.prepareForReuse() - } - - public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - _init() - } - - public required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - - public override func setHighlighted(_ highlighted: Bool, animated: Bool) { - super.setHighlighted(highlighted, animated: animated) - - optionView.alpha = highlighted ? 0.5 : 1 - } - -} - -extension PollOptionTableViewCell { - - private func _init() { - selectionStyle = .none - backgroundColor = .clear - - optionView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(optionView) - NSLayoutConstraint.activate([ - optionView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: PollOptionTableViewCell.margin), - optionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - optionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - contentView.bottomAnchor.constraint(equalTo: optionView.bottomAnchor, constant: PollOptionTableViewCell.margin), - optionView.heightAnchor.constraint(equalToConstant: PollOptionView.height).priority(.required - 1), - ]) - optionView.setup(style: .plain) - - // accessibility - accessibilityElements = [optionView] - optionView.isAccessibilityElement = true - - } - -} diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TouchBlockingViewRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TouchBlockingViewRepresentable.swift new file mode 100644 index 00000000..a7086686 --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TouchBlockingViewRepresentable.swift @@ -0,0 +1,21 @@ +// +// TouchBlockingViewRepresentable.swift +// +// +// Created by MainasuK on 2023/3/22. +// + +import SwiftUI + +public struct TouchBlockingViewRepresentable: UIViewRepresentable { + + public func makeUIView(context: Context) -> TouchBlockingView { + let view = TouchBlockingView() + return view + } + + public func updateUIView(_ view: TouchBlockingView, context: Context) { + // do nothing + } + +} diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Banner.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Banner.swift index f8c9bf05..2d315b66 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Banner.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Banner.swift @@ -45,6 +45,22 @@ extension DataSourceFacade { } extension DataSourceFacade { + + @MainActor + public static func presentErrorBanner( + error: LocalizedError + ) { + var config = SwiftMessages.defaultConfig + config.duration = .seconds(seconds: 3) + config.interactiveHide = true + let bannerView = NotificationBannerView() + bannerView.configure(style: .warning) + bannerView.titleLabel.text = error.errorDescription ?? "Unknown Error" + let message = [error.failureReason, error.recoverySuggestion].compactMap { $0 }.joined(separator: "\n") + bannerView.messageLabel.text = message + bannerView.messageLabel.isHidden = message.isEmpty + SwiftMessages.show(config: config, view: bannerView) + } @MainActor public static func presentForbiddenBanner( diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Poll.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Poll.swift index ef5e6333..d1dd5d13 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Poll.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Poll.swift @@ -10,12 +10,42 @@ import UIKit import TwidereCore import CoreDataStack +extension DataSourceFacade { + public static func responseToStatusPollUpdate( + provider: DataSourceProvider & AuthContextProvider, + status: StatusRecord + ) async throws { + let managedObjectContext = provider.context.managedObjectContext + switch (status, provider.authContext.authenticationContext) { + case (.twitter(let status), .twitter(let authenticationContext)): + let _statusID = await managedObjectContext.perform { + return status.object(in: managedObjectContext)?.id + } + guard let statusID = _statusID else { + assertionFailure() + return + } + _ = try await provider.context.apiService.twitterStatus( + statusIDs: [statusID], + authenticationContext: authenticationContext + ) + case (.mastodon(let status), .mastodon(let authenticationContext)): + _ = try await provider.context.apiService.viewMastodonStatusPoll( + status: status, + authenticationContext: authenticationContext + ) + default: + assertionFailure() + } + } +} + extension DataSourceFacade { public static func responseToStatusPollOption( provider: DataSourceProvider, target: StatusTarget, status: StatusRecord, - didSelectRowAt indexPath: IndexPath + didSelectRowAt index: Int ) async { let _redirectRecord = await DataSourceFacade.status( managedObjectContext: provider.context.managedObjectContext, @@ -27,16 +57,16 @@ extension DataSourceFacade { await responseToStatusPollOption( provider: provider, status: redirectRecord, - didSelectRowAt: indexPath + didSelectRowAt: index ) } static func responseToStatusPollOption( provider: DataSourceProvider, status: StatusRecord, - didSelectRowAt indexPath: IndexPath + didSelectRowAt index: Int ) async { - // should use same context on UI to make transient property trigger update + // use same context on UI to make transient property trigger update let managedObjectContext = provider.context.managedObjectContext do { @@ -55,8 +85,7 @@ extension DataSourceFacade { guard !poll.isVoting else { return } - - guard let option = poll.options.first(where: { $0.index == indexPath.row }) else { + guard let option = poll.options.first(where: { $0.index == index }) else { assertionFailure() return } @@ -81,51 +110,24 @@ extension DataSourceFacade { extension DataSourceFacade { - public static func responseToStatusPollOption( - provider: DataSourceProvider & AuthContextProvider, - target: StatusTarget, - status: StatusRecord, - voteButtonDidPressed button: UIButton - ) async { - let _redirectRecord = await DataSourceFacade.status( - managedObjectContext: provider.context.managedObjectContext, - status: status, - target: target - ) - guard let redirectRecord = _redirectRecord else { return } - - await responseToStatusPollOption( - provider: provider, - status: redirectRecord, - voteButtonDidPressed: button - ) - } - - static func responseToStatusPollOption( + static func responseToStatusPollVote( provider: DataSourceProvider & AuthContextProvider, - status: StatusRecord, - voteButtonDidPressed button: UIButton - ) async { - do { - switch status { - case .twitter: - assertionFailure() - case .mastodon(let record): - try await responseToStatusPollOption( - provider: provider, - status: record, - voteButtonDidPressed: button - ) - } - } catch { - // TODO: handle error + status: StatusRecord + ) async throws { + switch status { + case .twitter: + assertionFailure() + case .mastodon(let record): + try await responseToStatusPollVote( + provider: provider, + status: record + ) } } - private static func responseToStatusPollOption( + private static func responseToStatusPollVote( provider: DataSourceProvider & AuthContextProvider, - status: ManagedObjectRecord, - voteButtonDidPressed button: UIButton + status: ManagedObjectRecord ) async throws { guard case let .mastodon(authenticationContext) = provider.authContext.authenticationContext else { return } @@ -153,7 +155,7 @@ extension DataSourceFacade { return choices.map { Int($0) } } - await Task.sleep(1_000_000_000) // 1s + try? await Task.sleep(nanoseconds: 1 * .second) // 1s let response = try await provider.context.apiService.voteMastodonStatusPoll( status: status, choices: choices, @@ -162,7 +164,6 @@ extension DataSourceFacade { provider.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): did vote poll: \(response.value.id) with choices: \(choices.debugDescription)") } catch { - assertionFailure(error.localizedDescription) _error = error } @@ -180,6 +181,10 @@ extension DataSourceFacade { _error = error } + if let error = _error as? LocalizedError { + await DataSourceFacade.presentErrorBanner(error: error) + } + if let error = _error { throw error } diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift index 1fc0669c..d14c4c2d 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift @@ -261,6 +261,7 @@ extension DataSourceFacade { provider: DataSourceProvider, status: StatusRecord ) async throws { + // use same context on UI to make transient property trigger update let managedObjectContext = provider.context.managedObjectContext try await managedObjectContext.performChanges { guard let object = status.object(in: managedObjectContext) else { return } diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift index 85977dcb..e29bd04d 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift @@ -276,6 +276,68 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC // MARK: - poll extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { + func tableViewCell( + _ cell: UITableViewCell, + viewModel: StatusView.ViewModel, + pollVoteActionForViewModel pollViewModel: PollView.ViewModel + ) { + Task { + guard let status = viewModel.status else { + assertionFailure() + return + } + + try await DataSourceFacade.responseToStatusPollVote( + provider: self, + status: status + ) + } // end Task + } + + func tableViewCell( + _ cell: UITableViewCell, + viewModel: StatusView.ViewModel, + pollUpdateIfNeedsForViewModel pollViewModel: PollView.ViewModel + ) { + Task { + guard await pollViewModel.needsUpdate else { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Poll] not needs update. skip") + return + } + guard let status = viewModel.status else { + assertionFailure() + return + } + + try await DataSourceFacade.responseToStatusPollUpdate( + provider: self, + status: status + ) + } // end Task + } + + + func tableViewCell( + _ cell: UITableViewCell, + viewModel: StatusView.ViewModel, + pollViewModel: PollView.ViewModel, + pollOptionDidSelectForViewModel optionViewModel: PollOptionView.ViewModel + ) { + Task { + guard let status = viewModel.status else { + assertionFailure() + return + } + + await DataSourceFacade.responseToStatusPollOption( + provider: self, + target: .status, + status: status, + didSelectRowAt: optionViewModel.index + ) + } // end Task + } + // func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, pollTableView tableView: UITableView, didSelectRowAt indexPath: IndexPath) { // Task { // let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift index 1b79e797..aea5ba17 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -13,6 +13,11 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid func aspectTableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): indexPath: \(indexPath.debugDescription)") + + // TODO: tweak cell selection background color + // let cell = tableView.cellForRow(at: indexPath) + // cell?.backgroundColor = .red + Task { let source = DataSourceItem.Source(tableViewCell: nil, indexPath: indexPath) guard let item = await item(from: source) else { diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift index 97cb5397..e8db74bf 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift @@ -44,6 +44,8 @@ class StatusTableViewCell: UITableViewCell { extension StatusTableViewCell { private func _init() { + selectionStyle = .none + // statusView.translatesAutoresizingMaskIntoConstraints = false // contentView.addSubview(statusView) // NSLayoutConstraint.activate([ diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift index f6a37227..f8af8733 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift @@ -29,6 +29,9 @@ protocol StatusViewTableViewCellDelegate: AutoGenerateProtocolDelegate { func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel, previewActionContext: ContextMenuInteractionPreviewActionContext) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, toggleContentWarningOverlayDisplay isReveal: Bool) + func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, pollVoteActionForViewModel pollViewModel: PollView.ViewModel) + func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, pollUpdateIfNeedsForViewModel pollViewModel: PollView.ViewModel) + func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, pollViewModel: PollView.ViewModel, pollOptionDidSelectForViewModel optionViewModel: PollOptionView.ViewModel) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, statusToolbarViewModel: StatusToolbarView.ViewModel, statusToolbarButtonDidPressed action: StatusToolbarView.Action) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, viewHeightDidChange: Void) // sourcery:end @@ -54,6 +57,18 @@ extension StatusViewDelegate where Self: StatusViewContainerTableViewCell { delegate?.tableViewCell(self, viewModel: viewModel, toggleContentWarningOverlayDisplay: isReveal) } + func statusView(_ viewModel: StatusView.ViewModel, pollVoteActionForViewModel pollViewModel: PollView.ViewModel) { + delegate?.tableViewCell(self, viewModel: viewModel, pollVoteActionForViewModel: pollViewModel) + } + + func statusView(_ viewModel: StatusView.ViewModel, pollUpdateIfNeedsForViewModel pollViewModel: PollView.ViewModel) { + delegate?.tableViewCell(self, viewModel: viewModel, pollUpdateIfNeedsForViewModel: pollViewModel) + } + + func statusView(_ viewModel: StatusView.ViewModel, pollViewModel: PollView.ViewModel, pollOptionDidSelectForViewModel optionViewModel: PollOptionView.ViewModel) { + delegate?.tableViewCell(self, viewModel: viewModel, pollViewModel: pollViewModel, pollOptionDidSelectForViewModel: optionViewModel) + } + func statusView(_ viewModel: StatusView.ViewModel, statusToolbarViewModel: StatusToolbarView.ViewModel, statusToolbarButtonDidPressed action: StatusToolbarView.Action) { delegate?.tableViewCell(self, viewModel: viewModel, statusToolbarViewModel: statusToolbarViewModel, statusToolbarButtonDidPressed: action) } diff --git a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift index 4092ab97..6a880095 100644 --- a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift @@ -28,7 +28,7 @@ class ListTimelineViewController: TimelineViewController { tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .none tableView.selfSizingInvalidation = .enabled - tableView.cellLayoutMarginsFollowReadableWidth = true + tableView.cellLayoutMarginsFollowReadableWidth = true return tableView }() From d2aa554a48dd12e2260f4fab8ea53c69f945a01f Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 23 Mar 2023 21:03:04 +0800 Subject: [PATCH 050/128] fix: custom emoji attachment not display for user nickname issue --- .../LabelRepresentable.swift | 64 +++++-------------- 1 file changed, 17 insertions(+), 47 deletions(-) diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/LabelRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/LabelRepresentable.swift index 9c37e9e7..f135680c 100644 --- a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/LabelRepresentable.swift +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/LabelRepresentable.swift @@ -9,72 +9,42 @@ import UIKit import SwiftUI import TwidereCore import MetaTextKit +import MetaLabel public struct LabelRepresentable: UIViewRepresentable { - let label: UILabel = { - let label = UILabel() - label.numberOfLines = 1 - label.backgroundColor = .clear - label.adjustsFontSizeToFitWidth = false - label.allowsDefaultTighteningForTruncation = false - label.lineBreakMode = .byTruncatingTail - label.setContentHuggingPriority(.defaultHigh, for: .horizontal) // always try grow vertical - label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - return label - }() + let label: MetaLabel // input - public let metaContent: MetaContent - public let textStyle: TextStyle - let setupLabel: (UILabel) -> Void + let metaContent: MetaContent + let textStyle: TextStyle + let setupLabel: (MetaLabel) -> Void public init( metaContent: MetaContent, textStyle: TextStyle, - setupLabel: @escaping (UILabel) -> Void + setupLabel: @escaping (MetaLabel) -> Void ) { self.metaContent = metaContent self.textStyle = textStyle self.setupLabel = setupLabel + self.label = { + let label = MetaLabel(style: textStyle) + label.textArea.textContainer.lineBreakMode = .byTruncatingTail + label.setContentHuggingPriority(.defaultHigh, for: .horizontal) // always try grow vertical + label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + return label + }() } - public func makeUIView(context: Context) -> UILabel { - let label = self.label + public func makeUIView(context: Context) -> UIView { setupLabel(label) - - let attributedString = NSMutableAttributedString(string: metaContent.string) - let textAttributes: [NSAttributedString.Key: Any] = [ - .font: textStyle.font, - .foregroundColor: textStyle.textColor, - ] - let linkAttributes: [NSAttributedString.Key: Any] = [ - .font: textStyle.font, - .foregroundColor: UIColor.tintColor, - ] - let paragraphStyle: NSMutableParagraphStyle = { - let style = NSMutableParagraphStyle() - let fontMargin = textStyle.font.lineHeight - textStyle.font.pointSize - style.lineSpacing = 3 - fontMargin - style.paragraphSpacing = 8 - fontMargin - return style - }() - - MetaText.setAttributes( - for: attributedString, - textAttributes: textAttributes, - linkAttributes: linkAttributes, - paragraphStyle: paragraphStyle, - content: metaContent - ) - - label.attributedText = attributedString - + label.configure(content: metaContent) return label } - public func updateUIView(_ view: UILabel, context: Context) { - + public func updateUIView(_ view: UIView, context: Context) { + // do nothing } public func makeCoordinator() -> Coordinator { From 0982d3d723924e2c746c7b8736158096e2cb0c47 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 27 Mar 2023 20:15:28 +0800 Subject: [PATCH 051/128] feat: add metric view for conversation root post --- .../TwidereUI/Content/StatusHeaderView.swift | 2 +- .../TwidereUI/Content/StatusMetricView.swift | 209 +++++++ .../TwidereUI/Content/StatusToolbarView.swift | 67 +- .../Content/StatusView+ViewModel.swift | 114 +++- .../TwidereUI/Content/StatusView.swift | 154 +++-- .../ComposeContent/ComposeContentView.swift | 2 +- TwidereX/Coordinator/SceneCoordinator.swift | 2 +- TwidereX/Diffable/Status/StatusItem.swift | 48 -- TwidereX/Diffable/Status/StatusSection.swift | 14 - .../DataSourceFacade+StatusThread.swift | 66 +- ...der+MediaInfoDescriptionViewDelegate.swift | 6 +- ...ider+StatusViewTableViewCellDelegate.swift | 17 + ...taSourceProvider+UITableViewDelegate.swift | 8 +- .../StatusViewTableViewCellDelegate.swift | 5 + .../MastodonStatusThreadViewModel.swift | 487 +++++++------- ...eadViewController+DataSourceProvider.swift | 14 +- .../StatusThreadViewController.swift | 79 ++- .../StatusThreadViewModel+Diffable.swift | 535 ++++++++-------- ...tatusThreadViewModel+LoadThreadState.swift | 592 +++++++++--------- .../StatusThread/StatusThreadViewModel.swift | 295 +++++---- .../TwitterStatusThreadLeafViewModel.swift | 134 ++-- .../TwitterStatusThreadReplyViewModel.swift | 76 +-- ...meTimelineViewController+DebugAction.swift | 28 +- 23 files changed, 1672 insertions(+), 1282 deletions(-) create mode 100644 TwidereSDK/Sources/TwidereUI/Content/StatusMetricView.swift diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift index 7f07acf4..7cc14349 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift @@ -31,7 +31,7 @@ public struct StatusHeaderView: View { - iconImageDimension - StatusHeaderView.iconImageTrailingSpacing Color.clear - .frame(width: max(.zero, width)) + .frame(width: max(.leastNonzeroMagnitude, width)) } Button { diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusMetricView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusMetricView.swift new file mode 100644 index 00000000..22686e0f --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusMetricView.swift @@ -0,0 +1,209 @@ +// +// StatusMetricView.swift +// +// +// Created by MainasuK on 2023/3/27. +// + +import os.log +import SwiftUI +import CoreDataStack + +public struct StatusMetricView: View { + + static let logger = Logger(subsystem: "StatusMetricView", category: "View") + var logger: Logger { StatusView.logger } + + @ObservedObject public var viewModel: ViewModel + public let handler: (Action) -> Void + + public var body: some View { + VStack { + HStack { + Text(viewModel.timestampText) + .font(Font(TextStyle.statusMetrics.font)) + .foregroundColor(Color(uiColor: TextStyle.statusMetrics.textColor)) + } + HStack(spacing: 16) { + Spacer() + replyButton + repostButton + switch viewModel.platform { + case .twitter: + quoteButton + case .mastodon: + EmptyView() + case .none: + EmptyView() + } + likeButton + Spacer() + } + } + } +} + +extension StatusMetricView { + public var replyButton: some View { + ToolbarButton( + handler: { action in + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): reply") + handler(action) + }, + action: .reply, + image: Asset.Arrows.arrowTurnUpLeftMini.image.withRenderingMode(.alwaysTemplate), + count: viewModel.replyCount, + tintColor: nil + ) + } + + public var repostButton: some View { + ToolbarButton( + handler: { action in + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): repost") + handler(action) + }, + action: .repost, + image: Asset.Media.repeatMini.image.withRenderingMode(.alwaysTemplate), + count: viewModel.repostCount, + tintColor: nil + ) + } + + public var quoteButton: some View { + ToolbarButton( + handler: { action in + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): quote") + handler(action) + }, + action: .quote, + image: Asset.TextFormatting.textQuoteMini.image.withRenderingMode(.alwaysTemplate), + count: viewModel.quoteCount, + tintColor: nil + ) + } + + public var likeButton: some View { + ToolbarButton( + handler: { action in + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): like") + handler(action) + }, + action: .like, + image: Asset.Health.heartMini.image.withRenderingMode(.alwaysTemplate), + count: viewModel.likeCount, + tintColor: nil + ) + } +} + +extension StatusMetricView { + public enum Action: Hashable, CaseIterable { + case reply + case repost + case quote + case like + + public var text: String { + switch self { + case .reply: return L10n.Common.Controls.Status.Actions.reply + case .repost: return L10n.Common.Controls.Status.Actions.repost + case .quote: return L10n.Common.Controls.Status.Actions.quote + case .like: return L10n.Common.Controls.Status.Actions.like + } + } + + public var icon: UIImage { + switch self { + case .reply: return Asset.Arrows.arrowTurnUpLeft.image + case .repost: return Asset.Media.repeat.image + case .quote: return Asset.TextFormatting.textQuote.image + case .like: return Asset.Health.heartFill.image + } + } + } +} + +extension StatusMetricView { + public struct ToolbarButton: View { + let handler: (Action) -> Void + let action: Action + let image: UIImage + let count: Int? + let tintColor: UIColor? + + // output + var text: String { + Self.metric(count: count) + } + + public init( + handler: @escaping (Action) -> Void, + action: Action, + image: UIImage, + count: Int?, + tintColor: UIColor? + ) { + self.handler = handler + self.action = action + self.image = image + self.count = count + self.tintColor = tintColor + } + + public var body: some View { + Button { + handler(action) + } label: { + HStack { + Image(uiImage: image) + Text(text) + .font(Font(TextStyle.statusMetrics.font)) + .lineLimit(1) + } + } + .buttonStyle(.borderless) + .tint(Color(uiColor: tintColor ?? .secondaryLabel)) + .foregroundColor(Color(uiColor: tintColor ?? .secondaryLabel)) + } + + static func metric(count: Int?) -> String { + guard let count = count, count > 0 else { + return "0" + } + return "\(count)" + } + } +} + +extension StatusMetricView { + public class ViewModel: ObservableObject { + // input + public let platform: Platform + public let timestamp: Date + + @Published public var source: String? + @Published public var replyCount: Int = 0 + @Published public var repostCount: Int = 0 + @Published public var quoteCount: Int = 0 + @Published public var likeCount: Int = 0 + + // output + public let timestampText: String + + public init( + platform: Platform, + timestamp: Date + ) { + self.platform = platform + self.timestamp = timestamp + self.timestampText = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .medium + let text = formatter.string(from: timestamp) + return text + }() + } + } +} diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift index 614fa354..1ce15a22 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift @@ -41,6 +41,22 @@ public struct StatusToolbarView: View { } +extension StatusToolbarView { + var isMetricCountDisplay: Bool { + switch viewModel.style { + case .inline: return true + case .plain: return false + } + } + + var isExtraSpacerDisplay: Bool { + switch viewModel.style { + case .inline: return true + case .plain: return false + } + } +} + extension StatusToolbarView { public var replyButton: some View { ToolbarButton( @@ -49,8 +65,15 @@ extension StatusToolbarView { handler(action) }, action: .reply, - image: Asset.Arrows.arrowTurnUpLeftMini.image.withRenderingMode(.alwaysTemplate), - count: viewModel.replyCount, + image: { + switch viewModel.style { + case .inline: + return Asset.Arrows.arrowTurnUpLeftMini.image.withRenderingMode(.alwaysTemplate) + case .plain: + return Asset.Arrows.arrowTurnUpLeft.image.withRenderingMode(.alwaysTemplate) + } + }(), + count: isMetricCountDisplay ? viewModel.replyCount : nil, tintColor: nil ) } @@ -62,8 +85,15 @@ extension StatusToolbarView { handler(action) }, action: .repost, - image: Asset.Media.repeatMini.image.withRenderingMode(.alwaysTemplate), - count: viewModel.repostCount, + image: { + switch viewModel.style { + case .inline: + return Asset.Media.repeatMini.image.withRenderingMode(.alwaysTemplate) + case .plain: + return Asset.Media.repeat.image.withRenderingMode(.alwaysTemplate) + } + }(), + count: isMetricCountDisplay ? viewModel.repostCount : nil, tintColor: viewModel.isReposted ? Asset.Scene.Status.Toolbar.repost.color : nil ) } @@ -106,8 +136,16 @@ extension StatusToolbarView { handler(action) }, action: .like, - image: viewModel.isLiked ? Asset.Health.heartFillMini.image.withRenderingMode(.alwaysTemplate) : Asset.Health.heartMini.image.withRenderingMode(.alwaysTemplate), - count: viewModel.likeCount, + image: { + switch viewModel.style { + case .inline: + return viewModel.isLiked ? Asset.Health.heartFillMini.image.withRenderingMode(.alwaysTemplate) : Asset.Health.heartMini.image.withRenderingMode(.alwaysTemplate) + case .plain: + return viewModel.isLiked ? Asset.Health.heartFill.image.withRenderingMode(.alwaysTemplate) : Asset.Health.heart.image.withRenderingMode(.alwaysTemplate) + } + + }(), + count: isMetricCountDisplay ? viewModel.likeCount : nil, tintColor: viewModel.isLiked ? Asset.Scene.Status.Toolbar.like.color : nil ) } @@ -129,12 +167,12 @@ extension StatusToolbarView { } label: { HStack { let image: UIImage = { -// switch viewModel.kind { -// case .conversationRoot: - return Asset.Editing.ellipsisMini.image.withRenderingMode(.alwaysTemplate) -// default: -// return Asset.Editing.ellipsisMini.image.withRenderingMode(.alwaysTemplate) -// } + switch viewModel.style { + case .inline: + return Asset.Editing.ellipsisMini.image.withRenderingMode(.alwaysTemplate) + case .plain: + return Asset.Editing.ellipsis.image.withRenderingMode(.alwaysTemplate) + } }() Image(uiImage: image) .foregroundColor(.secondary) @@ -151,6 +189,7 @@ extension StatusToolbarView { // input @Published var platform: Platform = .none + @Published var style: Style = .inline @Published var replyCount: Int? @Published var repostCount: Int? @Published var likeCount: Int? @@ -165,6 +204,10 @@ extension StatusToolbarView { } extension StatusToolbarView { + public enum Style: Hashable { + case inline + case plain + } public enum Action: Hashable, CaseIterable { case reply case repost diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index ff6d6126..4c81e1e0 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -180,6 +180,9 @@ extension StatusView { return authContext.authenticationContext.userIdentifier == authorUserIdentifier } + // metric + @Published public var metricViewModel: StatusMetricView.ViewModel? + @Published public var isBottomConversationLinkLineViewDisplay = false private init( @@ -268,6 +271,8 @@ extension StatusView { // } // .assign(to: &$isRepostEnabled) + toolbarViewModel.style = kind == .conversationRoot ? .plain : .inline + UserDefaults.shared.publisher(for: \.translateButtonPreference) .map { $0 } .assign(to: &$translateButtonPreference) @@ -898,36 +903,59 @@ extension StatusView.ViewModel { } extension StatusView.ViewModel { - var hasHangingAvatar: Bool { + var contentWidth: CGFloat { + let width = containerWidth - 2 * margin + return max(width, .leastNonzeroMagnitude) + } + + var containerWidth: CGFloat { + let width: CGFloat = { + var width = parentViewModel?.containerWidth ?? viewLayoutFrame.readableContentLayoutFrame.width + width -= containerMargin + return width + }() + return max(width, .leastNonzeroMagnitude) + } + + var containerMargin: CGFloat { + var width: CGFloat = 0 switch kind { - case .conversationRoot, .quote: - return false + case .timeline, .referenceReplyTo: + width += StatusView.hangingAvatarButtonDimension + width += StatusView.hangingAvatarButtonTrailingSapcing default: - return true + break } + return width } - public var cellTopMargin: CGFloat { + public var margin: CGFloat { switch kind { - case .timeline: - return repostViewModel == nil ? 12 : 8 - default: - return .zero + case .quote: return 12 + default: return .zero } } - public var margin: CGFloat { + var hasHangingAvatar: Bool { switch kind { - case .quote: - return 12 + case .conversationRoot, .quote: + return false default: - return .zero + return true } } + public var cellTopMargin: CGFloat { + return .zero +// switch kind { +// default: +// return 12 +// } + } + public var hasToolbar: Bool { switch kind { - case .timeline, .repost, .conversationRoot, .conversationThread: + case .timeline, .conversationRoot, .conversationThread: return true default: return false @@ -1010,7 +1038,7 @@ extension StatusView.ViewModel { self.init( status: .twitter(record: status.asRecrod), authContext: authContext, - kind: kind, + kind: status.repost != nil ? .repost : kind, delegate: delegate, viewLayoutFramePublisher: viewLayoutFramePublisher ) @@ -1020,7 +1048,7 @@ extension StatusView.ViewModel { let _repostViewModel = StatusView.ViewModel( status: repost, authContext: authContext, - kind: .repost, + kind: kind, delegate: delegate, parentViewModel: self, viewLayoutFramePublisher: viewLayoutFramePublisher @@ -1064,10 +1092,10 @@ extension StatusView.ViewModel { // timestamp switch kind { - case .timeline, .repost: - timestampLabelViewModel = TimestampLabelView.ViewModel(timestamp: status.createdAt) - default: + case .conversationRoot: break + default: + timestampLabelViewModel = TimestampLabelView.ViewModel(timestamp: status.createdAt) } // content @@ -1130,6 +1158,26 @@ extension StatusView.ViewModel { } else { // do nothing } + + // metric + switch kind { + case .conversationRoot: + let _metricViewModel = StatusMetricView.ViewModel(platform: .twitter, timestamp: status.createdAt) + metricViewModel = _metricViewModel + status.publisher(for: \.source) + .assign(to: &_metricViewModel.$source) + status.publisher(for: \.replyCount) + .map { Int($0) } + .assign(to: &_metricViewModel.$replyCount) + status.publisher(for: \.repostCount) + .map { Int($0) } + .assign(to: &_metricViewModel.$repostCount) + status.publisher(for: \.likeCount) + .map { Int($0) } + .assign(to: &_metricViewModel.$likeCount) + default: + break + } } // end init } @@ -1145,7 +1193,7 @@ extension StatusView.ViewModel { self.init( status: .mastodon(record: status.asRecrod), authContext: authContext, - kind: kind, + kind: status.repost != nil ? .repost : kind, delegate: delegate, viewLayoutFramePublisher: viewLayoutFramePublisher ) @@ -1155,7 +1203,7 @@ extension StatusView.ViewModel { let _repostViewModel = StatusView.ViewModel( status: repost, authContext: authContext, - kind: .repost, + kind: kind, delegate: delegate, parentViewModel: self, viewLayoutFramePublisher: viewLayoutFramePublisher @@ -1194,10 +1242,10 @@ extension StatusView.ViewModel { // timestamp switch kind { - case .timeline, .repost: - timestampLabelViewModel = TimestampLabelView.ViewModel(timestamp: status.createdAt) - default: + case .conversationRoot: break + default: + timestampLabelViewModel = TimestampLabelView.ViewModel(timestamp: status.createdAt) } // spoiler content @@ -1279,5 +1327,23 @@ extension StatusView.ViewModel { } else { // do nothing } + + // metric + switch kind { + case .conversationRoot: + let _metricViewModel = StatusMetricView.ViewModel(platform: .mastodon, timestamp: status.createdAt) + metricViewModel = _metricViewModel + status.publisher(for: \.replyCount) + .map { Int($0) } + .assign(to: &_metricViewModel.$replyCount) + status.publisher(for: \.repostCount) + .map { Int($0) } + .assign(to: &_metricViewModel.$repostCount) + status.publisher(for: \.likeCount) + .map { Int($0) } + .assign(to: &_metricViewModel.$likeCount) + default: + break + } } } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index 12413817..4a86d4c0 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -33,16 +33,17 @@ public protocol StatusViewDelegate: AnyObject { func statusView(_ viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel) func statusView(_ viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel, previewActionContext: ContextMenuInteractionPreviewActionContext) func statusView(_ viewModel: StatusView.ViewModel, toggleContentWarningOverlayDisplay isReveal: Bool) -// func statusView(_ statusView: StatusView, quoteStatusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, didTapMediaView mediaView: MediaView, at index: Int) -// + + // poll func statusView(_ viewModel: StatusView.ViewModel, pollVoteActionForViewModel pollViewModel: PollView.ViewModel) func statusView(_ viewModel: StatusView.ViewModel, pollUpdateIfNeedsForViewModel pollViewModel: PollView.ViewModel) func statusView(_ viewModel: StatusView.ViewModel, pollViewModel: PollView.ViewModel, pollOptionDidSelectForViewModel optionViewModel: PollOptionView.ViewModel) -// func statusView(_ statusView: StatusView, quoteStatusViewDidPressed quoteStatusView: StatusView) -// + // metric + func statusView(_ viewModel: StatusView.ViewModel, statusMetricViewModel: StatusMetricView.ViewModel, statusMetricButtonDidPressed action: StatusMetricView.Action) + + // toolbar func statusView(_ viewModel: StatusView.ViewModel, statusToolbarViewModel: StatusToolbarView.ViewModel, statusToolbarButtonDidPressed action: StatusToolbarView.Action) -// func statusView(_ statusView: StatusView, statusToolbar: StatusToolbar, menuActionDidPressed action: StatusToolbar.MenuAction, menuButton button: UIButton) // // func statusView(_ statusView: StatusView, translateButtonDidPressed button: UIButton) @@ -89,10 +90,9 @@ public struct StatusView: View { VStack(spacing: contentSpacing) { // authorView authorView - .padding(.horizontal, viewModel.margin) + // spoiler content (Mastodon) if viewModel.spoilerContent != nil { spoilerContentView - .padding(.horizontal, viewModel.margin) if !viewModel.isContentEmpty { Button { // force to trigger view update without animation @@ -110,19 +110,17 @@ public struct StatusView: View { } .buttonStyle(.borderless) .id(UUID()) // fix animation issue - .padding(.horizontal, viewModel.margin) } } // content if viewModel.isContentReveal { contentView - .padding(.horizontal, viewModel.margin) } // media if !viewModel.mediaViewModels.isEmpty { MediaGridContainerView( viewModels: viewModel.mediaViewModels, - idealWidth: contentWidth, + idealWidth: viewModel.contentWidth, idealHeight: 280, previewAction: { mediaViewModel in viewModel.delegate?.statusView(viewModel, previewActionForMediaViewModel: mediaViewModel) @@ -138,7 +136,6 @@ public struct StatusView: View { } .cornerRadius(MediaGridContainerView.cornerRadius) } - .padding(.horizontal, viewModel.margin) } // poll if let pollViewModel = viewModel.pollViewModel { @@ -150,7 +147,6 @@ public struct StatusView: View { viewModel.delegate?.statusView(viewModel, pollVoteActionForViewModel: pollViewModel) } ) - .padding(.horizontal, viewModel.margin) .onAppear { viewModel.delegate?.statusView(viewModel, pollUpdateIfNeedsForViewModel: pollViewModel) } @@ -163,27 +159,65 @@ public struct StatusView: View { } .cornerRadius(12) } + // metric + if let metricViewModel = viewModel.metricViewModel { + StatusMetricView(viewModel: metricViewModel) { action in + + } + .padding(.vertical, 8) + } + // toolbar if viewModel.hasToolbar { toolbarView - .padding(.top, -contentSpacing) + .overlay(alignment: .top) { + switch viewModel.kind { + case .conversationRoot: + VStack(spacing: .zero) { + Color.clear + .frame(height: 1) + Divider() + .frame(width: viewModel.viewLayoutFrame.safeAreaLayoutFrame.width) + .fixedSize() + Spacer() + } + default: + EmptyView() + } + } } } // end VStack + .padding(.top, viewModel.margin) // container margin + .padding(.horizontal, viewModel.margin) // container margin + .padding(.bottom, viewModel.hasToolbar ? .zero : viewModel.margin) // container margin + .frame(width: viewModel.containerWidth) + //.overlay { + // Text("\(viewModel.containerWidth.description)") + // .font(.title) + //} + //.border(.red, width: 1) .overlay(alignment: .bottom) { switch viewModel.kind { - case .timeline, .repost, .conversationRoot, .conversationThread: + case .timeline, .repost, .conversationThread: VStack(spacing: .zero) { Spacer() Divider() Color.clear .frame(height: 1) } + case .conversationRoot: + VStack(spacing: .zero) { + Spacer() + Divider() + .frame(width: viewModel.viewLayoutFrame.safeAreaLayoutFrame.width) + .fixedSize() + Color.clear + .frame(height: 1) + } default: EmptyView() } } } // end HStack - .padding(.top, viewModel.margin) // container margin - .padding(.bottom, viewModel.hasToolbar ? .zero : viewModel.margin) // container margin .overlay { if viewModel.isBottomConversationLinkLineViewDisplay { HStack(alignment: .top, spacing: .zero) { @@ -211,18 +245,6 @@ public struct StatusView: View { } extension StatusView { - var contentWidth: CGFloat { - let width: CGFloat = { - switch viewModel.kind { - case .conversationRoot: - return viewModel.viewLayoutFrame.readableContentLayoutFrame.width - 2 * viewModel.margin - default: - return viewModel.viewLayoutFrame.readableContentLayoutFrame.width - 2 * viewModel.margin - StatusView.hangingAvatarButtonDimension - StatusView.hangingAvatarButtonTrailingSapcing - } - }() - return max(width, .leastNonzeroMagnitude) - } - public var authorView: some View { HStack(alignment: .center) { if !viewModel.hasHangingAvatar { @@ -230,13 +252,26 @@ extension StatusView { avatarButton } // info - let infoLayout = dynamicTypeSize < .accessibility1 ? - AnyLayout(HStackLayout(alignment: .center, spacing: 6)) : - AnyLayout(VStackLayout(alignment: .leading, spacing: .zero)) + let infoLayout: AnyLayout = { + if dynamicTypeSize < .accessibility1 { + return AnyLayout(HStackLayout(alignment: .center, spacing: 6)) + } else { + return AnyLayout(VStackLayout(alignment: .leading, spacing: .zero)) + } + }() infoLayout { - let nameLayout = dynamicTypeSize < .accessibility1 ? - AnyLayout(HStackLayout(alignment: .bottom, spacing: 6)) : - AnyLayout(VStackLayout(alignment: .leading, spacing: .zero)) + let nameLayout: AnyLayout = { + switch viewModel.kind { + case .conversationRoot: + return AnyLayout(VStackLayout(alignment: .leading, spacing: .zero)) + default: + if dynamicTypeSize < .accessibility1 { + return AnyLayout(HStackLayout(alignment: .bottom, spacing: 6)) + } else { + return AnyLayout(VStackLayout(alignment: .leading, spacing: .zero)) + } + } + }() nameLayout { // name LabelRepresentable( @@ -249,6 +284,15 @@ extension StatusView { ) .fixedSize(horizontal: false, vertical: true) .layoutPriority(0.618) + .background(GeometryReader { proxy in + Color.clear.preference( + key: ViewHeightKey.self, + value: proxy.frame(in: .local).size.height + ) + }) + .onPreferenceChange(ViewHeightKey.self) { height in + self.visibilityIconImageDimension = height + } // username LabelRepresentable( metaContent: PlaintextMetaContent(string: "@" + viewModel.authorUsernme), @@ -260,16 +304,26 @@ extension StatusView { ) .fixedSize(horizontal: false, vertical: true) .layoutPriority(0.382) - } + } // end nameLayout .frame(alignment: .leading) + .background(GeometryReader { proxy in + Color.clear.preference( + key: ViewHeightKey.self, + value: proxy.frame(in: .local).size.height + ) + }) + .onPreferenceChange(ViewHeightKey.self) { height in + self.viewModel.authorAvatarDimension = height + } Spacer() HStack(spacing: 6) { // mastodon visibility if let visibilityIconImage = viewModel.visibilityIconImage, visibilityIconImageDimension > 0 { let dimension = ceil(visibilityIconImageDimension * 0.8) + let alignment: Alignment = viewModel.kind == .conversationRoot ? .top : .center Color.clear .frame(width: dimension) - .overlay { + .overlay(alignment: alignment) { VectorImageView(image: visibilityIconImage, tintColor: TextStyle.statusTimestamp.textColor) .frame(width: dimension, height: dimension) } @@ -277,27 +331,9 @@ extension StatusView { // timestamp if let timestampLabelViewModel = viewModel.timestampLabelViewModel { TimestampLabelView(viewModel: timestampLabelViewModel) - .background(GeometryReader { proxy in - Color.clear.preference( - key: ViewHeightKey.self, - value: proxy.frame(in: .local).size.height - ) - }) - .onPreferenceChange(ViewHeightKey.self) { height in - self.visibilityIconImageDimension = height - } } } - } - .background(GeometryReader { proxy in - Color.clear.preference( - key: ViewHeightKey.self, - value: proxy.frame(in: .local).size.height - ) - }) - .onPreferenceChange(ViewHeightKey.self) { height in - self.viewModel.authorAvatarDimension = height - } + } // end infoLayout // add spacer to make the infoLayout leading align if dynamicTypeSize < .accessibility1 { // do nothing @@ -336,9 +372,9 @@ extension StatusView { TextViewRepresentable( metaContent: viewModel.spoilerContent ?? PlaintextMetaContent(string: ""), textStyle: .statusContent, - width: contentWidth + width: viewModel.contentWidth ) - .frame(width: contentWidth) + .frame(width: viewModel.contentWidth) } } @@ -347,9 +383,9 @@ extension StatusView { TextViewRepresentable( metaContent: viewModel.content, textStyle: .statusContent, - width: contentWidth + width: viewModel.contentWidth ) - .frame(width: contentWidth) + .frame(width: viewModel.contentWidth) } } diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift index 3eb4b20a..60fac2c1 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift @@ -43,7 +43,7 @@ public struct ComposeContentView: View { case .reply: if let replyToStatusViewModel = viewModel.replyToStatusViewModel { StatusView(viewModel: replyToStatusViewModel) - .frame(width: viewModel.viewLayoutFrame.readableContentLayoutFrame.width) + // .frame(width: viewModel.viewLayoutFrame.readableContentLayoutFrame.width) .padding(.top, ComposeContentView.contentRowTopPadding) } default: diff --git a/TwidereX/Coordinator/SceneCoordinator.swift b/TwidereX/Coordinator/SceneCoordinator.swift index 199746bb..884ad560 100644 --- a/TwidereX/Coordinator/SceneCoordinator.swift +++ b/TwidereX/Coordinator/SceneCoordinator.swift @@ -535,7 +535,7 @@ extension SceneCoordinator { let statusThreadViewModel = StatusThreadViewModel( context: context, authContext: authConext, - root: .root(context: .init(status: .mastodon(record: root.asRecrod))) + kind: .status(.mastodon(record: root.asRecrod)) ) present( scene: .statusThread(viewModel: statusThreadViewModel), diff --git a/TwidereX/Diffable/Status/StatusItem.swift b/TwidereX/Diffable/Status/StatusItem.swift index 48e63f0a..763add5f 100644 --- a/TwidereX/Diffable/Status/StatusItem.swift +++ b/TwidereX/Diffable/Status/StatusItem.swift @@ -14,54 +14,6 @@ enum StatusItem: Hashable { case feed(record: ManagedObjectRecord) case feedLoader(record: ManagedObjectRecord) case status(StatusRecord) - case thread(Thread) case topLoader case bottomLoader } - -extension StatusItem { - enum Thread: Hashable { - case root(context: Context) - case reply(context: Context) - case leaf(context: Context) - - public var statusRecord: StatusRecord { - switch self { - case .root(let threadContext), - .reply(let threadContext), - .leaf(let threadContext): - return threadContext.status - } - } - } -} - -extension StatusItem.Thread { - class Context: Hashable { - let status: StatusRecord - var displayUpperConversationLink: Bool - var displayBottomConversationLink: Bool - - init( - status: StatusRecord, - displayUpperConversationLink: Bool = false, - displayBottomConversationLink: Bool = false - ) { - self.status = status - self.displayUpperConversationLink = displayUpperConversationLink - self.displayBottomConversationLink = displayBottomConversationLink - } - - static func == (lhs: StatusItem.Thread.Context, rhs: StatusItem.Thread.Context) -> Bool { - return lhs.status == rhs.status - && lhs.displayUpperConversationLink == rhs.displayUpperConversationLink - && lhs.displayBottomConversationLink == rhs.displayBottomConversationLink - } - - func hash(into hasher: inout Hasher) { - hasher.combine(status) - hasher.combine(displayUpperConversationLink) - hasher.combine(displayBottomConversationLink) - } - } -} diff --git a/TwidereX/Diffable/Status/StatusSection.swift b/TwidereX/Diffable/Status/StatusSection.swift index eb47d6f6..a42c2d92 100644 --- a/TwidereX/Diffable/Status/StatusSection.swift +++ b/TwidereX/Diffable/Status/StatusSection.swift @@ -105,20 +105,6 @@ extension StatusSection { // } // end switch // } return cell - - case .thread(let thread): - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell - - return cell -// return StatusSection.dequeueConfiguredReusableCell( -// context: context, -// tableView: tableView, -// indexPath: indexPath, -// configuration: ThreadCellRegistrationConfiguration( -// thread: thread, -// configuration: configuration -// ) -// ) case .topLoader: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+StatusThread.swift b/TwidereX/Protocol/Provider/DataSourceFacade+StatusThread.swift index 61bb41b8..a382d3d5 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+StatusThread.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+StatusThread.swift @@ -11,62 +11,15 @@ import CoreData import CoreDataStack extension DataSourceFacade { - static func coordinateToStatusThreadScene( - provider: DataSourceProvider & AuthContextProvider, - target: StatusTarget, - status: StatusRecord - ) async { - let _root: StatusItem.Thread? = await { - let _redirectRecord = await DataSourceFacade.status( - managedObjectContext: provider.context.managedObjectContext, - status: status, - target: target - ) - guard let redirectRecord = _redirectRecord else { return nil } - - switch redirectRecord { - case .twitter(let record): - let context = StatusItem.Thread.Context( - status: .twitter(record: record), - displayUpperConversationLink: await provider.context.managedObjectContext.perform { - guard let status = record.object(in: provider.context.managedObjectContext) else { return false } - return status.replyToStatusID != nil - }, - displayBottomConversationLink: false - ) - return StatusItem.Thread.root(context: context) - case .mastodon(let record): - let context = StatusItem.Thread.Context( - status: .mastodon(record: record), - displayUpperConversationLink: await provider.context.managedObjectContext.perform { - guard let status = record.object(in: provider.context.managedObjectContext) else { return false } - return status.replyToStatusID != nil - }, - displayBottomConversationLink: false - ) - return StatusItem.Thread.root(context: context) - } - }() - guard let root = _root else { - assertionFailure() - return - } - - await coordinateToStatusThreadScene( - provider: provider, - root: root - ) - } - @MainActor static func coordinateToStatusThreadScene( provider: DataSourceProvider & AuthContextProvider, - root: StatusItem.Thread + kind: StatusThreadViewModel.Kind ) async { let statusThreadViewModel = StatusThreadViewModel( context: provider.context, authContext: provider.authContext, - root: root + kind: kind ) provider.coordinator.present( scene: .statusThread(viewModel: statusThreadViewModel), @@ -74,12 +27,13 @@ extension DataSourceFacade { transition: .show ) - Task { - guard case let .root(threadContext) = root else { return } - await recordStatusHistory( - denpendency: provider, - status: threadContext.status - ) - } // end Task + // FIXME: +// Task { +// guard case let .root(threadContext) = root else { return } +// await recordStatusHistory( +// denpendency: provider, +// status: threadContext.status +// ) +// } // end Task } } diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift index 93069799..8f07221c 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift @@ -44,8 +44,7 @@ extension MediaInfoDescriptionViewDelegate where Self: DataSourceProvider & Auth await DataSourceFacade.coordinateToStatusThreadScene( provider: self, - target: .repost, // keep repost wrapper - status: status + kind: .status(status) ) } } @@ -63,8 +62,7 @@ extension MediaInfoDescriptionViewDelegate where Self: DataSourceProvider & Auth await DataSourceFacade.coordinateToStatusThreadScene( provider: self, - target: .repost, // keep repost wrapper - status: status + kind: .status(status) ) } } diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift index e29bd04d..43139f4d 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift @@ -401,6 +401,23 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC // } } +// MARK: - metric +extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { + func tableViewCell( + _ cell: UITableViewCell, + viewModel: StatusView.ViewModel, + statusMetricViewModel: StatusMetricView.ViewModel, + statusMetricButtonDidPressed action: StatusMetricView.Action + ) { + Task { + guard let status = viewModel.status else { + assertionFailure() + return + } + // TODO: + } // end Task + } +} // MARK: - toolbar extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift index aea5ba17..45881fc1 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -27,8 +27,7 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid case .status(let status): await DataSourceFacade.coordinateToStatusThreadScene( provider: self, - target: .repost, // keep repost wrapper - status: status + kind: .status(status) ) case .user(let user): await DataSourceFacade.coordinateToProfileScene( @@ -46,13 +45,12 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid if let status = notification.status { await DataSourceFacade.coordinateToStatusThreadScene( provider: self, - target: .repost, // keep repost wrapper - status: .mastodon(record: .init(objectID: status.objectID)) + kind: .status(.mastodon(record: status.asRecrod)) ) } else { await DataSourceFacade.coordinateToProfileScene( provider: self, - user: .mastodon(record: .init(objectID: notification.account.objectID)) + user: .mastodon(record: notification.account.asRecrod) ) } } diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift index f8af8733..f6be20ea 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift @@ -32,6 +32,7 @@ protocol StatusViewTableViewCellDelegate: AutoGenerateProtocolDelegate { func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, pollVoteActionForViewModel pollViewModel: PollView.ViewModel) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, pollUpdateIfNeedsForViewModel pollViewModel: PollView.ViewModel) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, pollViewModel: PollView.ViewModel, pollOptionDidSelectForViewModel optionViewModel: PollOptionView.ViewModel) + func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, statusMetricViewModel: StatusMetricView.ViewModel, statusMetricButtonDidPressed action: StatusMetricView.Action) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, statusToolbarViewModel: StatusToolbarView.ViewModel, statusToolbarButtonDidPressed action: StatusToolbarView.Action) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, viewHeightDidChange: Void) // sourcery:end @@ -69,6 +70,10 @@ extension StatusViewDelegate where Self: StatusViewContainerTableViewCell { delegate?.tableViewCell(self, viewModel: viewModel, pollViewModel: pollViewModel, pollOptionDidSelectForViewModel: optionViewModel) } + func statusView(_ viewModel: StatusView.ViewModel, statusMetricViewModel: StatusMetricView.ViewModel, statusMetricButtonDidPressed action: StatusMetricView.Action) { + delegate?.tableViewCell(self, viewModel: viewModel, statusMetricViewModel: statusMetricViewModel, statusMetricButtonDidPressed: action) + } + func statusView(_ viewModel: StatusView.ViewModel, statusToolbarViewModel: StatusToolbarView.ViewModel, statusToolbarButtonDidPressed action: StatusToolbarView.Action) { delegate?.tableViewCell(self, viewModel: viewModel, statusToolbarViewModel: statusToolbarViewModel, statusToolbarButtonDidPressed: action) } diff --git a/TwidereX/Scene/StatusThread/Mastodon/MastodonStatusThreadViewModel.swift b/TwidereX/Scene/StatusThread/Mastodon/MastodonStatusThreadViewModel.swift index 1fac2b6d..b21cf1d5 100644 --- a/TwidereX/Scene/StatusThread/Mastodon/MastodonStatusThreadViewModel.swift +++ b/TwidereX/Scene/StatusThread/Mastodon/MastodonStatusThreadViewModel.swift @@ -16,263 +16,262 @@ import MastodonMeta final class MastodonStatusThreadViewModel { - var disposeBag = Set() - - // input - let context: AppContext - @Published private(set) var deletedObjectIDs: Set = Set() - - // output - @Published var _ancestors: [StatusItem] = [] - let ancestors = CurrentValueSubject<[StatusItem], Never>([]) - - @Published var _descendants: [StatusItem] = [] - let descendants = CurrentValueSubject<[StatusItem], Never>([]) - - init(context: AppContext) { - self.context = context - - Publishers.CombineLatest( - $_ancestors, - $deletedObjectIDs - ) - .sink { [weak self] items, deletedObjectIDs in - guard let self = self else { return } - let newItems = items.filter { item in - switch item { - case .thread(let thread): - return !deletedObjectIDs.contains(thread.statusRecord.objectID) - default: - assertionFailure() - return false - } - } - self.ancestors.value = newItems - } - .store(in: &disposeBag) - - Publishers.CombineLatest( - $_descendants, - $deletedObjectIDs - ) - .sink { [weak self] items, deletedObjectIDs in - guard let self = self else { return } - let newItems = items.filter { item in - switch item { - case .thread(let thread): - return !deletedObjectIDs.contains(thread.statusRecord.objectID) - default: - assertionFailure() - return false - } - } - self.descendants.value = newItems - } - .store(in: &disposeBag) - } - - deinit { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - } +// var disposeBag = Set() +// +// // input +// let context: AppContext +// @Published private(set) var deletedObjectIDs: Set = Set() +// +// // output +// @Published var _ancestors: [StatusItem] = [] +// let ancestors = CurrentValueSubject<[StatusItem], Never>([]) +// +// @Published var _descendants: [StatusItem] = [] +// let descendants = CurrentValueSubject<[StatusItem], Never>([]) +// +// init(context: AppContext) { +// self.context = context +// +// Publishers.CombineLatest( +// $_ancestors, +// $deletedObjectIDs +// ) +// .sink { [weak self] items, deletedObjectIDs in +// guard let self = self else { return } +// let newItems = items.filter { item in +// switch item { +// case .thread(let thread): +// return !deletedObjectIDs.contains(thread.statusRecord.objectID) +// default: +// assertionFailure() +// return false +// } +// } +// self.ancestors.value = newItems +// } +// .store(in: &disposeBag) +// +// Publishers.CombineLatest( +// $_descendants, +// $deletedObjectIDs +// ) +// .sink { [weak self] items, deletedObjectIDs in +// guard let self = self else { return } +// let newItems = items.filter { item in +// switch item { +// case .thread(let thread): +// return !deletedObjectIDs.contains(thread.statusRecord.objectID) +// default: +// assertionFailure() +// return false +// } +// } +// self.descendants.value = newItems +// } +// .store(in: &disposeBag) +// } +// +// deinit { +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) +// } } extension MastodonStatusThreadViewModel { - func appendAncestor( - domain: String, - nodes: [Node] - ) { - let ids = nodes.map { $0.statusID } - var dictionary: [MastodonStatus.ID: MastodonStatus] = [:] - do { - let request = MastodonStatus.sortedFetchRequest - request.predicate = MastodonStatus.predicate(domain: domain, ids: ids) - let statuses = try self.context.managedObjectContext.fetch(request) - for status in statuses { - dictionary[status.id] = status - } - } catch { - os_log("%{public}s[%{public}ld], %{public}s: fetch conversation fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) - return - } - - var newItems: [StatusItem] = [] - for (i, node) in nodes.enumerated() { - guard let status = dictionary[node.statusID] else { continue } - let isLast = i == nodes.count - 1 - - let record = ManagedObjectRecord(objectID: status.objectID) - let context = StatusItem.Thread.Context( - status: .mastodon(record: record), - displayUpperConversationLink: !isLast, - displayBottomConversationLink: true - ) - let item = StatusItem.thread(.leaf(context: context)) - newItems.append(item) - } - - let items = self._ancestors + newItems - self._ancestors = items - } +// func appendAncestor( +// domain: String, +// nodes: [Node] +// ) { +// let ids = nodes.map { $0.statusID } +// var dictionary: [MastodonStatus.ID: MastodonStatus] = [:] +// do { +// let request = MastodonStatus.sortedFetchRequest +// request.predicate = MastodonStatus.predicate(domain: domain, ids: ids) +// let statuses = try self.context.managedObjectContext.fetch(request) +// for status in statuses { +// dictionary[status.id] = status +// } +// } catch { +// os_log("%{public}s[%{public}ld], %{public}s: fetch conversation fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) +// return +// } +// +// var newItems: [StatusItem] = [] +// for (i, node) in nodes.enumerated() { +// guard let status = dictionary[node.statusID] else { continue } +// let isLast = i == nodes.count - 1 +// +// let record = ManagedObjectRecord(objectID: status.objectID) +// let context = StatusItem.Thread.Context( +// status: .mastodon(record: record), +// displayUpperConversationLink: !isLast, +// displayBottomConversationLink: true +// ) +// let item = StatusItem.thread(.leaf(context: context)) +// newItems.append(item) +// } +// +// let items = self._ancestors + newItems +// self._ancestors = items +// } - func appendDescendant( - domain: String, - nodes: [Node] - ) { - let childrenIDs = nodes - .map { node in [node.statusID, node.children.first?.statusID].compactMap { $0 } } - .flatMap { $0 } - var dictionary: [MastodonStatus.ID: MastodonStatus] = [:] - do { - let request = MastodonStatus.sortedFetchRequest - request.predicate = MastodonStatus.predicate(domain: domain, ids: childrenIDs) - let statuses = try self.context.managedObjectContext.fetch(request) - for status in statuses { - dictionary[status.id] = status - } - } catch { - os_log("%{public}s[%{public}ld], %{public}s: fetch conversation fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) - return - } - - var newItems: [StatusItem] = [] - for node in nodes { - guard let status = dictionary[node.statusID] else { continue } - // first tier - let record = ManagedObjectRecord(objectID: status.objectID) - let context = StatusItem.Thread.Context( - status: .mastodon(record: record) - ) - let item = StatusItem.thread(.leaf(context: context)) - newItems.append(item) - - // second tier - if let child = node.children.first { - guard let secondaryStatus = dictionary[child.statusID] else { continue } - let secondaryRecord = ManagedObjectRecord(objectID: secondaryStatus.objectID) - let secondaryContext = StatusItem.Thread.Context( - status: .mastodon(record: secondaryRecord), - displayUpperConversationLink: true - ) - let secondaryItem = StatusItem.thread(.leaf(context: secondaryContext)) - newItems.append(secondaryItem) - - // update first tier context - context.displayBottomConversationLink = true - } - } - - var items = self._descendants - for item in newItems { - guard !items.contains(item) else { continue } - items.append(item) - } - self._descendants = items - } +// func appendDescendant( +// domain: String, +// nodes: [Node] +// ) { +// let childrenIDs = nodes +// .map { node in [node.statusID, node.children.first?.statusID].compactMap { $0 } } +// .flatMap { $0 } +// var dictionary: [MastodonStatus.ID: MastodonStatus] = [:] +// do { +// let request = MastodonStatus.sortedFetchRequest +// request.predicate = MastodonStatus.predicate(domain: domain, ids: childrenIDs) +// let statuses = try self.context.managedObjectContext.fetch(request) +// for status in statuses { +// dictionary[status.id] = status +// } +// } catch { +// os_log("%{public}s[%{public}ld], %{public}s: fetch conversation fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) +// return +// } +// +// var newItems: [StatusItem] = [] +// for node in nodes { +// guard let status = dictionary[node.statusID] else { continue } +// // first tier +// let record = ManagedObjectRecord(objectID: status.objectID) +// let context = StatusItem.Thread.Context( +// status: .mastodon(record: record) +// ) +// let item = StatusItem.thread(.leaf(context: context)) +// newItems.append(item) +// +// // second tier +// if let child = node.children.first { +// guard let secondaryStatus = dictionary[child.statusID] else { continue } +// let secondaryRecord = ManagedObjectRecord(objectID: secondaryStatus.objectID) +// let secondaryContext = StatusItem.Thread.Context( +// status: .mastodon(record: secondaryRecord), +// displayUpperConversationLink: true +// ) +// let secondaryItem = StatusItem.thread(.leaf(context: secondaryContext)) +// newItems.append(secondaryItem) +// +// // update first tier context +// context.displayBottomConversationLink = true +// } +// } +// +// var items = self._descendants +// for item in newItems { +// guard !items.contains(item) else { continue } +// items.append(item) +// } +// self._descendants = items +// } } extension MastodonStatusThreadViewModel { - class Node { - typealias ID = String - - let statusID: ID - let children: [Node] - - init( - statusID: ID, - children: [MastodonStatusThreadViewModel.Node] - ) { - self.statusID = statusID - self.children = children - } - } +// class Node { +// typealias ID = String +// +// let statusID: ID +// let children: [Node] +// +// init( +// statusID: ID, +// children: [MastodonStatusThreadViewModel.Node] +// ) { +// self.statusID = statusID +// self.children = children +// } +// } } -extension MastodonStatusThreadViewModel.Node { - static func replyToThread( - for replyToID: Mastodon.Entity.Status.ID?, - from statuses: [Mastodon.Entity.Status] - ) -> [MastodonStatusThreadViewModel.Node] { - guard let replyToID = replyToID else { - return [] - } - - var dict: [Mastodon.Entity.Status.ID: Mastodon.Entity.Status] = [:] - for status in statuses { - dict[status.id] = status - } - - var nextID: Mastodon.Entity.Status.ID? = replyToID - var nodes: [MastodonStatusThreadViewModel.Node] = [] - while let _nextID = nextID { - guard let status = dict[_nextID] else { break } - nodes.append(MastodonStatusThreadViewModel.Node( - statusID: _nextID, - children: [] - )) - nextID = status.inReplyToID - } - - return nodes - } -} +//extension MastodonStatusThreadViewModel.Node { +// static func replyToThread( +// for replyToID: Mastodon.Entity.Status.ID?, +// from statuses: [Mastodon.Entity.Status] +// ) -> [MastodonStatusThreadViewModel.Node] { +// guard let replyToID = replyToID else { +// return [] +// } +// +// var dict: [Mastodon.Entity.Status.ID: Mastodon.Entity.Status] = [:] +// for status in statuses { +// dict[status.id] = status +// } +// +// var nextID: Mastodon.Entity.Status.ID? = replyToID +// var nodes: [MastodonStatusThreadViewModel.Node] = [] +// while let _nextID = nextID { +// guard let status = dict[_nextID] else { break } +// nodes.append(MastodonStatusThreadViewModel.Node( +// statusID: _nextID, +// children: [] +// )) +// nextID = status.inReplyToID +// } +// +// return nodes +// } +//} -extension MastodonStatusThreadViewModel.Node { - static func children( - of statusID: ID, - from statuses: [Mastodon.Entity.Status] - ) -> [MastodonStatusThreadViewModel.Node] { - var dictionary: [ID: Mastodon.Entity.Status] = [:] - var mapping: [ID: Set] = [:] - - for status in statuses { - dictionary[status.id] = status - guard let replyToID = status.inReplyToID else { continue } - if var set = mapping[replyToID] { - set.insert(status.id) - mapping[replyToID] = set - } else { - mapping[replyToID] = Set([status.id]) - } - } - - var children: [MastodonStatusThreadViewModel.Node] = [] - let replies = Array(mapping[statusID] ?? Set()) - .compactMap { dictionary[$0] } - .sorted(by: { $0.createdAt > $1.createdAt }) - for reply in replies { - let child = child(of: reply.id, dictionary: dictionary, mapping: mapping) - children.append(child) - } - return children - } - - static func child( - of statusID: ID, - dictionary: [ID: Mastodon.Entity.Status], - mapping: [ID: Set] - ) -> MastodonStatusThreadViewModel.Node { - let childrenIDs = mapping[statusID] ?? [] - let children = Array(childrenIDs) - .compactMap { dictionary[$0] } - .sorted(by: { $0.createdAt > $1.createdAt }) - .map { status in child(of: status.id, dictionary: dictionary, mapping: mapping) } - return MastodonStatusThreadViewModel.Node( - statusID: statusID, - children: children - ) - } - -} +//extension MastodonStatusThreadViewModel.Node { +// static func children( +// of statusID: ID, +// from statuses: [Mastodon.Entity.Status] +// ) -> [MastodonStatusThreadViewModel.Node] { +// var dictionary: [ID: Mastodon.Entity.Status] = [:] +// var mapping: [ID: Set] = [:] +// +// for status in statuses { +// dictionary[status.id] = status +// guard let replyToID = status.inReplyToID else { continue } +// if var set = mapping[replyToID] { +// set.insert(status.id) +// mapping[replyToID] = set +// } else { +// mapping[replyToID] = Set([status.id]) +// } +// } +// +// var children: [MastodonStatusThreadViewModel.Node] = [] +// let replies = Array(mapping[statusID] ?? Set()) +// .compactMap { dictionary[$0] } +// .sorted(by: { $0.createdAt > $1.createdAt }) +// for reply in replies { +// let child = child(of: reply.id, dictionary: dictionary, mapping: mapping) +// children.append(child) +// } +// return children +// } +// +// static func child( +// of statusID: ID, +// dictionary: [ID: Mastodon.Entity.Status], +// mapping: [ID: Set] +// ) -> MastodonStatusThreadViewModel.Node { +// let childrenIDs = mapping[statusID] ?? [] +// let children = Array(childrenIDs) +// .compactMap { dictionary[$0] } +// .sorted(by: { $0.createdAt > $1.createdAt }) +// .map { status in child(of: status.id, dictionary: dictionary, mapping: mapping) } +// return MastodonStatusThreadViewModel.Node( +// statusID: statusID, +// children: children +// ) +// } +//} extension MastodonStatusThreadViewModel { - func delete(objectIDs: [NSManagedObjectID]) { - var set = deletedObjectIDs - for objectID in objectIDs { - set.insert(objectID) - } - self.deletedObjectIDs = set - } +// func delete(objectIDs: [NSManagedObjectID]) { +// var set = deletedObjectIDs +// for objectID in objectIDs { +// set.insert(objectID) +// } +// self.deletedObjectIDs = set +// } } diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewController+DataSourceProvider.swift b/TwidereX/Scene/StatusThread/StatusThreadViewController+DataSourceProvider.swift index c75a2396..c9f48c62 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewController+DataSourceProvider.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewController+DataSourceProvider.swift @@ -20,12 +20,14 @@ extension StatusThreadViewController: DataSourceProvider { return nil } - guard case let .thread(thread) = item else { return nil } - switch thread { - case .reply(let threadContext), - .root(let threadContext), - .leaf(let threadContext): - return .status(threadContext.status) + switch item { + case .root: + guard let status = viewModel.status?.asRecord else { return nil } + return .status(status) + case .status(let status): + return .status(status) + case .topLoader, .bottomLoader: + return nil } } diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewController.swift b/TwidereX/Scene/StatusThread/StatusThreadViewController.swift index 4b5b407b..f2a42861 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewController.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewController.swift @@ -44,7 +44,9 @@ extension StatusThreadViewController { override func viewDidLoad() { super.viewDidLoad() + title = "Detail" view.backgroundColor = .systemBackground + viewModel.viewLayoutFrame.update(view: view) tableView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tableView) @@ -60,22 +62,22 @@ extension StatusThreadViewController { tableView: tableView, statusViewTableViewCellDelegate: self ) - viewModel.topListBatchFetchViewModel.setup(scrollView: tableView) - viewModel.bottomListBatchFetchViewModel.setup(scrollView: tableView) - viewModel.topListBatchFetchViewModel.shouldFetch - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - guard let self = self else { return } - self.viewModel.twitterStatusThreadReplyViewModel.stateMachine.enter(TwitterStatusThreadReplyViewModel.State.Loading.self) - } - .store(in: &disposeBag) - viewModel.bottomListBatchFetchViewModel.shouldFetch - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - guard let self = self else { return } - self.viewModel.loadThreadStateMachine.enter(StatusThreadViewModel.LoadThreadState.Loading.self) - } - .store(in: &disposeBag) +// viewModel.topListBatchFetchViewModel.setup(scrollView: tableView) +// viewModel.bottomListBatchFetchViewModel.setup(scrollView: tableView) +// viewModel.topListBatchFetchViewModel.shouldFetch +// .receive(on: DispatchQueue.main) +// .sink { [weak self] _ in +// guard let self = self else { return } +// self.viewModel.twitterStatusThreadReplyViewModel.stateMachine.enter(TwitterStatusThreadReplyViewModel.State.Loading.self) +// } +// .store(in: &disposeBag) +// viewModel.bottomListBatchFetchViewModel.shouldFetch +// .receive(on: DispatchQueue.main) +// .sink { [weak self] _ in +// guard let self = self else { return } +// self.viewModel.loadThreadStateMachine.enter(StatusThreadViewModel.LoadThreadState.Loading.self) +// } +// .store(in: &disposeBag) } override func viewWillAppear(_ animated: Bool) { @@ -87,25 +89,44 @@ extension StatusThreadViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - viewModel.viewDidAppear.send() +// viewModel.viewDidAppear.send() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + viewModel.viewLayoutFrame.update(view: view) + } + + override func viewSafeAreaInsetsDidChange() { + super.viewSafeAreaInsetsDidChange() + + viewModel.viewLayoutFrame.update(view: view) + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + coordinator.animate { _ in + self.viewModel.viewLayoutFrame.update(view: self.view) + } } } // MARK: - UITableViewDelegate extension StatusThreadViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { - func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { - guard let diffableDataSource = viewModel.diffableDataSource else { return indexPath } - guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return indexPath } - guard case let .thread(thread) = item else { return indexPath } - - switch thread { - case .root: - return nil - case .reply, .leaf: - return indexPath - } - } +// func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { +// guard let diffableDataSource = viewModel.diffableDataSource else { return indexPath } +// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return indexPath } +// guard case let .thread(thread) = item else { return indexPath } +// +// switch thread { +// case .root: +// return nil +// case .reply, .leaf: +// return indexPath +// } +// } // sourcery:inline:StatusThreadViewController.AutoGenerateTableViewDelegate diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift index 5884d838..9502dc83 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift @@ -8,6 +8,7 @@ import os.log import UIKit +import SwiftUI import Combine import CoreData import CoreDataStack @@ -18,261 +19,307 @@ extension StatusThreadViewModel { tableView: UITableView, statusViewTableViewCellDelegate: StatusViewTableViewCellDelegate ) { - let configuration = StatusSection.Configuration( - statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, - timelineMiddleLoaderTableViewCellDelegate: nil, - viewLayoutFramePublisher: $viewLayoutFrame - ) + tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self)) + // tableView.register(LoaderTableViewCell.self, forCellReuseIdentifier: String(describing: LoaderTableViewCell.self)) - diffableDataSource = StatusSection.diffableDataSource( - tableView: tableView, - context: context, - authContext: authContext, - configuration: configuration - ) - - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.main]) - if hasReplyTo { - snapshot.appendItems([.topLoader], toSection: .main) - } - if let root = self.root.value, case let .root(threadContext) = root { - switch threadContext.status { - case .twitter(let record): - if twitterStatusThreadReplyViewModel.root == nil { - twitterStatusThreadReplyViewModel.root = record - } - case .mastodon: - break - } + diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { [weak self] tableView, indexPath, item in + guard let self = self else { return UITableViewCell() } - let item = StatusItem.thread(root) - snapshot.appendItems([item, .bottomLoader], toSection: .main) - } else { - root.eraseToAnyPublisher() - .sink { [weak self] root in - guard let self = self else { return } - - guard case .root(let threadContext) = root else { return } - guard case let .twitter(record) = threadContext.status else { return } - - guard self.twitterStatusThreadReplyViewModel.root == nil else { return } - self.twitterStatusThreadReplyViewModel.root = record + switch item { + case .status(let status): + return UITableViewCell() + case .root: + let cell = self.conversationRootTableViewCell + guard let statusViewModel = self.statusViewModel else { + return UITableViewCell() } - .store(in: &disposeBag) - } - diffableDataSource?.apply(snapshot) - - // trigger thread loading - loadThreadStateMachine.enter(LoadThreadState.Prepare.self) - - Publishers.CombineLatest3( - root, - $replies, - $leafs - ) - .throttle(for: 0.3, scheduler: DispatchQueue.main, latest: true) - .sink { [weak self] root, replies, leafs in - guard let self = self else { return } - guard let diffableDataSource = self.diffableDataSource else { return } - - Task { @MainActor in - let oldSnapshot = diffableDataSource.snapshot() - - var newSnapshot = NSDiffableDataSourceSnapshot() - newSnapshot.appendSections([.main]) - - // top loader - if self.hasReplyTo, case let .root(threadContext) = root { - switch threadContext.status { - case .twitter: - let state = self.twitterStatusThreadReplyViewModel.stateMachine.currentState - if state is TwitterStatusThreadReplyViewModel.State.NoMore { - // do nothing - } else { - newSnapshot.appendItems([.topLoader], toSection: .main) - } - case .mastodon: - let state = self.loadThreadStateMachine.currentState - if state is LoadThreadState.NoMore { - // do nothing - } else { - newSnapshot.appendItems([.topLoader], toSection: .main) - } - } - } - // replies - newSnapshot.appendItems(replies.reversed(), toSection: .main) - // root - if let root = root { - let item = StatusItem.thread(root) - newSnapshot.appendItems([item], toSection: .main) - } - // leafs - newSnapshot.appendItems(leafs, toSection: .main) - // bottom loader - if let currentState = self.loadThreadStateMachine.currentState { - switch currentState { - case is LoadThreadState.Prepare, - is LoadThreadState.Idle, - is LoadThreadState.Loading: - newSnapshot.appendItems([.bottomLoader], toSection: .main) - default: - break - } - } - - let hasChanges = newSnapshot.itemIdentifiers != oldSnapshot.itemIdentifiers - if !hasChanges { - self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): snapshot not changes") - return - } else { - self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): snapshot has changes") + cell.delegate = statusViewTableViewCellDelegate + cell.contentConfiguration = UIHostingConfiguration { + StatusView(viewModel: statusViewModel) } - - guard let difference = self.calculateReloadSnapshotDifference( - tableView: tableView, - oldSnapshot: oldSnapshot, - newSnapshot: newSnapshot - ) else { - await self.updateDataSource(snapshot: newSnapshot, animatingDifferences: false) - self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): applied new snapshot without tweak") - return - } - - self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Snapshot] oldSnapshot: \(oldSnapshot.itemIdentifiers.debugDescription)") - self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Snapshot] newSnapshot: \(newSnapshot.itemIdentifiers.debugDescription)") - await self.updateSnapshotUsingReloadData( - tableView: tableView, - oldSnapshot: oldSnapshot, - newSnapshot: newSnapshot, - difference: difference - ) + .margins(.vertical, 0) // remove vertical margins + return cell + case .topLoader, .bottomLoader: + return UITableViewCell() } + } // end diffableDataSource = UITableViewDiffableDataSource + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + switch kind { + case .status(let status): + // if status.inReplyToUserID != nil { + // snapshot.appendItems([.topLoader]) + // } + break + case .twitter, .mastodon: + break } - .store(in: &disposeBag) + snapshot.appendItems([.root]) + diffableDataSource?.apply(snapshot, animatingDifferences: false, completion: nil) + + // switch bottomCursor { + // case .noMore: + // break + // default: + // snapshot.appendItems([.bottomLoader]) + // } + +// let configuration = StatusSection.Configuration( +// statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, +// timelineMiddleLoaderTableViewCellDelegate: nil, +// viewLayoutFramePublisher: $viewLayoutFrame +// ) +// +// diffableDataSource = StatusSection.diffableDataSource( +// tableView: tableView, +// context: context, +// authContext: authContext, +// configuration: configuration +// ) +// +// var snapshot = NSDiffableDataSourceSnapshot() +// snapshot.appendSections([.main]) +// if hasReplyTo { +// snapshot.appendItems([.topLoader], toSection: .main) +// } +// if let root = self.root.value, case let .root(threadContext) = root { +// switch threadContext.status { +// case .twitter(let record): +// if twitterStatusThreadReplyViewModel.root == nil { +// twitterStatusThreadReplyViewModel.root = record +// } +// case .mastodon: +// break +// } +// +// let item = StatusItem.thread(root) +// snapshot.appendItems([item, .bottomLoader], toSection: .main) +// } else { +// root.eraseToAnyPublisher() +// .sink { [weak self] root in +// guard let self = self else { return } +// +// guard case .root(let threadContext) = root else { return } +// guard case let .twitter(record) = threadContext.status else { return } +// +// guard self.twitterStatusThreadReplyViewModel.root == nil else { return } +// self.twitterStatusThreadReplyViewModel.root = record +// } +// .store(in: &disposeBag) +// } +// diffableDataSource?.apply(snapshot) +// +// // trigger thread loading +// loadThreadStateMachine.enter(LoadThreadState.Prepare.self) +// +// Publishers.CombineLatest3( +// root, +// $replies, +// $leafs +// ) +// .throttle(for: 0.3, scheduler: DispatchQueue.main, latest: true) +// .sink { [weak self] root, replies, leafs in +// guard let self = self else { return } +// guard let diffableDataSource = self.diffableDataSource else { return } +// +// Task { @MainActor in +// let oldSnapshot = diffableDataSource.snapshot() +// +// var newSnapshot = NSDiffableDataSourceSnapshot() +// newSnapshot.appendSections([.main]) +// +// // top loader +// if self.hasReplyTo, case let .root(threadContext) = root { +// switch threadContext.status { +// case .twitter: +// let state = self.twitterStatusThreadReplyViewModel.stateMachine.currentState +// if state is TwitterStatusThreadReplyViewModel.State.NoMore { +// // do nothing +// } else { +// newSnapshot.appendItems([.topLoader], toSection: .main) +// } +// case .mastodon: +// let state = self.loadThreadStateMachine.currentState +// if state is LoadThreadState.NoMore { +// // do nothing +// } else { +// newSnapshot.appendItems([.topLoader], toSection: .main) +// } +// } +// } +// // replies +// newSnapshot.appendItems(replies.reversed(), toSection: .main) +// // root +// if let root = root { +// let item = StatusItem.thread(root) +// newSnapshot.appendItems([item], toSection: .main) +// } +// // leafs +// newSnapshot.appendItems(leafs, toSection: .main) +// // bottom loader +// if let currentState = self.loadThreadStateMachine.currentState { +// switch currentState { +// case is LoadThreadState.Prepare, +// is LoadThreadState.Idle, +// is LoadThreadState.Loading: +// newSnapshot.appendItems([.bottomLoader], toSection: .main) +// default: +// break +// } +// } +// +// let hasChanges = newSnapshot.itemIdentifiers != oldSnapshot.itemIdentifiers +// if !hasChanges { +// self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): snapshot not changes") +// return +// } else { +// self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): snapshot has changes") +// } +// +// guard let difference = self.calculateReloadSnapshotDifference( +// tableView: tableView, +// oldSnapshot: oldSnapshot, +// newSnapshot: newSnapshot +// ) else { +// await self.updateDataSource(snapshot: newSnapshot, animatingDifferences: false) +// self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): applied new snapshot without tweak") +// return +// } +// +// self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Snapshot] oldSnapshot: \(oldSnapshot.itemIdentifiers.debugDescription)") +// self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Snapshot] newSnapshot: \(newSnapshot.itemIdentifiers.debugDescription)") +// await self.updateSnapshotUsingReloadData( +// tableView: tableView, +// oldSnapshot: oldSnapshot, +// newSnapshot: newSnapshot, +// difference: difference +// ) +// } +// } +// .store(in: &disposeBag) } - @MainActor private func updateDataSource( - snapshot: NSDiffableDataSourceSnapshot, - animatingDifferences: Bool - ) async { - await self.diffableDataSource?.apply(snapshot, animatingDifferences: animatingDifferences) - } +// @MainActor private func updateDataSource( +// snapshot: NSDiffableDataSourceSnapshot, +// animatingDifferences: Bool +// ) async { +// await self.diffableDataSource?.apply(snapshot, animatingDifferences: animatingDifferences) +// } // Some UI tweaks to present replies and conversation smoothly - @MainActor private func updateSnapshotUsingReloadData( - tableView: UITableView, - oldSnapshot: NSDiffableDataSourceSnapshot, - newSnapshot: NSDiffableDataSourceSnapshot, - difference: StatusThreadViewModel.Difference // - ) async { - let replies: [StatusItem] = { - newSnapshot.itemIdentifiers.filter { item in - guard case let .thread(thread) = item else { return false } - guard case .reply = thread else { return false } - return true - } - }() - // additional margin for .topLoader - let oldTopMargin: CGFloat = { - let marginHeight = TimelineTopLoaderTableViewCell.cellHeight - if oldSnapshot.itemIdentifiers.contains(.topLoader) || !replies.isEmpty { - return marginHeight - } - return .zero - }() - - await self.diffableDataSource?.applySnapshotUsingReloadData(newSnapshot) - - // note: - // tweak the content offset and bottom inset - // make the table view stable when data reload - // the keypoint is set the bottom inset to make the root padding with "TopLoaderHeight" to top edge - // and restore the "TopLoaderHeight" when bottom inset adjusted - - // set bottom inset. Make root item pin to top. - if let item = root.value.flatMap({ StatusItem.thread($0) }), - let index = newSnapshot.indexOfItem(item), - let cell = tableView.cellForRow(at: IndexPath(row: index, section: 0)) - { - // always set bottom inset due to lazy reply loading - // otherwise tableView will jump when insert replies - let bottomSpacing = tableView.safeAreaLayoutGuide.layoutFrame.height - cell.frame.height - oldTopMargin - let additionalInset = round(tableView.contentSize.height - cell.frame.maxY) - - tableView.contentInset.bottom = max(0, bottomSpacing - additionalInset) - self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): content inset bottom: \(tableView.contentInset.bottom)") - } - - // set scroll position - tableView.scrollToRow(at: difference.targetIndexPath, at: .top, animated: false) - tableView.contentOffset.y = { - var offset: CGFloat = tableView.contentOffset.y - difference.sourceDistanceToTableViewTopEdge - if tableView.contentInset.bottom != 0.0 { - // needs restore top margin if bottom inset adjusted - offset += oldTopMargin - } - return offset - }() - self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): applied new snapshot") - } +// @MainActor private func updateSnapshotUsingReloadData( +// tableView: UITableView, +// oldSnapshot: NSDiffableDataSourceSnapshot, +// newSnapshot: NSDiffableDataSourceSnapshot, +// difference: StatusThreadViewModel.Difference // +// ) async { +// let replies: [StatusItem] = { +// newSnapshot.itemIdentifiers.filter { item in +// guard case let .thread(thread) = item else { return false } +// guard case .reply = thread else { return false } +// return true +// } +// }() +// // additional margin for .topLoader +// let oldTopMargin: CGFloat = { +// let marginHeight = TimelineTopLoaderTableViewCell.cellHeight +// if oldSnapshot.itemIdentifiers.contains(.topLoader) || !replies.isEmpty { +// return marginHeight +// } +// return .zero +// }() +// +// await self.diffableDataSource?.applySnapshotUsingReloadData(newSnapshot) +// +// // note: +// // tweak the content offset and bottom inset +// // make the table view stable when data reload +// // the keypoint is set the bottom inset to make the root padding with "TopLoaderHeight" to top edge +// // and restore the "TopLoaderHeight" when bottom inset adjusted +// +// // set bottom inset. Make root item pin to top. +// if let item = root.value.flatMap({ StatusItem.thread($0) }), +// let index = newSnapshot.indexOfItem(item), +// let cell = tableView.cellForRow(at: IndexPath(row: index, section: 0)) +// { +// // always set bottom inset due to lazy reply loading +// // otherwise tableView will jump when insert replies +// let bottomSpacing = tableView.safeAreaLayoutGuide.layoutFrame.height - cell.frame.height - oldTopMargin +// let additionalInset = round(tableView.contentSize.height - cell.frame.maxY) +// +// tableView.contentInset.bottom = max(0, bottomSpacing - additionalInset) +// self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): content inset bottom: \(tableView.contentInset.bottom)") +// } +// +// // set scroll position +// tableView.scrollToRow(at: difference.targetIndexPath, at: .top, animated: false) +// tableView.contentOffset.y = { +// var offset: CGFloat = tableView.contentOffset.y - difference.sourceDistanceToTableViewTopEdge +// if tableView.contentInset.bottom != 0.0 { +// // needs restore top margin if bottom inset adjusted +// offset += oldTopMargin +// } +// return offset +// }() +// self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): applied new snapshot") +// } } extension StatusThreadViewModel { - struct Difference { - let item: StatusItem - let sourceIndexPath: IndexPath - let sourceDistanceToTableViewTopEdge: CGFloat - let targetIndexPath: IndexPath - } - - @MainActor private func calculateReloadSnapshotDifference( - tableView: UITableView, - oldSnapshot: NSDiffableDataSourceSnapshot, - newSnapshot: NSDiffableDataSourceSnapshot - ) -> Difference? { - guard oldSnapshot.numberOfItems != 0 else { return nil } - guard let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows?.sorted() else { return nil } - - // find index of the first visible item in both old and new snapshot - var _index: Int? - let items = oldSnapshot.itemIdentifiers(inSection: .main) - for (i, item) in items.enumerated() { - guard let indexPath = indexPathsForVisibleRows.first(where: { $0.row == i }) else { continue } - guard newSnapshot.indexOfItem(item) != nil else { continue } - let rectForCell = tableView.rectForRow(at: indexPath) - let distanceToTableViewTopEdge = tableView.convert(rectForCell, to: nil).origin.y - tableView.safeAreaInsets.top - guard distanceToTableViewTopEdge >= 0 else { continue } - _index = i - break - } - - guard let index = _index else { return nil } - let sourceIndexPath = IndexPath(row: index, section: 0) - - let rectForSourceItemCell = tableView.rectForRow(at: sourceIndexPath) - let sourceDistanceToTableViewTopEdge = tableView.convert(rectForSourceItemCell, to: nil).origin.y - tableView.safeAreaInsets.top - - guard sourceIndexPath.section < oldSnapshot.numberOfSections, - sourceIndexPath.row < oldSnapshot.numberOfItems(inSection: oldSnapshot.sectionIdentifiers[sourceIndexPath.section]) - else { return nil } - - let sectionIdentifier = oldSnapshot.sectionIdentifiers[sourceIndexPath.section] - let item = oldSnapshot.itemIdentifiers(inSection: sectionIdentifier)[sourceIndexPath.row] - - guard let targetIndexPathRow = newSnapshot.indexOfItem(item), - let newSectionIdentifier = newSnapshot.sectionIdentifier(containingItem: item), - let targetIndexPathSection = newSnapshot.indexOfSection(newSectionIdentifier) - else { return nil } - - let targetIndexPath = IndexPath(row: targetIndexPathRow, section: targetIndexPathSection) - - return Difference( - item: item, - sourceIndexPath: sourceIndexPath, - sourceDistanceToTableViewTopEdge: sourceDistanceToTableViewTopEdge, - targetIndexPath: targetIndexPath - ) - } +// struct Difference { +// let item: StatusItem +// let sourceIndexPath: IndexPath +// let sourceDistanceToTableViewTopEdge: CGFloat +// let targetIndexPath: IndexPath +// } +// +// @MainActor private func calculateReloadSnapshotDifference( +// tableView: UITableView, +// oldSnapshot: NSDiffableDataSourceSnapshot, +// newSnapshot: NSDiffableDataSourceSnapshot +// ) -> Difference? { +// guard oldSnapshot.numberOfItems != 0 else { return nil } +// guard let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows?.sorted() else { return nil } +// +// // find index of the first visible item in both old and new snapshot +// var _index: Int? +// let items = oldSnapshot.itemIdentifiers(inSection: .main) +// for (i, item) in items.enumerated() { +// guard let indexPath = indexPathsForVisibleRows.first(where: { $0.row == i }) else { continue } +// guard newSnapshot.indexOfItem(item) != nil else { continue } +// let rectForCell = tableView.rectForRow(at: indexPath) +// let distanceToTableViewTopEdge = tableView.convert(rectForCell, to: nil).origin.y - tableView.safeAreaInsets.top +// guard distanceToTableViewTopEdge >= 0 else { continue } +// _index = i +// break +// } +// +// guard let index = _index else { return nil } +// let sourceIndexPath = IndexPath(row: index, section: 0) +// +// let rectForSourceItemCell = tableView.rectForRow(at: sourceIndexPath) +// let sourceDistanceToTableViewTopEdge = tableView.convert(rectForSourceItemCell, to: nil).origin.y - tableView.safeAreaInsets.top +// +// guard sourceIndexPath.section < oldSnapshot.numberOfSections, +// sourceIndexPath.row < oldSnapshot.numberOfItems(inSection: oldSnapshot.sectionIdentifiers[sourceIndexPath.section]) +// else { return nil } +// +// let sectionIdentifier = oldSnapshot.sectionIdentifiers[sourceIndexPath.section] +// let item = oldSnapshot.itemIdentifiers(inSection: sectionIdentifier)[sourceIndexPath.row] +// +// guard let targetIndexPathRow = newSnapshot.indexOfItem(item), +// let newSectionIdentifier = newSnapshot.sectionIdentifier(containingItem: item), +// let targetIndexPathSection = newSnapshot.indexOfSection(newSectionIdentifier) +// else { return nil } +// +// let targetIndexPath = IndexPath(row: targetIndexPathRow, section: targetIndexPathSection) +// +// return Difference( +// item: item, +// sourceIndexPath: sourceIndexPath, +// sourceDistanceToTableViewTopEdge: sourceDistanceToTableViewTopEdge, +// targetIndexPath: targetIndexPath +// ) +// } } diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel+LoadThreadState.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel+LoadThreadState.swift index e63fe69f..32cc626b 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel+LoadThreadState.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel+LoadThreadState.swift @@ -48,21 +48,21 @@ extension StatusThreadViewModel.LoadThreadState { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard case let .root(threadContext) = viewModel.root.value else { - assertionFailure() - stateMachine.enter(PrepareFail.self) - return - } - - Task { - switch threadContext.status { - case .twitter(let record): - await prepareTwitterStatusThread(record: record) - case .mastodon(let record): - await prepareMastodonStatusThread(record: record) - } - } +// guard let viewModel = viewModel, let stateMachine = stateMachine else { return } +// guard case let .root(threadContext) = viewModel.root.value else { +// assertionFailure() +// stateMachine.enter(PrepareFail.self) +// return +// } +// +// Task { +// switch threadContext.status { +// case .twitter(let record): +// await prepareTwitterStatusThread(record: record) +// case .mastodon(let record): +// await prepareMastodonStatusThread(record: record) +// } +// } } // prepare ThreadContext @@ -70,97 +70,97 @@ extension StatusThreadViewModel.LoadThreadState { // The conversationID is V2 only API. // Needs query conversationID via V2 endpoint if the status persisted from V1 API. func prepareTwitterStatusThread(record: ManagedObjectRecord) async { - guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - - let managedObjectContext = viewModel.context.managedObjectContext - let _twitterConversation: StatusThreadViewModel.ThreadContext.TwitterConversation? = await managedObjectContext.perform { - guard let _status = record.object(in: managedObjectContext) else { return nil } - self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): resolve status \(_status.id) in local DB") - - // Note: - // make sure unwrap the repost wrapper - let status = _status.repost ?? _status - self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): resolve conversationID \(status.conversationID ?? "")") - return StatusThreadViewModel.ThreadContext.TwitterConversation( - statusID: status.id, - authorID: status.author.id, - authorUsername: status.author.username, - createdAt: status.createdAt, - conversationID: status.conversationID - ) - } - guard let twitterConversation = _twitterConversation else { - stateMachine.enter(PrepareFail.self) - return - } - - if twitterConversation.conversationID == nil { - // fetch conversationID if not exist - guard case let .twitter(authenticationContext) = viewModel.authContext.authenticationContext else { - await enter(state: PrepareFail.self) - return - } - do { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetching conversationID of \(twitterConversation.statusID)...") - let response = try await viewModel.context.apiService.twitterStatus( - statusIDs: [twitterConversation.statusID], - authenticationContext: authenticationContext - ) - guard let conversationID = response.value.data?.first?.conversationID else { - // assertionFailure() - await enter(state: PrepareFail.self) - return - } - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch conversationID: \(conversationID)") - let newTwitterConversation = StatusThreadViewModel.ThreadContext.TwitterConversation( - statusID: twitterConversation.statusID, - authorID: twitterConversation.authorID, - authorUsername: twitterConversation.authorUsername, - createdAt: twitterConversation.createdAt, - conversationID: conversationID - ) - viewModel.threadContext.value = .twitter(newTwitterConversation) - await enter(state: Idle.self) - await enter(state: Loading.self) - } catch { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch conversationID failure: \(error.localizedDescription)") - await enter(state: PrepareFail.self) - } - } else { - // use cached conversationID - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch cached conversationID: \(twitterConversation.conversationID ?? "")") - viewModel.threadContext.value = .twitter(twitterConversation) - await enter(state: Idle.self) - await enter(state: Loading.self) - } +// guard let viewModel = viewModel, let stateMachine = stateMachine else { return } +// +// let managedObjectContext = viewModel.context.managedObjectContext +// let _twitterConversation: StatusThreadViewModel.ThreadContext.TwitterConversation? = await managedObjectContext.perform { +// guard let _status = record.object(in: managedObjectContext) else { return nil } +// self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): resolve status \(_status.id) in local DB") +// +// // Note: +// // make sure unwrap the repost wrapper +// let status = _status.repost ?? _status +// self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): resolve conversationID \(status.conversationID ?? "")") +// return StatusThreadViewModel.ThreadContext.TwitterConversation( +// statusID: status.id, +// authorID: status.author.id, +// authorUsername: status.author.username, +// createdAt: status.createdAt, +// conversationID: status.conversationID +// ) +// } +// guard let twitterConversation = _twitterConversation else { +// stateMachine.enter(PrepareFail.self) +// return +// } +// +// if twitterConversation.conversationID == nil { +// // fetch conversationID if not exist +// guard case let .twitter(authenticationContext) = viewModel.authContext.authenticationContext else { +// await enter(state: PrepareFail.self) +// return +// } +// do { +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetching conversationID of \(twitterConversation.statusID)...") +// let response = try await viewModel.context.apiService.twitterStatus( +// statusIDs: [twitterConversation.statusID], +// authenticationContext: authenticationContext +// ) +// guard let conversationID = response.value.data?.first?.conversationID else { +// // assertionFailure() +// await enter(state: PrepareFail.self) +// return +// } +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch conversationID: \(conversationID)") +// let newTwitterConversation = StatusThreadViewModel.ThreadContext.TwitterConversation( +// statusID: twitterConversation.statusID, +// authorID: twitterConversation.authorID, +// authorUsername: twitterConversation.authorUsername, +// createdAt: twitterConversation.createdAt, +// conversationID: conversationID +// ) +// viewModel.threadContext.value = .twitter(newTwitterConversation) +// await enter(state: Idle.self) +// await enter(state: Loading.self) +// } catch { +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch conversationID failure: \(error.localizedDescription)") +// await enter(state: PrepareFail.self) +// } +// } else { +// // use cached conversationID +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch cached conversationID: \(twitterConversation.conversationID ?? "")") +// viewModel.threadContext.value = .twitter(twitterConversation) +// await enter(state: Idle.self) +// await enter(state: Loading.self) +// } } func prepareMastodonStatusThread(record: ManagedObjectRecord) async { - guard let viewModel = viewModel else { return } - - let managedObjectContext = viewModel.context.managedObjectContext - let _mastodonContext: StatusThreadViewModel.ThreadContext.MastodonContext? = await managedObjectContext.perform { - guard let _status = record.object(in: managedObjectContext) else { return nil } - - // Note: - // make sure unwrap the repost wrapper - let status = _status.repost ?? _status - return StatusThreadViewModel.ThreadContext.MastodonContext( - domain: status.domain, - contextID: status.id, - replyToStatusID: status.replyToStatusID - ) - } - - guard let mastodonContext = _mastodonContext else { - await enter(state: PrepareFail.self) - return - } - - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch cached contextID: \(mastodonContext.contextID)") - viewModel.threadContext.value = .mastodon(mastodonContext) - await enter(state: Idle.self) - await enter(state: Loading.self) +// guard let viewModel = viewModel else { return } +// +// let managedObjectContext = viewModel.context.managedObjectContext +// let _mastodonContext: StatusThreadViewModel.ThreadContext.MastodonContext? = await managedObjectContext.perform { +// guard let _status = record.object(in: managedObjectContext) else { return nil } +// +// // Note: +// // make sure unwrap the repost wrapper +// let status = _status.repost ?? _status +// return StatusThreadViewModel.ThreadContext.MastodonContext( +// domain: status.domain, +// contextID: status.id, +// replyToStatusID: status.replyToStatusID +// ) +// } +// +// guard let mastodonContext = _mastodonContext else { +// await enter(state: PrepareFail.self) +// return +// } +// +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch cached contextID: \(mastodonContext.contextID)") +// viewModel.threadContext.value = .mastodon(mastodonContext) +// await enter(state: Idle.self) +// await enter(state: Loading.self) } @MainActor @@ -226,222 +226,222 @@ extension StatusThreadViewModel.LoadThreadState { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - guard let viewModel = viewModel else { return } - guard let threadContext = viewModel.threadContext.value else { - assertionFailure() - return - } - - Task { - switch threadContext { - case .twitter(let twitterConversation): - if needsFallback { - let nodes = await fetchFallback(twitterConversation: twitterConversation) - await append(nodes: nodes) - } else { - let nodes = await fetch(twitterConversation: twitterConversation) - await append(nodes: nodes) - } - case .mastodon(let mastodonContext): - let response = await fetch(mastodonContext: mastodonContext) - await append(response: response) - } - } +// guard let viewModel = viewModel else { return } +// guard let threadContext = viewModel.threadContext.value else { +// assertionFailure() +// return +// } +// +// Task { +// switch threadContext { +// case .twitter(let twitterConversation): +// if needsFallback { +// let nodes = await fetchFallback(twitterConversation: twitterConversation) +// await append(nodes: nodes) +// } else { +// let nodes = await fetch(twitterConversation: twitterConversation) +// await append(nodes: nodes) +// } +// case .mastodon(let mastodonContext): +// let response = await fetch(mastodonContext: mastodonContext) +// await append(response: response) +// } +// } } // TODO: group into `StatusListFetchViewModel` // fetch thread via V2 API - func fetch( - twitterConversation: StatusThreadViewModel.ThreadContext.TwitterConversation - ) async -> [TwitterStatusThreadLeafViewModel.Node] { - guard let viewModel = viewModel, let stateMachine = stateMachine else { return [] } - guard case let .twitter(authenticationContext) = viewModel.authContext.authenticationContext, - let conversationID = twitterConversation.conversationID - else { - await enter(state: Fail.self) - return [] - } - - let sevenDaysAgo = Date(timeInterval: -((7 * 24 * 60 * 60) - (5 * 60)), since: Date()) - var sinceID: Twitter.Entity.V2.Tweet.ID? - var startTime: Date? - - if twitterConversation.createdAt < sevenDaysAgo { - startTime = sevenDaysAgo - } else { - sinceID = twitterConversation.statusID - } - - do { - let response = try await viewModel.context.apiService.searchTwitterStatus( - conversationID: conversationID, - authorID: twitterConversation.authorID, - sinceID: sinceID, - startTime: startTime, - nextToken: nextToken, - authenticationContext: authenticationContext - ) - let nodes = TwitterStatusThreadLeafViewModel.Node.children( - of: twitterConversation.statusID, - from: response.value - ) - - var hasMore = response.value.meta.resultCount != 0 - if let nextToken = response.value.meta.nextToken { - self.nextToken = nextToken - } else { - hasMore = false - } - - if hasMore { - await enter(state: Idle.self) - } else { - await enter(state: NoMore.self) - } - - return nodes - } catch let error as Twitter.API.Error.ResponseError where error.twitterAPIError == .rateLimitExceeded { - self.needsFallback = true - stateMachine.enter(Idle.self) - stateMachine.enter(Loading.self) - return [] - } catch { - await enter(state: Fail.self) - return [] - } - } +// func fetch( +// twitterConversation: StatusThreadViewModel.ThreadContext.TwitterConversation +// ) async -> [TwitterStatusThreadLeafViewModel.Node] { +// guard let viewModel = viewModel, let stateMachine = stateMachine else { return [] } +// guard case let .twitter(authenticationContext) = viewModel.authContext.authenticationContext, +// let conversationID = twitterConversation.conversationID +// else { +// await enter(state: Fail.self) +// return [] +// } +// +// let sevenDaysAgo = Date(timeInterval: -((7 * 24 * 60 * 60) - (5 * 60)), since: Date()) +// var sinceID: Twitter.Entity.V2.Tweet.ID? +// var startTime: Date? +// +// if twitterConversation.createdAt < sevenDaysAgo { +// startTime = sevenDaysAgo +// } else { +// sinceID = twitterConversation.statusID +// } +// +// do { +// let response = try await viewModel.context.apiService.searchTwitterStatus( +// conversationID: conversationID, +// authorID: twitterConversation.authorID, +// sinceID: sinceID, +// startTime: startTime, +// nextToken: nextToken, +// authenticationContext: authenticationContext +// ) +// let nodes = TwitterStatusThreadLeafViewModel.Node.children( +// of: twitterConversation.statusID, +// from: response.value +// ) +// +// var hasMore = response.value.meta.resultCount != 0 +// if let nextToken = response.value.meta.nextToken { +// self.nextToken = nextToken +// } else { +// hasMore = false +// } +// +// if hasMore { +// await enter(state: Idle.self) +// } else { +// await enter(state: NoMore.self) +// } +// +// return nodes +// } catch let error as Twitter.API.Error.ResponseError where error.twitterAPIError == .rateLimitExceeded { +// self.needsFallback = true +// stateMachine.enter(Idle.self) +// stateMachine.enter(Loading.self) +// return [] +// } catch { +// await enter(state: Fail.self) +// return [] +// } +// } // fetch thread via V1 API - func fetchFallback( - twitterConversation: StatusThreadViewModel.ThreadContext.TwitterConversation - ) async -> [TwitterStatusThreadLeafViewModel.Node] { - guard let viewModel = viewModel, let stateMachine = stateMachine else { return [] } - guard case let .twitter(authenticationContext) = viewModel.authContext.authenticationContext, - let _ = twitterConversation.conversationID - else { - await enter(state: Fail.self) - return [] - } - - do { - let response = try await viewModel.context.apiService.searchTwitterStatusV1( - conversationRootTweetID: twitterConversation.statusID, - authorUsername: twitterConversation.authorUsername, - maxID: maxID, - authenticationContext: authenticationContext - ) - let nodes = TwitterStatusThreadLeafViewModel.Node.children( - of: twitterConversation.statusID, - from: response.value - ) - - var hasMore = false - if let nextResult = response.value.searchMetadata.nextResults, - let components = URLComponents(string: nextResult), - let maxID = components.queryItems?.first(where: { $0.name == "max_id" })?.value, - maxID != self.maxID - { - self.maxID = maxID - hasMore = !(response.value.statuses ?? []).isEmpty - } - - if hasMore { - await enter(state: Idle.self) - } else { - await enter(state: NoMore.self) - } - - return nodes - } catch let error as Twitter.API.Error.ResponseError where error.twitterAPIError == .rateLimitExceeded { - self.needsFallback = true - stateMachine.enter(Idle.self) - stateMachine.enter(Loading.self) - return [] - } catch { - await enter(state: Fail.self) - return [] - } - } +// func fetchFallback( +// twitterConversation: StatusThreadViewModel.ThreadContext.TwitterConversation +// ) async -> [TwitterStatusThreadLeafViewModel.Node] { +// guard let viewModel = viewModel, let stateMachine = stateMachine else { return [] } +// guard case let .twitter(authenticationContext) = viewModel.authContext.authenticationContext, +// let _ = twitterConversation.conversationID +// else { +// await enter(state: Fail.self) +// return [] +// } +// +// do { +// let response = try await viewModel.context.apiService.searchTwitterStatusV1( +// conversationRootTweetID: twitterConversation.statusID, +// authorUsername: twitterConversation.authorUsername, +// maxID: maxID, +// authenticationContext: authenticationContext +// ) +// let nodes = TwitterStatusThreadLeafViewModel.Node.children( +// of: twitterConversation.statusID, +// from: response.value +// ) +// +// var hasMore = false +// if let nextResult = response.value.searchMetadata.nextResults, +// let components = URLComponents(string: nextResult), +// let maxID = components.queryItems?.first(where: { $0.name == "max_id" })?.value, +// maxID != self.maxID +// { +// self.maxID = maxID +// hasMore = !(response.value.statuses ?? []).isEmpty +// } +// +// if hasMore { +// await enter(state: Idle.self) +// } else { +// await enter(state: NoMore.self) +// } +// +// return nodes +// } catch let error as Twitter.API.Error.ResponseError where error.twitterAPIError == .rateLimitExceeded { +// self.needsFallback = true +// stateMachine.enter(Idle.self) +// stateMachine.enter(Loading.self) +// return [] +// } catch { +// await enter(state: Fail.self) +// return [] +// } +// } @MainActor private func append(nodes: [TwitterStatusThreadLeafViewModel.Node]) async { guard let viewModel = viewModel else { return } - viewModel.twitterStatusThreadLeafViewModel.append(nodes: nodes) +// viewModel.twitterStatusThreadLeafViewModel.append(nodes: nodes) } @MainActor private func append(response: MastodonContextResponse) async { guard let viewModel = viewModel else { return } - viewModel.mastodonStatusThreadViewModel.appendAncestor( - domain: response.domain, - nodes: response.ancestorNodes - ) - - viewModel.mastodonStatusThreadViewModel.appendDescendant( - domain: response.domain, - nodes: response.descendantNodes - ) +// viewModel.mastodonStatusThreadViewModel.appendAncestor( +// domain: response.domain, +// nodes: response.ancestorNodes +// ) +// +// viewModel.mastodonStatusThreadViewModel.appendDescendant( +// domain: response.domain, +// nodes: response.descendantNodes +// ) } struct MastodonContextResponse { let domain: String - let ancestorNodes: [MastodonStatusThreadViewModel.Node] - let descendantNodes: [MastodonStatusThreadViewModel.Node] +// let ancestorNodes: [MastodonStatusThreadViewModel.Node] +// let descendantNodes: [MastodonStatusThreadViewModel.Node] } // fetch thread - func fetch( - mastodonContext: StatusThreadViewModel.ThreadContext.MastodonContext - ) async -> MastodonContextResponse { - guard let viewModel = viewModel else { - return MastodonContextResponse( - domain: "", - ancestorNodes: [], - descendantNodes: [] - ) - } - guard case let .mastodon(authenticationContext) = viewModel.authContext.authenticationContext - else { - await enter(state: Fail.self) - return MastodonContextResponse( - domain: "", - ancestorNodes: [], - descendantNodes: [] - ) - } - - do { - let response = try await viewModel.context.apiService.mastodonStatusContext( - statusID: mastodonContext.contextID, - authenticationContext: authenticationContext - ) - let ancestorNodes = MastodonStatusThreadViewModel.Node.replyToThread( - for: mastodonContext.replyToStatusID, - from: response.value.ancestors - ) - let descendantNodes = MastodonStatusThreadViewModel.Node.children( - of: mastodonContext.contextID, - from: response.value.descendants - ) - - // update state - await enter(state: NoMore.self) - - return MastodonContextResponse( - domain: mastodonContext.domain, - ancestorNodes: ancestorNodes, - descendantNodes: descendantNodes - ) - } catch { - await enter(state: Fail.self) - return MastodonContextResponse( - domain: "", - ancestorNodes: [], - descendantNodes: [] - ) - } - } +// func fetch( +// mastodonContext: StatusThreadViewModel.ThreadContext.MastodonContext +// ) async -> MastodonContextResponse { +// guard let viewModel = viewModel else { +// return MastodonContextResponse( +// domain: "", +// ancestorNodes: [], +// descendantNodes: [] +// ) +// } +// guard case let .mastodon(authenticationContext) = viewModel.authContext.authenticationContext +// else { +// await enter(state: Fail.self) +// return MastodonContextResponse( +// domain: "", +// ancestorNodes: [], +// descendantNodes: [] +// ) +// } +// +// do { +// let response = try await viewModel.context.apiService.mastodonStatusContext( +// statusID: mastodonContext.contextID, +// authenticationContext: authenticationContext +// ) +// let ancestorNodes = MastodonStatusThreadViewModel.Node.replyToThread( +// for: mastodonContext.replyToStatusID, +// from: response.value.ancestors +// ) +// let descendantNodes = MastodonStatusThreadViewModel.Node.children( +// of: mastodonContext.contextID, +// from: response.value.descendants +// ) +// +// // update state +// await enter(state: NoMore.self) +// +// return MastodonContextResponse( +// domain: mastodonContext.domain, +// ancestorNodes: ancestorNodes, +// descendantNodes: descendantNodes +// ) +// } catch { +// await enter(state: Fail.self) +// return MastodonContextResponse( +// domain: "", +// ancestorNodes: [], +// descendantNodes: [] +// ) +// } +// } @MainActor func enter(state: StatusThreadViewModel.LoadThreadState.Type) { diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift index 8ea96f35..0d66f1f2 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift @@ -16,158 +16,219 @@ import CoreData import CoreDataStack import TwidereCore +@MainActor final class StatusThreadViewModel { var disposeBag = Set() let logger = Logger(subsystem: "StatusThreadViewModel", category: "ViewModel") + @Published public var viewLayoutFrame = ViewLayoutFrame() + + let conversationRootTableViewCell = StatusTableViewCell() + // input let context: AppContext let authContext: AuthContext - let twitterStatusThreadReplyViewModel: TwitterStatusThreadReplyViewModel - let twitterStatusThreadLeafViewModel: TwitterStatusThreadLeafViewModel - let mastodonStatusThreadViewModel: MastodonStatusThreadViewModel - let topListBatchFetchViewModel = ListBatchFetchViewModel(direction: .top) - let bottomListBatchFetchViewModel = ListBatchFetchViewModel(direction: .bottom) - let viewDidAppear = PassthroughSubject() + let kind: Kind + +// let twitterStatusThreadReplyViewModel: TwitterStatusThreadReplyViewModel +// let twitterStatusThreadLeafViewModel: TwitterStatusThreadLeafViewModel +// let mastodonStatusThreadViewModel: MastodonStatusThreadViewModel +// let topListBatchFetchViewModel = ListBatchFetchViewModel(direction: .top) +// let bottomListBatchFetchViewModel = ListBatchFetchViewModel(direction: .bottom) +// let viewDidAppear = PassthroughSubject() - @Published public var viewLayoutFrame = ViewLayoutFrame() // output - var diffableDataSource: UITableViewDiffableDataSource? - var root: CurrentValueSubject - var threadContext = CurrentValueSubject(nil) - @Published var replies: [StatusItem] = [] - @Published var leafs: [StatusItem] = [] - @Published var hasReplyTo = false + var diffableDataSource: UITableViewDiffableDataSource? + + @Published private(set) var status: StatusObject? + @Published private(set) var statusViewModel: StatusView.ViewModel? + +// var root: CurrentValueSubject +// var threadContext = CurrentValueSubject(nil) +// @Published var replies: [StatusItem] = [] +// @Published var leafs: [StatusItem] = [] +// @Published var hasReplyTo = false // thread - @MainActor private(set) lazy var loadThreadStateMachine: GKStateMachine = { - let stateMachine = GKStateMachine(states: [ - LoadThreadState.Initial(viewModel: self), - LoadThreadState.Prepare(viewModel: self), - LoadThreadState.PrepareFail(viewModel: self), - LoadThreadState.Idle(viewModel: self), - LoadThreadState.Loading(viewModel: self), - LoadThreadState.Fail(viewModel: self), - LoadThreadState.NoMore(viewModel: self), - - ]) - stateMachine.enter(LoadThreadState.Initial.self) - return stateMachine - }() +// @MainActor private(set) lazy var loadThreadStateMachine: GKStateMachine = { +// let stateMachine = GKStateMachine(states: [ +// LoadThreadState.Initial(viewModel: self), +// LoadThreadState.Prepare(viewModel: self), +// LoadThreadState.PrepareFail(viewModel: self), +// LoadThreadState.Idle(viewModel: self), +// LoadThreadState.Loading(viewModel: self), +// LoadThreadState.Fail(viewModel: self), +// LoadThreadState.NoMore(viewModel: self), +// +// ]) +// stateMachine.enter(LoadThreadState.Initial.self) +// return stateMachine +// }() - private init( + public init( context: AppContext, authContext: AuthContext, - optionalRoot: StatusItem.Thread? + kind: Kind ) { self.context = context self.authContext = authContext - self.twitterStatusThreadReplyViewModel = TwitterStatusThreadReplyViewModel(context: context, authContext: authContext) - self.twitterStatusThreadLeafViewModel = TwitterStatusThreadLeafViewModel(context: context) - self.mastodonStatusThreadViewModel = MastodonStatusThreadViewModel(context: context) - self.root = CurrentValueSubject(optionalRoot) + self.kind = kind // end init - viewDidAppear - .subscribe(twitterStatusThreadReplyViewModel.viewDidAppear) - .store(in: &disposeBag) - - // TODO: handle lazy thread loading - hasReplyTo = { - guard case let .root(threadContext) = optionalRoot else { return false } - guard let status = threadContext.status.object(in: context.managedObjectContext) else { return false } - switch status { - case .twitter(let _status): - let status = _status.repost ?? _status - return status.replyToStatusID != nil - case .mastodon(let _status): - let status = _status.repost ?? _status - return status.replyToStatusID != nil - } - }() - - ManagedObjectObserver.observe(context: context.managedObjectContext) - .sink(receiveCompletion: { completion in - // do nohting - }, receiveValue: { [weak self] changes in - guard let self = self else { return } - - let objectIDs: [NSManagedObjectID] = changes.changeTypes.compactMap { changeType in - guard case let .delete(object) = changeType else { return nil } - return object.objectID - } - - self.delete(objectIDs: objectIDs) - }) - .store(in: &disposeBag) - - Publishers.CombineLatest( - twitterStatusThreadReplyViewModel.$items, - mastodonStatusThreadViewModel.ancestors - ) - .map { $0 + $1 } - .assign(to: &$replies) + switch kind { + case .status(let status): + update(status: status) + case .twitter, .mastodon: + break + } + + // self.twitterStatusThreadReplyViewModel = TwitterStatusThreadReplyViewModel(context: context, authContext: authContext) +// self.twitterStatusThreadLeafViewModel = TwitterStatusThreadLeafViewModel(context: context) +// self.mastodonStatusThreadViewModel = MastodonStatusThreadViewModel(context: context) +// self.root = CurrentValueSubject(optionalRoot) - Publishers.CombineLatest( - twitterStatusThreadLeafViewModel.items, - mastodonStatusThreadViewModel.descendants - ) - .map { $0 + $1 } - .assign(to: &$leafs) - } - - convenience init( - context: AppContext, - authContext: AuthContext, - root: StatusItem.Thread - ) { - self.init( - context: context, - authContext: authContext, - optionalRoot: root - ) +// viewDidAppear +// .subscribe(twitterStatusThreadReplyViewModel.viewDidAppear) +// .store(in: &disposeBag) +// +// // TODO: handle lazy thread loading +// hasReplyTo = { +// guard case let .root(threadContext) = optionalRoot else { return false } +// guard let status = threadContext.status.object(in: context.managedObjectContext) else { return false } +// switch status { +// case .twitter(let _status): +// let status = _status.repost ?? _status +// return status.replyToStatusID != nil +// case .mastodon(let _status): +// let status = _status.repost ?? _status +// return status.replyToStatusID != nil +// } +// }() +// +// ManagedObjectObserver.observe(context: context.managedObjectContext) +// .sink(receiveCompletion: { completion in +// // do nohting +// }, receiveValue: { [weak self] changes in +// guard let self = self else { return } +// +// let objectIDs: [NSManagedObjectID] = changes.changeTypes.compactMap { changeType in +// guard case let .delete(object) = changeType else { return nil } +// return object.objectID +// } +// +// self.delete(objectIDs: objectIDs) +// }) +// .store(in: &disposeBag) +// +// Publishers.CombineLatest( +// twitterStatusThreadReplyViewModel.$items, +// mastodonStatusThreadViewModel.ancestors +// ) +// .map { $0 + $1 } +// .assign(to: &$replies) +// +// Publishers.CombineLatest( +// twitterStatusThreadLeafViewModel.items, +// mastodonStatusThreadViewModel.descendants +// ) +// .map { $0 + $1 } +// .assign(to: &$leafs) } } extension StatusThreadViewModel { - enum ThreadContext { - case twitter(TwitterConversation) - case mastodon(MastodonContext) + enum Kind { + case status(StatusRecord) + case twitter(Twitter.Entity.V2.Tweet.ID) + case mastodon(Mastodon.Entity.Status.ID) + } + + public enum Section: Hashable { + case main + } // end Section + + public enum Item: Hashable { + // case + case status(status: StatusRecord) + case root + case topLoader + case bottomLoader + + public static func == (lhs: StatusThreadViewModel.Item, rhs: StatusThreadViewModel.Item) -> Bool { + switch (lhs, rhs) { + case (.status(let lhs), .status(let rhs)): + return lhs.objectID == rhs.objectID + case (.root, .root): + return true + case (.topLoader, .topLoader): + return true + case (.bottomLoader, .bottomLoader): + return true + default: + return false + } + } - struct TwitterConversation { - let statusID: Twitter.Entity.V2.Tweet.ID - let authorID: Twitter.Entity.User.ID - let authorUsername: String - let createdAt: Date - - // V2 only - let conversationID: Twitter.Entity.V2.Tweet.ConversationID? + public func hash(into hasher: inout Hasher) { + switch self { + case .status(let status): + hasher.combine(String(describing: Item.status.self)) + hasher.combine(status.objectID) + case .root: + hasher.combine(String(describing: Item.root.self)) + case .topLoader: + hasher.combine(String(describing: Item.topLoader.self)) + case .bottomLoader: + hasher.combine(String(describing: Item.bottomLoader.self)) + } } - struct MastodonContext { - let domain: String - let contextID: Mastodon.Entity.Status.ID - let replyToStatusID: Mastodon.Entity.Status.ID? + public var isTransient: Bool { + switch self { + case .topLoader, .bottomLoader: + return true + default: + return false + } } - } + } // end Item } extension StatusThreadViewModel { - func delete(objectIDs: [NSManagedObjectID]) { - if let root = root.value, - case let .root(threadContext) = root, - objectIDs.contains(threadContext.status.objectID) - { - self.root.value = nil - self.twitterStatusThreadReplyViewModel.root = nil + @MainActor + func update(status record: StatusRecord) { + guard statusViewModel == nil else { return } + guard let status = record.object(in: context.managedObjectContext) else { + assertionFailure() + return } - - self.twitterStatusThreadReplyViewModel.delete(objectIDs: objectIDs) - self.twitterStatusThreadLeafViewModel.delete(objectIDs: objectIDs) - self.mastodonStatusThreadViewModel.delete(objectIDs: objectIDs) + let _statusViewViewModel = StatusView.ViewModel( + status: status, + authContext: authContext, + kind: .conversationRoot, + delegate: conversationRootTableViewCell, + viewLayoutFramePublisher: $viewLayoutFrame + ) + self.statusViewModel = _statusViewViewModel } } + +extension StatusThreadViewModel { +// func delete(objectIDs: [NSManagedObjectID]) { +// if let root = root.value, +// case let .root(threadContext) = root, +// objectIDs.contains(threadContext.status.objectID) +// { +// self.root.value = nil +// self.twitterStatusThreadReplyViewModel.root = nil +// } +// +// self.twitterStatusThreadReplyViewModel.delete(objectIDs: objectIDs) +// self.twitterStatusThreadLeafViewModel.delete(objectIDs: objectIDs) +// self.mastodonStatusThreadViewModel.delete(objectIDs: objectIDs) +// } +} diff --git a/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadLeafViewModel.swift b/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadLeafViewModel.swift index fbbef0a4..8465128f 100644 --- a/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadLeafViewModel.swift +++ b/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadLeafViewModel.swift @@ -28,24 +28,24 @@ final class TwitterStatusThreadLeafViewModel { init(context: AppContext) { self.context = context - Publishers.CombineLatest( - $_items, - $deletedObjectIDs - ) - .sink { [weak self] items, deletedObjectIDs in - guard let self = self else { return } - let newItems = items.filter { item in - switch item { - case .thread(let thread): - return !deletedObjectIDs.contains(thread.statusRecord.objectID) - default: - assertionFailure() - return false - } - } - self.items.value = newItems - } - .store(in: &disposeBag) +// Publishers.CombineLatest( +// $_items, +// $deletedObjectIDs +// ) +// .sink { [weak self] items, deletedObjectIDs in +// guard let self = self else { return } +// let newItems = items.filter { item in +// switch item { +// case .thread(let thread): +// return !deletedObjectIDs.contains(thread.statusRecord.objectID) +// default: +// assertionFailure() +// return false +// } +// } +// self.items.value = newItems +// } +// .store(in: &disposeBag) } deinit { @@ -58,55 +58,55 @@ extension TwitterStatusThreadLeafViewModel { // FIXME: handle node remove func append(nodes: [Node]) { - let childrenIDs = nodes - .map { node in [node.statusID, node.children.first?.statusID].compactMap { $0 } } - .flatMap { $0 } - var dictionary: [TwitterStatus.ID: TwitterStatus] = [:] - do { - let request = TwitterStatus.sortedFetchRequest - request.predicate = TwitterStatus.predicate(ids: childrenIDs) - let statuses = try self.context.managedObjectContext.fetch(request) - for status in statuses { - dictionary[status.id] = status - } - } catch { - os_log("%{public}s[%{public}ld], %{public}s: fetch conversation fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) - return - } - - var newItems: [StatusItem] = [] - for node in nodes { - guard let status = dictionary[node.statusID] else { continue } - // first tier - let record = ManagedObjectRecord(objectID: status.objectID) - let context = StatusItem.Thread.Context( - status: .twitter(record: record) - ) - let item = StatusItem.thread(.leaf(context: context)) - newItems.append(item) - - // second tier - if let child = node.children.first { - guard let secondaryStatus = dictionary[child.statusID] else { continue } - let secondaryRecord = ManagedObjectRecord(objectID: secondaryStatus.objectID) - let secondaryContext = StatusItem.Thread.Context( - status: .twitter(record: secondaryRecord), - displayUpperConversationLink: true - ) - let secondaryItem = StatusItem.thread(.leaf(context: secondaryContext)) - newItems.append(secondaryItem) - - // update first tier context - context.displayBottomConversationLink = true - } - } - - var items = self._items - for item in newItems { - guard !items.contains(item) else { continue } - items.append(item) - } - self._items = items +// let childrenIDs = nodes +// .map { node in [node.statusID, node.children.first?.statusID].compactMap { $0 } } +// .flatMap { $0 } +// var dictionary: [TwitterStatus.ID: TwitterStatus] = [:] +// do { +// let request = TwitterStatus.sortedFetchRequest +// request.predicate = TwitterStatus.predicate(ids: childrenIDs) +// let statuses = try self.context.managedObjectContext.fetch(request) +// for status in statuses { +// dictionary[status.id] = status +// } +// } catch { +// os_log("%{public}s[%{public}ld], %{public}s: fetch conversation fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) +// return +// } +// +// var newItems: [StatusItem] = [] +// for node in nodes { +// guard let status = dictionary[node.statusID] else { continue } +// // first tier +// let record = ManagedObjectRecord(objectID: status.objectID) +// let context = StatusItem.Thread.Context( +// status: .twitter(record: record) +// ) +// let item = StatusItem.thread(.leaf(context: context)) +// newItems.append(item) +// +// // second tier +// if let child = node.children.first { +// guard let secondaryStatus = dictionary[child.statusID] else { continue } +// let secondaryRecord = ManagedObjectRecord(objectID: secondaryStatus.objectID) +// let secondaryContext = StatusItem.Thread.Context( +// status: .twitter(record: secondaryRecord), +// displayUpperConversationLink: true +// ) +// let secondaryItem = StatusItem.thread(.leaf(context: secondaryContext)) +// newItems.append(secondaryItem) +// +// // update first tier context +// context.displayBottomConversationLink = true +// } +// } +// +// var items = self._items +// for item in newItems { +// guard !items.contains(item) else { continue } +// items.append(item) +// } +// self._items = items } } diff --git a/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadReplyViewModel.swift b/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadReplyViewModel.swift index 3e5e56a7..4587cd4c 100644 --- a/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadReplyViewModel.swift +++ b/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadReplyViewModel.swift @@ -53,44 +53,44 @@ final class TwitterStatusThreadReplyViewModel { self.authContext = authContext // end init - Publishers.CombineLatest( - $root, - viewDidAppear.eraseToAnyPublisher() - ) - .receive(on: DispatchQueue.main) // <- required here due to state machine access $root value - .sink { [weak self] root, _ in - guard let self = self else { return } - guard root != nil else { return } - - Task { - if await self.stateMachine.currentState is State.Initial { - await self.stateMachine.enter(State.Prepare.self) - } - } - } - .store(in: &disposeBag) - - Publishers.CombineLatest( - $nodes, - $deletedObjectIDs - ) - .map { nodes, deletedObjectIDs in - var items: [StatusItem] = [] - for (i, node) in nodes.enumerated() { - guard case let .success(record) = node.status else { continue } - guard !deletedObjectIDs.contains(record.objectID) else { continue } - let isLast = i == nodes.count - 1 - let context = StatusItem.Thread.Context( - status: .twitter(record: record), - displayUpperConversationLink: !isLast, - displayBottomConversationLink: true - ) - let thread = StatusItem.Thread.reply(context: context) - items.append(.thread(thread)) - } - return items - } - .assign(to: &$items) +// Publishers.CombineLatest( +// $root, +// viewDidAppear.eraseToAnyPublisher() +// ) +// .receive(on: DispatchQueue.main) // <- required here due to state machine access $root value +// .sink { [weak self] root, _ in +// guard let self = self else { return } +// guard root != nil else { return } +// +// Task { +// if await self.stateMachine.currentState is State.Initial { +// await self.stateMachine.enter(State.Prepare.self) +// } +// } +// } +// .store(in: &disposeBag) +// +// Publishers.CombineLatest( +// $nodes, +// $deletedObjectIDs +// ) +// .map { nodes, deletedObjectIDs in +// var items: [StatusItem] = [] +// for (i, node) in nodes.enumerated() { +// guard case let .success(record) = node.status else { continue } +// guard !deletedObjectIDs.contains(record.objectID) else { continue } +// let isLast = i == nodes.count - 1 +// let context = StatusItem.Thread.Context( +// status: .twitter(record: record), +// displayUpperConversationLink: !isLast, +// displayBottomConversationLink: true +// ) +// let thread = StatusItem.Thread.reply(context: context) +// items.append(.thread(thread)) +// } +// return items +// } +// .assign(to: &$items) } deinit { diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift index c06c5ae4..5e109e86 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift @@ -264,22 +264,11 @@ extension HomeTimelineViewController { Task { @MainActor in let authenticationContext = self.viewModel.authContext.authenticationContext switch authenticationContext { - case .twitter(let authenticationContext): - _ = try await self.context.apiService.twitterStatus( - statusIDs: [id], - authenticationContext: authenticationContext - ) - let request = TwitterStatus.sortedFetchRequest - request.predicate = TwitterStatus.predicate(id: id) - request.fetchLimit = 1 - let _status = try self.context.managedObjectContext.fetch(request).first - guard let status = _status else { - return - } + case .twitter: let statusThreadViewModel = StatusThreadViewModel( context: self.context, authContext: self.authContext, - root: .root(context: .init(status: .twitter(record: .init(objectID: status.objectID)))) + kind: .twitter(id) ) self.coordinator.present( scene: .statusThread(viewModel: statusThreadViewModel), @@ -287,9 +276,16 @@ extension HomeTimelineViewController { transition: .show ) case .mastodon: - assertionFailure("TODO:") - default: - assertionFailure() + let statusThreadViewModel = StatusThreadViewModel( + context: self.context, + authContext: self.authContext, + kind: .mastodon(id) + ) + self.coordinator.present( + scene: .statusThread(viewModel: statusThreadViewModel), + from: self, + transition: .show + ) } } // end Task } From 57e35863b84aa6158ce6161ab95421e08fdc9919 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 28 Mar 2023 18:10:40 +0800 Subject: [PATCH 052/128] feat: migrate to Twitter conversation API --- TwidereSDK/Package.swift | 2 +- .../MastodonSDK/API/Mastodon+API+Status.swift | 51 +- .../APIService+Status+Conversation.swift | 35 ++ ...p.swift => APIService+Status+Lookup.swift} | 37 ++ .../TwidereCore/State/AuthContext.swift | 48 +- .../Container/MediaGridContainerView.swift | 9 +- .../Sources/TwidereUI/Content/MediaView.swift | 1 + .../TwidereUI/Content/StatusHeaderView.swift | 13 +- .../TwidereUI/Content/StatusToolbarView.swift | 5 +- .../Content/StatusView+ViewModel.swift | 7 +- .../TwidereUI/Content/StatusView.swift | 49 +- .../Content/TimestampLabelView.swift | 2 +- .../TextViewRepresentable.swift | 33 +- .../TwitterSDK/API/Twitter+API+Guest.swift | 74 +++ .../Sources/TwitterSDK/API/Twitter+API.swift | 43 ++ .../API/{ => V1}/Twitter+API+Account.swift | 0 .../{ => V1}/Twitter+API+Application.swift | 0 .../API/{ => V1}/Twitter+API+Block.swift | 0 .../API/{ => V1}/Twitter+API+Favorites.swift | 0 .../{ => V1}/Twitter+API+Friendships.swift | 0 .../API/{ => V1}/Twitter+API+Geo.swift | 0 .../API/{ => V1}/Twitter+API+List.swift | 0 .../API/{ => V1}/Twitter+API+Lookup.swift | 0 .../{ => V1}/Twitter+API+Media+Metadata.swift | 0 .../API/{ => V1}/Twitter+API+Media.swift | 0 .../API/{ => V1}/Twitter+API+Mute.swift | 0 .../{ => V1}/Twitter+API+SavedSearch.swift | 0 .../API/{ => V1}/Twitter+API+Search.swift | 0 .../Twitter+API+Statuses+Timeline.swift | 0 .../API/{ => V1}/Twitter+API+Statuses.swift | 0 .../API/{ => V1}/Twitter+API+Trend.swift | 0 .../{ => V1}/Twitter+API+Users+Search.swift | 0 .../API/{ => V1}/Twitter+API+Users.swift | 0 .../V2/Twitter+API+V2+Status+Timeline.swift | 244 +++++++++ .../{ => V1}/Twitter+Entity+Coordinates.swift | 2 +- .../Twitter+Entity+ExtendedEntities.swift | 0 .../V1/Twitter+Entity+Internal+Tweet.swift | 99 ++++ .../Entity/V1/Twitter+Entity+Internal.swift | 12 + .../Entity/{ => V1}/Twitter+Entity+List.swift | 0 .../{ => V1}/Twitter+Entity+Place.swift | 0 .../Twitter+Entity+QuotedStatus.swift | 0 .../Twitter+Entity+RateLimitStatus.swift | 0 .../Twitter+Entity+Relationship.swift | 0 .../Twitter+Entity+RetweetedStatus.swift | 0 .../{ => V1}/Twitter+Entity+SavedSearch.swift | 0 .../{ => V1}/Twitter+Entity+Trend+Place.swift | 0 .../{ => V1}/Twitter+Entity+Trend.swift | 0 .../Twitter+Entity+Tweet+Entities.swift | 0 .../{ => V1}/Twitter+Entity+Tweet.swift | 0 .../Twitter+Entity+User+Entities.swift | 0 .../Entity/{ => V1}/Twitter+Entity+User.swift | 0 TwidereX.xcodeproj/project.pbxproj | 31 +- ...D687BB06-F0F4-4247-8624-A87B4DA38312.plist | 22 + .../Info.plist | 21 + .../xcschemes/TwidereX - Core Data.xcscheme | 4 +- .../xcschemes/TwidereX - Performance.xcscheme | 120 +++++ .../xcschemes/TwidereX - Profile.xcscheme | 4 +- .../xcschemes/TwidereX - Release.xcscheme | 4 +- .../xcshareddata/xcschemes/TwidereX.xcscheme | 4 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- TwidereX/Diffable/Status/StatusItem.swift | 9 +- ...eadViewController+DataSourceProvider.swift | 2 +- .../StatusThreadViewController.swift | 42 +- .../StatusThreadViewModel+Diffable.swift | 484 ++++++++++-------- .../StatusThread/StatusThreadViewModel.swift | 274 +++++++++- .../List/ListTimelineViewModel+Diffable.swift | 27 +- ...meTimelineViewController+DebugAction.swift | 4 +- TwidereX/Supporting Files/SceneDelegate.swift | 6 +- .../TwidereX-Performance.xctestplan | 4 +- TwidereX/{ => TestPlan}/TwidereX.xctestplan | 5 +- TwidereXTests/CombineTests.swift | 125 ++--- TwidereXTests/TwidereXTests+Issue92.swift | 2 +- TwidereXTests/TwidereXTests.swift | 1 - .../TwidereXUITests+Onboarding.swift | 1 + .../TwidereXUITests+Performance.swift | 44 ++ 75 files changed, 1599 insertions(+), 411 deletions(-) create mode 100644 TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Conversation.swift rename TwidereSDK/Sources/TwidereCore/Service/APIService/{APIService+Lookup.swift => APIService+Status+Lookup.swift} (72%) create mode 100644 TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Guest.swift rename TwidereSDK/Sources/TwitterSDK/API/{ => V1}/Twitter+API+Account.swift (100%) rename TwidereSDK/Sources/TwitterSDK/API/{ => V1}/Twitter+API+Application.swift (100%) rename TwidereSDK/Sources/TwitterSDK/API/{ => V1}/Twitter+API+Block.swift (100%) rename TwidereSDK/Sources/TwitterSDK/API/{ => V1}/Twitter+API+Favorites.swift (100%) rename TwidereSDK/Sources/TwitterSDK/API/{ => V1}/Twitter+API+Friendships.swift (100%) rename TwidereSDK/Sources/TwitterSDK/API/{ => V1}/Twitter+API+Geo.swift (100%) rename TwidereSDK/Sources/TwitterSDK/API/{ => V1}/Twitter+API+List.swift (100%) rename TwidereSDK/Sources/TwitterSDK/API/{ => V1}/Twitter+API+Lookup.swift (100%) rename TwidereSDK/Sources/TwitterSDK/API/{ => V1}/Twitter+API+Media+Metadata.swift (100%) rename TwidereSDK/Sources/TwitterSDK/API/{ => V1}/Twitter+API+Media.swift (100%) rename TwidereSDK/Sources/TwitterSDK/API/{ => V1}/Twitter+API+Mute.swift (100%) rename TwidereSDK/Sources/TwitterSDK/API/{ => V1}/Twitter+API+SavedSearch.swift (100%) rename TwidereSDK/Sources/TwitterSDK/API/{ => V1}/Twitter+API+Search.swift (100%) rename TwidereSDK/Sources/TwitterSDK/API/{ => V1}/Twitter+API+Statuses+Timeline.swift (100%) rename TwidereSDK/Sources/TwitterSDK/API/{ => V1}/Twitter+API+Statuses.swift (100%) rename TwidereSDK/Sources/TwitterSDK/API/{ => V1}/Twitter+API+Trend.swift (100%) rename TwidereSDK/Sources/TwitterSDK/API/{ => V1}/Twitter+API+Users+Search.swift (100%) rename TwidereSDK/Sources/TwitterSDK/API/{ => V1}/Twitter+API+Users.swift (100%) create mode 100644 TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Status+Timeline.swift rename TwidereSDK/Sources/TwitterSDK/Entity/{ => V1}/Twitter+Entity+Coordinates.swift (95%) rename TwidereSDK/Sources/TwitterSDK/Entity/{ => V1}/Twitter+Entity+ExtendedEntities.swift (100%) create mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Internal+Tweet.swift create mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Internal.swift rename TwidereSDK/Sources/TwitterSDK/Entity/{ => V1}/Twitter+Entity+List.swift (100%) rename TwidereSDK/Sources/TwitterSDK/Entity/{ => V1}/Twitter+Entity+Place.swift (100%) rename TwidereSDK/Sources/TwitterSDK/Entity/{ => V1}/Twitter+Entity+QuotedStatus.swift (100%) rename TwidereSDK/Sources/TwitterSDK/Entity/{ => V1}/Twitter+Entity+RateLimitStatus.swift (100%) rename TwidereSDK/Sources/TwitterSDK/Entity/{ => V1}/Twitter+Entity+Relationship.swift (100%) rename TwidereSDK/Sources/TwitterSDK/Entity/{ => V1}/Twitter+Entity+RetweetedStatus.swift (100%) rename TwidereSDK/Sources/TwitterSDK/Entity/{ => V1}/Twitter+Entity+SavedSearch.swift (100%) rename TwidereSDK/Sources/TwitterSDK/Entity/{ => V1}/Twitter+Entity+Trend+Place.swift (100%) rename TwidereSDK/Sources/TwitterSDK/Entity/{ => V1}/Twitter+Entity+Trend.swift (100%) rename TwidereSDK/Sources/TwitterSDK/Entity/{ => V1}/Twitter+Entity+Tweet+Entities.swift (100%) rename TwidereSDK/Sources/TwitterSDK/Entity/{ => V1}/Twitter+Entity+Tweet.swift (100%) rename TwidereSDK/Sources/TwitterSDK/Entity/{ => V1}/Twitter+Entity+User+Entities.swift (100%) rename TwidereSDK/Sources/TwitterSDK/Entity/{ => V1}/Twitter+Entity+User.swift (100%) create mode 100644 TwidereX.xcodeproj/xcshareddata/xcbaselines/DBDA8E3E24FCF8A7006750DC.xcbaseline/D687BB06-F0F4-4247-8624-A87B4DA38312.plist create mode 100644 TwidereX.xcodeproj/xcshareddata/xcbaselines/DBDA8E3E24FCF8A7006750DC.xcbaseline/Info.plist create mode 100644 TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX - Performance.xcscheme rename TwidereX/{ => TestPlan}/TwidereX-Performance.xctestplan (88%) rename TwidereX/{ => TestPlan}/TwidereX.xctestplan (86%) create mode 100644 TwidereXUITests/TwidereXUITests+Performance.swift diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index dcd94433..b8267618 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -30,7 +30,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-nio.git", from: "2.34.0"), .package(url: "https://github.com/Flipboard/FLAnimatedImage.git", from: "1.0.0"), .package(url: "https://github.com/MainasuK/CommonOSLog", from: "0.1.1"), - .package(url: "https://github.com/TwidereProject/MetaTextKit.git", exact: "4.4.0"), + .package(url: "https://github.com/TwidereProject/MetaTextKit.git", exact: "4.4.2"), .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.5.0"), .package(url: "https://github.com/Alamofire/AlamofireImage.git", from: "4.1.0"), .package(url: "https://github.com/Alamofire/AlamofireNetworkActivityIndicator.git", from: "3.1.0"), diff --git a/TwidereSDK/Sources/MastodonSDK/API/Mastodon+API+Status.swift b/TwidereSDK/Sources/MastodonSDK/API/Mastodon+API+Status.swift index 65908b13..7dab087c 100644 --- a/TwidereSDK/Sources/MastodonSDK/API/Mastodon+API+Status.swift +++ b/TwidereSDK/Sources/MastodonSDK/API/Mastodon+API+Status.swift @@ -113,7 +113,6 @@ extension Mastodon.API.Status { } } - extension Mastodon.API.Status { static func statusEndpointURL(domain: String, statusID: Mastodon.Entity.Status.ID) -> URL { @@ -122,6 +121,56 @@ extension Mastodon.API.Status { .appendingPathComponent(statusID) } + /// View a single status + /// + /// Obtain information about a status. + /// + /// - Since: 0.0.0 + /// - Version: 4.1.0 + /// # Last Update + /// 2023/3/28 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/statuses/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - query: `LookupStatusQuery` + /// - authorization: User token + /// - Returns: `Status` nested in the response + public static func lookup( + session: URLSession, + domain: String, + query: LookupStatusQuery, + authorization: Mastodon.API.OAuth.Authorization? + ) async throws -> Mastodon.Response.Content { + let request = Mastodon.API.request( + url: statusEndpointURL(domain: domain, statusID: query.id), + method: .GET, + query: query, + authorization: authorization + ) + let (data, response) = try await session.data(for: request, delegate: nil) + let value = try Mastodon.API.decode(type: Mastodon.Entity.Status.self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + + public struct LookupStatusQuery: JSONEncodeQuery { + + public let id: Mastodon.Entity.Status.ID + + public init( + id: Mastodon.Entity.Status.ID + ) { + self.id = id + } + + var queryItems: [URLQueryItem]? { nil } + } + +} + +extension Mastodon.API.Status { + /// Delete status /// /// Delete one of your own statuses. diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Conversation.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Conversation.swift new file mode 100644 index 00000000..1686f1e0 --- /dev/null +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Conversation.swift @@ -0,0 +1,35 @@ +// +// APIService+Status+Conversation.swift +// +// +// Created by MainasuK on 2023/3/28. +// + +import Foundation +import CoreDataStack +import TwitterSDK + +extension APIService { + public func twitterStatusConversation( + conversationRootStatusID: Twitter.Entity.V2.Tweet.ID, + query: Twitter.API.V2.Status.Timeline.TimelineQuery, + guestAuthentication: Twitter.API.Guest.GuestAuthorization, + authenticationContext: TwitterAuthenticationContext + ) async throws -> Twitter.Response.Content { + let response = try await Twitter.API.V2.Status.Timeline.conversation( + session: URLSession(configuration: .ephemeral), + statusID: conversationRootStatusID, + query: query, + authorization: guestAuthentication + ) + + let statusIDs = response.value.globalObjects.tweets.map { $0.idStr } + + _ = try await twitterStatus( + statusIDs: statusIDs, + authenticationContext: authenticationContext + ) + + return response + } +} diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Lookup.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Lookup.swift similarity index 72% rename from TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Lookup.swift rename to TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Lookup.swift index c2a74a93..15b928e9 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Lookup.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Lookup.swift @@ -9,6 +9,7 @@ import os.log import Foundation import Combine import TwitterSDK +import MastodonSDK import CoreDataStack import CommonOSLog import func QuartzCore.CACurrentMediaTime @@ -98,3 +99,39 @@ extension APIService { return response } } + +extension APIService { + public func mastodonStatus( + statusID: Mastodon.Entity.Status.ID, + authenticationContext: MastodonAuthenticationContext + ) async throws -> Mastodon.Response.Content { + let domain = authenticationContext.domain + let authorization = authenticationContext.authorization + let managedObjectContext = backgroundManagedObjectContext + + let response = try await Mastodon.API.Status.lookup( + session: session, + domain: domain, + query: Mastodon.API.Status.LookupStatusQuery(id: statusID), + authorization: authorization + ) + + try await managedObjectContext.performChanges { + let entity = response.value + let me = authenticationContext.authenticationRecord.object(in: managedObjectContext)?.user + _ = Persistence.MastodonStatus.createOrMerge( + in: managedObjectContext, + context: .init( + domain: authenticationContext.domain, + entity: entity, + me: me, + statusCache: nil, + userCache: nil, + networkDate: response.networkDate + ) + ) + } + + return response + } +} diff --git a/TwidereSDK/Sources/TwidereCore/State/AuthContext.swift b/TwidereSDK/Sources/TwidereCore/State/AuthContext.swift index ef665ff0..b5c199e4 100644 --- a/TwidereSDK/Sources/TwidereCore/State/AuthContext.swift +++ b/TwidereSDK/Sources/TwidereCore/State/AuthContext.swift @@ -6,20 +6,45 @@ // import Foundation +import Combine import CoreData import CoreDataStack import TwidereCommon +import TwitterSDK public protocol AuthContextProvider { var authContext: AuthContext { get } } public class AuthContext { - + var disposeBag = Set() + + // authentication public let authenticationContext: AuthenticationContext + // Twitter Guest + public private(set) var twitterGuestAuthorization: Twitter.API.Guest.GuestAuthorization? + private var lastTwitterGuestAuthorizationTimestamp = Date() + public init(authenticationContext: AuthenticationContext) { self.authenticationContext = authenticationContext + // end init + + Timer.publish(every: 10, on: .main, in: .common) + .autoconnect() + .sink { [weak self] _ in + guard let self = self else { + return + } + Task { + if self.twitterGuestAuthorization == nil { + try await self.refreshTwitterGuestAuthorization() + } else if abs(self.lastTwitterGuestAuthorizationTimestamp.timeIntervalSinceNow) > 1 * 60 { + try await self.refreshTwitterGuestAuthorization() + } + } // end Task + } + .store(in: &disposeBag) } public convenience init?(authenticationIndex: AuthenticationIndex) { @@ -35,6 +60,27 @@ public class AuthContext { } +extension AuthContext { + public func twitterGuestAuthorization() async throws -> Twitter.API.Guest.GuestAuthorization { + if let twitterGuestAuthorization = twitterGuestAuthorization { + return twitterGuestAuthorization + } else { + return try await refreshTwitterGuestAuthorization() + } + } + + @discardableResult + public func refreshTwitterGuestAuthorization() async throws -> Twitter.API.Guest.GuestAuthorization { + let response = try await Twitter.API.Guest.active( + session: URLSession(configuration: .ephemeral) + ) + let guestAuthorization = Twitter.API.Guest.GuestAuthorization(token: response.value.guestToken) + twitterGuestAuthorization = guestAuthorization + lastTwitterGuestAuthorizationTimestamp = Date() + return guestAuthorization + } +} + #if DEBUG extension AuthContext { public static func mock(context: AppContext) -> AuthContext? { diff --git a/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift b/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift index 90968aa7..bd34016f 100644 --- a/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift +++ b/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift @@ -22,7 +22,7 @@ public struct MediaGridContainerView: View { public let previewAction: (MediaView.ViewModel) -> Void public let previewActionWithContext: (MediaView.ViewModel, ContextMenuInteractionPreviewActionContext) -> Void - + public var body: some View { VStack { switch viewModels.count { @@ -278,7 +278,7 @@ public struct MediaViewFrameModifer: ViewModifier { let asepctRatio: CGFloat? let idealWidth: CGFloat? let idealHeight: CGFloat - + public init( asepctRatio: CGFloat?, idealWidth: CGFloat?, @@ -291,8 +291,11 @@ public struct MediaViewFrameModifer: ViewModifier { public func body(content: Content) -> some View { if let idealWidth = idealWidth { + let minHeight: CGFloat = 44 + let maxHeight = ceil(3 * idealWidth) + let height = min(maxHeight, max(minHeight, idealWidth / (asepctRatio ?? 1.0))) content - .frame(width: idealWidth, height: idealWidth / (asepctRatio ?? 1.0)) + .frame(width: idealWidth, height: height) } else { content .frame(maxHeight: idealHeight) diff --git a/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift b/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift index 444efbf0..2f531a1d 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift @@ -31,6 +31,7 @@ public struct MediaView: View { .placeholder { progress in Image(uiImage: Asset.Logo.mediaPlaceholder.image.withRenderingMode(.alwaysTemplate)) } + .aspectRatio(contentMode: .fill) .overlay { switch viewModel.mediaKind { case .animatedGIF: diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift index 7cc14349..d227c826 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift @@ -21,8 +21,8 @@ public struct StatusHeaderView: View { @ObservedObject public var viewModel: ViewModel - @State private var iconImageDimension = CGFloat.zero - + @ScaledMetric(relativeTo: .footnote) private var iconImageDimension: CGFloat = 16 + public var body: some View { HStack(spacing: .zero) { if viewModel.hasHangingAvatar { @@ -46,15 +46,6 @@ public struct StatusHeaderView: View { // do nothing } ) - .background(GeometryReader { proxy in - Color.clear.preference( - key: ViewHeightKey.self, - value: proxy.frame(in: .local).size.height - ) - }) - .onPreferenceChange(ViewHeightKey.self) { height in - self.iconImageDimension = height - } .overlay(alignment: .leading) { VectorImageView(image: viewModel.image) .frame(width: iconImageDimension, height: iconImageDimension) diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift index 1ce15a22..00d5c0ea 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift @@ -270,9 +270,7 @@ extension StatusToolbarView { let tintColor: UIColor? // output - var text: String { - Self.metric(count: count) - } + let text: String public init( handler: @escaping (Action) -> Void, @@ -286,6 +284,7 @@ extension StatusToolbarView { self.image = image self.count = count self.tintColor = tintColor + self.text = Self.metric(count: count) } public var body: some View { diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index 4c81e1e0..88cc9a32 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -40,7 +40,6 @@ extension StatusView { public weak var delegate: StatusViewDelegate? weak var parentViewModel: StatusView.ViewModel? - @Published public var authorAvatarDimension: CGFloat = .zero // output @@ -946,11 +945,7 @@ extension StatusView.ViewModel { } public var cellTopMargin: CGFloat { - return .zero -// switch kind { -// default: -// return 12 -// } + return parentViewModel == nil ? 12 : 0 } public var hasToolbar: Bool { diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index 4a86d4c0..6a298c8b 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -18,14 +18,13 @@ import TwidereCore public protocol StatusViewDelegate: AnyObject { // func statusView(_ statusView: StatusView, headerDidPressed header: UIView) -// + // func statusView(_ statusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) // func statusView(_ statusView: StatusView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) // // func statusView(_ statusView: StatusView, expandContentButtonDidPressed button: UIButton) func statusView(_ viewModel: StatusView.ViewModel, toggleContentDisplay isReveal: Bool) -// // func statusView(_ statusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) // func statusView(_ statusView: StatusView, quoteStatusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) @@ -41,15 +40,14 @@ public protocol StatusViewDelegate: AnyObject { // metric func statusView(_ viewModel: StatusView.ViewModel, statusMetricViewModel: StatusMetricView.ViewModel, statusMetricButtonDidPressed action: StatusMetricView.Action) - + // toolbar func statusView(_ viewModel: StatusView.ViewModel, statusToolbarViewModel: StatusToolbarView.ViewModel, statusToolbarButtonDidPressed action: StatusToolbarView.Action) -// + // func statusView(_ statusView: StatusView, translateButtonDidPressed button: UIButton) - + func statusView(_ viewModel: StatusView.ViewModel, viewHeightDidChange: Void) -// // // a11y // func statusView(_ statusView: StatusView, accessibilityActivate: Void) } @@ -65,8 +63,9 @@ public struct StatusView: View { @ObservedObject public private(set) var viewModel: ViewModel @Environment(\.dynamicTypeSize) var dynamicTypeSize - @State private var visibilityIconImageDimension = CGFloat.zero - + @ScaledMetric(relativeTo: .subheadline) private var visibilityIconImageDimension: CGFloat = 16 + @ScaledMetric(relativeTo: .headline) private var inlineAvatarButtonDimension: CGFloat = 20 + public init(viewModel: StatusView.ViewModel) { self.viewModel = viewModel } @@ -162,7 +161,7 @@ public struct StatusView: View { // metric if let metricViewModel = viewModel.metricViewModel { StatusMetricView(viewModel: metricViewModel) { action in - + } .padding(.vertical, 8) } @@ -245,7 +244,7 @@ public struct StatusView: View { } extension StatusView { - public var authorView: some View { + var authorView: some View { HStack(alignment: .center) { if !viewModel.hasHangingAvatar { // avatar @@ -284,15 +283,6 @@ extension StatusView { ) .fixedSize(horizontal: false, vertical: true) .layoutPriority(0.618) - .background(GeometryReader { proxy in - Color.clear.preference( - key: ViewHeightKey.self, - value: proxy.frame(in: .local).size.height - ) - }) - .onPreferenceChange(ViewHeightKey.self) { height in - self.visibilityIconImageDimension = height - } // username LabelRepresentable( metaContent: PlaintextMetaContent(string: "@" + viewModel.authorUsernme), @@ -306,20 +296,11 @@ extension StatusView { .layoutPriority(0.382) } // end nameLayout .frame(alignment: .leading) - .background(GeometryReader { proxy in - Color.clear.preference( - key: ViewHeightKey.self, - value: proxy.frame(in: .local).size.height - ) - }) - .onPreferenceChange(ViewHeightKey.self) { height in - self.viewModel.authorAvatarDimension = height - } Spacer() HStack(spacing: 6) { // mastodon visibility - if let visibilityIconImage = viewModel.visibilityIconImage, visibilityIconImageDimension > 0 { - let dimension = ceil(visibilityIconImageDimension * 0.8) + if let visibilityIconImage = viewModel.visibilityIconImage { + let dimension = visibilityIconImageDimension let alignment: Alignment = viewModel.kind == .conversationRoot ? .top : .center Color.clear .frame(width: dimension) @@ -343,14 +324,14 @@ extension StatusView { } // end HStack } - public var avatarButton: some View { + var avatarButton: some View { Button { } label: { let dimension: CGFloat = { switch viewModel.kind { case .quote: - return viewModel.authorAvatarDimension + return inlineAvatarButtonDimension default: return StatusView.hangingAvatarButtonDimension } @@ -367,7 +348,7 @@ extension StatusView { .buttonStyle(.borderless) } - public var spoilerContentView: some View { + var spoilerContentView: some View { VStack(alignment: .leading, spacing: .zero) { TextViewRepresentable( metaContent: viewModel.spoilerContent ?? PlaintextMetaContent(string: ""), @@ -378,7 +359,7 @@ extension StatusView { } } - public var contentView: some View { + var contentView: some View { VStack(alignment: .leading, spacing: .zero) { TextViewRepresentable( metaContent: viewModel.content, diff --git a/TwidereSDK/Sources/TwidereUI/Content/TimestampLabelView.swift b/TwidereSDK/Sources/TwidereUI/Content/TimestampLabelView.swift index 062665a4..b1b895d4 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/TimestampLabelView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/TimestampLabelView.swift @@ -23,7 +23,7 @@ public struct TimestampLabelView: View { TimelineView(.periodic(from: .now, by: 1.0)) { timeline in let timeAgo = viewModel.timeAgo(now: timeline.date) Text("\(timeAgo)") - .font(Font.subheadline.monospacedDigit()) + .font(Font(TextStyle.statusTimestamp.font).monospacedDigit()) .foregroundColor(Color(uiColor: TextStyle.statusTimestamp.textColor)) } } diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift index 956e8cbd..c6b59619 100644 --- a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift @@ -9,24 +9,21 @@ import UIKit import SwiftUI import TwidereCore import MetaTextKit +import MetaTextArea public struct TextViewRepresentable: UIViewRepresentable { - - let textView: WrappedTextView = { - let textView = WrappedTextView() + + let textView: MetaTextAreaView = { + let textView = MetaTextAreaView() textView.backgroundColor = .clear - textView.isScrollEnabled = false - textView.isEditable = false - textView.textContainerInset = .zero - textView.textContainer.lineFragmentPadding = 0 textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) textView.setContentHuggingPriority(.defaultHigh, for: .vertical) return textView }() // input - public let metaContent: MetaContent - public let textStyle: TextStyle + let metaContent: MetaContent + let textStyle: TextStyle let width: CGFloat public init( @@ -39,9 +36,10 @@ public struct TextViewRepresentable: UIViewRepresentable { self.width = width } - public func makeUIView(context: Context) -> UITextView { + public func makeUIView(context: Context) -> MetaTextAreaView { let textView = self.textView - + textView.delegate = context.coordinator + let attributedString = NSMutableAttributedString(string: metaContent.string) let textAttributes: [NSAttributedString.Key: Any] = [ .font: textStyle.font, @@ -67,7 +65,8 @@ public struct TextViewRepresentable: UIViewRepresentable { ) textView.frame.size.width = width - textView.textStorage.setAttributedString(attributedString) + textView.preferredMaxLayoutWidth = width + textView.setAttributedString(attributedString) textView.invalidateIntrinsicContentSize() textView.setNeedsLayout() textView.layoutIfNeeded() @@ -75,7 +74,7 @@ public struct TextViewRepresentable: UIViewRepresentable { return textView } - public func updateUIView(_ view: UITextView, context: Context) { + public func updateUIView(_ view: MetaTextAreaView, context: Context) { textView.frame.size.width = width textView.invalidateIntrinsicContentSize() textView.setNeedsLayout() @@ -96,6 +95,13 @@ public struct TextViewRepresentable: UIViewRepresentable { } } +// MARK: - MetaTextAreaViewDelegate +extension TextViewRepresentable.Coordinator: MetaTextAreaViewDelegate { + public func metaTextAreaView(_ metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) { + + } +} + class WrappedTextView: UITextView { private var lastWidth: CGFloat = 0 @@ -121,3 +127,4 @@ class WrappedTextView: UITextView { } } + diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Guest.swift b/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Guest.swift new file mode 100644 index 00000000..87fa485b --- /dev/null +++ b/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Guest.swift @@ -0,0 +1,74 @@ +// +// Twitter+API+Guest.swift +// +// +// Created by MainasuK on 2022-8-22. +// + +import Foundation + +extension Twitter.API { + public enum Guest { } +} + +extension Twitter.API.Guest { + public struct GuestAuthorization: Hashable { + public static var userAgent: String { + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0 Safari/605.1.15" + } + + public static var authorization: String { + "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw" + } + + + public let userAgent: String + public let authorization: String + public let token: String + + public init( + userAgent: String = GuestAuthorization.userAgent, + authorization: String = GuestAuthorization.authorization, + token: String + ) { + self.userAgent = userAgent + self.authorization = authorization + self.token = token + } + } +} + +extension Twitter.API.Guest { + + private static var activeEndpoint: URL { + Twitter.API.endpointURL + .appendingPathComponent("guest") + .appendingPathComponent("activate") + .appendingPathExtension("json") + } + + public static func active( + session: URLSession + ) async throws -> Twitter.Response.Content { + var request = URLRequest( + url: activeEndpoint, + cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, + timeoutInterval: Twitter.API.timeoutInterval + ) + request.httpMethod = "POST" + request.setValue(GuestAuthorization.authorization, forHTTPHeaderField: "Authorization") + request.setValue(GuestAuthorization.userAgent, forHTTPHeaderField: "User-Agent") + let (data, response) = try await session.data(for: request, delegate: nil) + let value = try Twitter.API.decode(type: ActiveContent.self, from: data, response: response) + return Twitter.Response.Content(value: value, response: response) + } + + public struct ActiveContent: Codable { + public let guestToken: String + + enum CodingKeys: String, CodingKey { + case guestToken = "guest_token" + } + } + +} diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API.swift b/TwidereSDK/Sources/TwitterSDK/API/Twitter+API.swift index 5848150c..1fd8291b 100644 --- a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API.swift +++ b/TwidereSDK/Sources/TwitterSDK/API/Twitter+API.swift @@ -135,6 +135,49 @@ extension Twitter.API { return request } + static func request( + url: URL, + method: Method, + query: Query?, + authorization: Twitter.API.Guest.GuestAuthorization + ) -> URLRequest { + var components = URLComponents(string: url.absoluteString)! + components.queryItems = query?.queryItems + if let encodedQueryItems = query?.encodedQueryItems { + let percentEncodedQueryItems = (components.percentEncodedQueryItems ?? []) + encodedQueryItems + components.percentEncodedQueryItems = percentEncodedQueryItems + } + + let requestURL = components.url! + var request = URLRequest( + url: requestURL, + cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, + timeoutInterval: Twitter.API.timeoutInterval + ) + request.httpMethod = method.rawValue + + request.setValue( + authorization.userAgent, + forHTTPHeaderField: "User-Agent" + ) + request.setValue( + authorization.authorization, + forHTTPHeaderField: Twitter.API.OAuth.authorizationField + ) + request.setValue( + authorization.token, + forHTTPHeaderField: "x-guest-token" + ) + if let body = query?.body { + request.httpBody = body + } + if let contentType = query?.contentType { + request.setValue(contentType, forHTTPHeaderField: "Content-Type") + } + + return request + } + } extension Twitter.API { diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Account.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Account.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Account.swift rename to TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Account.swift diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Application.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Application.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Application.swift rename to TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Application.swift diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Block.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Block.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Block.swift rename to TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Block.swift diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Favorites.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Favorites.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Favorites.swift rename to TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Favorites.swift diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Friendships.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Friendships.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Friendships.swift rename to TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Friendships.swift diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Geo.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Geo.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Geo.swift rename to TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Geo.swift diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+List.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+List.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/API/Twitter+API+List.swift rename to TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+List.swift diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Lookup.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Lookup.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Lookup.swift rename to TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Lookup.swift diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Media+Metadata.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Media+Metadata.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Media+Metadata.swift rename to TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Media+Metadata.swift diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Media.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Media.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Media.swift rename to TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Media.swift diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Mute.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Mute.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Mute.swift rename to TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Mute.swift diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+SavedSearch.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+SavedSearch.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/API/Twitter+API+SavedSearch.swift rename to TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+SavedSearch.swift diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Search.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Search.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Search.swift rename to TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Search.swift diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Statuses+Timeline.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Statuses+Timeline.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Statuses+Timeline.swift rename to TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Statuses+Timeline.swift diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Statuses.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Statuses.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Statuses.swift rename to TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Statuses.swift diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Trend.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Trend.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Trend.swift rename to TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Trend.swift diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Users+Search.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Users+Search.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Users+Search.swift rename to TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Users+Search.swift diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Users.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Users.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Users.swift rename to TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Users.swift diff --git a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Status+Timeline.swift b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Status+Timeline.swift new file mode 100644 index 00000000..61bc15f8 --- /dev/null +++ b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Status+Timeline.swift @@ -0,0 +1,244 @@ +// +// Twitter+API+V2+Status+Timeline.swift +// +// +// Created by MainasuK on 2023/3/27. +// + +import Foundation + +extension Twitter.API.V2.Status { + public enum Timeline { } +} + +extension Twitter.API.V2.Status.Timeline { + private static func conversationEndpointURL(statusID: Twitter.Entity.V2.Tweet.ID) -> URL { + return Twitter.API.endpointV2URL + .appendingPathComponent("timeline") + .appendingPathComponent("conversation") + .appendingPathComponent(statusID) + .appendingPathExtension("json") + } + + public static func conversation( + session: URLSession, + statusID: Twitter.Entity.V2.Tweet.ID, + query: TimelineQuery, + authorization: Twitter.API.Guest.GuestAuthorization + ) async throws -> Twitter.Response.Content { + let request = Twitter.API.request( + url: conversationEndpointURL(statusID: statusID), + method: .GET, + query: query, + authorization: authorization + ) + let (data, response) = try await session.data(for: request, delegate: nil) + let value = try Twitter.API.decode(type: Twitter.API.V2.Status.Timeline.ConversationContent.self, from: data, response: response) + return Twitter.Response.Content(value: value, response: response) + } + + public struct TimelineQuery: Query { + public let cursor: String? + + public init(cursor: String?) { + self.cursor = cursor + } + + public var queryItems: [URLQueryItem]? { nil } + public var encodedQueryItems: [URLQueryItem]? { + var items: [URLQueryItem] = [] + cursor.flatMap { items.append(URLQueryItem(name: "cursor", value: $0.urlEncoded)) } + guard !items.isEmpty else { return nil } + return items + } + public var formQueryItems: [URLQueryItem]? { nil } + public var contentType: String? { nil } + public var body: Data? { nil } + } + + public struct ConversationContent: Decodable { + public let globalObjects: GlobalObjects + public let timeline: Timeline + + public struct GlobalObjects: Codable { + public let tweets: [Twitter.Entity.Internal.Tweet] + public let users: [Twitter.Entity.User] + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + tweets = try { + let dict = try values.decode([String: Twitter.Entity.Internal.Tweet].self, forKey: .tweets) + return Array(dict.values) + }() + users = try { + let dict = try values.decode([String: Twitter.Entity.User].self, forKey: .users) + return Array(dict.values) + }() + } + } + + public struct Timeline: Decodable { + public let id: String + public let entries: [Entry] + public let topCursor: String? + public let bottomCursor: String? + + public enum Entry { + case tweet(statusID: Twitter.Entity.Internal.Tweet.ID) + case conversationThread(components: [Twitter.Entity.Internal.Tweet.ID]) + } + + enum CodingKeys: String, CodingKey { + case id + case instructions + } + + enum InstructionKeys: String, CodingKey { + case clearCache + case addEntries + } + + enum EntryContainerKeys: String, CodingKey { + case entries + } + + enum EntryKeys: String, CodingKey { + case content + } + + enum EntryItemKeys: String, CodingKey { + case item + } + + enum EntryOprationKeys: String, CodingKey { + case item + } + + enum TweetKeys: String, CodingKey { + case id + } + + enum CursorKeys: String, CodingKey { + case value + case cursorType + } + + public init(from decoder: Decoder) throws { + // -> timeline + let values = try decoder.container(keyedBy: CodingKeys.self) + // -> timeline.id + self.id = try values.decode(String.self, forKey: .id) + + var entriesResult: [Entry] = [] + var topCursor: String? + var bottomCursor: String? + + // -> timeline.instructions[] + var container = try values.nestedUnkeyedContainer(forKey: .instructions) + while !container.isAtEnd { + // -> timeline.instructions[index] + let instructionContainer = try container.nestedContainer(keyedBy: InstructionKeys.self) + // -> timeline.instructions[index].addEntries + guard let entryContainer = try? instructionContainer.nestedContainer(keyedBy: EntryContainerKeys.self, forKey: .addEntries) else { + continue + } + + // -> timeline.instructions[index] { .addEntries } + var entries = try entryContainer.nestedUnkeyedContainer(forKey: .entries) + while !entries.isAtEnd { + // -> timeline.instructions[index] { .addEntries.entries } + let entry = try entries.nestedContainer(keyedBy: EntryKeys.self) + + if let content = try? entry.decode(ItemTweetEntry.self, forKey: .content) { + entriesResult.append(.tweet(statusID: content.item.content.tweet.id)) + } else if let content = try? entry.decode(ItemConversationThreadEntry.self, forKey: .content) { + var components: [Twitter.Entity.V2.Tweet.ID] = [] + for component in content.item.content.conversationThread.conversationComponents { + let id = component.conversationTweetComponent.tweet.id + components.append(id) + } + entriesResult.append(.conversationThread(components: components)) + } else if let content = try? entry.decode(OperationEntry.self, forKey: .content) { + switch content.operation.cursor.cursorType.lowercased() { + case "top": + topCursor = content.operation.cursor.value + case "bottom": + bottomCursor = content.operation.cursor.value + case "ShowMoreThreads".lowercased(): + bottomCursor = content.operation.cursor.value + case "ShowMoreThreadsPrompt".lowercased(): + bottomCursor = content.operation.cursor.value + default: + assertionFailure() + continue + } + } else { + continue + } + } + } + + self.entries = entriesResult + self.topCursor = topCursor + self.bottomCursor = bottomCursor + } // end init + } + + struct ItemTweetEntry: Codable { + let item: Item + + struct Item: Codable { + let content: Content + + struct Content: Codable { + let tweet: Tweet + + struct Tweet: Codable { + let id: String + } + } + } + } // end ItemTweetEntry + + struct ItemConversationThreadEntry: Codable { + let item: Item + + struct Item: Codable { + let content: Content + + struct Content: Codable { + let conversationThread: ConversationThread + + struct ConversationThread: Codable { + let conversationComponents: [ConversationComponent] + + struct ConversationComponent: Codable { + let conversationTweetComponent: ConversationTweetComponent + } + + struct ConversationTweetComponent: Codable { + let tweet: Tweet + + struct Tweet: Codable { + let id: String + } + } + } + } + } + } // end ItemConversationThreadEntry + + struct OperationEntry: Codable { + let operation: Operation + + struct Operation: Codable { + let cursor: Cursor + + struct Cursor: Codable { + let value: String + let cursorType: String + } + } + } // end OperationEntry + } +} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+Coordinates.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Coordinates.swift similarity index 95% rename from TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+Coordinates.swift rename to TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Coordinates.swift index 54a7971c..271359ab 100644 --- a/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+Coordinates.swift +++ b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Coordinates.swift @@ -1,6 +1,6 @@ // // Twitter+Entity+Coordinates.swift -// TwitterAPI +// TwitterSDK // // Created by Cirno MainasuK on 2020-9-16. // diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+ExtendedEntities.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+ExtendedEntities.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+ExtendedEntities.swift rename to TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+ExtendedEntities.swift diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Internal+Tweet.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Internal+Tweet.swift new file mode 100644 index 00000000..c2a94138 --- /dev/null +++ b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Internal+Tweet.swift @@ -0,0 +1,99 @@ +// +// Twitter+Tweet.swift +// TwitterAPI +// +// Created by Cirno MainasuK on 2020-9-3. +// + +import Foundation + +extension Twitter.Entity.Internal { + public class Tweet: Codable { + + public typealias ID = String + + // Fundamental + public let createdAt: Date + public let idStr: ID + + public let text: String? + + public let userIDStr: Twitter.Entity.User.ID + + public let entities: Twitter.Entity.Tweet.Entities? + + // public let coordinates: Coordinates? + // public let place: Place? + + //let contributors: JSONNull? + public let favoriteCount: Int? + + public let retweeted: Bool + public let retweetCount: Int? + public let retweetedStatusIDStr: ID? + + public let inReplyToScreenName: String? + public let inReplyToStatusIDStr: ID? + public let inReplyToUserIDStr: Twitter.Entity.User.ID? + + public let isQuoteStatus: Bool + public let quotedStatusIDStr: String? + + public let lang: String? + + //let possiblySensitive: Bool? + //let possiblySensitiveAppealable: Bool? + + public let source: String? + public let truncated: Bool? + + public enum CodingKeys: String, CodingKey { + // Fundamental + case createdAt = "created_at" + case idStr = "id_str" + + case text + + case userIDStr = "user_id_str" + + case entities + + // case coordinates = "coordinates" + // case place = "place" + + //case contributors = "contributors" + case favoriteCount = "favorite_count" + + case retweeted = "retweeted" + case retweetCount = "retweet_count" + case retweetedStatusIDStr = "retweeted_status_id_str" + + case inReplyToScreenName = "in_reply_to_screen_name" + case inReplyToStatusIDStr = "in_reply_to_status_id_str" + case inReplyToUserIDStr = "in_reply_to_user_id_str" + + case isQuoteStatus = "is_quote_status" + case quotedStatusIDStr = "quoted_status_id_str" + + case lang + + //case possiblySensitive = "possibly_sensitive" + //case possiblySensitiveAppealable = "possibly_sensitive_appealable" + + case source + case truncated + } + } +} + +extension Twitter.Entity.Internal.Tweet: Hashable { + + public static func == (lhs: Twitter.Entity.Internal.Tweet, rhs: Twitter.Entity.Internal.Tweet) -> Bool { + lhs.idStr == rhs.idStr + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(idStr) + } + +} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Internal.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Internal.swift new file mode 100644 index 00000000..6e4efcdb --- /dev/null +++ b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Internal.swift @@ -0,0 +1,12 @@ +// +// Twitter+Entity+Internal.swift +// +// +// Created by MainasuK on 2022-9-5. +// + +import Foundation + +extension Twitter.Entity { + public enum Internal { } +} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+List.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+List.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+List.swift rename to TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+List.swift diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+Place.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Place.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+Place.swift rename to TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Place.swift diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+QuotedStatus.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+QuotedStatus.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+QuotedStatus.swift rename to TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+QuotedStatus.swift diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+RateLimitStatus.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+RateLimitStatus.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+RateLimitStatus.swift rename to TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+RateLimitStatus.swift diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+Relationship.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Relationship.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+Relationship.swift rename to TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Relationship.swift diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+RetweetedStatus.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+RetweetedStatus.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+RetweetedStatus.swift rename to TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+RetweetedStatus.swift diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+SavedSearch.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+SavedSearch.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+SavedSearch.swift rename to TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+SavedSearch.swift diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+Trend+Place.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Trend+Place.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+Trend+Place.swift rename to TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Trend+Place.swift diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+Trend.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Trend.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+Trend.swift rename to TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Trend.swift diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+Tweet+Entities.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Tweet+Entities.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+Tweet+Entities.swift rename to TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Tweet+Entities.swift diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+Tweet.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Tweet.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+Tweet.swift rename to TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Tweet.swift diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+User+Entities.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+User+Entities.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+User+Entities.swift rename to TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+User+Entities.swift diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+User.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+User.swift similarity index 100% rename from TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity+User.swift rename to TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+User.swift diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index f38ed74f..989ef6ed 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -249,6 +249,7 @@ DB88E62E2890DF7E009A01F5 /* BehaviorsPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB88E62D2890DF7E009A01F5 /* BehaviorsPreferenceView.swift */; }; DB89F591257A2A70005ECD04 /* TimelineLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89F590257A2A70005ECD04 /* TimelineLoaderTableViewCell.swift */; }; DB8AC0F825401BA200E636BE /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AC0F725401BA200E636BE /* UIViewController.swift */; }; + DB8BFB1C29D1F52900535092 /* TwidereXUITests+Performance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8BFB1B29D1F52900535092 /* TwidereXUITests+Performance.swift */; }; DB8E4FD92563CD6900459DA2 /* TwitterPinBasedAuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8E4FD82563CD6900459DA2 /* TwitterPinBasedAuthenticationViewController.swift */; }; DB8E4FEC2563D42800459DA2 /* TwitterPinBasedAuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8E4FEB2563D42800459DA2 /* TwitterPinBasedAuthenticationViewModel.swift */; }; DB914C4A26C3AC7C00E85F1A /* ContentOffsetFixedCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB914C4926C3AC7C00E85F1A /* ContentOffsetFixedCollectionView.swift */; }; @@ -706,6 +707,7 @@ DB89F590257A2A70005ECD04 /* TimelineLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLoaderTableViewCell.swift; sourceTree = ""; }; DB8AC0F725401BA200E636BE /* UIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = ""; }; DB8AC18F2542DA9500E636BE /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveLabel.swift; sourceTree = ""; }; + DB8BFB1B29D1F52900535092 /* TwidereXUITests+Performance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TwidereXUITests+Performance.swift"; sourceTree = ""; }; DB8E4FD82563CD6900459DA2 /* TwitterPinBasedAuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterPinBasedAuthenticationViewController.swift; sourceTree = ""; }; DB8E4FEB2563D42800459DA2 /* TwitterPinBasedAuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterPinBasedAuthenticationViewModel.swift; sourceTree = ""; }; DB914C4926C3AC7C00E85F1A /* ContentOffsetFixedCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentOffsetFixedCollectionView.swift; sourceTree = ""; }; @@ -1567,6 +1569,15 @@ path = Trend; sourceTree = ""; }; + DB6CD2AA29D1F2AD003AE784 /* TestPlan */ = { + isa = PBXGroup; + children = ( + DB6B8B2C25AF235F00F20FD5 /* TwidereX.xctestplan */, + DB6B8B2D25AF235F00F20FD5 /* TwidereX-Performance.xctestplan */, + ); + path = TestPlan; + sourceTree = ""; + }; DB7274F0273BB25A00577D95 /* Notification */ = { isa = PBXGroup; children = ( @@ -2076,8 +2087,6 @@ children = ( DBDA8E2F24FCF8A6006750DC /* Info.plist */, DBE76CD2250095D900DEB0FC /* TwidereX.entitlements */, - DB6B8B2C25AF235F00F20FD5 /* TwidereX.xctestplan */, - DB6B8B2D25AF235F00F20FD5 /* TwidereX-Performance.xctestplan */, DBA122CA256C13B000928671 /* GoogleService-Info.plist */, DBCB408E255D8C2E00DD8D8F /* Activity */, DB2C8710274F4B1C00CE0398 /* Coordinator */, @@ -2088,6 +2097,7 @@ DBF81C8B27F843D700004A56 /* Generated */, DBE76D312502147200DEB0FC /* Extension */, DBED96DC253F5D7B00C5383A /* Protocol */, + DB6CD2AA29D1F2AD003AE784 /* TestPlan */, DBDA8E5324FDF3D6006750DC /* Supporting Files */, ); path = TwidereX; @@ -2110,6 +2120,7 @@ DBDA8E4524FCF8A7006750DC /* Info.plist */, DBDA8E4324FCF8A7006750DC /* TwidereXUITests.swift */, DBDA7F1927D1CBCA00BA6BE1 /* TwidereXUITests+Onboarding.swift */, + DB8BFB1B29D1F52900535092 /* TwidereXUITests+Performance.swift */, ); path = TwidereXUITests; sourceTree = ""; @@ -2519,6 +2530,7 @@ buildRules = ( ); dependencies = ( + DB8BFB1E29D1FC6200535092 /* PBXTargetDependency */, DBDA8E4124FCF8A7006750DC /* PBXTargetDependency */, ); name = TwidereXUITests; @@ -3209,6 +3221,7 @@ buildActionMask = 2147483647; files = ( DBDA7F1A27D1CBCA00BA6BE1 /* TwidereXUITests+Onboarding.swift in Sources */, + DB8BFB1C29D1F52900535092 /* TwidereXUITests+Performance.swift in Sources */, DBDA8E4424FCF8A7006750DC /* TwidereXUITests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3239,6 +3252,10 @@ target = DB7FF05D28853A7F00BFD55E /* NotificationService */; targetProxy = DB7FF06328853A7F00BFD55E /* PBXContainerItemProxy */; }; + DB8BFB1E29D1FC6200535092 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = DB8BFB1D29D1FC6200535092 /* TwidereSDK */; + }; DBBBBE822744F39C007ACB4B /* PBXTargetDependency */ = { isa = PBXTargetDependency; targetProxy = DBBBBE812744F39C007ACB4B /* PBXContainerItemProxy */; @@ -3451,6 +3468,7 @@ CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3474,6 +3492,7 @@ CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3888,6 +3907,7 @@ CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3913,6 +3933,7 @@ CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3936,6 +3957,7 @@ CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3959,6 +3981,7 @@ CURRENT_PROJECT_VERSION = 118; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4124,6 +4147,10 @@ isa = XCSwiftPackageProductDependency; productName = TwidereSDK; }; + DB8BFB1D29D1FC6200535092 /* TwidereSDK */ = { + isa = XCSwiftPackageProductDependency; + productName = TwidereSDK; + }; DBFADA882872BEDA00B512D6 /* CoverFlowStackCollectionViewLayout */ = { isa = XCSwiftPackageProductDependency; productName = CoverFlowStackCollectionViewLayout; diff --git a/TwidereX.xcodeproj/xcshareddata/xcbaselines/DBDA8E3E24FCF8A7006750DC.xcbaseline/D687BB06-F0F4-4247-8624-A87B4DA38312.plist b/TwidereX.xcodeproj/xcshareddata/xcbaselines/DBDA8E3E24FCF8A7006750DC.xcbaseline/D687BB06-F0F4-4247-8624-A87B4DA38312.plist new file mode 100644 index 00000000..99cf8774 --- /dev/null +++ b/TwidereX.xcodeproj/xcshareddata/xcbaselines/DBDA8E3E24FCF8A7006750DC.xcbaseline/D687BB06-F0F4-4247-8624-A87B4DA38312.plist @@ -0,0 +1,22 @@ + + + + + classNames + + TwidereXUITests_Performance + + testHomeTimelineScrollingAnimationPerformance() + + com.apple.dt.XCTMetric_OSSignpost-Scroll_DraggingAndDeceleration.animation.hitch.number + + baselineAverage + 0.000000 + baselineIntegrationDisplayName + Local Baseline + + + + + + diff --git a/TwidereX.xcodeproj/xcshareddata/xcbaselines/DBDA8E3E24FCF8A7006750DC.xcbaseline/Info.plist b/TwidereX.xcodeproj/xcshareddata/xcbaselines/DBDA8E3E24FCF8A7006750DC.xcbaseline/Info.plist new file mode 100644 index 00000000..9a2d9f0e --- /dev/null +++ b/TwidereX.xcodeproj/xcshareddata/xcbaselines/DBDA8E3E24FCF8A7006750DC.xcbaseline/Info.plist @@ -0,0 +1,21 @@ + + + + + runDestinationsByUUID + + D687BB06-F0F4-4247-8624-A87B4DA38312 + + targetArchitecture + arm64 + targetDevice + + modelCode + iPhone14,6 + platformIdentifier + com.apple.platform.iphoneos + + + + + diff --git a/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX - Core Data.xcscheme b/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX - Core Data.xcscheme index 420a130a..bb2c58c2 100644 --- a/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX - Core Data.xcscheme +++ b/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX - Core Data.xcscheme @@ -29,10 +29,10 @@ shouldUseLaunchSchemeArgsEnv = "YES"> + reference = "container:TwidereX/New Group/TwidereX-Performance.xctestplan"> diff --git a/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX - Performance.xcscheme b/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX - Performance.xcscheme new file mode 100644 index 00000000..5887e377 --- /dev/null +++ b/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX - Performance.xcscheme @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX - Profile.xcscheme b/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX - Profile.xcscheme index 574ca276..ced81db7 100644 --- a/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX - Profile.xcscheme +++ b/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX - Profile.xcscheme @@ -29,10 +29,10 @@ shouldUseLaunchSchemeArgsEnv = "YES"> + reference = "container:TwidereX/New Group/TwidereX-Performance.xctestplan"> diff --git a/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX - Release.xcscheme b/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX - Release.xcscheme index 5f5fec0b..4cf04b3b 100644 --- a/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX - Release.xcscheme +++ b/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX - Release.xcscheme @@ -29,10 +29,10 @@ shouldUseLaunchSchemeArgsEnv = "YES"> + reference = "container:TwidereX/New Group/TwidereX-Performance.xctestplan"> diff --git a/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX.xcscheme b/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX.xcscheme index 3ab0eef7..a1237242 100644 --- a/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX.xcscheme +++ b/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX.xcscheme @@ -29,10 +29,10 @@ shouldUseLaunchSchemeArgsEnv = "YES"> + reference = "container:TwidereX/New Group/TwidereX-Performance.xctestplan"> diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index eede46b6..8e9d3b27 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -132,8 +132,8 @@ "repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git", "state": { "branch": null, - "revision": "988b72a09662246f209f41c0aaa427f249dcbd6d", - "version": "4.4.0" + "revision": "c2a3d5e4355c1e3c9046c89433b28795f3374876", + "version": "4.4.2" } }, { diff --git a/TwidereX/Diffable/Status/StatusItem.swift b/TwidereX/Diffable/Status/StatusItem.swift index 763add5f..9d6b4d81 100644 --- a/TwidereX/Diffable/Status/StatusItem.swift +++ b/TwidereX/Diffable/Status/StatusItem.swift @@ -10,10 +10,17 @@ import Foundation import CoreDataStack import TwidereCore -enum StatusItem: Hashable { +enum StatusItem: Hashable, DifferenceItem { case feed(record: ManagedObjectRecord) case feedLoader(record: ManagedObjectRecord) case status(StatusRecord) case topLoader case bottomLoader + + var isTransient: Bool { + switch self { + case .topLoader, .bottomLoader: return true + default: return false + } + } } diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewController+DataSourceProvider.swift b/TwidereX/Scene/StatusThread/StatusThreadViewController+DataSourceProvider.swift index c9f48c62..c0af79ef 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewController+DataSourceProvider.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewController+DataSourceProvider.swift @@ -22,7 +22,7 @@ extension StatusThreadViewController: DataSourceProvider { switch item { case .root: - guard let status = viewModel.status?.asRecord else { return nil } + guard let status = viewModel.statusViewModel?.status else { return nil } return .status(status) case .status(let status): return .status(status) diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewController.swift b/TwidereX/Scene/StatusThread/StatusThreadViewController.swift index f2a42861..f09dfa88 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewController.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewController.swift @@ -115,18 +115,36 @@ extension StatusThreadViewController { // MARK: - UITableViewDelegate extension StatusThreadViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { -// func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { -// guard let diffableDataSource = viewModel.diffableDataSource else { return indexPath } -// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return indexPath } -// guard case let .thread(thread) = item else { return indexPath } -// -// switch thread { -// case .root: -// return nil -// case .reply, .leaf: -// return indexPath -// } -// } + func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { + guard let diffableDataSource = viewModel.diffableDataSource else { return indexPath } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return indexPath } + + switch item { + case .root: + // cancel textView selection + view.endEditing(true) + return nil + default: return indexPath + } + } + + func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + guard let diffableDataSource = viewModel.diffableDataSource else { return } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + + switch item { + case .topLoader: + Task { + try await viewModel.loadTop() + } // end Task + case .bottomLoader: + Task { + try await viewModel.loadBottom() + } // end Task + default: + break + } + } // sourcery:inline:StatusThreadViewController.AutoGenerateTableViewDelegate diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift index 9502dc83..32f4c709 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift @@ -20,14 +20,29 @@ extension StatusThreadViewModel { statusViewTableViewCellDelegate: StatusViewTableViewCellDelegate ) { tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self)) - // tableView.register(LoaderTableViewCell.self, forCellReuseIdentifier: String(describing: LoaderTableViewCell.self)) - + tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) + diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { [weak self] tableView, indexPath, item in guard let self = self else { return UITableViewCell() } switch item { - case .status(let status): - return UITableViewCell() + case .status(let record): + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell + cell.delegate = statusViewTableViewCellDelegate + self.context.managedObjectContext.performAndWait { + guard let status = record.object(in: self.context.managedObjectContext) else { return } + let viewModel = StatusView.ViewModel( + status: status, + authContext: self.authContext, + delegate: cell, + viewLayoutFramePublisher: self.$viewLayoutFrame + ) + cell.contentConfiguration = UIHostingConfiguration { + StatusView(viewModel: viewModel) + } + .margins(.vertical, 0) // remove vertical margins + } + return cell case .root: let cell = self.conversationRootTableViewCell guard let statusViewModel = self.statusViewModel else { @@ -40,30 +55,36 @@ extension StatusThreadViewModel { .margins(.vertical, 0) // remove vertical margins return cell case .topLoader, .bottomLoader: - return UITableViewCell() + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell + cell.activityIndicatorView.startAnimating() + return cell } } // end diffableDataSource = UITableViewDiffableDataSource + // initial snapshot var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) switch kind { case .status(let status): - // if status.inReplyToUserID != nil { - // snapshot.appendItems([.topLoader]) - // } - break + // top loader + let hasReplyTo: Bool = { + guard let status = status.object(in: context.managedObjectContext) else { return false } + switch status { + case .twitter(let status): return status.replyToStatusID != nil + case .mastodon(let status): return status.replyToStatusID != nil + } + }() + if hasReplyTo { + snapshot.appendItems([.topLoader], toSection: .main) + } + // root + snapshot.appendItems([.root]) + // bottom loader + snapshot.appendItems([.bottomLoader]) case .twitter, .mastodon: break } - snapshot.appendItems([.root]) diffableDataSource?.apply(snapshot, animatingDifferences: false, completion: nil) - - // switch bottomCursor { - // case .noMore: - // break - // default: - // snapshot.appendItems([.bottomLoader]) - // } // let configuration = StatusSection.Configuration( // statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, @@ -113,213 +134,234 @@ extension StatusThreadViewModel { // // trigger thread loading // loadThreadStateMachine.enter(LoadThreadState.Prepare.self) // -// Publishers.CombineLatest3( -// root, -// $replies, -// $leafs -// ) -// .throttle(for: 0.3, scheduler: DispatchQueue.main, latest: true) -// .sink { [weak self] root, replies, leafs in -// guard let self = self else { return } -// guard let diffableDataSource = self.diffableDataSource else { return } -// -// Task { @MainActor in -// let oldSnapshot = diffableDataSource.snapshot() -// -// var newSnapshot = NSDiffableDataSourceSnapshot() -// newSnapshot.appendSections([.main]) -// -// // top loader -// if self.hasReplyTo, case let .root(threadContext) = root { -// switch threadContext.status { -// case .twitter: -// let state = self.twitterStatusThreadReplyViewModel.stateMachine.currentState -// if state is TwitterStatusThreadReplyViewModel.State.NoMore { -// // do nothing -// } else { -// newSnapshot.appendItems([.topLoader], toSection: .main) -// } -// case .mastodon: -// let state = self.loadThreadStateMachine.currentState -// if state is LoadThreadState.NoMore { -// // do nothing -// } else { -// newSnapshot.appendItems([.topLoader], toSection: .main) -// } -// } -// } -// // replies -// newSnapshot.appendItems(replies.reversed(), toSection: .main) -// // root -// if let root = root { -// let item = StatusItem.thread(root) -// newSnapshot.appendItems([item], toSection: .main) -// } -// // leafs -// newSnapshot.appendItems(leafs, toSection: .main) -// // bottom loader -// if let currentState = self.loadThreadStateMachine.currentState { -// switch currentState { -// case is LoadThreadState.Prepare, -// is LoadThreadState.Idle, -// is LoadThreadState.Loading: -// newSnapshot.appendItems([.bottomLoader], toSection: .main) -// default: -// break -// } -// } -// -// let hasChanges = newSnapshot.itemIdentifiers != oldSnapshot.itemIdentifiers -// if !hasChanges { -// self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): snapshot not changes") -// return -// } else { -// self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): snapshot has changes") -// } -// -// guard let difference = self.calculateReloadSnapshotDifference( -// tableView: tableView, -// oldSnapshot: oldSnapshot, -// newSnapshot: newSnapshot -// ) else { -// await self.updateDataSource(snapshot: newSnapshot, animatingDifferences: false) -// self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): applied new snapshot without tweak") -// return -// } -// -// self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Snapshot] oldSnapshot: \(oldSnapshot.itemIdentifiers.debugDescription)") -// self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Snapshot] newSnapshot: \(newSnapshot.itemIdentifiers.debugDescription)") -// await self.updateSnapshotUsingReloadData( -// tableView: tableView, -// oldSnapshot: oldSnapshot, -// newSnapshot: newSnapshot, -// difference: difference -// ) -// } -// } -// .store(in: &disposeBag) + Publishers.CombineLatest4( + $status, + $topThreads.removeDuplicates(), + $bottomThreads.removeDuplicates(), + $deleteStatusIDs.removeDuplicates() + ) + .debounce(for: 0.3, scheduler: DispatchQueue.main) + .dropFirst() + .sink { [weak self] status, topThreads, bottomThreads, deleteStatusIDs in + guard let self = self else { return } + guard let diffableDataSource = self.diffableDataSource else { return } + + Task { @MainActor in + let oldSnapshot = diffableDataSource.snapshot() + + var newSnapshot = NSDiffableDataSourceSnapshot() + newSnapshot.appendSections([.main]) + + // top loader + switch self.topCursor { + case .noMore: + break + default: + // top loader + let hasReplyTo: Bool = { + switch status { + case .twitter(let status): return status.replyToStatusID != nil + case .mastodon(let status): return status.replyToStatusID != nil + case nil: return false + } + }() + if hasReplyTo { + newSnapshot.appendItems([.topLoader], toSection: .main) + } + } + // self reply + let topItems: [Item] = topThreads.compactMap { thread -> Item? in + switch thread { + case .selfThread(let status): return .status(status: status) + default: return nil + } + }.removingDuplicates() + newSnapshot.appendItems(topItems, toSection: .main) + // root + newSnapshot.appendItems([.root], toSection: .main) + if let status = status, deleteStatusIDs.contains(status.id) { + newSnapshot.deleteItems([.root]) + } + // bottom reply + let bottomItems: [Item] = bottomThreads.compactMap { thread -> [Item]? in + switch thread { + case .conversationThread(let components): + return components.compactMap { status -> Item? in +// guard !deleteStatusIDs.contains(status.id) else { +// return nil +// } + return Item.status(status: status) + } + default: + assertionFailure() + return nil + } + } + .flatMap { $0 } + .removingDuplicates() + newSnapshot.appendItems(bottomItems, toSection: .main) + // bottom loader + switch self.bottomCursor { + case .noMore: + break + default: + newSnapshot.appendItems([.bottomLoader], toSection: .main) + } + + let hasChanges = newSnapshot.itemIdentifiers != oldSnapshot.itemIdentifiers + if !hasChanges { + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): snapshot not changes") + return + } else { + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): snapshot has changes") + } + + guard let difference = self.calculateReloadSnapshotDifference( + tableView: tableView, + oldSnapshot: oldSnapshot, + newSnapshot: newSnapshot + ) else { + self.updateDataSource(snapshot: newSnapshot, animatingDifferences: false) + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): applied new snapshot without tweak") + return + } + + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Snapshot] oldSnapshot: \(oldSnapshot.itemIdentifiers.debugDescription)") + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Snapshot] newSnapshot: \(newSnapshot.itemIdentifiers.debugDescription)") + self.reloadSnapshotWithDifference( + tableView: tableView, + oldSnapshot: oldSnapshot, + newSnapshot: newSnapshot, + difference: difference + ) + + } // end Task + } + .store(in: &disposeBag) + } + +} + +extension StatusThreadViewModel { + struct Difference: CustomStringConvertible { + let item: T + let sourceIndexPath: IndexPath + let sourceDistanceToTableViewTopEdge: CGFloat + let targetIndexPath: IndexPath + + var description: String { + """ + source: \(sourceIndexPath.debugDescription) + target: \(targetIndexPath.debugDescription) + offset: \(sourceDistanceToTableViewTopEdge) + item: \(String(describing: item)) + """ + } } -// @MainActor private func updateDataSource( -// snapshot: NSDiffableDataSourceSnapshot, -// animatingDifferences: Bool -// ) async { -// await self.diffableDataSource?.apply(snapshot, animatingDifferences: animatingDifferences) -// } - - // Some UI tweaks to present replies and conversation smoothly -// @MainActor private func updateSnapshotUsingReloadData( -// tableView: UITableView, -// oldSnapshot: NSDiffableDataSourceSnapshot, -// newSnapshot: NSDiffableDataSourceSnapshot, -// difference: StatusThreadViewModel.Difference // -// ) async { -// let replies: [StatusItem] = { -// newSnapshot.itemIdentifiers.filter { item in -// guard case let .thread(thread) = item else { return false } -// guard case .reply = thread else { return false } -// return true -// } -// }() -// // additional margin for .topLoader -// let oldTopMargin: CGFloat = { -// let marginHeight = TimelineTopLoaderTableViewCell.cellHeight -// if oldSnapshot.itemIdentifiers.contains(.topLoader) || !replies.isEmpty { -// return marginHeight -// } -// return .zero -// }() -// -// await self.diffableDataSource?.applySnapshotUsingReloadData(newSnapshot) -// -// // note: -// // tweak the content offset and bottom inset -// // make the table view stable when data reload -// // the keypoint is set the bottom inset to make the root padding with "TopLoaderHeight" to top edge -// // and restore the "TopLoaderHeight" when bottom inset adjusted -// -// // set bottom inset. Make root item pin to top. -// if let item = root.value.flatMap({ StatusItem.thread($0) }), -// let index = newSnapshot.indexOfItem(item), -// let cell = tableView.cellForRow(at: IndexPath(row: index, section: 0)) -// { -// // always set bottom inset due to lazy reply loading -// // otherwise tableView will jump when insert replies -// let bottomSpacing = tableView.safeAreaLayoutGuide.layoutFrame.height - cell.frame.height - oldTopMargin -// let additionalInset = round(tableView.contentSize.height - cell.frame.maxY) -// -// tableView.contentInset.bottom = max(0, bottomSpacing - additionalInset) -// self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): content inset bottom: \(tableView.contentInset.bottom)") -// } -// -// // set scroll position -// tableView.scrollToRow(at: difference.targetIndexPath, at: .top, animated: false) -// tableView.contentOffset.y = { -// var offset: CGFloat = tableView.contentOffset.y - difference.sourceDistanceToTableViewTopEdge -// if tableView.contentInset.bottom != 0.0 { -// // needs restore top margin if bottom inset adjusted -// offset += oldTopMargin -// } -// return offset -// }() -// self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): applied new snapshot") -// } + @MainActor func calculateReloadSnapshotDifference( + tableView: UITableView, + oldSnapshot: NSDiffableDataSourceSnapshot, + newSnapshot: NSDiffableDataSourceSnapshot + ) -> Difference? { + guard oldSnapshot.numberOfItems != 0 else { return nil } + guard let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows?.sorted() else { return nil } + + // find index of the first visible item in both old and new snapshot + var _index: Int? + let items = oldSnapshot.itemIdentifiers + for (i, item) in items.enumerated() { + guard let _ = indexPathsForVisibleRows.first(where: { $0.row == i }) else { continue } + guard !item.isTransient else { continue } + guard newSnapshot.indexOfItem(item) != nil else { continue } + _index = i + break + } + + guard let index = _index else { return nil } + let sourceIndexPath = IndexPath(row: index, section: 0) + + let rectForSourceItemCell = tableView.rectForRow(at: sourceIndexPath) + let sourceDistanceToTableViewTopEdge: CGFloat = { + if tableView.window != nil { + return tableView.convert(rectForSourceItemCell, to: nil).origin.y - tableView.safeAreaInsets.top + } else { + return rectForSourceItemCell.origin.y - tableView.contentOffset.y - tableView.safeAreaInsets.top + } + }() + + let sectionIdentifier = oldSnapshot.sectionIdentifiers[sourceIndexPath.section] + let item = oldSnapshot.itemIdentifiers(inSection: sectionIdentifier)[sourceIndexPath.row] + + guard let targetIndexPathRow = newSnapshot.indexOfItem(item), + let newSectionIdentifier = newSnapshot.sectionIdentifier(containingItem: item), + let targetIndexPathSection = newSnapshot.indexOfSection(newSectionIdentifier) + else { return nil } + + let targetIndexPath = IndexPath(row: targetIndexPathRow, section: targetIndexPathSection) + + return Difference( + item: item, + sourceIndexPath: sourceIndexPath, + sourceDistanceToTableViewTopEdge: sourceDistanceToTableViewTopEdge, + targetIndexPath: targetIndexPath + ) + } } extension StatusThreadViewModel { -// struct Difference { -// let item: StatusItem -// let sourceIndexPath: IndexPath -// let sourceDistanceToTableViewTopEdge: CGFloat -// let targetIndexPath: IndexPath -// } -// -// @MainActor private func calculateReloadSnapshotDifference( -// tableView: UITableView, -// oldSnapshot: NSDiffableDataSourceSnapshot, -// newSnapshot: NSDiffableDataSourceSnapshot -// ) -> Difference? { -// guard oldSnapshot.numberOfItems != 0 else { return nil } -// guard let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows?.sorted() else { return nil } -// -// // find index of the first visible item in both old and new snapshot -// var _index: Int? -// let items = oldSnapshot.itemIdentifiers(inSection: .main) -// for (i, item) in items.enumerated() { -// guard let indexPath = indexPathsForVisibleRows.first(where: { $0.row == i }) else { continue } -// guard newSnapshot.indexOfItem(item) != nil else { continue } -// let rectForCell = tableView.rectForRow(at: indexPath) -// let distanceToTableViewTopEdge = tableView.convert(rectForCell, to: nil).origin.y - tableView.safeAreaInsets.top -// guard distanceToTableViewTopEdge >= 0 else { continue } -// _index = i -// break -// } -// -// guard let index = _index else { return nil } -// let sourceIndexPath = IndexPath(row: index, section: 0) -// -// let rectForSourceItemCell = tableView.rectForRow(at: sourceIndexPath) -// let sourceDistanceToTableViewTopEdge = tableView.convert(rectForSourceItemCell, to: nil).origin.y - tableView.safeAreaInsets.top -// -// guard sourceIndexPath.section < oldSnapshot.numberOfSections, -// sourceIndexPath.row < oldSnapshot.numberOfItems(inSection: oldSnapshot.sectionIdentifiers[sourceIndexPath.section]) -// else { return nil } -// -// let sectionIdentifier = oldSnapshot.sectionIdentifiers[sourceIndexPath.section] -// let item = oldSnapshot.itemIdentifiers(inSection: sectionIdentifier)[sourceIndexPath.row] -// -// guard let targetIndexPathRow = newSnapshot.indexOfItem(item), -// let newSectionIdentifier = newSnapshot.sectionIdentifier(containingItem: item), -// let targetIndexPathSection = newSnapshot.indexOfSection(newSectionIdentifier) -// else { return nil } -// -// let targetIndexPath = IndexPath(row: targetIndexPathRow, section: targetIndexPathSection) -// -// return Difference( -// item: item, -// sourceIndexPath: sourceIndexPath, -// sourceDistanceToTableViewTopEdge: sourceDistanceToTableViewTopEdge, -// targetIndexPath: targetIndexPath -// ) -// } + @MainActor func updateDataSource( + snapshot: NSDiffableDataSourceSnapshot, + animatingDifferences: Bool + ) { + diffableDataSource?.apply(snapshot, animatingDifferences: animatingDifferences) + } + + @MainActor func updateSnapshotUsingReloadData( + snapshot: NSDiffableDataSourceSnapshot + ) { + diffableDataSource?.applySnapshotUsingReloadData(snapshot) + } + + @MainActor func reloadSnapshotWithDifference( + tableView: UITableView, + oldSnapshot: NSDiffableDataSourceSnapshot, + newSnapshot: NSDiffableDataSourceSnapshot, + difference: Difference + ) { + tableView.isUserInteractionEnabled = false + tableView.panGestureRecognizer.isEnabled = false + defer { + tableView.isUserInteractionEnabled = true + tableView.panGestureRecognizer.isEnabled = true + } + diffableDataSource?.applySnapshotUsingReloadData(newSnapshot) + + // set bottom inset + + guard let index = newSnapshot.indexOfItem(.root), + let lastItem = newSnapshot.itemIdentifiers.last, + let lastIndex = newSnapshot.indexOfItem(lastItem) + else { + return + } + let rectForCell = tableView.rectForRow(at: IndexPath(row: index, section: 0)) + let rectForLastCell = tableView.rectForRow(at: IndexPath(row: lastIndex, section: 0)) + let rectForTargetCell = tableView.rectForRow(at: difference.targetIndexPath) + + // always set bottom inset due to lazy reply loading + // otherwise tableView will jump when insert replies + let bottomSpacing = tableView.safeAreaLayoutGuide.layoutFrame.height - rectForCell.height - TimelineLoaderTableViewCell.cellHeight + let additionalInset = round(rectForLastCell.maxY - rectForCell.maxY) + let inset = bottomSpacing - max(0, additionalInset) + tableView.contentInset.bottom = max(0, inset) + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): content inset bottom: \(tableView.contentInset.bottom)") + + tableView.contentOffset.y = { + var offset: CGFloat = rectForTargetCell.minY + offset -= tableView.safeAreaInsets.top + offset -= difference.sourceDistanceToTableViewTopEdge + return offset + }() + } } diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift index 0d66f1f2..74ff2075 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift @@ -26,12 +26,15 @@ final class StatusThreadViewModel { @Published public var viewLayoutFrame = ViewLayoutFrame() let conversationRootTableViewCell = StatusTableViewCell() + + var fetchInitalConversationTask: AnyCancellable? // input let context: AppContext let authContext: AuthContext let kind: Kind + @Published var deleteStatusIDs = Set() // let twitterStatusThreadReplyViewModel: TwitterStatusThreadReplyViewModel // let twitterStatusThreadLeafViewModel: TwitterStatusThreadLeafViewModel // let mastodonStatusThreadViewModel: MastodonStatusThreadViewModel @@ -46,6 +49,15 @@ final class StatusThreadViewModel { @Published private(set) var status: StatusObject? @Published private(set) var statusViewModel: StatusView.ViewModel? + @Published var topPendingThreads: [Thread] = [] + @Published var topThreads: [Thread] = [] + @Published var bottomThreads: [Thread] = [] + + @Published var topCursor: Cursor = .none + @Published var bottomCursor: Cursor = .none + @Published var isLoadTop: Bool = false + @Published var isLoadBottom: Bool = false + // var root: CurrentValueSubject // var threadContext = CurrentValueSubject(nil) // @Published var replies: [StatusItem] = [] @@ -82,9 +94,27 @@ final class StatusThreadViewModel { case .status(let status): update(status: status) case .twitter, .mastodon: - break + Task { + await fetch(kind: kind) + } // end Task } - + + fetchInitalConversationTask = Timer.publish(every: 3, on: .main, in: .common) + .autoconnect() + .map { _ in () } + .prepend(()) + .sink { [weak self] in + guard let self = self else { return } + guard let status = self.status else { return } + guard self.topCursor.isNone && self.bottomCursor.isNone else { + self.fetchInitalConversationTask = nil + return + } + let record = status.asRecord + Task { + try await self.fetchConversation(status: record, cursor: .none) + } // end Task + } // self.twitterStatusThreadReplyViewModel = TwitterStatusThreadReplyViewModel(context: context, authContext: authContext) // self.twitterStatusThreadLeafViewModel = TwitterStatusThreadLeafViewModel(context: context) // self.mastodonStatusThreadViewModel = MastodonStatusThreadViewModel(context: context) @@ -144,14 +174,46 @@ extension StatusThreadViewModel { enum Kind { case status(StatusRecord) case twitter(Twitter.Entity.V2.Tweet.ID) - case mastodon(Mastodon.Entity.Status.ID) + case mastodon(domain: String, Mastodon.Entity.Status.ID) + } + + enum Thread: Hashable { + case selfThread(status: StatusRecord) + case conversationThread(components: [StatusRecord]) + } + + enum Cursor { + case none + case value(String) + case noMore + + var isNone: Bool { + switch self { + case .none: return true + default: return false + } + } + + var isNoMore: Bool { + switch self { + case .noMore: return true + default: return false + } + } + + var value: String? { + switch self { + case .value(let value): return value + default: return nil + } + } } public enum Section: Hashable { case main } // end Section - public enum Item: Hashable { + public enum Item: Hashable, DifferenceItem { // case case status(status: StatusRecord) case root @@ -189,10 +251,8 @@ extension StatusThreadViewModel { public var isTransient: Bool { switch self { - case .topLoader, .bottomLoader: - return true - default: - return false + case .topLoader, .bottomLoader: return true + default: return false } } } // end Item @@ -201,11 +261,13 @@ extension StatusThreadViewModel { extension StatusThreadViewModel { @MainActor func update(status record: StatusRecord) { - guard statusViewModel == nil else { return } guard let status = record.object(in: context.managedObjectContext) else { assertionFailure() return } + self.status = status + + guard statusViewModel == nil else { return } let _statusViewViewModel = StatusView.ViewModel( status: status, authContext: authContext, @@ -215,6 +277,200 @@ extension StatusThreadViewModel { ) self.statusViewModel = _statusViewViewModel } + + @MainActor + func fetch(kind: Kind) async { + guard status == nil else { return } + + do { + switch kind { + case .status: + return + case .twitter(let statusID): + guard let authenticationContext = authContext.authenticationContext.twitterAuthenticationContext else { return } + _ = try await context.apiService.twitterStatus( + statusIDs: [statusID], + authenticationContext: authenticationContext + ) + let request = TwitterStatus.sortedFetchRequest + request.predicate = TwitterStatus.predicate(id: statusID) + request.fetchLimit = 1 + guard let result = try context.managedObjectContext.fetch(request).first else { + return + } + update(status: .twitter(record: result.asRecrod)) + case .mastodon(let domain, let statusID): + guard let authenticationContext = authContext.authenticationContext.mastodonAuthenticationContext else { return } + _ = try await context.apiService.mastodonStatus( + statusID: statusID, + authenticationContext: authenticationContext + ) + let request = MastodonStatus.sortedFetchRequest + request.predicate = MastodonStatus.predicate(domain: domain, id: statusID) + request.fetchLimit = 1 + guard let result = try context.managedObjectContext.fetch(request).first else { + return + } + update(status: .mastodon(record: result.asRecrod)) + } + } catch { + try? await Task.sleep(nanoseconds: 3 * .second) + await fetch(kind: kind) + } + } + + @MainActor + func loadTop() async throws { + guard !isLoadTop else { return } + isLoadTop = true + defer { isLoadTop = false } + + guard let status = self.statusViewModel?.status else { return } + guard case .value(let cursor) = topCursor else { return } + try await fetchConversation(status: status, cursor: .value(cursor)) + } + + @MainActor + func loadBottom() async throws { + guard !isLoadBottom else { return } + isLoadBottom = true + defer { isLoadBottom = false } + + guard let status = self.statusViewModel?.status else { return } + guard case .value(let cursor) = bottomCursor else { return } + try await fetchConversation(status: status, cursor: .value(cursor)) + } + + @MainActor + func appendBottom(threads: [Thread]) { + var result = self.bottomThreads + result.append(contentsOf: threads) + self.bottomThreads = result + } + + @MainActor + func enqueueTop(threads: [Thread]) { + var result = self.topThreads + result.insert(contentsOf: threads, at: 0) + self.topThreads = result + } + +} + +extension StatusThreadViewModel { + private func fetchConversation( + status: StatusRecord, + cursor: Cursor + ) async throws { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch conversation, cursor: \(String(describing: cursor))") + switch status { + case .twitter(let record): + try await fetchConversation(status: record, cursor: cursor) + case .mastodon(let record): + try await fetchConversation(status: record, cursor: cursor) + } + } + + @MainActor + private func fetchConversation( + status: ManagedObjectRecord, + cursor: Cursor + ) async throws { + guard let authenticationContext = authContext.authenticationContext.twitterAuthenticationContext else { return } + let _conversationRootStatusID: TwitterStatus.ID? = await context.managedObjectContext.perform { + guard let status = status.object(in: self.context.managedObjectContext) else { return nil } + let statusID = (status.repost ?? status).id // remove repost wrapper + return statusID + } + guard let conversationRootStatusID = _conversationRootStatusID else { + assertionFailure() + return + } + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch conversation for \(conversationRootStatusID), cursor: \(cursor.value ?? "")") + let guestAuthorization = try await authContext.twitterGuestAuthorization() + let response = try await context.apiService.twitterStatusConversation( + conversationRootStatusID: conversationRootStatusID, + query: .init(cursor: cursor.value), + guestAuthentication: guestAuthorization, + authenticationContext: authenticationContext + ) + + // update cursor + if let cursor = response.value.timeline.topCursor { + self.topCursor = .value(cursor) + } else { + self.topCursor = .noMore + } + if let cursor = response.value.timeline.bottomCursor { + self.bottomCursor = .value(cursor) + } else { + self.bottomCursor = .noMore + } + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch conversation: \(response.value.globalObjects.tweets.count) tweets, \(response.value.globalObjects.users.count) users, top cursor: \(response.value.timeline.topCursor ?? ""), bottom cursor: \(response.value.timeline.bottomCursor ?? ""), timeline entries \(response.value.timeline.entries.count)") + + let timeline = response.value.timeline + let statusDict: [Twitter.Entity.V2.Tweet.ID: ManagedObjectRecord] = { + var dict: [TwitterStatus.ID: ManagedObjectRecord] = [:] + let request = TwitterStatus.sortedFetchRequest + let statusIDs = response.value.globalObjects.tweets.map { $0.idStr } + request.predicate = TwitterStatus.predicate(ids: statusIDs) + let result = try? context.managedObjectContext.fetch(request) + for status in result ?? [] { + guard status.id != conversationRootStatusID else { continue } + dict[status.id] = status.asRecrod + } + return dict + }() + let topThreads: [Thread] = { + var threads: [Thread] = [] + for entry in timeline.entries { + switch entry { + case .tweet(let statusID): + guard let status = statusDict[statusID] else { + continue + } + threads.append(.selfThread(status: .twitter(record: status))) + default: + continue + } + } + return threads + }() + let bottomThreads: [Thread] = { + var threads: [Thread] = [] + for entry in timeline.entries { + switch entry { + case .conversationThread(let componentIDs): + let components = componentIDs + .compactMap { statusDict[$0] } + .map { StatusRecord.twitter(record: $0) } + guard !components.isEmpty else { + assertionFailure() + continue + } + threads.append(.conversationThread(components: components)) + default: + continue + } + } + return threads + }() + enqueueTop(threads: topThreads) + appendBottom(threads: bottomThreads) + + if topThreads.isEmpty && bottomThreads.isEmpty { + // trigger data source update + update(status: .twitter(record: status)) + } + } + + @MainActor + private func fetchConversation( + status: ManagedObjectRecord, + cursor: Cursor + ) async throws { + + } } extension StatusThreadViewModel { diff --git a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel+Diffable.swift index d9bd3b53..f0d72b67 100644 --- a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel+Diffable.swift @@ -12,6 +12,10 @@ import Combine import CoreData import CoreDataStack +public protocol DifferenceItem { + var isTransient: Bool { get } +} + extension ListTimelineViewModel { struct Difference: CustomStringConvertible { @@ -30,12 +34,28 @@ extension ListTimelineViewModel { } } - @MainActor func calculateReloadSnapshotDifference( + @MainActor func calculateReloadSnapshotDifference( tableView: UITableView, oldSnapshot: NSDiffableDataSourceSnapshot, newSnapshot: NSDiffableDataSourceSnapshot ) -> Difference? { - guard let sourceIndexPath = (tableView.indexPathsForVisibleRows ?? []).sorted().first else { return nil } + guard oldSnapshot.numberOfItems != 0 else { return nil } + guard let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows?.sorted() else { return nil } + + // find index of the first visible item in both old and new snapshot + var _index: Int? + let items = oldSnapshot.itemIdentifiers + for (i, item) in items.enumerated() { + guard let _ = indexPathsForVisibleRows.first(where: { $0.row == i }) else { continue } + guard !item.isTransient else { continue } + guard newSnapshot.indexOfItem(item) != nil else { continue } + _index = i + break + } + + guard let index = _index else { return nil } + let sourceIndexPath = IndexPath(row: index, section: 0) + let rectForSourceItemCell = tableView.rectForRow(at: sourceIndexPath) let sourceDistanceToTableViewTopEdge: CGFloat = { if tableView.window != nil { @@ -44,9 +64,6 @@ extension ListTimelineViewModel { return rectForSourceItemCell.origin.y - tableView.contentOffset.y - tableView.safeAreaInsets.top } }() - guard sourceIndexPath.section < oldSnapshot.numberOfSections, - sourceIndexPath.row < oldSnapshot.numberOfItems(inSection: oldSnapshot.sectionIdentifiers[sourceIndexPath.section]) - else { return nil } let sectionIdentifier = oldSnapshot.sectionIdentifiers[sourceIndexPath.section] let item = oldSnapshot.itemIdentifiers(inSection: sectionIdentifier)[sourceIndexPath.row] diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift index 5e109e86..b878b79f 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift @@ -275,11 +275,11 @@ extension HomeTimelineViewController { from: self, transition: .show ) - case .mastodon: + case .mastodon(let authenticationContext): let statusThreadViewModel = StatusThreadViewModel( context: self.context, authContext: self.authContext, - kind: .mastodon(id) + kind: .mastodon(domain: authenticationContext.domain, id) ) self.coordinator.present( scene: .statusThread(viewModel: statusThreadViewModel), diff --git a/TwidereX/Supporting Files/SceneDelegate.swift b/TwidereX/Supporting Files/SceneDelegate.swift index 983ce94a..baa2a997 100644 --- a/TwidereX/Supporting Files/SceneDelegate.swift +++ b/TwidereX/Supporting Files/SceneDelegate.swift @@ -21,7 +21,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? var coordinator: SceneCoordinator? - #if DEBUG + #if DEBUG || PROFILE var fpsIndicator: FPSIndicator? #endif @@ -31,7 +31,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let window = UIWindow(windowScene: windowScene) self.window = window - #if DEBUG + #if DEBUG && !PROFILE guard !SceneDelegate.isXcodeUnitTest else { window.rootViewController = UIViewController() return @@ -62,7 +62,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window.makeKeyAndVisible() - #if DEBUG + #if DEBUG || PROFILE fpsIndicator = FPSIndicator(windowScene: windowScene) #endif } diff --git a/TwidereX/TwidereX-Performance.xctestplan b/TwidereX/TestPlan/TwidereX-Performance.xctestplan similarity index 88% rename from TwidereX/TwidereX-Performance.xctestplan rename to TwidereX/TestPlan/TwidereX-Performance.xctestplan index 553ffcde..0f267004 100644 --- a/TwidereX/TwidereX-Performance.xctestplan +++ b/TwidereX/TestPlan/TwidereX-Performance.xctestplan @@ -16,8 +16,8 @@ }, "testTargets" : [ { - "selectedTests" : [ - "TwidereXUITests\/testLaunchPerformance()" + "skippedTests" : [ + "TwidereXUITests" ], "target" : { "containerPath" : "container:TwidereX.xcodeproj", diff --git a/TwidereX/TwidereX.xctestplan b/TwidereX/TestPlan/TwidereX.xctestplan similarity index 86% rename from TwidereX/TwidereX.xctestplan rename to TwidereX/TestPlan/TwidereX.xctestplan index c31985d2..e2ee199a 100644 --- a/TwidereX/TwidereX.xctestplan +++ b/TwidereX/TestPlan/TwidereX.xctestplan @@ -25,10 +25,7 @@ } }, { - "selectedTests" : [ - "TwidereXUITests\/testExample()", - "TwidereXUITests\/testLaunchPerformance()" - ], + "enabled" : false, "target" : { "containerPath" : "container:TwidereX.xcodeproj", "identifier" : "DBDA8E3E24FCF8A7006750DC", diff --git a/TwidereXTests/CombineTests.swift b/TwidereXTests/CombineTests.swift index f3e99499..0c863535 100644 --- a/TwidereXTests/CombineTests.swift +++ b/TwidereXTests/CombineTests.swift @@ -9,7 +9,8 @@ import os.log import XCTest import Combine -@testable import TwidereX +import TwidereCore +import TwidereCommon class CombineTests: XCTestCase { @@ -22,67 +23,67 @@ class CombineTests: XCTestCase { /// /// This test case crash on the iOS 14.1 /// EXC_BAD_INSTRUCTION - func testMapRetrySwitchToLatest() throws { - let outputExpectation = self.expectation(description: "future") - - let inputA = PassthroughSubject() - let inputB = PassthroughSubject() - - Publishers.CombineLatest( - inputA.eraseToAnyPublisher(), - inputB.eraseToAnyPublisher() - ) - .compactMap { inputA, inputB -> (Int, Int)? in - guard let inputA = inputA, let inputB = inputB else { return nil } - return (inputA, inputB) - } - .setFailureType(to: Error.self) - .map { inputA, inputB -> AnyPublisher in - return Future { promise in - AppContext.shared.backgroundManagedObjectContext.perform { - promise(.success(inputA + inputB)) - } - } - .tryMap { output -> AnyPublisher in - guard output != 0 else { - throw StubError.stub - } - - return AppContext.shared.backgroundManagedObjectContext.performChanges { - // do nothing - } - .setFailureType(to: Error.self) - .tryMap { result -> Int in - switch result { - case .success: return output - case .failure(let error): throw error - } - } - .eraseToAnyPublisher() - } - .switchToLatest() - .eraseToAnyPublisher() - .retry(3) - .eraseToAnyPublisher() - } - .switchToLatest() - .sink { completion in - switch completion { - case .failure(let error): - outputExpectation.fulfill() - case .finished: - break - } - } receiveValue: { response in - self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(response)") - } - .store(in: &disposeBag) - - inputA.send(1) - inputB.send(-1) - - wait(for: [outputExpectation], timeout: 20) - } +// func testMapRetrySwitchToLatest() throws { +// let outputExpectation = self.expectation(description: "future") +// +// let inputA = PassthroughSubject() +// let inputB = PassthroughSubject() +// +// Publishers.CombineLatest( +// inputA.eraseToAnyPublisher(), +// inputB.eraseToAnyPublisher() +// ) +// .compactMap { inputA, inputB -> (Int, Int)? in +// guard let inputA = inputA, let inputB = inputB else { return nil } +// return (inputA, inputB) +// } +// .setFailureType(to: Error.self) +// .map { inputA, inputB -> AnyPublisher in +// return Future { promise in +// AppContext.shared.backgroundManagedObjectContext.perform { +// promise(.success(inputA + inputB)) +// } +// } +// .tryMap { output -> AnyPublisher in +// guard output != 0 else { +// throw StubError.stub +// } +// +// return AppContext.shared.backgroundManagedObjectContext.performChanges { +// // do nothing +// } +// .setFailureType(to: Error.self) +// .tryMap { result -> Int in +// switch result { +// case .success: return output +// case .failure(let error): throw error +// } +// } +// .eraseToAnyPublisher() +// } +// .switchToLatest() +// .eraseToAnyPublisher() +// .retry(3) +// .eraseToAnyPublisher() +// } +// .switchToLatest() +// .sink { completion in +// switch completion { +// case .failure(let error): +// outputExpectation.fulfill() +// case .finished: +// break +// } +// } receiveValue: { response in +// self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(response)") +// } +// .store(in: &disposeBag) +// +// inputA.send(1) +// inputB.send(-1) +// +// wait(for: [outputExpectation], timeout: 20) +// } } diff --git a/TwidereXTests/TwidereXTests+Issue92.swift b/TwidereXTests/TwidereXTests+Issue92.swift index e89bdc20..96636c7b 100644 --- a/TwidereXTests/TwidereXTests+Issue92.swift +++ b/TwidereXTests/TwidereXTests+Issue92.swift @@ -7,7 +7,7 @@ // import XCTest -@testable import TwidereX +import TwidereCore // https://github.com/TwidereProject/TwidereX-iOS/issues/92 extension TwidereXTests { diff --git a/TwidereXTests/TwidereXTests.swift b/TwidereXTests/TwidereXTests.swift index 36a35584..e1f65d67 100644 --- a/TwidereXTests/TwidereXTests.swift +++ b/TwidereXTests/TwidereXTests.swift @@ -7,7 +7,6 @@ import os.log import XCTest -@testable import TwidereX class TwidereXTests: XCTestCase { diff --git a/TwidereXUITests/TwidereXUITests+Onboarding.swift b/TwidereXUITests/TwidereXUITests+Onboarding.swift index 94d236d7..d5c0776a 100644 --- a/TwidereXUITests/TwidereXUITests+Onboarding.swift +++ b/TwidereXUITests/TwidereXUITests+Onboarding.swift @@ -21,6 +21,7 @@ extension TwidereXUITests { try await authorizeTwitter(app: app) } + } extension TwidereXUITests { diff --git a/TwidereXUITests/TwidereXUITests+Performance.swift b/TwidereXUITests/TwidereXUITests+Performance.swift new file mode 100644 index 00000000..cb01199a --- /dev/null +++ b/TwidereXUITests/TwidereXUITests+Performance.swift @@ -0,0 +1,44 @@ +// +// TwidereXUITests+Performance.swift +// TwidereXUITests +// +// Created by MainasuK on 2023/3/28. +// Copyright © 2023 Twidere. All rights reserved. +// + +import XCTest + +final class TwidereXUITests_Performance: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + +} + +extension TwidereXUITests_Performance { + + func testHomeTimelineScrollingAnimationPerformance() throws { + let app = XCUIApplication() + app.launch() + + let tableView = app.tables.firstMatch + XCTAssert(tableView.waitForExistence(timeout: 5)) + + let measureOptions = XCTMeasureOptions() + measureOptions.invocationOptions = [.manuallyStop] + + measure(metrics: [XCTOSSignpostMetric.scrollingAndDecelerationMetric], options: measureOptions) { + tableView.swipeUp(velocity: .fast) + tableView.swipeUp(velocity: .fast) + stopMeasuring() + tableView.swipeDown(velocity: .fast) + tableView.swipeDown(velocity: .fast) + } + } + +} From 68623bf61b7e3100a2cc4033f4f9fdf2ade3e55f Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 29 Mar 2023 17:17:18 +0800 Subject: [PATCH 053/128] feat: add Mastodon conversation fetch logic and conversation link for reply and reply to post --- .../TwidereUI/Content/PollOptionView.swift | 29 +- .../Sources/TwidereUI/Content/PollView.swift | 7 + .../TwidereUI/Content/StatusHeaderView.swift | 5 +- .../Content/StatusView+ViewModel.swift | 25 +- .../TwidereUI/Content/StatusView.swift | 48 +- .../ComposeContent/ComposeContentView.swift | 2 - .../TimelineBottomLoaderTableViewCell.swift | 4 +- .../Loader}/TimelineLoaderTableViewCell.swift | 12 +- ...eMiddleLoaderTableViewCell+ViewModel.swift | 0 .../TimelineMiddleLoaderTableViewCell.swift | 4 +- .../TimelineTopLoaderTableViewCell.swift | 2 +- TwidereX.xcodeproj/project.pbxproj | 20 - .../MastodonStatusThreadViewModel.swift | 184 +++--- .../StatusThreadViewModel+Diffable.swift | 122 ++-- ...tatusThreadViewModel+LoadThreadState.swift | 322 +++++------ .../StatusThread/StatusThreadViewModel.swift | 119 +++- .../TwitterStatusThreadLeafViewModel.swift | 316 +++++------ ...tterStatusThreadReplyViewModel+State.swift | 528 +++++++++--------- .../TwitterStatusThreadReplyViewModel.swift | 168 +++--- 19 files changed, 1004 insertions(+), 913 deletions(-) rename {TwidereX/Scene/Share/View/TableViewCell => TwidereSDK/Sources/TwidereUI/TableViewCell/Loader}/TimelineBottomLoaderTableViewCell.swift (75%) rename {TwidereX/Scene/Share/View/TableViewCell => TwidereSDK/Sources/TwidereUI/TableViewCell/Loader}/TimelineLoaderTableViewCell.swift (87%) rename {TwidereX/Scene/Share/View/TableViewCell => TwidereSDK/Sources/TwidereUI/TableViewCell/Loader}/TimelineMiddleLoaderTableViewCell+ViewModel.swift (100%) rename {TwidereX/Scene/Share/View/TableViewCell => TwidereSDK/Sources/TwidereUI/TableViewCell/Loader}/TimelineMiddleLoaderTableViewCell.swift (93%) rename {TwidereX/Scene/Share/View/TableViewCell => TwidereSDK/Sources/TwidereUI/TableViewCell/Loader}/TimelineTopLoaderTableViewCell.swift (69%) diff --git a/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift b/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift index 69161d22..f942b359 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift @@ -55,17 +55,21 @@ public struct PollOptionView: View { .padding(markViewPadding) .frame(width: rowHeight, height: rowHeight) .opacity(viewModel.canSelect || viewModel.isOptionVoted ? 1 : 0) - LabelRepresentable( - metaContent: viewModel.content, - textStyle: .pollOptionTitle, - setupLabel: { label in - label.setContentHuggingPriority(.required, for: .horizontal) - label.setContentCompressionResistancePriority(.required, for: .horizontal) + ScrollView(.horizontal, showsIndicators: false) { + HStack(alignment: .center) { + LabelRepresentable( + metaContent: viewModel.content, + textStyle: .pollOptionTitle, + setupLabel: { label in + label.setContentHuggingPriority(.required, for: .horizontal) + label.setContentCompressionResistancePriority(.required, for: .horizontal) + } + ) + .fixedSize(horizontal: false, vertical: true) + Spacer() } - ) - .fixedSize(horizontal: false, vertical: true) - .border(.red, width: 1) - Spacer() + } // end ScrollView + // TODO: https://developer.apple.com/documentation/swiftui/view/scrollbouncebehavior(_:axes:)?changes=latest_minor Text(viewModel.percentageText) .font(Font(bodyFont)) .foregroundColor(.secondary) @@ -166,6 +170,7 @@ extension PollOptionView { isClosed = true // cannot vote for Twitter isMulitpleChoice = false isSelected = false + votes = Int(option.votes) option.publisher(for: \.votes) .map { Int($0) } .assign(to: &$votes) @@ -183,9 +188,7 @@ extension PollOptionView { isMulitpleChoice = option.poll.multiple option.poll.publisher(for: \.expired) .assign(to: &$isClosed) - option.poll.publisher(for: \.votesCount) - .map { Int($0) } - .assign(to: &$totalVotes) + votes = Int(option.votesCount) option.publisher(for: \.votesCount) .map { Int($0) } .assign(to: &$votes) diff --git a/TwidereSDK/Sources/TwidereUI/Content/PollView.swift b/TwidereSDK/Sources/TwidereUI/Content/PollView.swift index 0dec275c..4675d57c 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/PollView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/PollView.swift @@ -241,6 +241,13 @@ extension PollView { } // collect votes into votesCount + + let votesCount = options + .map { $0.votes } + .reduce(0, +) + options + .forEach { $0.totalVotes = votesCount } + Publishers.MergeMany(options.map { $0.$votes }) .receive(on: DispatchQueue.main) .compactMap { [weak self] _ in diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift index d227c826..8afbcaa7 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift @@ -27,7 +27,7 @@ public struct StatusHeaderView: View { HStack(spacing: .zero) { if viewModel.hasHangingAvatar { let width = viewModel.avatarDimension - + StatusView.hangingAvatarButtonTrailingSapcing + + StatusView.hangingAvatarButtonTrailingSpacing - iconImageDimension - StatusHeaderView.iconImageTrailingSpacing Color.clear @@ -67,6 +67,9 @@ extension StatusHeaderView { @Published public var hasHangingAvatar: Bool = false @Published public var avatarDimension: CGFloat = StatusView.hangingAvatarButtonDimension + // output + public var viewSize: CGSize = .zero + public init( image: UIImage, label: MetaContent diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index 88cc9a32..f511e01c 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -182,6 +182,8 @@ extension StatusView { // metric @Published public var metricViewModel: StatusMetricView.ViewModel? + // conversation link + @Published public var isTopConversationLinkLineViewDisplay = false @Published public var isBottomConversationLinkLineViewDisplay = false private init( @@ -921,14 +923,14 @@ extension StatusView.ViewModel { switch kind { case .timeline, .referenceReplyTo: width += StatusView.hangingAvatarButtonDimension - width += StatusView.hangingAvatarButtonTrailingSapcing + width += StatusView.hangingAvatarButtonTrailingSpacing default: break } return width } - public var margin: CGFloat { + var margin: CGFloat { switch kind { case .quote: return 12 default: return .zero @@ -944,11 +946,21 @@ extension StatusView.ViewModel { } } - public var cellTopMargin: CGFloat { + var cellTopMargin: CGFloat { return parentViewModel == nil ? 12 : 0 } - public var hasToolbar: Bool { + var topConversationLinkViewHeight: CGFloat { + var height: CGFloat = cellTopMargin + if let statusHeaderViewModel = statusHeaderViewModel { + height += statusHeaderViewModel.viewSize.height + height += StatusView.statusHeaderBottomSpacing + height += parentViewModel?.cellTopMargin ?? 0 + } + return height + } + + var hasToolbar: Bool { switch kind { case .timeline, .conversationRoot, .conversationThread: return true @@ -1060,7 +1072,10 @@ extension StatusView.ViewModel { return label }() ) - _statusHeaderViewModel.hasHangingAvatar = _repostViewModel.hasHangingAvatar + _statusHeaderViewModel.hasHangingAvatar = { + if kind == .conversationRoot { return true } + return _repostViewModel.hasHangingAvatar + }() _repostViewModel.statusHeaderViewModel = _statusHeaderViewModel } if let quote = status.quote { diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index 6a298c8b..e7178e6a 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -57,8 +57,9 @@ public struct StatusView: View { static let logger = Logger(subsystem: "StatusView", category: "View") var logger: Logger { StatusView.logger } + static var statusHeaderBottomSpacing: CGFloat { 6.0 } static var hangingAvatarButtonDimension: CGFloat { 44.0 } - static var hangingAvatarButtonTrailingSapcing: CGFloat { 10.0 } + static var hangingAvatarButtonTrailingSpacing: CGFloat { 10.0 } @ObservedObject public private(set) var viewModel: ViewModel @@ -71,11 +72,21 @@ public struct StatusView: View { } public var body: some View { - VStack { + VStack(spacing: .zero) { if let repostViewModel = viewModel.repostViewModel { // header if let statusHeaderViewModel = repostViewModel.statusHeaderViewModel { StatusHeaderView(viewModel: statusHeaderViewModel) + .background(GeometryReader { proxy in + Color.clear.preference( + key: ViewFrameKey.self, + value: proxy.frame(in: .local) + ) + .onPreferenceChange(ViewFrameKey.self) { frame in + statusHeaderViewModel.viewSize = frame.size + } + }) + Color.clear.frame(height: StatusView.statusHeaderBottomSpacing) } // post StatusView(viewModel: repostViewModel) @@ -83,7 +94,7 @@ public struct StatusView: View { HStack(alignment: .top, spacing: .zero) { if viewModel.hasHangingAvatar { avatarButton - .padding(.trailing, StatusView.hangingAvatarButtonTrailingSapcing) + .padding(.trailing, Self.hangingAvatarButtonTrailingSpacing) } let contentSpacing: CGFloat = 4 VStack(spacing: contentSpacing) { @@ -218,19 +229,30 @@ public struct StatusView: View { } } // end HStack .overlay { - if viewModel.isBottomConversationLinkLineViewDisplay { - HStack(alignment: .top, spacing: .zero) { - VStack(spacing: 0) { - Color.clear - .frame(width: StatusView.hangingAvatarButtonDimension, height: StatusView.hangingAvatarButtonDimension) + HStack(alignment: .top, spacing: .zero) { + VStack(alignment: .center, spacing: 0) { + Color.clear + .frame(width: StatusView.hangingAvatarButtonDimension, height: StatusView.hangingAvatarButtonDimension) + // bottom conversation link + Rectangle() + .foregroundColor(Color(uiColor: .separator)) + .background(.clear) + .frame(width: 1) + .opacity(viewModel.isBottomConversationLinkLineViewDisplay ? 1 : 0) + } + .overlay(alignment: .top) { + Group { + // top conversation link Rectangle() .foregroundColor(Color(uiColor: .separator)) .background(.clear) - .frame(width: 1) - } - Spacer() - } // end HStack - } // end if + .frame(width: 1, height: viewModel.topConversationLinkViewHeight) + .offset(y: -viewModel.topConversationLinkViewHeight) + .opacity(viewModel.isTopConversationLinkLineViewDisplay ? 1 : 0) + } // end Group + } + Spacer() + } // end HStack } // end overlay } // end if … else … } // end VStack diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift index 60fac2c1..9344ddcc 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift @@ -43,8 +43,6 @@ public struct ComposeContentView: View { case .reply: if let replyToStatusViewModel = viewModel.replyToStatusViewModel { StatusView(viewModel: replyToStatusViewModel) - // .frame(width: viewModel.viewLayoutFrame.readableContentLayoutFrame.width) - .padding(.top, ComposeContentView.contentRowTopPadding) } default: EmptyView() diff --git a/TwidereX/Scene/Share/View/TableViewCell/TimelineBottomLoaderTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/Loader/TimelineBottomLoaderTableViewCell.swift similarity index 75% rename from TwidereX/Scene/Share/View/TableViewCell/TimelineBottomLoaderTableViewCell.swift rename to TwidereSDK/Sources/TwidereUI/TableViewCell/Loader/TimelineBottomLoaderTableViewCell.swift index 1df29881..e394fdcd 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/TimelineBottomLoaderTableViewCell.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/Loader/TimelineBottomLoaderTableViewCell.swift @@ -8,9 +8,9 @@ import UIKit import Combine -final class TimelineBottomLoaderTableViewCell: TimelineLoaderTableViewCell { +public final class TimelineBottomLoaderTableViewCell: TimelineLoaderTableViewCell { - override func prepareForReuse() { + public override func prepareForReuse() { super.prepareForReuse() activityIndicatorView.startAnimating() diff --git a/TwidereX/Scene/Share/View/TableViewCell/TimelineLoaderTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/Loader/TimelineLoaderTableViewCell.swift similarity index 87% rename from TwidereX/Scene/Share/View/TableViewCell/TimelineLoaderTableViewCell.swift rename to TwidereSDK/Sources/TwidereUI/TableViewCell/Loader/TimelineLoaderTableViewCell.swift index 93146e52..593b562c 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/TimelineLoaderTableViewCell.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/Loader/TimelineLoaderTableViewCell.swift @@ -10,9 +10,9 @@ import UIKit import Combine import TwidereCore -class TimelineLoaderTableViewCell: UITableViewCell { +public class TimelineLoaderTableViewCell: UITableViewCell { - static let cellHeight: CGFloat = 48 + public static let cellHeight: CGFloat = 48 var disposeBag = Set() @@ -26,24 +26,24 @@ class TimelineLoaderTableViewCell: UITableViewCell { return button }() - let activityIndicatorView: UIActivityIndicatorView = { + public let activityIndicatorView: UIActivityIndicatorView = { let activityIndicatorView = UIActivityIndicatorView(style: .medium) activityIndicatorView.tintColor = .systemFill activityIndicatorView.hidesWhenStopped = true return activityIndicatorView }() - override func prepareForReuse() { + public override func prepareForReuse() { super.prepareForReuse() disposeBag.removeAll() } - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) _init() } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { super.init(coder: coder) _init() } diff --git a/TwidereX/Scene/Share/View/TableViewCell/TimelineMiddleLoaderTableViewCell+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/Loader/TimelineMiddleLoaderTableViewCell+ViewModel.swift similarity index 100% rename from TwidereX/Scene/Share/View/TableViewCell/TimelineMiddleLoaderTableViewCell+ViewModel.swift rename to TwidereSDK/Sources/TwidereUI/TableViewCell/Loader/TimelineMiddleLoaderTableViewCell+ViewModel.swift diff --git a/TwidereX/Scene/Share/View/TableViewCell/TimelineMiddleLoaderTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/Loader/TimelineMiddleLoaderTableViewCell.swift similarity index 93% rename from TwidereX/Scene/Share/View/TableViewCell/TimelineMiddleLoaderTableViewCell.swift rename to TwidereSDK/Sources/TwidereUI/TableViewCell/Loader/TimelineMiddleLoaderTableViewCell.swift index 5ecc40ca..129c837c 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/TimelineMiddleLoaderTableViewCell.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/Loader/TimelineMiddleLoaderTableViewCell.swift @@ -10,11 +10,11 @@ import UIKit import Combine import CoreData -protocol TimelineMiddleLoaderTableViewCellDelegate: AnyObject { +public protocol TimelineMiddleLoaderTableViewCellDelegate: AnyObject { func timelineMiddleLoaderTableViewCell(_ cell: TimelineMiddleLoaderTableViewCell, loadMoreButtonDidPressed button: UIButton) } -final class TimelineMiddleLoaderTableViewCell: TimelineLoaderTableViewCell { +public final class TimelineMiddleLoaderTableViewCell: TimelineLoaderTableViewCell { weak var delegate: TimelineMiddleLoaderTableViewCellDelegate? diff --git a/TwidereX/Scene/Share/View/TableViewCell/TimelineTopLoaderTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/Loader/TimelineTopLoaderTableViewCell.swift similarity index 69% rename from TwidereX/Scene/Share/View/TableViewCell/TimelineTopLoaderTableViewCell.swift rename to TwidereSDK/Sources/TwidereUI/TableViewCell/Loader/TimelineTopLoaderTableViewCell.swift index f8f6fe34..244e89fc 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/TimelineTopLoaderTableViewCell.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/Loader/TimelineTopLoaderTableViewCell.swift @@ -9,4 +9,4 @@ import UIKit import Combine -final class TimelineTopLoaderTableViewCell: TimelineLoaderTableViewCell { } +public final class TimelineTopLoaderTableViewCell: TimelineLoaderTableViewCell { } diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 989ef6ed..34999dc3 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -125,7 +125,6 @@ DB47AB2D27CE085900CD73C7 /* ListViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47AB2C27CE085800CD73C7 /* ListViewModel+Diffable.swift */; }; DB47AB2E27CE097C00CD73C7 /* ListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47AB1F27CCC18500CD73C7 /* ListItem.swift */; }; DB47AB2F27CE097C00CD73C7 /* ListSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47AB2027CCC18500CD73C7 /* ListSection.swift */; }; - DB47AB3E27CE1F9E00CD73C7 /* TimelineTopLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47AB3D27CE1F9E00CD73C7 /* TimelineTopLoaderTableViewCell.swift */; }; DB51DC1A2715581E00A0D8FB /* ProfileDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51DC192715581E00A0D8FB /* ProfileDashboardView.swift */; }; DB51DC1C2715588E00A0D8FB /* ProfileDashboardMeterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51DC1B2715588E00A0D8FB /* ProfileDashboardMeterView.swift */; }; DB51DC3B2716F82000A0D8FB /* CellFrameCacheContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51DC3A2716F82000A0D8FB /* CellFrameCacheContainer.swift */; }; @@ -247,7 +246,6 @@ DB88E6292890DF67009A01F5 /* BehaviorsPreferenceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB88E6282890DF67009A01F5 /* BehaviorsPreferenceViewController.swift */; }; DB88E62C2890DF79009A01F5 /* BehaviorsPreferenceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB88E62B2890DF79009A01F5 /* BehaviorsPreferenceViewModel.swift */; }; DB88E62E2890DF7E009A01F5 /* BehaviorsPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB88E62D2890DF7E009A01F5 /* BehaviorsPreferenceView.swift */; }; - DB89F591257A2A70005ECD04 /* TimelineLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89F590257A2A70005ECD04 /* TimelineLoaderTableViewCell.swift */; }; DB8AC0F825401BA200E636BE /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AC0F725401BA200E636BE /* UIViewController.swift */; }; DB8BFB1C29D1F52900535092 /* TwidereXUITests+Performance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8BFB1B29D1F52900535092 /* TwidereXUITests+Performance.swift */; }; DB8E4FD92563CD6900459DA2 /* TwitterPinBasedAuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8E4FD82563CD6900459DA2 /* TwitterPinBasedAuthenticationViewController.swift */; }; @@ -263,8 +261,6 @@ DB932E5327FEC7390036A824 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = DB932E5527FEC7390036A824 /* Intents.intentdefinition */; }; DB94B6B426C65BE100A2E8A1 /* MastodonAuthenticationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB94B6B326C65BE100A2E8A1 /* MastodonAuthenticationController.swift */; }; DB969A66253064FE0053CB31 /* DispatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB969A65253064FE0053CB31 /* DispatchQueue.swift */; }; - DB98DC0E2507862D0087E30F /* TimelineMiddleLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98DC0D2507862D0087E30F /* TimelineMiddleLoaderTableViewCell.swift */; }; - DB98DC10250787420087E30F /* TimelineBottomLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98DC0F250787420087E30F /* TimelineBottomLoaderTableViewCell.swift */; }; DB9B324D285732A400AC818D /* UserTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9B324C285732A400AC818D /* UserTimelineViewController.swift */; }; DB9B3251285735F200AC818D /* GridTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9B3250285735F200AC818D /* GridTimelineViewController.swift */; }; DB9B32542857369800AC818D /* GridTimelineViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9B32532857369800AC818D /* GridTimelineViewController+DataSourceProvider.swift */; }; @@ -302,7 +298,6 @@ DBBBBE8D2744F9D9007ACB4B /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBBBE8C2744F9D9007ACB4B /* ComposeViewModel.swift */; }; DBBBBE8F2744FB42007ACB4B /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBBBE8E2744FB42007ACB4B /* ComposeViewModel.swift */; }; DBBF70F726D7B62F00971656 /* AccountListTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF70F626D7B62F00971656 /* AccountListTableViewCell+ViewModel.swift */; }; - DBC17A5C26E0A4C1005C0D79 /* TimelineMiddleLoaderTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC17A5B26E0A4C1005C0D79 /* TimelineMiddleLoaderTableViewCell+ViewModel.swift */; }; DBC747E1259DBD5400787EEF /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC747E0259DBD5400787EEF /* AvatarBarButtonItem.swift */; }; DBC8E04B2576337F00401E20 /* DisposeBagCollectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC8E04A2576337F00401E20 /* DisposeBagCollectable.swift */; }; DBC8E050257653E100401E20 /* SavePhotoActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC8E04F257653E100401E20 /* SavePhotoActivity.swift */; }; @@ -591,7 +586,6 @@ DB47AB1F27CCC18500CD73C7 /* ListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListItem.swift; sourceTree = ""; }; DB47AB2027CCC18500CD73C7 /* ListSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListSection.swift; sourceTree = ""; }; DB47AB2C27CE085800CD73C7 /* ListViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ListViewModel+Diffable.swift"; sourceTree = ""; }; - DB47AB3D27CE1F9E00CD73C7 /* TimelineTopLoaderTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineTopLoaderTableViewCell.swift; sourceTree = ""; }; DB51DC192715581E00A0D8FB /* ProfileDashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDashboardView.swift; sourceTree = ""; }; DB51DC1B2715588E00A0D8FB /* ProfileDashboardMeterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDashboardMeterView.swift; sourceTree = ""; }; DB51DC372716AEF000A0D8FB /* TabBarPager */ = {isa = PBXFileReference; lastKnownFileType = folder; path = TabBarPager; sourceTree = ""; }; @@ -704,7 +698,6 @@ DB88E6282890DF67009A01F5 /* BehaviorsPreferenceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BehaviorsPreferenceViewController.swift; sourceTree = ""; }; DB88E62B2890DF79009A01F5 /* BehaviorsPreferenceViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BehaviorsPreferenceViewModel.swift; sourceTree = ""; }; DB88E62D2890DF7E009A01F5 /* BehaviorsPreferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BehaviorsPreferenceView.swift; sourceTree = ""; }; - DB89F590257A2A70005ECD04 /* TimelineLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLoaderTableViewCell.swift; sourceTree = ""; }; DB8AC0F725401BA200E636BE /* UIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = ""; }; DB8AC18F2542DA9500E636BE /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveLabel.swift; sourceTree = ""; }; DB8BFB1B29D1F52900535092 /* TwidereXUITests+Performance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TwidereXUITests+Performance.swift"; sourceTree = ""; }; @@ -733,8 +726,6 @@ DB94B6B326C65BE100A2E8A1 /* MastodonAuthenticationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAuthenticationController.swift; sourceTree = ""; }; DB969A65253064FE0053CB31 /* DispatchQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DispatchQueue.swift; sourceTree = ""; }; DB97D1F4256CF7710056F8C2 /* ProgressBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBarView.swift; sourceTree = ""; }; - DB98DC0D2507862D0087E30F /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = ""; }; - DB98DC0F250787420087E30F /* TimelineBottomLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderTableViewCell.swift; sourceTree = ""; }; DB9B324C285732A400AC818D /* UserTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTimelineViewController.swift; sourceTree = ""; }; DB9B3250285735F200AC818D /* GridTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridTimelineViewController.swift; sourceTree = ""; }; DB9B32532857369800AC818D /* GridTimelineViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GridTimelineViewController+DataSourceProvider.swift"; sourceTree = ""; }; @@ -784,7 +775,6 @@ DBBBBE8C2744F9D9007ACB4B /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = ""; }; DBBBBE8E2744FB42007ACB4B /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = ""; }; DBBF70F626D7B62F00971656 /* AccountListTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountListTableViewCell+ViewModel.swift"; sourceTree = ""; }; - DBC17A5B26E0A4C1005C0D79 /* TimelineMiddleLoaderTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimelineMiddleLoaderTableViewCell+ViewModel.swift"; sourceTree = ""; }; DBC747E0259DBD5400787EEF /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = ""; }; DBC8E04A2576337F00401E20 /* DisposeBagCollectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisposeBagCollectable.swift; sourceTree = ""; }; DBC8E04F257653E100401E20 /* SavePhotoActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavePhotoActivity.swift; sourceTree = ""; }; @@ -1821,11 +1811,6 @@ DB97D1E7256CBA5D0056F8C2 /* TableViewCell */ = { isa = PBXGroup; children = ( - DB89F590257A2A70005ECD04 /* TimelineLoaderTableViewCell.swift */, - DB47AB3D27CE1F9E00CD73C7 /* TimelineTopLoaderTableViewCell.swift */, - DB98DC0D2507862D0087E30F /* TimelineMiddleLoaderTableViewCell.swift */, - DBC17A5B26E0A4C1005C0D79 /* TimelineMiddleLoaderTableViewCell+ViewModel.swift */, - DB98DC0F250787420087E30F /* TimelineBottomLoaderTableViewCell.swift */, DB3B906026E8AB480010F64C /* StatusViewTableViewCellDelegate.swift */, DB30ADDB26CFC7EE00B2D2BE /* StatusTableViewCell.swift */, DB5FD9B226D8D94600CF5439 /* StatusTableViewCell+ViewModel.swift */, @@ -2933,7 +2918,6 @@ DB76A659275F498F00A50673 /* MediaPreviewImageView.swift in Sources */, DB580EB8288187BD00BC4A0F /* AccountPreferenceViewController.swift in Sources */, DB6BCD72277AEED600847054 /* TrendViewModel+Diffable.swift in Sources */, - DB98DC10250787420087E30F /* TimelineBottomLoaderTableViewCell.swift in Sources */, DB0AD4DF285872BE0002ABDB /* UserMediaTimelineViewController.swift in Sources */, DB25C4D32779ADD800EC1435 /* SearchResultContainerViewController.swift in Sources */, DB02C777273520B8007EA0BF /* SearchHashtagViewModel+State.swift in Sources */, @@ -2974,7 +2958,6 @@ DB92570A251C8FE0004FEFB5 /* ProfileHeaderViewController.swift in Sources */, DB47AB1D27CCB88000CD73C7 /* ListViewModel.swift in Sources */, DB8761BF274552F800BA7EE2 /* StatusItem.swift in Sources */, - DB89F591257A2A70005ECD04 /* TimelineLoaderTableViewCell.swift in Sources */, DB747FF9251C496A000C4BD7 /* ProfileViewController.swift in Sources */, DB697E0C27904C56004EF2F7 /* FederatedTimelineViewModel.swift in Sources */, DB01A9A427637FE60055FABC /* DataSourceFacade+Meta.swift in Sources */, @@ -3041,7 +3024,6 @@ DB12BEE727329F55002AA635 /* SearchUserViewController+DataSourceProvider.swift in Sources */, DB42411426C3E55200B6C5F8 /* WelcomeView.swift in Sources */, DB1D7B5525B5A87200397DCD /* TwitterAuthenticationOptionViewModel.swift in Sources */, - DB98DC0E2507862D0087E30F /* TimelineMiddleLoaderTableViewCell.swift in Sources */, DBA210382759EA91000B7CB2 /* FollowingListViewController+DataSourceProvider.swift in Sources */, DB01A9A2276347B60055FABC /* DataSourceFacade+Poll.swift in Sources */, DB2FFF3E258B78B0003DBC19 /* AVPlayer.swift in Sources */, @@ -3075,7 +3057,6 @@ DB2C8744274F4B7D00CE0398 /* SettingListViewController.swift in Sources */, DB58F1EC298BD07400836FBE /* MeProfileViewModel.swift in Sources */, DBE6357F288555AE001C114B /* PushNotificationScratchView.swift in Sources */, - DB47AB3E27CE1F9E00CD73C7 /* TimelineTopLoaderTableViewCell.swift in Sources */, DB0AD4E62858742D0002ABDB /* UserMediaTimelineViewModel+Diffable.swift in Sources */, DB02C77027350D8A007EA0BF /* SearchHashtagViewModel.swift in Sources */, DB5632B226DCF1CD00FC893F /* StatusThreadRootTableViewCell+ViewModel.swift in Sources */, @@ -3086,7 +3067,6 @@ DB8761C2274552F800BA7EE2 /* UserSection.swift in Sources */, DB76A65A275F49AE00A50673 /* ProgressBarView.swift in Sources */, DB76A655275F30E800A50673 /* DataSourceFacade+Media.swift in Sources */, - DBC17A5C26E0A4C1005C0D79 /* TimelineMiddleLoaderTableViewCell+ViewModel.swift in Sources */, DB47AB1A27CCB7EC00CD73C7 /* ListViewController.swift in Sources */, DBF63A04259B4811009E12C8 /* TouchBlockingCollectionView.swift in Sources */, DB76A64B275DF9FA00A50673 /* TwitterStatusThreadReplyViewModel.swift in Sources */, diff --git a/TwidereX/Scene/StatusThread/Mastodon/MastodonStatusThreadViewModel.swift b/TwidereX/Scene/StatusThread/Mastodon/MastodonStatusThreadViewModel.swift index b21cf1d5..71277a88 100644 --- a/TwidereX/Scene/StatusThread/Mastodon/MastodonStatusThreadViewModel.swift +++ b/TwidereX/Scene/StatusThread/Mastodon/MastodonStatusThreadViewModel.swift @@ -77,8 +77,8 @@ final class MastodonStatusThreadViewModel { } -extension MastodonStatusThreadViewModel { - +//extension MastodonStatusThreadViewModel { +// // func appendAncestor( // domain: String, // nodes: [Node] @@ -170,101 +170,101 @@ extension MastodonStatusThreadViewModel { // } // self._descendants = items // } - -} +// +//} extension MastodonStatusThreadViewModel { -// class Node { -// typealias ID = String -// -// let statusID: ID -// let children: [Node] -// -// init( -// statusID: ID, -// children: [MastodonStatusThreadViewModel.Node] -// ) { -// self.statusID = statusID -// self.children = children -// } -// } + class Node { + typealias ID = String + + let statusID: ID + let children: [Node] + + init( + statusID: ID, + children: [MastodonStatusThreadViewModel.Node] + ) { + self.statusID = statusID + self.children = children + } + } } -//extension MastodonStatusThreadViewModel.Node { -// static func replyToThread( -// for replyToID: Mastodon.Entity.Status.ID?, -// from statuses: [Mastodon.Entity.Status] -// ) -> [MastodonStatusThreadViewModel.Node] { -// guard let replyToID = replyToID else { -// return [] -// } -// -// var dict: [Mastodon.Entity.Status.ID: Mastodon.Entity.Status] = [:] -// for status in statuses { -// dict[status.id] = status -// } -// -// var nextID: Mastodon.Entity.Status.ID? = replyToID -// var nodes: [MastodonStatusThreadViewModel.Node] = [] -// while let _nextID = nextID { -// guard let status = dict[_nextID] else { break } -// nodes.append(MastodonStatusThreadViewModel.Node( -// statusID: _nextID, -// children: [] -// )) -// nextID = status.inReplyToID -// } -// -// return nodes -// } -//} +extension MastodonStatusThreadViewModel.Node { + static func replyToThread( + for replyToID: Mastodon.Entity.Status.ID?, + from statuses: [Mastodon.Entity.Status] + ) -> [MastodonStatusThreadViewModel.Node] { + guard let replyToID = replyToID else { + return [] + } -//extension MastodonStatusThreadViewModel.Node { -// static func children( -// of statusID: ID, -// from statuses: [Mastodon.Entity.Status] -// ) -> [MastodonStatusThreadViewModel.Node] { -// var dictionary: [ID: Mastodon.Entity.Status] = [:] -// var mapping: [ID: Set] = [:] -// -// for status in statuses { -// dictionary[status.id] = status -// guard let replyToID = status.inReplyToID else { continue } -// if var set = mapping[replyToID] { -// set.insert(status.id) -// mapping[replyToID] = set -// } else { -// mapping[replyToID] = Set([status.id]) -// } -// } -// -// var children: [MastodonStatusThreadViewModel.Node] = [] -// let replies = Array(mapping[statusID] ?? Set()) -// .compactMap { dictionary[$0] } -// .sorted(by: { $0.createdAt > $1.createdAt }) -// for reply in replies { -// let child = child(of: reply.id, dictionary: dictionary, mapping: mapping) -// children.append(child) -// } -// return children -// } -// -// static func child( -// of statusID: ID, -// dictionary: [ID: Mastodon.Entity.Status], -// mapping: [ID: Set] -// ) -> MastodonStatusThreadViewModel.Node { -// let childrenIDs = mapping[statusID] ?? [] -// let children = Array(childrenIDs) -// .compactMap { dictionary[$0] } -// .sorted(by: { $0.createdAt > $1.createdAt }) -// .map { status in child(of: status.id, dictionary: dictionary, mapping: mapping) } -// return MastodonStatusThreadViewModel.Node( -// statusID: statusID, -// children: children -// ) -// } -//} + var dict: [Mastodon.Entity.Status.ID: Mastodon.Entity.Status] = [:] + for status in statuses { + dict[status.id] = status + } + + var nextID: Mastodon.Entity.Status.ID? = replyToID + var nodes: [MastodonStatusThreadViewModel.Node] = [] + while let _nextID = nextID { + guard let status = dict[_nextID] else { break } + nodes.append(MastodonStatusThreadViewModel.Node( + statusID: _nextID, + children: [] + )) + nextID = status.inReplyToID + } + + return nodes + } +} + +extension MastodonStatusThreadViewModel.Node { + static func children( + of statusID: ID, + from statuses: [Mastodon.Entity.Status] + ) -> [MastodonStatusThreadViewModel.Node] { + var dictionary: [ID: Mastodon.Entity.Status] = [:] + var mapping: [ID: Set] = [:] + + for status in statuses { + dictionary[status.id] = status + guard let replyToID = status.inReplyToID else { continue } + if var set = mapping[replyToID] { + set.insert(status.id) + mapping[replyToID] = set + } else { + mapping[replyToID] = Set([status.id]) + } + } + + var children: [MastodonStatusThreadViewModel.Node] = [] + let replies = Array(mapping[statusID] ?? Set()) + .compactMap { dictionary[$0] } + .sorted(by: { $0.createdAt > $1.createdAt }) + for reply in replies { + let child = child(of: reply.id, dictionary: dictionary, mapping: mapping) + children.append(child) + } + return children + } + + static func child( + of statusID: ID, + dictionary: [ID: Mastodon.Entity.Status], + mapping: [ID: Set] + ) -> MastodonStatusThreadViewModel.Node { + let childrenIDs = mapping[statusID] ?? [] + let children = Array(childrenIDs) + .compactMap { dictionary[$0] } + .sorted(by: { $0.createdAt > $1.createdAt }) + .map { status in child(of: status.id, dictionary: dictionary, mapping: mapping) } + return MastodonStatusThreadViewModel.Node( + statusID: statusID, + children: children + ) + } +} extension MastodonStatusThreadViewModel { // func delete(objectIDs: [NSManagedObjectID]) { diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift index 32f4c709..4a8940df 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift @@ -37,6 +37,10 @@ extension StatusThreadViewModel { delegate: cell, viewLayoutFramePublisher: self.$viewLayoutFrame ) + if let linkConfiguration = self.conversationLinkConfiguration[record] { + viewModel.isTopConversationLinkLineViewDisplay = linkConfiguration.isTopLinkDisplay + viewModel.isBottomConversationLinkLineViewDisplay = linkConfiguration.isBottomLinkDisplay + } cell.contentConfiguration = UIHostingConfiguration { StatusView(viewModel: viewModel) } @@ -45,12 +49,13 @@ extension StatusThreadViewModel { return cell case .root: let cell = self.conversationRootTableViewCell - guard let statusViewModel = self.statusViewModel else { + guard let viewModel = self.statusViewModel else { return UITableViewCell() } cell.delegate = statusViewTableViewCellDelegate + self.updateConversationRootLink(viewModel: viewModel) cell.contentConfiguration = UIHostingConfiguration { - StatusView(viewModel: statusViewModel) + StatusView(viewModel: viewModel) } .margins(.vertical, 0) // remove vertical margins return cell @@ -70,7 +75,7 @@ extension StatusThreadViewModel { let hasReplyTo: Bool = { guard let status = status.object(in: context.managedObjectContext) else { return false } switch status { - case .twitter(let status): return status.replyToStatusID != nil + case .twitter(let status): return (status.repost ?? status).replyToStatusID != nil case .mastodon(let status): return status.replyToStatusID != nil } }() @@ -86,54 +91,6 @@ extension StatusThreadViewModel { } diffableDataSource?.apply(snapshot, animatingDifferences: false, completion: nil) -// let configuration = StatusSection.Configuration( -// statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, -// timelineMiddleLoaderTableViewCellDelegate: nil, -// viewLayoutFramePublisher: $viewLayoutFrame -// ) -// -// diffableDataSource = StatusSection.diffableDataSource( -// tableView: tableView, -// context: context, -// authContext: authContext, -// configuration: configuration -// ) -// -// var snapshot = NSDiffableDataSourceSnapshot() -// snapshot.appendSections([.main]) -// if hasReplyTo { -// snapshot.appendItems([.topLoader], toSection: .main) -// } -// if let root = self.root.value, case let .root(threadContext) = root { -// switch threadContext.status { -// case .twitter(let record): -// if twitterStatusThreadReplyViewModel.root == nil { -// twitterStatusThreadReplyViewModel.root = record -// } -// case .mastodon: -// break -// } -// -// let item = StatusItem.thread(root) -// snapshot.appendItems([item, .bottomLoader], toSection: .main) -// } else { -// root.eraseToAnyPublisher() -// .sink { [weak self] root in -// guard let self = self else { return } -// -// guard case .root(let threadContext) = root else { return } -// guard case let .twitter(record) = threadContext.status else { return } -// -// guard self.twitterStatusThreadReplyViewModel.root == nil else { return } -// self.twitterStatusThreadReplyViewModel.root = record -// } -// .store(in: &disposeBag) -// } -// diffableDataSource?.apply(snapshot) -// -// // trigger thread loading -// loadThreadStateMachine.enter(LoadThreadState.Prepare.self) -// Publishers.CombineLatest4( $status, $topThreads.removeDuplicates(), @@ -154,13 +111,11 @@ extension StatusThreadViewModel { // top loader switch self.topCursor { - case .noMore: - break - default: + case .none: // top loader let hasReplyTo: Bool = { switch status { - case .twitter(let status): return status.replyToStatusID != nil + case .twitter(let status): return (status.repost ?? status).replyToStatusID != nil case .mastodon(let status): return status.replyToStatusID != nil case nil: return false } @@ -168,12 +123,24 @@ extension StatusThreadViewModel { if hasReplyTo { newSnapshot.appendItems([.topLoader], toSection: .main) } + case .value: + newSnapshot.appendItems([.topLoader], toSection: .main) + default: + break } // self reply - let topItems: [Item] = topThreads.compactMap { thread -> Item? in + let topItems: [Item] = topThreads.enumerated().compactMap { index, thread -> Item? in switch thread { - case .selfThread(let status): return .status(status: status) - default: return nil + case .selfThread(let status): + let isFirst = index == 0 + let linkConfiguration = LinkConfiguration( + isTopLinkDisplay: isFirst ? self.topCursor.value != nil : true, + isBottomLinkDisplay: true + ) + self.conversationLinkConfiguration[status] = linkConfiguration + return .status(status: status) + default: + return nil } }.removingDuplicates() newSnapshot.appendItems(topItems, toSection: .main) @@ -186,10 +153,14 @@ extension StatusThreadViewModel { let bottomItems: [Item] = bottomThreads.compactMap { thread -> [Item]? in switch thread { case .conversationThread(let components): - return components.compactMap { status -> Item? in -// guard !deleteStatusIDs.contains(status.id) else { -// return nil -// } + return components.enumerated().compactMap { index, status -> Item? in + let isFirst = index == 0 + let isLast = index == components.count - 1 + let linkConfiguration = LinkConfiguration( + isTopLinkDisplay: !isFirst, + isBottomLinkDisplay: !isLast + ) + self.conversationLinkConfiguration[status] = linkConfiguration return Item.status(status: status) } default: @@ -202,10 +173,10 @@ extension StatusThreadViewModel { newSnapshot.appendItems(bottomItems, toSection: .main) // bottom loader switch self.bottomCursor { - case .noMore: - break - default: + case .none, .value: newSnapshot.appendItems([.bottomLoader], toSection: .main) + default: + break } let hasChanges = newSnapshot.itemIdentifiers != oldSnapshot.itemIdentifiers @@ -239,6 +210,16 @@ extension StatusThreadViewModel { } .store(in: &disposeBag) } + + private func updateConversationRootLink(viewModel: StatusView.ViewModel) { + guard let record = viewModel.status else { return } + guard let linkConfiguration = conversationLinkConfiguration[record] else { return } + + viewModel.isTopConversationLinkLineViewDisplay = linkConfiguration.isTopLinkDisplay + viewModel.isBottomConversationLinkLineViewDisplay = linkConfiguration.isBottomLinkDisplay + viewModel.repostViewModel?.isTopConversationLinkLineViewDisplay = linkConfiguration.isTopLinkDisplay + viewModel.repostViewModel?.isBottomConversationLinkLineViewDisplay = linkConfiguration.isBottomLinkDisplay + } } @@ -336,8 +317,6 @@ extension StatusThreadViewModel { tableView.panGestureRecognizer.isEnabled = true } diffableDataSource?.applySnapshotUsingReloadData(newSnapshot) - - // set bottom inset guard let index = newSnapshot.indexOfItem(.root), let lastItem = newSnapshot.itemIdentifiers.last, @@ -345,6 +324,11 @@ extension StatusThreadViewModel { else { return } + + // fix contentOffset update delay issue + tableView.scrollToRow(at: difference.targetIndexPath, at: .top, animated: false) + tableView.layoutIfNeeded() + let rectForCell = tableView.rectForRow(at: IndexPath(row: index, section: 0)) let rectForLastCell = tableView.rectForRow(at: IndexPath(row: lastIndex, section: 0)) let rectForTargetCell = tableView.rectForRow(at: difference.targetIndexPath) @@ -357,11 +341,13 @@ extension StatusThreadViewModel { tableView.contentInset.bottom = max(0, inset) self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): content inset bottom: \(tableView.contentInset.bottom)") - tableView.contentOffset.y = { + let contentOffsetY: CGFloat = { var offset: CGFloat = rectForTargetCell.minY offset -= tableView.safeAreaInsets.top offset -= difference.sourceDistanceToTableViewTopEdge return offset }() + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): contentOffsetY: \(contentOffsetY)") + tableView.contentOffset.y = contentOffsetY } } diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel+LoadThreadState.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel+LoadThreadState.swift index 32cc626b..b46da2ae 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel+LoadThreadState.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel+LoadThreadState.swift @@ -12,42 +12,42 @@ import GameplayKit import CoreDataStack import TwitterSDK -extension StatusThreadViewModel { - class LoadThreadState: GKState, NamingState { - weak var viewModel: StatusThreadViewModel? - var name: String { "Base" } - - init(viewModel: StatusThreadViewModel) { - self.viewModel = viewModel - } - - override func didEnter(from previousState: GKState?) { - super.didEnter(from: previousState) - os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, name, (previousState as? NamingState)?.name ?? previousState?.description ?? "nil") - // guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - } - } -} - -extension StatusThreadViewModel.LoadThreadState { - class Initial: StatusThreadViewModel.LoadThreadState { - override var name: String { "Initial" } - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Prepare.self - } - } - - class Prepare: StatusThreadViewModel.LoadThreadState { - override var name: String { "Prepare" } - let logger = Logger(subsystem: "StatusThreadViewModel.LoadThreadState", category: "Prepare") - - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Idle.self || stateClass == PrepareFail.self - } - - override func didEnter(from previousState: GKState?) { - super.didEnter(from: previousState) - +//extension StatusThreadViewModel { +// class LoadThreadState: GKState, NamingState { +// weak var viewModel: StatusThreadViewModel? +// var name: String { "Base" } +// +// init(viewModel: StatusThreadViewModel) { +// self.viewModel = viewModel +// } +// +// override func didEnter(from previousState: GKState?) { +// super.didEnter(from: previousState) +// os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, name, (previousState as? NamingState)?.name ?? previousState?.description ?? "nil") +// // guard let viewModel = viewModel, let stateMachine = stateMachine else { return } +// } +// } +//} +// +//extension StatusThreadViewModel.LoadThreadState { +// class Initial: StatusThreadViewModel.LoadThreadState { +// override var name: String { "Initial" } +// override func isValidNextState(_ stateClass: AnyClass) -> Bool { +// return stateClass == Prepare.self +// } +// } +// +// class Prepare: StatusThreadViewModel.LoadThreadState { +// override var name: String { "Prepare" } +// let logger = Logger(subsystem: "StatusThreadViewModel.LoadThreadState", category: "Prepare") +// +// override func isValidNextState(_ stateClass: AnyClass) -> Bool { +// return stateClass == Idle.self || stateClass == PrepareFail.self +// } +// +// override func didEnter(from previousState: GKState?) { +// super.didEnter(from: previousState) +// // guard let viewModel = viewModel, let stateMachine = stateMachine else { return } // guard case let .root(threadContext) = viewModel.root.value else { // assertionFailure() @@ -63,13 +63,13 @@ extension StatusThreadViewModel.LoadThreadState { // await prepareMastodonStatusThread(record: record) // } // } - } - +// } +// // prepare ThreadContext // note: // The conversationID is V2 only API. // Needs query conversationID via V2 endpoint if the status persisted from V1 API. - func prepareTwitterStatusThread(record: ManagedObjectRecord) async { +// func prepareTwitterStatusThread(record: ManagedObjectRecord) async { // guard let viewModel = viewModel, let stateMachine = stateMachine else { return } // // let managedObjectContext = viewModel.context.managedObjectContext @@ -133,9 +133,9 @@ extension StatusThreadViewModel.LoadThreadState { // await enter(state: Idle.self) // await enter(state: Loading.self) // } - } - - func prepareMastodonStatusThread(record: ManagedObjectRecord) async { +// } +// +// func prepareMastodonStatusThread(record: ManagedObjectRecord) async { // guard let viewModel = viewModel else { return } // // let managedObjectContext = viewModel.context.managedObjectContext @@ -161,71 +161,71 @@ extension StatusThreadViewModel.LoadThreadState { // viewModel.threadContext.value = .mastodon(mastodonContext) // await enter(state: Idle.self) // await enter(state: Loading.self) - } - - @MainActor - func enter(state: StatusThreadViewModel.LoadThreadState.Type) { - stateMachine?.enter(state) - } - - } // end class Prepare { … } - - class PrepareFail: StatusThreadViewModel.LoadThreadState { - override var name: String { "PrepareFail" } - var prepareFailCount = 0 - - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Prepare.self || stateClass == Fail.self - } - - override func didEnter(from previousState: GKState?) { - super.didEnter(from: previousState) - - // retry 3 times - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in - guard let self = self else { return } - guard let stateMachine = self.stateMachine else { return } - - guard self.prepareFailCount < 3 else { - stateMachine.enter(Fail.self) - return - } - self.prepareFailCount += 1 - stateMachine.enter(Prepare.self) - } - } - } - - class Idle: StatusThreadViewModel.LoadThreadState { - override var name: String { "Idle" } - - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Loading.self - } - } - - class Loading: StatusThreadViewModel.LoadThreadState { - override var name: String { "Loading" } - - var needsFallback = false - - var maxID: String? // v1 - var nextToken: String? // v2 - - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - switch stateClass { - case is Idle.Type, is NoMore.Type: - return true - case is Fail.Type: - return true - default: - return false - } - } - - override func didEnter(from previousState: GKState?) { - super.didEnter(from: previousState) - +// } +// +// @MainActor +// func enter(state: StatusThreadViewModel.LoadThreadState.Type) { +// stateMachine?.enter(state) +// } +// +// } // end class Prepare { … } +// +// class PrepareFail: StatusThreadViewModel.LoadThreadState { +// override var name: String { "PrepareFail" } +// var prepareFailCount = 0 +// +// override func isValidNextState(_ stateClass: AnyClass) -> Bool { +// return stateClass == Prepare.self || stateClass == Fail.self +// } +// +// override func didEnter(from previousState: GKState?) { +// super.didEnter(from: previousState) +// +// // retry 3 times +// DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in +// guard let self = self else { return } +// guard let stateMachine = self.stateMachine else { return } +// +// guard self.prepareFailCount < 3 else { +// stateMachine.enter(Fail.self) +// return +// } +// self.prepareFailCount += 1 +// stateMachine.enter(Prepare.self) +// } +// } +// } +// +// class Idle: StatusThreadViewModel.LoadThreadState { +// override var name: String { "Idle" } +// +// override func isValidNextState(_ stateClass: AnyClass) -> Bool { +// return stateClass == Loading.self +// } +// } +// +// class Loading: StatusThreadViewModel.LoadThreadState { +// override var name: String { "Loading" } +// +// var needsFallback = false +// +// var maxID: String? // v1 +// var nextToken: String? // v2 +// +// override func isValidNextState(_ stateClass: AnyClass) -> Bool { +// switch stateClass { +// case is Idle.Type, is NoMore.Type: +// return true +// case is Fail.Type: +// return true +// default: +// return false +// } +// } +// +// override func didEnter(from previousState: GKState?) { +// super.didEnter(from: previousState) +// // guard let viewModel = viewModel else { return } // guard let threadContext = viewModel.threadContext.value else { // assertionFailure() @@ -247,10 +247,10 @@ extension StatusThreadViewModel.LoadThreadState { // await append(response: response) // } // } - } - - // TODO: group into `StatusListFetchViewModel` - // fetch thread via V2 API +// } +// +// // TODO: group into `StatusListFetchViewModel` +// // fetch thread via V2 API // func fetch( // twitterConversation: StatusThreadViewModel.ThreadContext.TwitterConversation // ) async -> [TwitterStatusThreadLeafViewModel.Node] { @@ -310,7 +310,7 @@ extension StatusThreadViewModel.LoadThreadState { // return [] // } // } - +// // fetch thread via V1 API // func fetchFallback( // twitterConversation: StatusThreadViewModel.ThreadContext.TwitterConversation @@ -362,17 +362,17 @@ extension StatusThreadViewModel.LoadThreadState { // return [] // } // } - - @MainActor - private func append(nodes: [TwitterStatusThreadLeafViewModel.Node]) async { - guard let viewModel = viewModel else { return } +// +// @MainActor +// private func append(nodes: [TwitterStatusThreadLeafViewModel.Node]) async { +// guard let viewModel = viewModel else { return } // viewModel.twitterStatusThreadLeafViewModel.append(nodes: nodes) - } - - @MainActor - private func append(response: MastodonContextResponse) async { - guard let viewModel = viewModel else { return } - +// } +// +// @MainActor +// private func append(response: MastodonContextResponse) async { +// guard let viewModel = viewModel else { return } +// // viewModel.mastodonStatusThreadViewModel.appendAncestor( // domain: response.domain, // nodes: response.ancestorNodes @@ -382,15 +382,15 @@ extension StatusThreadViewModel.LoadThreadState { // domain: response.domain, // nodes: response.descendantNodes // ) - } - - struct MastodonContextResponse { - let domain: String +// } +// +// struct MastodonContextResponse { +// let domain: String // let ancestorNodes: [MastodonStatusThreadViewModel.Node] // let descendantNodes: [MastodonStatusThreadViewModel.Node] - } - - // fetch thread +// } +// +// // fetch thread // func fetch( // mastodonContext: StatusThreadViewModel.ThreadContext.MastodonContext // ) async -> MastodonContextResponse { @@ -442,35 +442,35 @@ extension StatusThreadViewModel.LoadThreadState { // ) // } // } - - @MainActor - func enter(state: StatusThreadViewModel.LoadThreadState.Type) { - stateMachine?.enter(state) - } - } - - class Fail: StatusThreadViewModel.LoadThreadState { - override var name: String { "Fail" } - - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Loading.self - } - - override func didEnter(from previousState: GKState?) { - super.didEnter(from: previousState) - } - } - - class NoMore: StatusThreadViewModel.LoadThreadState { - override var name: String { "NoMore" } - - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return false - } - - override func didEnter(from previousState: GKState?) { - super.didEnter(from: previousState) - } - } - -} +// +// @MainActor +// func enter(state: StatusThreadViewModel.LoadThreadState.Type) { +// stateMachine?.enter(state) +// } +// } +// +// class Fail: StatusThreadViewModel.LoadThreadState { +// override var name: String { "Fail" } +// +// override func isValidNextState(_ stateClass: AnyClass) -> Bool { +// return stateClass == Loading.self +// } +// +// override func didEnter(from previousState: GKState?) { +// super.didEnter(from: previousState) +// } +// } +// +// class NoMore: StatusThreadViewModel.LoadThreadState { +// override var name: String { "NoMore" } +// +// override func isValidNextState(_ stateClass: AnyClass) -> Bool { +// return false +// } +// +// override func didEnter(from previousState: GKState?) { +// super.didEnter(from: previousState) +// } +// } +// +//} diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift index 74ff2075..5504f844 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift @@ -58,27 +58,7 @@ final class StatusThreadViewModel { @Published var isLoadTop: Bool = false @Published var isLoadBottom: Bool = false -// var root: CurrentValueSubject -// var threadContext = CurrentValueSubject(nil) -// @Published var replies: [StatusItem] = [] -// @Published var leafs: [StatusItem] = [] -// @Published var hasReplyTo = false - - // thread -// @MainActor private(set) lazy var loadThreadStateMachine: GKStateMachine = { -// let stateMachine = GKStateMachine(states: [ -// LoadThreadState.Initial(viewModel: self), -// LoadThreadState.Prepare(viewModel: self), -// LoadThreadState.PrepareFail(viewModel: self), -// LoadThreadState.Idle(viewModel: self), -// LoadThreadState.Loading(viewModel: self), -// LoadThreadState.Fail(viewModel: self), -// LoadThreadState.NoMore(viewModel: self), -// -// ]) -// stateMachine.enter(LoadThreadState.Initial.self) -// return stateMachine -// }() + @Published var conversationLinkConfiguration: [StatusRecord: LinkConfiguration] = [:] public init( context: AppContext, @@ -209,6 +189,11 @@ extension StatusThreadViewModel { } } + struct LinkConfiguration { + let isTopLinkDisplay: Bool + let isBottomLinkDisplay: Bool + } + public enum Section: Hashable { case main } // end Section @@ -267,6 +252,9 @@ extension StatusThreadViewModel { } self.status = status + // setup link configuration for root + updateConversationRootLink(status: status) + guard statusViewModel == nil else { return } let _statusViewViewModel = StatusView.ViewModel( status: status, @@ -469,11 +457,100 @@ extension StatusThreadViewModel { status: ManagedObjectRecord, cursor: Cursor ) async throws { + guard let authenticationContext = authContext.authenticationContext.mastodonAuthenticationContext else { return } + guard let conversationRootStatus = status.object(in: context.managedObjectContext) else { + assertionFailure() + return + } + let conversationRootStatusID = conversationRootStatus.id + + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch conversation for \(conversationRootStatusID), cursor: \(cursor.value ?? "")") + + let response = try await context.apiService.mastodonStatusContext( + statusID: conversationRootStatusID, + authenticationContext: authenticationContext + ) + + // update cursor + self.topCursor = .noMore + self.bottomCursor = .noMore + + let ancestorNodes = MastodonStatusThreadViewModel.Node.replyToThread( + for: conversationRootStatus.replyToStatusID, + from: response.value.ancestors + ) + let descendantNodes = MastodonStatusThreadViewModel.Node.children( + of: conversationRootStatusID, + from: response.value.descendants + ) + let statusDict: [Mastodon.Entity.Status.ID: ManagedObjectRecord] = { + var dict: [MastodonStatus.ID: ManagedObjectRecord] = [:] + let request = MastodonStatus.sortedFetchRequest + var statusIDs: [MastodonStatus.ID] = [] + statusIDs += ancestorNodes.map { $0.statusID } + statusIDs += descendantNodes + .map { node in [node.statusID, node.children.first?.statusID].compactMap { $0 } } + .flatMap { $0 } + request.predicate = MastodonStatus.predicate(domain: authenticationContext.domain, ids: statusIDs) + let result = try? context.managedObjectContext.fetch(request) + for status in result ?? [] { + guard status.id != conversationRootStatusID else { continue } + dict[status.id] = status.asRecrod + } + return dict + }() + let topThreads: [Thread] = { + var threads: [Thread] = [] + for node in ancestorNodes { + guard let record = statusDict[node.statusID] else { continue } + threads.append(.selfThread(status: .mastodon(record: record))) + } + return threads + }() + let bottomThreads: [Thread] = { + var threads: [Thread] = [] + for node in descendantNodes { + guard let record = statusDict[node.statusID] else { continue } + var components: [StatusRecord] = [] + // first tier + components.append(.mastodon(record: record)) + // second tier + if let child = node.children.first, let secondRecord = statusDict[child.statusID] { + components.append(.mastodon(record: secondRecord)) + } + threads.append(.conversationThread(components: components)) + } + return threads + }() + enqueueTop(threads: topThreads) + appendBottom(threads: bottomThreads) + if topThreads.isEmpty && bottomThreads.isEmpty { + // trigger data source update + update(status: .mastodon(record: status)) + } } } extension StatusThreadViewModel { + private func updateConversationRootLink(status: StatusObject) { + switch status { + case .twitter(let status): + let hasReplyTo = (status.repost ?? status).replyToStatusID != nil + let linkConfiguration = LinkConfiguration( + isTopLinkDisplay: hasReplyTo, + isBottomLinkDisplay: false + ) + self.conversationLinkConfiguration[.twitter(record: status.asRecrod)] = linkConfiguration + case .mastodon(let status): + let hasReplyTo = (status.repost ?? status).replyToStatusID != nil + let linkConfiguration = LinkConfiguration( + isTopLinkDisplay: hasReplyTo, + isBottomLinkDisplay: false + ) + self.conversationLinkConfiguration[.mastodon(record: status.asRecrod)] = linkConfiguration + } + } // func delete(objectIDs: [NSManagedObjectID]) { // if let root = root.value, // case let .root(threadContext) = root, diff --git a/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadLeafViewModel.swift b/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadLeafViewModel.swift index 8465128f..3802d4da 100644 --- a/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadLeafViewModel.swift +++ b/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadLeafViewModel.swift @@ -13,21 +13,21 @@ import CoreData import CoreDataStack import TwitterSDK -final class TwitterStatusThreadLeafViewModel { - - var disposeBag = Set() - - // input - let context: AppContext - @Published private(set) var deletedObjectIDs: Set = Set() - - // output - @Published private var _items: [StatusItem] = [] - let items = CurrentValueSubject<[StatusItem], Never>([]) - - init(context: AppContext) { - self.context = context - +//final class TwitterStatusThreadLeafViewModel { +// +// var disposeBag = Set() +// +// // input +// let context: AppContext +// @Published private(set) var deletedObjectIDs: Set = Set() +// +// // output +// @Published private var _items: [StatusItem] = [] +// let items = CurrentValueSubject<[StatusItem], Never>([]) +// +// init(context: AppContext) { +// self.context = context +// // Publishers.CombineLatest( // $_items, // $deletedObjectIDs @@ -46,18 +46,18 @@ final class TwitterStatusThreadLeafViewModel { // self.items.value = newItems // } // .store(in: &disposeBag) - } - - deinit { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - } - -} - -extension TwitterStatusThreadLeafViewModel { - - // FIXME: handle node remove - func append(nodes: [Node]) { +// } +// +// deinit { +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) +// } +// +//} +// +//extension TwitterStatusThreadLeafViewModel { +// +// // FIXME: handle node remove +// func append(nodes: [Node]) { // let childrenIDs = nodes // .map { node in [node.statusID, node.children.first?.statusID].compactMap { $0 } } // .flatMap { $0 } @@ -73,7 +73,7 @@ extension TwitterStatusThreadLeafViewModel { // os_log("%{public}s[%{public}ld], %{public}s: fetch conversation fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) // return // } -// +// // var newItems: [StatusItem] = [] // for node in nodes { // guard let status = dictionary[node.statusID] else { continue } @@ -84,7 +84,7 @@ extension TwitterStatusThreadLeafViewModel { // ) // let item = StatusItem.thread(.leaf(context: context)) // newItems.append(item) -// +// // // second tier // if let child = node.children.first { // guard let secondaryStatus = dictionary[child.statusID] else { continue } @@ -95,142 +95,142 @@ extension TwitterStatusThreadLeafViewModel { // ) // let secondaryItem = StatusItem.thread(.leaf(context: secondaryContext)) // newItems.append(secondaryItem) -// +// // // update first tier context // context.displayBottomConversationLink = true // } // } -// +// // var items = self._items // for item in newItems { // guard !items.contains(item) else { continue } // items.append(item) // } // self._items = items - } - -} - -extension TwitterStatusThreadLeafViewModel { - class Node { - typealias ID = String - - let statusID: ID - let children: [Node] - - init(statusID: ID, children: [TwitterStatusThreadLeafViewModel.Node]) { - self.statusID = statusID - self.children = children - } - } -} - -extension TwitterStatusThreadLeafViewModel.Node { - // V1 - static func children( - of statusID: ID, - from content: Twitter.API.Search.Content - ) -> [TwitterStatusThreadLeafViewModel.Node] { - let statuses = content.statuses ?? [] - var dictionary: [ID: Twitter.Entity.Tweet] = [:] - var mapping: [ID: Set] = [:] - - for status in statuses { - dictionary[status.idStr] = status - guard let replyToID = status.inReplyToStatusIDStr else { continue } - if var set = mapping[replyToID] { - set.insert(status.idStr) - mapping[replyToID] = set - } else { - mapping[replyToID] = Set([status.idStr]) - } - } - - var children: [TwitterStatusThreadLeafViewModel.Node] = [] - let replies = Array(mapping[statusID] ?? Set()) - .compactMap { dictionary[$0] } - .sorted(by: { $0.createdAt > $1.createdAt }) - for reply in replies { - let child = child(of: reply.idStr, dictionary: dictionary, mapping: mapping) - children.append(child) - } - return children - } - - static func child( - of statusID: ID, - dictionary: [ID: Twitter.Entity.Tweet], - mapping: [ID: Set] - ) -> TwitterStatusThreadLeafViewModel.Node { - let childrenIDs = mapping[statusID] ?? [] - let children = Array(childrenIDs) - .compactMap { dictionary[$0] } - .sorted(by: { $0.createdAt > $1.createdAt }) - .map { status in child(of: status.idStr, dictionary: dictionary, mapping: mapping) } - return TwitterStatusThreadLeafViewModel.Node( - statusID: statusID, - children: children - ) - } - - // V2 - static func children( - of statusID: ID, - from content: Twitter.API.V2.Search.Content - ) -> [TwitterStatusThreadLeafViewModel.Node] { - let statuses = [content.data, content.includes?.tweets].compactMap { $0 }.flatMap { $0 } - var dictionary: [ID: Twitter.Entity.V2.Tweet] = [:] - var mapping: [ID: Set] = [:] - - for status in statuses { - dictionary[status.id] = status - guard let replyTo = status.referencedTweets?.first(where: { $0.type == .repliedTo }), - let replyToID = replyTo.id - else { continue } - - if var set = mapping[replyToID] { - set.insert(status.id) - mapping[replyToID] = set - } else { - mapping[replyToID] = Set([status.id]) - } - } - - var children: [TwitterStatusThreadLeafViewModel.Node] = [] - let replies = Array(mapping[statusID] ?? Set()) - .compactMap { dictionary[$0] } - .sorted(by: { $0.createdAt > $1.createdAt }) - for reply in replies { - let child = child(of: reply.id, dictionary: dictionary, mapping: mapping) - children.append(child) - } - return children - } - - static func child( - of statusID: ID, - dictionary: [ID: Twitter.Entity.V2.Tweet], - mapping: [ID: Set] - ) -> TwitterStatusThreadLeafViewModel.Node { - let childrenIDs = mapping[statusID] ?? [] - let children = Array(childrenIDs) - .compactMap { dictionary[$0] } - .sorted(by: { $0.createdAt > $1.createdAt }) - .map { status in child(of: status.id, dictionary: dictionary, mapping: mapping) } - return TwitterStatusThreadLeafViewModel.Node( - statusID: statusID, - children: children - ) - } - -} - -extension TwitterStatusThreadLeafViewModel { - func delete(objectIDs: [NSManagedObjectID]) { - var set = deletedObjectIDs - for objectID in objectIDs { - set.insert(objectID) - } - self.deletedObjectIDs = set - } -} +// } +// +//} +// +//extension TwitterStatusThreadLeafViewModel { +// class Node { +// typealias ID = String +// +// let statusID: ID +// let children: [Node] +// +// init(statusID: ID, children: [TwitterStatusThreadLeafViewModel.Node]) { +// self.statusID = statusID +// self.children = children +// } +// } +//} +// +//extension TwitterStatusThreadLeafViewModel.Node { +// // V1 +// static func children( +// of statusID: ID, +// from content: Twitter.API.Search.Content +// ) -> [TwitterStatusThreadLeafViewModel.Node] { +// let statuses = content.statuses ?? [] +// var dictionary: [ID: Twitter.Entity.Tweet] = [:] +// var mapping: [ID: Set] = [:] +// +// for status in statuses { +// dictionary[status.idStr] = status +// guard let replyToID = status.inReplyToStatusIDStr else { continue } +// if var set = mapping[replyToID] { +// set.insert(status.idStr) +// mapping[replyToID] = set +// } else { +// mapping[replyToID] = Set([status.idStr]) +// } +// } +// +// var children: [TwitterStatusThreadLeafViewModel.Node] = [] +// let replies = Array(mapping[statusID] ?? Set()) +// .compactMap { dictionary[$0] } +// .sorted(by: { $0.createdAt > $1.createdAt }) +// for reply in replies { +// let child = child(of: reply.idStr, dictionary: dictionary, mapping: mapping) +// children.append(child) +// } +// return children +// } +// +// static func child( +// of statusID: ID, +// dictionary: [ID: Twitter.Entity.Tweet], +// mapping: [ID: Set] +// ) -> TwitterStatusThreadLeafViewModel.Node { +// let childrenIDs = mapping[statusID] ?? [] +// let children = Array(childrenIDs) +// .compactMap { dictionary[$0] } +// .sorted(by: { $0.createdAt > $1.createdAt }) +// .map { status in child(of: status.idStr, dictionary: dictionary, mapping: mapping) } +// return TwitterStatusThreadLeafViewModel.Node( +// statusID: statusID, +// children: children +// ) +// } +// +// // V2 +// static func children( +// of statusID: ID, +// from content: Twitter.API.V2.Search.Content +// ) -> [TwitterStatusThreadLeafViewModel.Node] { +// let statuses = [content.data, content.includes?.tweets].compactMap { $0 }.flatMap { $0 } +// var dictionary: [ID: Twitter.Entity.V2.Tweet] = [:] +// var mapping: [ID: Set] = [:] +// +// for status in statuses { +// dictionary[status.id] = status +// guard let replyTo = status.referencedTweets?.first(where: { $0.type == .repliedTo }), +// let replyToID = replyTo.id +// else { continue } +// +// if var set = mapping[replyToID] { +// set.insert(status.id) +// mapping[replyToID] = set +// } else { +// mapping[replyToID] = Set([status.id]) +// } +// } +// +// var children: [TwitterStatusThreadLeafViewModel.Node] = [] +// let replies = Array(mapping[statusID] ?? Set()) +// .compactMap { dictionary[$0] } +// .sorted(by: { $0.createdAt > $1.createdAt }) +// for reply in replies { +// let child = child(of: reply.id, dictionary: dictionary, mapping: mapping) +// children.append(child) +// } +// return children +// } +// +// static func child( +// of statusID: ID, +// dictionary: [ID: Twitter.Entity.V2.Tweet], +// mapping: [ID: Set] +// ) -> TwitterStatusThreadLeafViewModel.Node { +// let childrenIDs = mapping[statusID] ?? [] +// let children = Array(childrenIDs) +// .compactMap { dictionary[$0] } +// .sorted(by: { $0.createdAt > $1.createdAt }) +// .map { status in child(of: status.id, dictionary: dictionary, mapping: mapping) } +// return TwitterStatusThreadLeafViewModel.Node( +// statusID: statusID, +// children: children +// ) +// } +// +//} +// +//extension TwitterStatusThreadLeafViewModel { +// func delete(objectIDs: [NSManagedObjectID]) { +// var set = deletedObjectIDs +// for objectID in objectIDs { +// set.insert(objectID) +// } +// self.deletedObjectIDs = set +// } +//} diff --git a/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadReplyViewModel+State.swift b/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadReplyViewModel+State.swift index ab684479..e3662e26 100644 --- a/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadReplyViewModel+State.swift +++ b/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadReplyViewModel+State.swift @@ -12,267 +12,267 @@ import GameplayKit import CoreDataStack import TwitterSDK -extension TwitterStatusThreadReplyViewModel { - class State: GKState, NamingState { - weak var viewModel: TwitterStatusThreadReplyViewModel? - var name: String { "Base" } - - init(viewModel: TwitterStatusThreadReplyViewModel) { - self.viewModel = viewModel - } - - override func didEnter(from previousState: GKState?) { - super.didEnter(from: previousState) - os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, name, (previousState as? NamingState)?.name ?? previousState?.description ?? "nil") - } - - @MainActor - func enter(state: TwitterStatusThreadReplyViewModel.State.Type) { - stateMachine?.enter(state) - } - - @MainActor - func apply(nodes: [TwitterStatusReplyNode]) { - self.viewModel?.nodes = nodes - } - } -} - -extension TwitterStatusThreadReplyViewModel.State { - class Initial: TwitterStatusThreadReplyViewModel.State { - override var name: String { "Initial" } - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - guard let viewModel = viewModel else { return false } - guard viewModel.root != nil else { return false } - - return stateClass == Prepare.self || stateClass == NoMore.self - } - } - - class Prepare: TwitterStatusThreadReplyViewModel.State { - - let logger = Logger(subsystem: "StatusThreadViewModel.LoadReplyState", category: "StateMachine") - - override var name: String { "Prepare" } - - static let throat = 20 - var previousResolvedNodeCount: Int? = nil - - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Loading.self || stateClass == Idle.self || stateClass == NoMore.self - } - - override func didEnter(from previousState: GKState?) { - super.didEnter(from: previousState) - - guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let record = viewModel.root else { - assertionFailure() - stateMachine.enter(NoMore.self) - return - } - - Task { - var nextState: TwitterStatusThreadReplyViewModel.State.Type? - var nodes: [TwitterStatusThreadReplyViewModel.TwitterStatusReplyNode] = [] - let managedObjectContext = viewModel.context.backgroundManagedObjectContext - try await managedObjectContext.performChanges { - guard let _status = record.object(in: managedObjectContext) else { - assertionFailure() - return - } - let status = _status.repost ?? _status - - var replyToArray: [TwitterStatus] = [] - var replyToNext: TwitterStatus? = status.replyTo - while let next = replyToNext { - replyToArray.append(next) - replyToNext = next.replyTo - } - - var replyNodes: [TwitterStatusThreadReplyViewModel.TwitterStatusReplyNode] = [] - for replyTo in replyToArray { - let node = TwitterStatusThreadReplyViewModel.TwitterStatusReplyNode( - statusID: replyTo.id, - replyToStatusID: replyTo.replyToStatusID, - status: .success(.init(objectID: replyTo.objectID)) - ) - replyNodes.append(node) - } - - let last = replyToArray.last ?? status - if let replyToStatusID = last.replyToStatusID { - // have reply to pointer but not resolved - // check local database and update relationship - do { - let request = TwitterStatus.sortedFetchRequest - request.fetchLimit = 1 - request.predicate = TwitterStatus.predicate(id: replyToStatusID) - let _replyToStatus = try managedObjectContext.fetch(request).first - - if let replyToStatus = _replyToStatus { - // find replyTo in local database - // update the entity - last.update(replyTo: replyToStatus) - - // append entity node - let node = TwitterStatusThreadReplyViewModel.TwitterStatusReplyNode( - statusID: replyToStatusID, - replyToStatusID: replyToStatus.replyToStatusID, - status: .success(.init(objectID: replyToStatus.objectID)) - ) - replyNodes.append(node) - - // append next placeholder node - if let nextReplyToStatusID = replyToStatus.replyToStatusID { - let nextNode = TwitterStatusThreadReplyViewModel.TwitterStatusReplyNode( - statusID: nextReplyToStatusID, - replyToStatusID: nil, - status: .notDetermined - ) - replyNodes.append(nextNode) - } - - } else { - // not find replyTo in local database - // create notDetermined placeholder node - let node = TwitterStatusThreadReplyViewModel.TwitterStatusReplyNode( - statusID: replyToStatusID, - replyToStatusID: nil, - status: .notDetermined - ) - replyNodes.append(node) - } - - } catch { - assertionFailure(error.localizedDescription) - } // end do { … } catch { … } - } // end if let replyToStatusID = last.replyToStatusID { … } - - nodes = replyNodes - - let pendingNodes = replyNodes.filter { node in - switch node.status { - case .notDetermined, .fail: return true - case .success: return false - } - } - - let _nextState: TwitterStatusThreadReplyViewModel.State.Type - if pendingNodes.isEmpty { - _nextState = NoMore.self - } else { - if replyNodes.count > Prepare.throat { - // stop reply auto lookup - _nextState = Idle.self - } else { - let resolvedNodeCount = replyNodes.count - pendingNodes.count - if let previousResolvedNodeCount = self.previousResolvedNodeCount { - if previousResolvedNodeCount == resolvedNodeCount { - _nextState = Fail.self - } else { - _nextState = Loading.self - } - } else { - self.previousResolvedNodeCount = resolvedNodeCount - _nextState = Loading.self - } - } - } // end if … else … - nextState = _nextState - } // end try await managedObjectContext.performChanges - - guard let nextState = nextState else { - assertionFailure() - return - } - - // set nodes before state update - self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): prepare reply nodes: \(nodes.debugDescription)") - await apply(nodes: nodes) - - await enter(state: nextState) - - } // end Task - } // end didEnter - - } - - class Idle: TwitterStatusThreadReplyViewModel.State { - override var name: String { "Idle" } - - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Loading.self - } - } - - class Loading: TwitterStatusThreadReplyViewModel.State { - - let logger = Logger(subsystem: "StatusThreadViewModel.LoadReplyState", category: "StateMachine") - - override var name: String { "Loading" } - - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Prepare.self || stateClass == Idle.self || stateClass == Fail.self || stateClass == NoMore.self - } - - override func didEnter(from previousState: GKState?) { - super.didEnter(from: previousState) - - guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard case let .twitter(twitterAuthenticationContext) = viewModel.authContext.authenticationContext else { - return - } - - let replyNodes = viewModel.nodes - let pendingNodes = replyNodes.filter { node in - switch node.status { - case .notDetermined, .fail: return true - case .success: return false - } - } - - guard !pendingNodes.isEmpty else { - stateMachine.enter(NoMore.self) - return - } - - let statusIDs = pendingNodes.map { $0.statusID } - - Task { - do { - // the APIService will persist entities into database - _ = try await viewModel.context.apiService.twitterStatus( - statusIDs: statusIDs, - authenticationContext: twitterAuthenticationContext - ) - - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch reply success: \(statusIDs.debugDescription)") - await enter(state: Prepare.self) - } catch { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch reply fail: \(error.localizedDescription)") - await enter(state: Fail.self) - // FIXME: needs retry logic here - } - } - } - - } // end class Loading - - class Fail: TwitterStatusThreadReplyViewModel.State { - override var name: String { "Fail" } - - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return false - } - } - - class NoMore: TwitterStatusThreadReplyViewModel.State { - override var name: String { "NoMore" } - - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return false - } - } - -} +//extension TwitterStatusThreadReplyViewModel { +// class State: GKState, NamingState { +// weak var viewModel: TwitterStatusThreadReplyViewModel? +// var name: String { "Base" } +// +// init(viewModel: TwitterStatusThreadReplyViewModel) { +// self.viewModel = viewModel +// } +// +// override func didEnter(from previousState: GKState?) { +// super.didEnter(from: previousState) +// os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, name, (previousState as? NamingState)?.name ?? previousState?.description ?? "nil") +// } +// +// @MainActor +// func enter(state: TwitterStatusThreadReplyViewModel.State.Type) { +// stateMachine?.enter(state) +// } +// +// @MainActor +// func apply(nodes: [TwitterStatusReplyNode]) { +// self.viewModel?.nodes = nodes +// } +// } +//} +// +//extension TwitterStatusThreadReplyViewModel.State { +// class Initial: TwitterStatusThreadReplyViewModel.State { +// override var name: String { "Initial" } +// override func isValidNextState(_ stateClass: AnyClass) -> Bool { +// guard let viewModel = viewModel else { return false } +// guard viewModel.root != nil else { return false } +// +// return stateClass == Prepare.self || stateClass == NoMore.self +// } +// } +// +// class Prepare: TwitterStatusThreadReplyViewModel.State { +// +// let logger = Logger(subsystem: "StatusThreadViewModel.LoadReplyState", category: "StateMachine") +// +// override var name: String { "Prepare" } +// +// static let throat = 20 +// var previousResolvedNodeCount: Int? = nil +// +// override func isValidNextState(_ stateClass: AnyClass) -> Bool { +// return stateClass == Loading.self || stateClass == Idle.self || stateClass == NoMore.self +// } +// +// override func didEnter(from previousState: GKState?) { +// super.didEnter(from: previousState) +// +// guard let viewModel = viewModel, let stateMachine = stateMachine else { return } +// guard let record = viewModel.root else { +// assertionFailure() +// stateMachine.enter(NoMore.self) +// return +// } +// +// Task { +// var nextState: TwitterStatusThreadReplyViewModel.State.Type? +// var nodes: [TwitterStatusThreadReplyViewModel.TwitterStatusReplyNode] = [] +// let managedObjectContext = viewModel.context.backgroundManagedObjectContext +// try await managedObjectContext.performChanges { +// guard let _status = record.object(in: managedObjectContext) else { +// assertionFailure() +// return +// } +// let status = _status.repost ?? _status +// +// var replyToArray: [TwitterStatus] = [] +// var replyToNext: TwitterStatus? = status.replyTo +// while let next = replyToNext { +// replyToArray.append(next) +// replyToNext = next.replyTo +// } +// +// var replyNodes: [TwitterStatusThreadReplyViewModel.TwitterStatusReplyNode] = [] +// for replyTo in replyToArray { +// let node = TwitterStatusThreadReplyViewModel.TwitterStatusReplyNode( +// statusID: replyTo.id, +// replyToStatusID: replyTo.replyToStatusID, +// status: .success(.init(objectID: replyTo.objectID)) +// ) +// replyNodes.append(node) +// } +// +// let last = replyToArray.last ?? status +// if let replyToStatusID = last.replyToStatusID { +// // have reply to pointer but not resolved +// // check local database and update relationship +// do { +// let request = TwitterStatus.sortedFetchRequest +// request.fetchLimit = 1 +// request.predicate = TwitterStatus.predicate(id: replyToStatusID) +// let _replyToStatus = try managedObjectContext.fetch(request).first +// +// if let replyToStatus = _replyToStatus { +// // find replyTo in local database +// // update the entity +// last.update(replyTo: replyToStatus) +// +// // append entity node +// let node = TwitterStatusThreadReplyViewModel.TwitterStatusReplyNode( +// statusID: replyToStatusID, +// replyToStatusID: replyToStatus.replyToStatusID, +// status: .success(.init(objectID: replyToStatus.objectID)) +// ) +// replyNodes.append(node) +// +// // append next placeholder node +// if let nextReplyToStatusID = replyToStatus.replyToStatusID { +// let nextNode = TwitterStatusThreadReplyViewModel.TwitterStatusReplyNode( +// statusID: nextReplyToStatusID, +// replyToStatusID: nil, +// status: .notDetermined +// ) +// replyNodes.append(nextNode) +// } +// +// } else { +// // not find replyTo in local database +// // create notDetermined placeholder node +// let node = TwitterStatusThreadReplyViewModel.TwitterStatusReplyNode( +// statusID: replyToStatusID, +// replyToStatusID: nil, +// status: .notDetermined +// ) +// replyNodes.append(node) +// } +// +// } catch { +// assertionFailure(error.localizedDescription) +// } // end do { … } catch { … } +// } // end if let replyToStatusID = last.replyToStatusID { … } +// +// nodes = replyNodes +// +// let pendingNodes = replyNodes.filter { node in +// switch node.status { +// case .notDetermined, .fail: return true +// case .success: return false +// } +// } +// +// let _nextState: TwitterStatusThreadReplyViewModel.State.Type +// if pendingNodes.isEmpty { +// _nextState = NoMore.self +// } else { +// if replyNodes.count > Prepare.throat { +// // stop reply auto lookup +// _nextState = Idle.self +// } else { +// let resolvedNodeCount = replyNodes.count - pendingNodes.count +// if let previousResolvedNodeCount = self.previousResolvedNodeCount { +// if previousResolvedNodeCount == resolvedNodeCount { +// _nextState = Fail.self +// } else { +// _nextState = Loading.self +// } +// } else { +// self.previousResolvedNodeCount = resolvedNodeCount +// _nextState = Loading.self +// } +// } +// } // end if … else … +// nextState = _nextState +// } // end try await managedObjectContext.performChanges +// +// guard let nextState = nextState else { +// assertionFailure() +// return +// } +// +// // set nodes before state update +// self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): prepare reply nodes: \(nodes.debugDescription)") +// await apply(nodes: nodes) +// +// await enter(state: nextState) +// +// } // end Task +// } // end didEnter +// +// } +// +// class Idle: TwitterStatusThreadReplyViewModel.State { +// override var name: String { "Idle" } +// +// override func isValidNextState(_ stateClass: AnyClass) -> Bool { +// return stateClass == Loading.self +// } +// } +// +// class Loading: TwitterStatusThreadReplyViewModel.State { +// +// let logger = Logger(subsystem: "StatusThreadViewModel.LoadReplyState", category: "StateMachine") +// +// override var name: String { "Loading" } +// +// override func isValidNextState(_ stateClass: AnyClass) -> Bool { +// return stateClass == Prepare.self || stateClass == Idle.self || stateClass == Fail.self || stateClass == NoMore.self +// } +// +// override func didEnter(from previousState: GKState?) { +// super.didEnter(from: previousState) +// +// guard let viewModel = viewModel, let stateMachine = stateMachine else { return } +// guard case let .twitter(twitterAuthenticationContext) = viewModel.authContext.authenticationContext else { +// return +// } +// +// let replyNodes = viewModel.nodes +// let pendingNodes = replyNodes.filter { node in +// switch node.status { +// case .notDetermined, .fail: return true +// case .success: return false +// } +// } +// +// guard !pendingNodes.isEmpty else { +// stateMachine.enter(NoMore.self) +// return +// } +// +// let statusIDs = pendingNodes.map { $0.statusID } +// +// Task { +// do { +// // the APIService will persist entities into database +// _ = try await viewModel.context.apiService.twitterStatus( +// statusIDs: statusIDs, +// authenticationContext: twitterAuthenticationContext +// ) +// +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch reply success: \(statusIDs.debugDescription)") +// await enter(state: Prepare.self) +// } catch { +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch reply fail: \(error.localizedDescription)") +// await enter(state: Fail.self) +// // FIXME: needs retry logic here +// } +// } +// } +// +// } // end class Loading +// +// class Fail: TwitterStatusThreadReplyViewModel.State { +// override var name: String { "Fail" } +// +// override func isValidNextState(_ stateClass: AnyClass) -> Bool { +// return false +// } +// } +// +// class NoMore: TwitterStatusThreadReplyViewModel.State { +// override var name: String { "NoMore" } +// +// override func isValidNextState(_ stateClass: AnyClass) -> Bool { +// return false +// } +// } +// +//} diff --git a/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadReplyViewModel.swift b/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadReplyViewModel.swift index 4587cd4c..e479f36d 100644 --- a/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadReplyViewModel.swift +++ b/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadReplyViewModel.swift @@ -15,44 +15,44 @@ import CoreData import CoreDataStack import TwidereCore -final class TwitterStatusThreadReplyViewModel { - - var disposeBag = Set() - - // input - let context: AppContext - let authContext: AuthContext - @Published var root: ManagedObjectRecord? - @Published var nodes: [TwitterStatusReplyNode] = [] - @Published private(set) var deletedObjectIDs: Set = Set() - let viewDidAppear = PassthroughSubject() - - // output - @Published var items: [StatusItem] = [] - - @MainActor private(set) lazy var stateMachine: GKStateMachine = { - // exclude timeline middle fetcher state - let stateMachine = GKStateMachine(states: [ - State.Initial(viewModel: self), - State.Prepare(viewModel: self), - State.Idle(viewModel: self), - State.Loading(viewModel: self), - State.Fail(viewModel: self), - State.NoMore(viewModel: self), - - ]) - stateMachine.enter(State.Initial.self) - return stateMachine - }() - - init( - context: AppContext, - authContext: AuthContext - ) { - self.context = context - self.authContext = authContext - // end init - +//final class TwitterStatusThreadReplyViewModel { +// +// var disposeBag = Set() +// +// // input +// let context: AppContext +// let authContext: AuthContext +// @Published var root: ManagedObjectRecord? +// @Published var nodes: [TwitterStatusReplyNode] = [] +// @Published private(set) var deletedObjectIDs: Set = Set() +// let viewDidAppear = PassthroughSubject() +// +// // output +// @Published var items: [StatusItem] = [] +// +// @MainActor private(set) lazy var stateMachine: GKStateMachine = { +// // exclude timeline middle fetcher state +// let stateMachine = GKStateMachine(states: [ +// State.Initial(viewModel: self), +// State.Prepare(viewModel: self), +// State.Idle(viewModel: self), +// State.Loading(viewModel: self), +// State.Fail(viewModel: self), +// State.NoMore(viewModel: self), +// +// ]) +// stateMachine.enter(State.Initial.self) +// return stateMachine +// }() +// +// init( +// context: AppContext, +// authContext: AuthContext +// ) { +// self.context = context +// self.authContext = authContext +// // end init +// // Publishers.CombineLatest( // $root, // viewDidAppear.eraseToAnyPublisher() @@ -91,49 +91,49 @@ final class TwitterStatusThreadReplyViewModel { // return items // } // .assign(to: &$items) - } - - deinit { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - } - -} - -extension TwitterStatusThreadReplyViewModel { - public class TwitterStatusReplyNode: CustomDebugStringConvertible { - let statusID: TwitterStatus.ID - let replyToStatusID: TwitterStatus.ID? - - let status: Status - - init( - statusID: TwitterStatus.ID, - replyToStatusID: TwitterStatus.ID?, - status: Status - ) { - self.statusID = statusID - self.replyToStatusID = replyToStatusID - self.status = status - } - - enum Status { - case notDetermined - case fail(Error) - case success(ManagedObjectRecord) - } - - public var debugDescription: String { - return "twitter status [\(statusID)] -> \(replyToStatusID ?? ""), \(status)" - } - } -} - -extension TwitterStatusThreadReplyViewModel { - func delete(objectIDs: [NSManagedObjectID]) { - var set = deletedObjectIDs - for objectID in objectIDs { - set.insert(objectID) - } - self.deletedObjectIDs = set - } -} +// } +// +// deinit { +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) +// } +// +//} +// +//extension TwitterStatusThreadReplyViewModel { +// public class TwitterStatusReplyNode: CustomDebugStringConvertible { +// let statusID: TwitterStatus.ID +// let replyToStatusID: TwitterStatus.ID? +// +// let status: Status +// +// init( +// statusID: TwitterStatus.ID, +// replyToStatusID: TwitterStatus.ID?, +// status: Status +// ) { +// self.statusID = statusID +// self.replyToStatusID = replyToStatusID +// self.status = status +// } +// +// enum Status { +// case notDetermined +// case fail(Error) +// case success(ManagedObjectRecord) +// } +// +// public var debugDescription: String { +// return "twitter status [\(statusID)] -> \(replyToStatusID ?? ""), \(status)" +// } +// } +//} +// +//extension TwitterStatusThreadReplyViewModel { +// func delete(objectIDs: [NSManagedObjectID]) { +// var set = deletedObjectIDs +// for objectID in objectIDs { +// set.insert(objectID) +// } +// self.deletedObjectIDs = set +// } +//} From f8db9fe49637ea2e7b6f0631c7920cda26778a75 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 7 Apr 2023 19:01:37 +0800 Subject: [PATCH 054/128] chore: migrate contentView to TextKit 2 with Meta support --- TwidereSDK/Package.swift | 4 +- .../CoreDataStack/TwitterStatus.swift | 26 ++++ .../Extension/CoreDataStack/TwitterUser.swift | 52 ++++--- .../Extension/MetaTextKit/MetaContent.swift | 18 ++- .../Notification/NotificationHeaderInfo.swift | 4 +- .../TwidereUI/Content/StatusHeaderView.swift | 40 +++--- .../Content/StatusView+ViewModel.swift | 90 +++++------- .../TwidereUI/Content/StatusView.swift | 31 +++- ...oseContentViewModel+MetaTextDelegate.swift | 4 +- .../TextViewRepresentable.swift | 136 +++++++++++++++--- TwidereX.xcodeproj/project.pbxproj | 44 ++++-- .../xcshareddata/swiftpm/Package.resolved | 61 ++++---- TwidereX/Diffable/Misc/List/ListSection.swift | 4 +- .../Diffable/Misc/Search/SearchSection.swift | 8 +- .../DataSourceFacade+Banner.swift | 0 .../DataSourceFacade+Block.swift | 0 .../DataSourceFacade+Friendship.swift | 0 .../DataSourceFacade+History.swift | 0 .../DataSourceFacade+LIst.swift | 0 .../DataSourceFacade+Like.swift | 0 .../DataSourceFacade+Media.swift | 0 .../DataSourceFacade+Meta.swift | 25 +--- .../DataSourceFacade+Model.swift | 0 .../DataSourceFacade+Mute.swift | 0 .../DataSourceFacade+Poll.swift | 0 .../DataSourceFacade+Profile.swift | 0 .../DataSourceFacade+Report.swift | 0 .../DataSourceFacade+Repost.swift | 0 .../DataSourceFacade+SavedSearch.swift | 0 .../DataSourceFacade+Share.swift | 0 .../DataSourceFacade+Status.swift | 0 .../DataSourceFacade+StatusThread.swift | 0 .../DataSourceFacade+Translate.swift | 8 +- .../DataSourceFacade+User.swift | 0 .../DataSourceFacade.swift | 0 ...ider+StatusViewTableViewCellDelegate.swift | 16 +++ ...der+UITableViewDataSourcePrefetching.swift | 33 +++++ .../Header/View/ProfileFieldContentView.swift | 2 +- .../View/ProfileHeaderView+ViewModel.swift | 9 +- .../Cell/HashtagTableViewCell+ViewModel.swift | 2 +- .../StatusViewTableViewCellDelegate.swift | 5 + .../List/ListTimelineViewController.swift | 7 + 42 files changed, 403 insertions(+), 226 deletions(-) rename TwidereX/Protocol/{Provider => Facade}/DataSourceFacade+Banner.swift (100%) rename TwidereX/Protocol/{Provider => Facade}/DataSourceFacade+Block.swift (100%) rename TwidereX/Protocol/{Provider => Facade}/DataSourceFacade+Friendship.swift (100%) rename TwidereX/Protocol/{Provider => Facade}/DataSourceFacade+History.swift (100%) rename TwidereX/Protocol/{Provider => Facade}/DataSourceFacade+LIst.swift (100%) rename TwidereX/Protocol/{Provider => Facade}/DataSourceFacade+Like.swift (100%) rename TwidereX/Protocol/{Provider => Facade}/DataSourceFacade+Media.swift (100%) rename TwidereX/Protocol/{Provider => Facade}/DataSourceFacade+Meta.swift (77%) rename TwidereX/Protocol/{Provider => Facade}/DataSourceFacade+Model.swift (100%) rename TwidereX/Protocol/{Provider => Facade}/DataSourceFacade+Mute.swift (100%) rename TwidereX/Protocol/{Provider => Facade}/DataSourceFacade+Poll.swift (100%) rename TwidereX/Protocol/{Provider => Facade}/DataSourceFacade+Profile.swift (100%) rename TwidereX/Protocol/{Provider => Facade}/DataSourceFacade+Report.swift (100%) rename TwidereX/Protocol/{Provider => Facade}/DataSourceFacade+Repost.swift (100%) rename TwidereX/Protocol/{Provider => Facade}/DataSourceFacade+SavedSearch.swift (100%) rename TwidereX/Protocol/{Provider => Facade}/DataSourceFacade+Share.swift (100%) rename TwidereX/Protocol/{Provider => Facade}/DataSourceFacade+Status.swift (100%) rename TwidereX/Protocol/{Provider => Facade}/DataSourceFacade+StatusThread.swift (100%) rename TwidereX/Protocol/{Provider => Facade}/DataSourceFacade+Translate.swift (85%) rename TwidereX/Protocol/{Provider => Facade}/DataSourceFacade+User.swift (100%) rename TwidereX/Protocol/{Provider => Facade}/DataSourceFacade.swift (100%) create mode 100644 TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDataSourcePrefetching.swift diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index b8267618..47a42e35 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -30,7 +30,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-nio.git", from: "2.34.0"), .package(url: "https://github.com/Flipboard/FLAnimatedImage.git", from: "1.0.0"), .package(url: "https://github.com/MainasuK/CommonOSLog", from: "0.1.1"), - .package(url: "https://github.com/TwidereProject/MetaTextKit.git", exact: "4.4.2"), + .package(url: "https://github.com/TwidereProject/MetaTextKit.git", exact: "4.5.1"), .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.5.0"), .package(url: "https://github.com/Alamofire/AlamofireImage.git", from: "4.1.0"), .package(url: "https://github.com/Alamofire/AlamofireNetworkActivityIndicator.git", from: "3.1.0"), @@ -51,6 +51,7 @@ let package = Package( .package(url: "https://github.com/uias/Tabman.git", from: "3.0.1"), .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.6.0"), .package(url: "https://github.com/tid-kijyun/Kanna.git", from: "5.2.7"), + .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"), .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"), ], targets: [ @@ -123,6 +124,7 @@ let package = Package( .product(name: "DateToolsSwift", package: "DateTools"), .product(name: "CryptoSwift", package: "CryptoSwift"), .product(name: "Kanna", package: "Kanna"), + .product(name: "CollectionConcurrencyKit", package: "CollectionConcurrencyKit"), ] ), .target( diff --git a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterStatus.swift b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterStatus.swift index ab51cb3b..c44a8276 100644 --- a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterStatus.swift +++ b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterStatus.swift @@ -7,6 +7,7 @@ import Foundation import CoreDataStack +import TwitterMeta extension TwitterStatus { public var statusURL: URL { @@ -21,11 +22,13 @@ extension TwitterStatus { let expandedURL = url.expandedURL else { continue } + // drop media URL guard !displayURL.hasPrefix("pic.twitter.com") else { text = text.replacingOccurrences(of: shortURL, with: "") continue } + // drop quote URL if let quote = quote { let quoteID = quote.id guard !displayURL.hasPrefix("twitter.com"), @@ -40,4 +43,27 @@ extension TwitterStatus { } return text } + + public var urlEntities: [TwitterContent.URLEntity] { + let results = entities?.urls?.map { entity in + TwitterContent.URLEntity(url: entity.url, expandedURL: entity.expandedURL, displayURL: entity.displayURL) + } + return results ?? [] + } +} + +extension TwitterStatus { + /// The tweet more then 240 characters + public var hasMore: Bool { + for url in entities?.urls ?? [] { + guard text.localizedCaseInsensitiveContains("… " + url.url) else { continue } + guard let expandedURL = url.expandedURL else { continue } + guard expandedURL.hasPrefix("https://twitter.com/") else { continue } + guard expandedURL.localizedCaseInsensitiveContains("status") else { continue } + guard expandedURL.hasSuffix(self.id) else { continue } + return true + } + + return false + } } diff --git a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterUser.swift b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterUser.swift index 1125d265..a6bcab08 100644 --- a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterUser.swift +++ b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterUser.swift @@ -80,52 +80,48 @@ extension String { } extension TwitterUser { - public func bioMetaContent(provider: TwitterTextProvider) -> TwitterMetaContent? { - let _bioContent: String? = bio.flatMap { text in - var text = text - for url in bioEntities?.urls ?? [] { - guard let expandedURL = url.expandedURL else { continue } - let shortURL = url.url - text = text.replacingOccurrences(of: shortURL, with: expandedURL) - } - return text + public var bioURLEntities: [TwitterContent.URLEntity] { + let results = bioEntities?.urls?.map { entity in + TwitterContent.URLEntity(url: entity.url, expandedURL: entity.expandedURL, displayURL: entity.displayURL) } - guard let bioContent = _bioContent else { return nil } - let content = TwitterContent(content: bioContent) + return results ?? [] + } + + public func bioMetaContent(provider: TwitterTextProvider) -> TwitterMetaContent? { + guard let bio = self.bio else { return nil } + let content = TwitterContent(content: bio, urlEntities: bioURLEntities) let metaContent = TwitterMetaContent.convert( - content: content, - urlMaximumLength: 50, + document: content, + urlMaximumLength: .max, twitterTextProvider: provider ) return metaContent } - public func urlMetaContent(provider: TwitterTextProvider) -> TwitterMetaContent? { - let _urlContent: String? = url.flatMap { text in - var text = text - for url in urlEntities?.urls ?? [] { - guard let expandedURL = url.expandedURL else { continue } - let shortURL = url.url - text = text.replacingOccurrences(of: shortURL, with: expandedURL) - } - return text + public var urlEntity: [TwitterContent.URLEntity] { + let results = urlEntities?.urls?.map { entity in + TwitterContent.URLEntity(url: entity.url, expandedURL: entity.expandedURL, displayURL: entity.displayURL) } - guard let urlContent = _urlContent else { return nil } - let content = TwitterContent(content: urlContent) + return results ?? [] + } + + public func urlMetaContent(provider: TwitterTextProvider) -> TwitterMetaContent? { + guard let url = self.url else { return nil } + let content = TwitterContent(content: url, urlEntities: urlEntity) let metaContent = TwitterMetaContent.convert( - content: content, - urlMaximumLength: 50, + document: content, + urlMaximumLength: .max, twitterTextProvider: provider ) return metaContent } public func locationMetaContent(provider: TwitterTextProvider) -> TwitterMetaContent? { - return location.flatMap { location in + return location.flatMap { location -> TwitterMetaContent? in let location = location.trimmingCharacters(in: .whitespacesAndNewlines) guard !location.isEmpty else { return nil } let metaContent = TwitterMetaContent.convert( - content: TwitterContent(content: location), + document: TwitterContent(content: location, urlEntities: []), urlMaximumLength: 50, twitterTextProvider: provider ) diff --git a/TwidereSDK/Sources/TwidereCore/Extension/MetaTextKit/MetaContent.swift b/TwidereSDK/Sources/TwidereCore/Extension/MetaTextKit/MetaContent.swift index 3321cf52..3dff2ad3 100644 --- a/TwidereSDK/Sources/TwidereCore/Extension/MetaTextKit/MetaContent.swift +++ b/TwidereSDK/Sources/TwidereCore/Extension/MetaTextKit/MetaContent.swift @@ -14,28 +14,26 @@ import Meta extension Meta { public enum Source { case plaintext(string: String) - case twitter(string: String, urlMaximumLength: Int = 30, provider: TwitterTextProvider) - case mastodon(string: String, emojis: MastodonContent.Emojis) + case twitter(content: TwitterContent, urlMaximumLength: Int = 30) + case mastodon(content: MastodonContent) } - public static func convert(from source: Source) -> MetaContent { + public static func convert(document source: Source) -> MetaContent { switch source { case .plaintext(let string): return PlaintextMetaContent(string: string) - case .twitter(let string, let urlMaximumLength, let provider): - let content = TwitterContent(content: string) + case .twitter(let content, let urlMaximumLength): return TwitterMetaContent.convert( - content: content, + document: content, urlMaximumLength: urlMaximumLength, - twitterTextProvider: provider + twitterTextProvider: SwiftTwitterTextProvider() ) - case .mastodon(let string, let emojis): + case .mastodon(let content): do { - let content = MastodonContent(content: string, emojis: emojis) return try MastodonMetaContent.convert(document: content) } catch { assertionFailure() - return PlaintextMetaContent(string: string) + return PlaintextMetaContent(string: content.content) } } } diff --git a/TwidereSDK/Sources/TwidereCore/Model/Notification/NotificationHeaderInfo.swift b/TwidereSDK/Sources/TwidereCore/Model/Notification/NotificationHeaderInfo.swift index 33fb1120..f7c05754 100644 --- a/TwidereSDK/Sources/TwidereCore/Model/Notification/NotificationHeaderInfo.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/Notification/NotificationHeaderInfo.swift @@ -12,6 +12,7 @@ import CoreDataStack import TwidereAsset import TwidereLocalization import MetaTextKit +import MastodonMeta public struct NotificationHeaderInfo { @@ -100,6 +101,7 @@ extension NotificationHeaderInfo { // assertionFailure() return nil } - return Meta.convert(from: .mastodon(string: text, emojis: user.emojis.asDictionary)) + let content = MastodonContent(content: text, emojis: user.emojis.asDictionary) + return Meta.convert(document: .mastodon(content: content)) } } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift index 8afbcaa7..c7336df3 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift @@ -32,30 +32,26 @@ public struct StatusHeaderView: View { - StatusHeaderView.iconImageTrailingSpacing Color.clear .frame(width: max(.leastNonzeroMagnitude, width)) - } - Button { - - } label: { - HStack(spacing: StatusHeaderView.iconImageTrailingSpacing) { - Color.clear - .frame(width: iconImageDimension) - LabelRepresentable( - metaContent: viewModel.label, - textStyle: .statusHeader, - setupLabel: { label in - // do nothing - } - ) - .overlay(alignment: .leading) { - VectorImageView(image: viewModel.image) - .frame(width: iconImageDimension, height: iconImageDimension) - .offset(x: -(StatusHeaderView.iconImageTrailingSpacing + iconImageDimension), y: 0) + } // end if + HStack(spacing: StatusHeaderView.iconImageTrailingSpacing) { + VectorImageView(image: viewModel.image) + .frame(width: iconImageDimension, height: iconImageDimension) + .offset(y: -1) + LabelRepresentable( + metaContent: viewModel.label, + textStyle: .statusHeader, + setupLabel: { label in + // do nothing } - Spacer() - } - } // end Button + ) + .fixedSize(horizontal: false, vertical: true) + Spacer() + } // HStack + } // HStack + .onTapGesture { + // TODO: } - } + } // end body } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index f511e01c..68e268ad 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -58,8 +58,6 @@ extension StatusView { // return formatter // }() // -// @Published public var header: Header = .none -// // @Published public var userIdentifier: UserIdentifier? // @Published public var authorAvatarImage: UIImage? // @Published public var authorAvatarImageURL: URL? @@ -113,31 +111,7 @@ extension StatusView { return isMediaSensitiveToggled ? isMediaSensitive : !isMediaSensitive } -// @Published public var isContentReveal: Bool = false -// -// @Published public var isMediaSensitive: Bool = false -// @Published public var isMediaSensitiveToggled: Bool = false -// -// @Published public var isMediaSensitiveSwitchable = false -// @Published public var isMediaReveal: Bool = false -// -// // poll input -// @Published public var pollItems: [PollItem] = [] -// @Published public var isVotable: Bool = false -// @Published public var isVoting: Bool = false -// @Published public var isVoteButtonEnabled: Bool = false -// @Published public var voterCount: Int? -// @Published public var voteCount = 0 -// @Published public var expireAt: Date? -// @Published public var expired: Bool = false -// -// // poll output -// @Published public var pollVoteDescription = "" -// @Published public var pollCountdownDescription: String? -// -// @Published public var location: String? -// @Published public var source: String? -// + // @Published public var isRepost = false // @Published public var isRepostEnabled = true @@ -171,6 +145,12 @@ extension StatusView { // timestamp @Published public var timestampLabelViewModel: TimestampLabelView.ViewModel? + // location + @Published public var location: String? + + // metric + @Published public var metricViewModel: StatusMetricView.ViewModel? + // toolbar public let toolbarViewModel = StatusToolbarView.ViewModel() public var canDelete: Bool { @@ -178,10 +158,7 @@ extension StatusView { guard let authorUserIdentifier = self.authorUserIdentifier else { return false } return authContext.authenticationContext.userIdentifier == authorUserIdentifier } - - // metric - @Published public var metricViewModel: StatusMetricView.ViewModel? - + // conversation link @Published public var isTopConversationLinkLineViewDisplay = false @Published public var isBottomConversationLinkLineViewDisplay = false @@ -1109,10 +1086,10 @@ extension StatusView.ViewModel { } // content - let content = TwitterContent(content: status.displayText) + let content = TwitterContent(content: status.displayText, urlEntities: status.urlEntities) let metaContent = TwitterMetaContent.convert( - content: content, - urlMaximumLength: 20, + document: content, + urlMaximumLength: .max, twitterTextProvider: SwiftTwitterTextProvider(), useParagraphMark: true ) @@ -1140,7 +1117,30 @@ extension StatusView.ViewModel { poll: .twitter(object: poll) ) } - + + // location + location = status.location?.fullName + + // metric + switch kind { + case .conversationRoot: + let _metricViewModel = StatusMetricView.ViewModel(platform: .twitter, timestamp: status.createdAt) + metricViewModel = _metricViewModel + status.publisher(for: \.source) + .assign(to: &_metricViewModel.$source) + status.publisher(for: \.replyCount) + .map { Int($0) } + .assign(to: &_metricViewModel.$replyCount) + status.publisher(for: \.repostCount) + .map { Int($0) } + .assign(to: &_metricViewModel.$repostCount) + status.publisher(for: \.likeCount) + .map { Int($0) } + .assign(to: &_metricViewModel.$likeCount) + default: + break + } + // toolbar toolbarViewModel.platform = .twitter status.publisher(for: \.replyCount) @@ -1168,26 +1168,6 @@ extension StatusView.ViewModel { } else { // do nothing } - - // metric - switch kind { - case .conversationRoot: - let _metricViewModel = StatusMetricView.ViewModel(platform: .twitter, timestamp: status.createdAt) - metricViewModel = _metricViewModel - status.publisher(for: \.source) - .assign(to: &_metricViewModel.$source) - status.publisher(for: \.replyCount) - .map { Int($0) } - .assign(to: &_metricViewModel.$replyCount) - status.publisher(for: \.repostCount) - .map { Int($0) } - .assign(to: &_metricViewModel.$repostCount) - status.publisher(for: \.likeCount) - .map { Int($0) } - .assign(to: &_metricViewModel.$likeCount) - default: - break - } } // end init } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index e7178e6a..20f187ff 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -25,6 +25,9 @@ public protocol StatusViewDelegate: AnyObject { // func statusView(_ statusView: StatusView, expandContentButtonDidPressed button: UIButton) func statusView(_ viewModel: StatusView.ViewModel, toggleContentDisplay isReveal: Bool) + // meta + func statusView(_ viewModel: StatusView.ViewModel, textViewDidSelectMeta meta: Meta) + // func statusView(_ statusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) // func statusView(_ statusView: StatusView, quoteStatusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) @@ -169,6 +172,17 @@ public struct StatusView: View { } .cornerRadius(12) } + // location (inline) + if let location = viewModel.location { + HStack { + Image(uiImage: Asset.ObjectTools.mappinMini.image.withRenderingMode(.alwaysTemplate)) + Text(location + location + location + location) + Spacer() + } + .foregroundColor(.secondary) + .font(Font(TextStyle.statusLocation.font)) + .frame(alignment: .leading) + } // metric if let metricViewModel = viewModel.metricViewModel { StatusMetricView(viewModel: metricViewModel) { action in @@ -200,11 +214,6 @@ public struct StatusView: View { .padding(.horizontal, viewModel.margin) // container margin .padding(.bottom, viewModel.hasToolbar ? .zero : viewModel.margin) // container margin .frame(width: viewModel.containerWidth) - //.overlay { - // Text("\(viewModel.containerWidth.description)") - // .font(.title) - //} - //.border(.red, width: 1) .overlay(alignment: .bottom) { switch viewModel.kind { case .timeline, .repost, .conversationThread: @@ -375,7 +384,11 @@ extension StatusView { TextViewRepresentable( metaContent: viewModel.spoilerContent ?? PlaintextMetaContent(string: ""), textStyle: .statusContent, - width: viewModel.contentWidth + width: viewModel.contentWidth, + isSelectable: viewModel.kind == .conversationRoot, + handler: { meta in + viewModel.delegate?.statusView(viewModel, textViewDidSelectMeta: meta) + } ) .frame(width: viewModel.contentWidth) } @@ -386,7 +399,11 @@ extension StatusView { TextViewRepresentable( metaContent: viewModel.content, textStyle: .statusContent, - width: viewModel.contentWidth + width: viewModel.contentWidth, + isSelectable: viewModel.kind == .conversationRoot, + handler: { meta in + viewModel.delegate?.statusView(viewModel, textViewDidSelectMeta: meta) + } ) .frame(width: viewModel.contentWidth) } diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel+MetaTextDelegate.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel+MetaTextDelegate.swift index 90b35421..95cd7a61 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel+MetaTextDelegate.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel+MetaTextDelegate.swift @@ -37,9 +37,9 @@ extension ComposeContentViewModel: MetaTextDelegate { switch platform { case .twitter: - let content = TwitterContent(content: textInput) + let content = TwitterContent(content: textInput, urlEntities: []) let metaContent = TwitterMetaContent.convert( - content: content, + text: content, urlMaximumLength: .max, twitterTextProvider: SwiftTwitterTextProvider() ) diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift index c6b59619..f1886334 100644 --- a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift @@ -5,6 +5,7 @@ // Created by MainasuK on 2023/2/3. // +import os.log import UIKit import SwiftUI import TwidereCore @@ -13,9 +14,14 @@ import MetaTextArea public struct TextViewRepresentable: UIViewRepresentable { - let textView: MetaTextAreaView = { - let textView = MetaTextAreaView() + let textView: WrappedTextView = { + let textView = WrappedTextView() textView.backgroundColor = .clear + textView.isScrollEnabled = false + textView.isEditable = false + textView.isSelectable = false + textView.textContainerInset = .zero + textView.textContainer.lineFragmentPadding = 0 textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) textView.setContentHuggingPriority(.defaultHigh, for: .vertical) return textView @@ -25,20 +31,28 @@ public struct TextViewRepresentable: UIViewRepresentable { let metaContent: MetaContent let textStyle: TextStyle let width: CGFloat + let isSelectable: Bool + let handler: (Meta) -> Void public init( metaContent: MetaContent, textStyle: TextStyle, - width: CGFloat + width: CGFloat, + isSelectable: Bool, + handler: @escaping (Meta) -> Void ) { self.metaContent = metaContent self.textStyle = textStyle self.width = width + self.isSelectable = isSelectable + self.handler = handler } - public func makeUIView(context: Context) -> MetaTextAreaView { + public func makeUIView(context: Context) -> UITextView { let textView = self.textView + textView.isSelectable = isSelectable textView.delegate = context.coordinator + textView.textViewDelegate = context.coordinator let attributedString = NSMutableAttributedString(string: metaContent.string) let textAttributes: [NSAttributedString.Key: Any] = [ @@ -65,8 +79,7 @@ public struct TextViewRepresentable: UIViewRepresentable { ) textView.frame.size.width = width - textView.preferredMaxLayoutWidth = width - textView.setAttributedString(attributedString) + textView.textStorage.setAttributedString(attributedString) textView.invalidateIntrinsicContentSize() textView.setNeedsLayout() textView.layoutIfNeeded() @@ -74,18 +87,22 @@ public struct TextViewRepresentable: UIViewRepresentable { return textView } - public func updateUIView(_ view: MetaTextAreaView, context: Context) { - textView.frame.size.width = width - textView.invalidateIntrinsicContentSize() - textView.setNeedsLayout() - textView.layoutIfNeeded() + public func updateUIView(_ view: UITextView, context: Context) { + if textView.frame.size.width != width { + textView.frame.size.width = width + textView.invalidateIntrinsicContentSize() + textView.setNeedsLayout() + textView.layoutIfNeeded() + } } public func makeCoordinator() -> Coordinator { Coordinator(self) } - public class Coordinator: NSObject, UITextViewDelegate { + public class Coordinator: NSObject { + let logger = Logger(subsystem: "TextViewRepresentable", category: "Coordinator") + let view: TextViewRepresentable init(_ view: TextViewRepresentable) { @@ -95,18 +112,55 @@ public struct TextViewRepresentable: UIViewRepresentable { } } -// MARK: - MetaTextAreaViewDelegate -extension TextViewRepresentable.Coordinator: MetaTextAreaViewDelegate { - public func metaTextAreaView(_ metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) { - +// MARK: - UITextViewDelegate +extension TextViewRepresentable.Coordinator: UITextViewDelegate { + public func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + return false + } + + public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + return false + } +} + +// MARK: - WrappedTextViewDelegate +extension TextViewRepresentable.Coordinator: WrappedTextViewDelegate { + public func wrappedTextView(_ wrappedTextView: WrappedTextView, didSelectMeta meta: Meta) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): meta: \(meta.debugDescription)") + view.handler(meta) } } -class WrappedTextView: UITextView { +public protocol WrappedTextViewDelegate: AnyObject { + func wrappedTextView(_ wrappedTextView: WrappedTextView, didSelectMeta meta: Meta) +} + +public class WrappedTextView: UITextView { + + let logger = Logger(subsystem: "WrappedTextView", category: "View") + + let tapGestureRecognizer = UITapGestureRecognizer() private var lastWidth: CGFloat = 0 - - override func layoutSubviews() { + + public weak var textViewDelegate: WrappedTextViewDelegate? + + public override init(frame: CGRect, textContainer: NSTextContainer?) { + super.init(frame: frame, textContainer: textContainer) + // end init + + addGestureRecognizer(tapGestureRecognizer) + tapGestureRecognizer.addTarget(self, action: #selector(WrappedTextView.tapGestureRecognizerHandler(_:))) + tapGestureRecognizer.delaysTouchesBegan = false + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func layoutSubviews() { super.layoutSubviews() if bounds.width != lastWidth { @@ -115,7 +169,7 @@ class WrappedTextView: UITextView { } } - override var intrinsicContentSize: CGSize { + public override var intrinsicContentSize: CGSize { let size = sizeThatFits(CGSize( width: lastWidth, height: UIView.layoutFittingExpandedSize.height @@ -128,3 +182,45 @@ class WrappedTextView: UITextView { } +extension WrappedTextView { + @objc private func tapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + switch sender.state { + case .ended: + let point = sender.location(in: self) + guard let meta = meta(at: point) else { return } + textViewDelegate?.wrappedTextView(self, didSelectMeta: meta) + default: + break + } + } +} + +extension WrappedTextView { + + public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + return meta(at: point) != nil || isSelectable + } + + func meta(at point: CGPoint) -> Meta? { + guard let fragment = textLayoutManager?.textLayoutFragment(for: point) else { return nil } + + let pointInFragmentFrame = CGPoint( + x: point.x - fragment.layoutFragmentFrame.origin.x, + y: point.y - fragment.layoutFragmentFrame.origin.y + ) + let lines = fragment.textLineFragments + guard let lineIndex = lines.firstIndex(where: { $0.typographicBounds.contains(pointInFragmentFrame) }) else { return nil } + guard lineIndex < lines.count else { return nil } + let line = lines[lineIndex] + + let characterIndex = line.characterIndex(for: point) + guard characterIndex >= 0, characterIndex < line.attributedString.length else { return nil } + + guard let meta = line.attributedString.attribute(.meta, at: characterIndex, effectiveRange: nil) as? Meta else { + return nil + } + return meta + } + +} diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 34999dc3..eb500720 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -134,6 +134,7 @@ DB51DC5027181DE500A0D8FB /* CoverFlowStackMediaCollectionCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51DC4F27181DE400A0D8FB /* CoverFlowStackMediaCollectionCell+ViewModel.swift */; }; DB522F1628869DAE0088017C /* Notification+Name+HandleTapAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE6358528869441001C114B /* Notification+Name+HandleTapAction.swift */; }; DB522F172886A08F0088017C /* UIStatusBarManager+HandleTapAction.m in Sources */ = {isa = PBXBuildFile; fileRef = DBE635832886940B001C114B /* UIStatusBarManager+HandleTapAction.m */; }; + DB55496029E01330004AF42A /* DataSourceProvider+UITableViewDataSourcePrefetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB55495F29E01330004AF42A /* DataSourceProvider+UITableViewDataSourcePrefetching.swift */; }; DB56329726DCBE1600FC893F /* StatusThreadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB56329626DCBE1600FC893F /* StatusThreadViewController.swift */; }; DB56329A26DCBE7300FC893F /* StatusThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB56329926DCBE7300FC893F /* StatusThreadViewModel.swift */; }; DB56329C26DCC23700FC893F /* DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB56329B26DCC23700FC893F /* DataSourceProvider.swift */; }; @@ -594,6 +595,8 @@ DB51DC422718117900A0D8FB /* StatusMediaGalleryCollectionCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusMediaGalleryCollectionCell+ViewModel.swift"; sourceTree = ""; }; DB51DC4D27181D7500A0D8FB /* CoverFlowStackMediaCollectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverFlowStackMediaCollectionCell.swift; sourceTree = ""; }; DB51DC4F27181DE400A0D8FB /* CoverFlowStackMediaCollectionCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoverFlowStackMediaCollectionCell+ViewModel.swift"; sourceTree = ""; }; + DB55495E29DFFCBC004AF42A /* MetaTextKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MetaTextKit; path = ../MetaTextKit; sourceTree = ""; }; + DB55495F29E01330004AF42A /* DataSourceProvider+UITableViewDataSourcePrefetching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceProvider+UITableViewDataSourcePrefetching.swift"; sourceTree = ""; }; DB56329626DCBE1600FC893F /* StatusThreadViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusThreadViewController.swift; sourceTree = ""; }; DB56329926DCBE7300FC893F /* StatusThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusThreadViewModel.swift; sourceTree = ""; }; DB56329B26DCC23700FC893F /* DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSourceProvider.swift; sourceTree = ""; }; @@ -1377,21 +1380,7 @@ path = List; sourceTree = ""; }; - DB56329826DCBE1900FC893F /* StatusThread */ = { - isa = PBXGroup; - children = ( - DB5632BD26DF503D00FC893F /* Twitter */, - DB01091626E5EB67005F67D7 /* Mastodon */, - DB56329626DCBE1600FC893F /* StatusThreadViewController.swift */, - DB3B905E26E8A9650010F64C /* StatusThreadViewController+DataSourceProvider.swift */, - DB56329926DCBE7300FC893F /* StatusThreadViewModel.swift */, - DB5632AC26DCE93900FC893F /* StatusThreadViewModel+Diffable.swift */, - DB5632B326DDE3F300FC893F /* StatusThreadViewModel+LoadThreadState.swift */, - ); - path = StatusThread; - sourceTree = ""; - }; - DB5632A526DCC82D00FC893F /* Provider */ = { + DB55496129E01338004AF42A /* Facade */ = { isa = PBXGroup; children = ( DB5632AA26DCCD3900FC893F /* DataSourceFacade.swift */, @@ -1415,8 +1404,30 @@ DB01A9A327637FE60055FABC /* DataSourceFacade+Meta.swift */, DB581A0027D89A3700C35B91 /* DataSourceFacade+LIst.swift */, DBF81C7B27F6A93E00004A56 /* DataSourceFacade+Translate.swift */, + ); + path = Facade; + sourceTree = ""; + }; + DB56329826DCBE1900FC893F /* StatusThread */ = { + isa = PBXGroup; + children = ( + DB5632BD26DF503D00FC893F /* Twitter */, + DB01091626E5EB67005F67D7 /* Mastodon */, + DB56329626DCBE1600FC893F /* StatusThreadViewController.swift */, + DB3B905E26E8A9650010F64C /* StatusThreadViewController+DataSourceProvider.swift */, + DB56329926DCBE7300FC893F /* StatusThreadViewModel.swift */, + DB5632AC26DCE93900FC893F /* StatusThreadViewModel+Diffable.swift */, + DB5632B326DDE3F300FC893F /* StatusThreadViewModel+LoadThreadState.swift */, + ); + path = StatusThread; + sourceTree = ""; + }; + DB5632A526DCC82D00FC893F /* Provider */ = { + isa = PBXGroup; + children = ( DB56329B26DCC23700FC893F /* DataSourceProvider.swift */, DB5632A626DCC84C00FC893F /* DataSourceProvider+UITableViewDelegate.swift */, + DB55495F29E01330004AF42A /* DataSourceProvider+UITableViewDataSourcePrefetching.swift */, DB01092326E6199C005F67D7 /* DataSourceProvider+StatusViewTableViewCellDelegate.swift */, DB01739B276B56C100AB844C /* DataSourceProvider+UserViewTableViewCellDelegate.swift */, DB76A65E275F587300A50673 /* DataSourceProvider+MediaInfoDescriptionViewDelegate.swift */, @@ -2038,6 +2049,7 @@ DBDA8E1524FCF8A3006750DC = { isa = PBXGroup; children = ( + DB55495E29DFFCBC004AF42A /* MetaTextKit */, DB33E5B42719637400EC2225 /* CoverFlowStackCollectionViewLayout */, DB51DC372716AEF000A0D8FB /* TabBarPager */, DB43239D251491EB004FAAEC /* TwidereSDK */, @@ -2244,6 +2256,7 @@ DBED96DC253F5D7B00C5383A /* Protocol */ = { isa = PBXGroup; children = ( + DB55496129E01338004AF42A /* Facade */, DB5632A526DCC82D00FC893F /* Provider */, DBF167FC27C394830001F75E /* NeedsDependency+AvatarBarButtonItemDelegate.swift */, DBED96D7253F5D7800C5383A /* NamingState.swift */, @@ -2976,6 +2989,7 @@ DB02C77927352217007EA0BF /* SearchHashtagViewModel+Diffable.swift in Sources */, DB25C4CA277999BE00EC1435 /* ButtonTableViewCell.swift in Sources */, DB0AD4EA28587B040002ABDB /* TimelineViewController.swift in Sources */, + DB55496029E01330004AF42A /* DataSourceProvider+UITableViewDataSourcePrefetching.swift in Sources */, DBA6B30E27E9CB6F004D052D /* AddListMemberViewController.swift in Sources */, DBF63A38259D7CCB009E12C8 /* CornerSmoothPreviewViewController.swift in Sources */, DB0CC4BA27D5F7BA00A051B4 /* CompositeListViewModel+Diffable.swift in Sources */, diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8e9d3b27..047a4eb8 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/Alamofire/Alamofire.git", "state": { "branch": null, - "revision": "354dda32d89fc8cd4f5c46487f64957d355f53d8", - "version": "5.6.1" + "revision": "78424be314842833c04bc3bef5b72e85fff99204", + "version": "5.6.4" } }, { @@ -28,6 +28,15 @@ "version": "3.1.0" } }, + { + "package": "CollectionConcurrencyKit", + "repositoryURL": "https://github.com/JohnSundell/CollectionConcurrencyKit.git", + "state": { + "branch": null, + "revision": "b4f23e24b5a1bff301efc5e70871083ca029ff95", + "version": "0.2.0" + } + }, { "package": "CommonOSLog", "repositoryURL": "https://github.com/MainasuK/CommonOSLog", @@ -42,8 +51,8 @@ "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git", "state": { "branch": null, - "revision": "19b3c3ceed117c5cc883517c4e658548315ba70b", - "version": "1.6.0" + "revision": "95c18f1c1bc44d5547728621ed680850368f7a45", + "version": "1.7.0" } }, { @@ -60,8 +69,8 @@ "repositoryURL": "https://github.com/Flipboard/FLAnimatedImage.git", "state": { "branch": null, - "revision": "e7f9fd4681ae41bf6f3056db08af4f401d61da52", - "version": "1.0.16" + "revision": "d4f07b6f164d53c1212c3e54d6460738b1981e9f", + "version": "1.0.17" } }, { @@ -69,7 +78,7 @@ "repositoryURL": "https://github.com/kciter/Floaty.git", "state": { "branch": "master", - "revision": "f8f6cf9970a3d92e98fcc2cf123b1bdef15e8778", + "revision": "d0e637db60828d49e403ff0b7dc11b1458e67095", "version": null } }, @@ -123,17 +132,8 @@ "repositoryURL": "https://github.com/onevcat/Kingfisher.git", "state": { "branch": null, - "revision": "59eb199fdb8dd47733624e8b0277822d0232579e", - "version": "7.2.2" - } - }, - { - "package": "MetaTextKit", - "repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git", - "state": { - "branch": null, - "revision": "c2a3d5e4355c1e3c9046c89433b28795f3374876", - "version": "4.4.2" + "revision": "af4be924ad984cf4d16f4ae4df424e79a443d435", + "version": "7.6.2" } }, { @@ -159,8 +159,17 @@ "repositoryURL": "https://github.com/SDWebImage/SDWebImage.git", "state": { "branch": null, - "revision": "2e63d0061da449ad0ed130768d05dceb1496de44", - "version": "5.12.5" + "revision": "fb50c1d20f24db5322b2f8f379de3618f75fe08e", + "version": "5.15.5" + } + }, + { + "package": "swift-atomics", + "repositoryURL": "https://github.com/apple/swift-atomics.git", + "state": { + "branch": null, + "revision": "6c89474e62719ddcc1e9614989fff2f68208fe10", + "version": "1.1.0" } }, { @@ -168,8 +177,8 @@ "repositoryURL": "https://github.com/apple/swift-collections.git", "state": { "branch": null, - "revision": "48254824bb4248676bf7ce56014ff57b142b77eb", - "version": "1.0.2" + "revision": "937e904258d22af6e447a0b72c0bc67583ef64a2", + "version": "1.0.4" } }, { @@ -177,8 +186,8 @@ "repositoryURL": "https://github.com/apple/swift-nio.git", "state": { "branch": null, - "revision": "124119f0bb12384cef35aa041d7c3a686108722d", - "version": "2.40.0" + "revision": "9b2848d76f5caad08b97e71a04345aa5bdb23a06", + "version": "2.49.0" } }, { @@ -195,8 +204,8 @@ "repositoryURL": "https://github.com/siteline/SwiftUI-Introspect.git", "state": { "branch": null, - "revision": "f2616860a41f9d9932da412a8978fec79c06fe24", - "version": "0.1.4" + "revision": "c18951c747ab62af7c15e17a81bd37d4fd5a9979", + "version": "0.2.3" } }, { diff --git a/TwidereX/Diffable/Misc/List/ListSection.swift b/TwidereX/Diffable/Misc/List/ListSection.swift index 2a6e24f4..27936934 100644 --- a/TwidereX/Diffable/Misc/List/ListSection.swift +++ b/TwidereX/Diffable/Misc/List/ListSection.swift @@ -129,7 +129,7 @@ extension ListSection { ) { switch list { case .twitter(let object): - let metaContent = Meta.convert(from: .plaintext(string: object.name)) + let metaContent = Meta.convert(document: .plaintext(string: object.name)) cell.primaryTextLabel.configure(content: metaContent) cell.accessoryImageView.image = Asset.ObjectTools.lockMiniInline.image.withRenderingMode(.alwaysTemplate) cell.accessoryImageView.contentMode = .scaleAspectFill @@ -137,7 +137,7 @@ extension ListSection { cell.accessoryImageView.isHidden = !object.private cell.accessoryType = .disclosureIndicator case .mastodon(let object): - let metaContent = Meta.convert(from: .plaintext(string: object.title)) + let metaContent = Meta.convert(document: .plaintext(string: object.title)) cell.primaryTextLabel.configure(content: metaContent) cell.accessoryType = .disclosureIndicator } diff --git a/TwidereX/Diffable/Misc/Search/SearchSection.swift b/TwidereX/Diffable/Misc/Search/SearchSection.swift index cf4c0495..34b30ca6 100644 --- a/TwidereX/Diffable/Misc/Search/SearchSection.swift +++ b/TwidereX/Diffable/Misc/Search/SearchSection.swift @@ -76,10 +76,10 @@ extension SearchSection { ) { switch object { case .twitter(let history): - let metaContent = Meta.convert(from: .plaintext(string: history.name)) + let metaContent = Meta.convert(document: .plaintext(string: history.name)) cell.primaryTextLabel.configure(content: metaContent) case .mastodon(let history): - let metaContent = Meta.convert(from: .plaintext(string: history.query)) + let metaContent = Meta.convert(document: .plaintext(string: history.query)) cell.primaryTextLabel.configure(content: metaContent) } } @@ -90,11 +90,11 @@ extension SearchSection { ) { switch object { case .twitter(let trend): - let metaContent = Meta.convert(from: .plaintext(string: trend.name)) + let metaContent = Meta.convert(document: .plaintext(string: trend.name)) cell.primaryLabel.configure(content: metaContent) cell.accessoryType = .disclosureIndicator case .mastodon(let tag): - let metaContent = Meta.convert(from: .plaintext(string: "#" + tag.name)) + let metaContent = Meta.convert(document: .plaintext(string: "#" + tag.name)) cell.primaryLabel.configure(content: metaContent) cell.secondaryLabel.text = L10n.Scene.Trends.accounts(tag.talkingPeopleCount ?? 0) diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Banner.swift b/TwidereX/Protocol/Facade/DataSourceFacade+Banner.swift similarity index 100% rename from TwidereX/Protocol/Provider/DataSourceFacade+Banner.swift rename to TwidereX/Protocol/Facade/DataSourceFacade+Banner.swift diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Block.swift b/TwidereX/Protocol/Facade/DataSourceFacade+Block.swift similarity index 100% rename from TwidereX/Protocol/Provider/DataSourceFacade+Block.swift rename to TwidereX/Protocol/Facade/DataSourceFacade+Block.swift diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Friendship.swift b/TwidereX/Protocol/Facade/DataSourceFacade+Friendship.swift similarity index 100% rename from TwidereX/Protocol/Provider/DataSourceFacade+Friendship.swift rename to TwidereX/Protocol/Facade/DataSourceFacade+Friendship.swift diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+History.swift b/TwidereX/Protocol/Facade/DataSourceFacade+History.swift similarity index 100% rename from TwidereX/Protocol/Provider/DataSourceFacade+History.swift rename to TwidereX/Protocol/Facade/DataSourceFacade+History.swift diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+LIst.swift b/TwidereX/Protocol/Facade/DataSourceFacade+LIst.swift similarity index 100% rename from TwidereX/Protocol/Provider/DataSourceFacade+LIst.swift rename to TwidereX/Protocol/Facade/DataSourceFacade+LIst.swift diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Like.swift b/TwidereX/Protocol/Facade/DataSourceFacade+Like.swift similarity index 100% rename from TwidereX/Protocol/Provider/DataSourceFacade+Like.swift rename to TwidereX/Protocol/Facade/DataSourceFacade+Like.swift diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Media.swift b/TwidereX/Protocol/Facade/DataSourceFacade+Media.swift similarity index 100% rename from TwidereX/Protocol/Provider/DataSourceFacade+Media.swift rename to TwidereX/Protocol/Facade/DataSourceFacade+Media.swift diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Meta.swift b/TwidereX/Protocol/Facade/DataSourceFacade+Meta.swift similarity index 77% rename from TwidereX/Protocol/Provider/DataSourceFacade+Meta.swift rename to TwidereX/Protocol/Facade/DataSourceFacade+Meta.swift index b7da9337..0543c575 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Meta.swift +++ b/TwidereX/Protocol/Facade/DataSourceFacade+Meta.swift @@ -13,32 +13,9 @@ import MetaTextArea import Meta extension DataSourceFacade { - static func responseToMetaTextAreaView( - provider: DataSourceProvider & AuthContextProvider, - target: StatusTarget, - status: StatusRecord, - metaTextAreaView: MetaTextAreaView, - didSelectMeta meta: Meta - ) async { - let _redirectRecord = await DataSourceFacade.status( - managedObjectContext: provider.context.managedObjectContext, - status: status, - target: target - ) - guard let redirectRecord = _redirectRecord else { return } - - await responseToMetaTextAreaView( - provider: provider, - status: redirectRecord, - metaTextAreaView: metaTextAreaView, - didSelectMeta: meta - ) - } - - static func responseToMetaTextAreaView( + static func responseToMetaText( provider: DataSourceProvider & AuthContextProvider, status: StatusRecord, - metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta ) async { switch meta { diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Model.swift b/TwidereX/Protocol/Facade/DataSourceFacade+Model.swift similarity index 100% rename from TwidereX/Protocol/Provider/DataSourceFacade+Model.swift rename to TwidereX/Protocol/Facade/DataSourceFacade+Model.swift diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Mute.swift b/TwidereX/Protocol/Facade/DataSourceFacade+Mute.swift similarity index 100% rename from TwidereX/Protocol/Provider/DataSourceFacade+Mute.swift rename to TwidereX/Protocol/Facade/DataSourceFacade+Mute.swift diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Poll.swift b/TwidereX/Protocol/Facade/DataSourceFacade+Poll.swift similarity index 100% rename from TwidereX/Protocol/Provider/DataSourceFacade+Poll.swift rename to TwidereX/Protocol/Facade/DataSourceFacade+Poll.swift diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Profile.swift b/TwidereX/Protocol/Facade/DataSourceFacade+Profile.swift similarity index 100% rename from TwidereX/Protocol/Provider/DataSourceFacade+Profile.swift rename to TwidereX/Protocol/Facade/DataSourceFacade+Profile.swift diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Report.swift b/TwidereX/Protocol/Facade/DataSourceFacade+Report.swift similarity index 100% rename from TwidereX/Protocol/Provider/DataSourceFacade+Report.swift rename to TwidereX/Protocol/Facade/DataSourceFacade+Report.swift diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Repost.swift b/TwidereX/Protocol/Facade/DataSourceFacade+Repost.swift similarity index 100% rename from TwidereX/Protocol/Provider/DataSourceFacade+Repost.swift rename to TwidereX/Protocol/Facade/DataSourceFacade+Repost.swift diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+SavedSearch.swift b/TwidereX/Protocol/Facade/DataSourceFacade+SavedSearch.swift similarity index 100% rename from TwidereX/Protocol/Provider/DataSourceFacade+SavedSearch.swift rename to TwidereX/Protocol/Facade/DataSourceFacade+SavedSearch.swift diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Share.swift b/TwidereX/Protocol/Facade/DataSourceFacade+Share.swift similarity index 100% rename from TwidereX/Protocol/Provider/DataSourceFacade+Share.swift rename to TwidereX/Protocol/Facade/DataSourceFacade+Share.swift diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift b/TwidereX/Protocol/Facade/DataSourceFacade+Status.swift similarity index 100% rename from TwidereX/Protocol/Provider/DataSourceFacade+Status.swift rename to TwidereX/Protocol/Facade/DataSourceFacade+Status.swift diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+StatusThread.swift b/TwidereX/Protocol/Facade/DataSourceFacade+StatusThread.swift similarity index 100% rename from TwidereX/Protocol/Provider/DataSourceFacade+StatusThread.swift rename to TwidereX/Protocol/Facade/DataSourceFacade+StatusThread.swift diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Translate.swift b/TwidereX/Protocol/Facade/DataSourceFacade+Translate.swift similarity index 85% rename from TwidereX/Protocol/Provider/DataSourceFacade+Translate.swift rename to TwidereX/Protocol/Facade/DataSourceFacade+Translate.swift index 1c6736f9..81a4fc5d 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Translate.swift +++ b/TwidereX/Protocol/Facade/DataSourceFacade+Translate.swift @@ -27,13 +27,15 @@ extension DataSourceFacade { case .mastodon(let status): let status = status.repost ?? status let spoilerText = status.spoilerText.flatMap { - Meta.convert(from: .mastodon(string: $0, emojis: [:])) + let content = MastodonContent(content: $0, emojis: [:]) + return Meta.convert(document: .mastodon(content: content)) } - let content = Meta.convert(from: .mastodon(string: status.content, emojis: [:])) + let content = MastodonContent(content: status.content, emojis: [:]) + let metaContent = Meta.convert(document: .mastodon(content: content)) return [ spoilerText?.string, - content.string + metaContent.string ] .compactMap { $0 } .joined(separator: "\n") diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+User.swift b/TwidereX/Protocol/Facade/DataSourceFacade+User.swift similarity index 100% rename from TwidereX/Protocol/Provider/DataSourceFacade+User.swift rename to TwidereX/Protocol/Facade/DataSourceFacade+User.swift diff --git a/TwidereX/Protocol/Provider/DataSourceFacade.swift b/TwidereX/Protocol/Facade/DataSourceFacade.swift similarity index 100% rename from TwidereX/Protocol/Provider/DataSourceFacade.swift rename to TwidereX/Protocol/Facade/DataSourceFacade.swift diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift index 43139f4d..4ebf153d 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift @@ -148,6 +148,22 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC ) } // end Task } + + func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, textViewDidSelectMeta meta: Meta) { + Task { @MainActor in + guard let status = viewModel.status else { + assertionFailure() + return + } + + await DataSourceFacade.responseToMetaText( + provider: self, + status: status, + didSelectMeta: meta + ) + } // end Task + } + } diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDataSourcePrefetching.swift b/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDataSourcePrefetching.swift new file mode 100644 index 00000000..e67d5457 --- /dev/null +++ b/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDataSourcePrefetching.swift @@ -0,0 +1,33 @@ +// +// DataSourceProvider+UITableViewDataSourcePrefetching.swift +// TwidereX +// +// Created by MainasuK on 2023/4/7. +// Copyright © 2023 Twidere. All rights reserved. +// + +import UIKit +import CollectionConcurrencyKit + +extension UITableViewDelegate where Self: DataSourceProvider { + func aspectTableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { + Task { +// let itmes: [DataSourceItem] = await indexPaths +// .concurrentCompactMap { [weak self] indexPath -> DataSourceItem? in +// guard let self = self else { return nil } +// return await self.item(from: .init(indexPath: indexPath)) +// } +// +// var statusRecords: [StatusRecord] = [] +// var userRecords: [UserRecord] = [] +// for item in itmes { +// switch item { +// case .status(let record): statusRecords.append(record) +// case .user(let record): userRecords.append(record) +// case .notification: +// continue +// } +// } + } // end Task + } +} diff --git a/TwidereX/Scene/Profile/Header/View/ProfileFieldContentView.swift b/TwidereX/Scene/Profile/Header/View/ProfileFieldContentView.swift index 2bd8d1b5..4f503afe 100644 --- a/TwidereX/Scene/Profile/Header/View/ProfileFieldContentView.swift +++ b/TwidereX/Scene/Profile/Header/View/ProfileFieldContentView.swift @@ -125,7 +125,7 @@ extension ProfileFieldContentView { guard let item = configuration.item else { return } _placeholderMetaLabel.setupAttributes(style: .profileFieldValue) - _placeholderMetaLabel.configure(content: Meta.convert(from: .plaintext(string: " "))) + _placeholderMetaLabel.configure(content: Meta.convert(document: .plaintext(string: " "))) if let symbol = item.symbol { symbolImageView.image = symbol.withRenderingMode(.alwaysTemplate) diff --git a/TwidereX/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift b/TwidereX/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift index aad0acad..0bde2fcd 100644 --- a/TwidereX/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift +++ b/TwidereX/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift @@ -230,7 +230,7 @@ extension ProfileHeaderView { // name user.publisher(for: \.name) .combineLatest(UIContentSizeCategory.publisher) { value, _ in - Meta.convert(from: .plaintext(string: value)) + Meta.convert(document: .plaintext(string: value)) } .assign(to: \.name, on: viewModel) .store(in: &viewModel.configureDisposeBag) @@ -353,7 +353,8 @@ extension ProfileHeaderView { UIContentSizeCategory.publisher ) .map { _, emojis, _ -> MetaContent in - Meta.convert(from: .mastodon(string: user.name, emojis: emojis.asDictionary)) + let content = MastodonContent(content: user.name, emojis: emojis.asDictionary) + return Meta.convert(document: .mastodon(content: content)) } .assign(to: \.name, on: viewModel) .store(in: &viewModel.configureDisposeBag) @@ -402,10 +403,10 @@ extension ProfileHeaderView { let emojis = emojis.asDictionary let items = fields.enumerated().map { i, field -> ProfileFieldListView.Item in let key = Meta.convert( - from: .mastodon(string: field.name, emojis: emojis) + document: .mastodon(content: MastodonContent(content: field.name, emojis: emojis)) ) let value = Meta.convert( - from: .mastodon(string: field.value, emojis: emojis) + document: .mastodon(content: MastodonContent(content: field.value, emojis: emojis)) ) return ProfileFieldListView.Item( index: i, diff --git a/TwidereX/Scene/Search/SearchResult/Hashtag/Cell/HashtagTableViewCell+ViewModel.swift b/TwidereX/Scene/Search/SearchResult/Hashtag/Cell/HashtagTableViewCell+ViewModel.swift index 4e1d3c78..7cdd6311 100644 --- a/TwidereX/Scene/Search/SearchResult/Hashtag/Cell/HashtagTableViewCell+ViewModel.swift +++ b/TwidereX/Scene/Search/SearchResult/Hashtag/Cell/HashtagTableViewCell+ViewModel.swift @@ -47,7 +47,7 @@ extension HashtagTableViewCell { func configure(tag: Mastodon.Entity.Tag) { // primary - let primaryContent = Meta.convert(from: .plaintext(string: "#" + tag.name)) + let primaryContent = Meta.convert(document: .plaintext(string: "#" + tag.name)) viewModel.primaryContent = primaryContent // secondary let count = tag.history?.sorted(by: { $0.day < $1.day }) diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift index f6be20ea..31a87447 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift @@ -26,6 +26,7 @@ protocol StatusViewContainerTableViewCell: UITableViewCell, AutoGenerateProtocol protocol StatusViewTableViewCellDelegate: AutoGenerateProtocolDelegate { // sourcery:inline:StatusViewTableViewCellDelegate.AutoGenerateProtocolDelegate func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, toggleContentDisplay isReveal: Bool) + func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, textViewDidSelectMeta meta: Meta) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel, previewActionContext: ContextMenuInteractionPreviewActionContext) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, toggleContentWarningOverlayDisplay isReveal: Bool) @@ -46,6 +47,10 @@ extension StatusViewDelegate where Self: StatusViewContainerTableViewCell { delegate?.tableViewCell(self, viewModel: viewModel, toggleContentDisplay: isReveal) } + func statusView(_ viewModel: StatusView.ViewModel, textViewDidSelectMeta meta: Meta) { + delegate?.tableViewCell(self, viewModel: viewModel, textViewDidSelectMeta: meta) + } + func statusView(_ viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel) { delegate?.tableViewCell(self, viewModel: viewModel, previewActionForMediaViewModel: mediaViewModel) } diff --git a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift index 6a880095..9bc29d48 100644 --- a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift @@ -292,6 +292,13 @@ extension ListTimelineViewController: UITableViewDelegate, AutoGenerateTableView } } +// MARK: - UITableViewDataSourcePrefetching +extension ListTimelineViewController: UITableViewDataSourcePrefetching { + func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { + aspectTableView(tableView, prefetchRowsAt: indexPaths) + } +} + // MARK: - ScrollViewContainer extension ListTimelineViewController: ScrollViewContainer { From 034ed595421668aebf9b9a4064edb6986121fb36 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 7 Apr 2023 19:02:22 +0800 Subject: [PATCH 055/128] chore: update MetaTextKit to v4.5.2 --- TwidereSDK/Package.swift | 2 +- TwidereX.xcodeproj/project.pbxproj | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index 47a42e35..89d3ef22 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -30,7 +30,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-nio.git", from: "2.34.0"), .package(url: "https://github.com/Flipboard/FLAnimatedImage.git", from: "1.0.0"), .package(url: "https://github.com/MainasuK/CommonOSLog", from: "0.1.1"), - .package(url: "https://github.com/TwidereProject/MetaTextKit.git", exact: "4.5.1"), + .package(url: "https://github.com/TwidereProject/MetaTextKit.git", exact: "4.5.2"), .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.5.0"), .package(url: "https://github.com/Alamofire/AlamofireImage.git", from: "4.1.0"), .package(url: "https://github.com/Alamofire/AlamofireNetworkActivityIndicator.git", from: "3.1.0"), diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index eb500720..0b8564b8 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -595,7 +595,6 @@ DB51DC422718117900A0D8FB /* StatusMediaGalleryCollectionCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusMediaGalleryCollectionCell+ViewModel.swift"; sourceTree = ""; }; DB51DC4D27181D7500A0D8FB /* CoverFlowStackMediaCollectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverFlowStackMediaCollectionCell.swift; sourceTree = ""; }; DB51DC4F27181DE400A0D8FB /* CoverFlowStackMediaCollectionCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoverFlowStackMediaCollectionCell+ViewModel.swift"; sourceTree = ""; }; - DB55495E29DFFCBC004AF42A /* MetaTextKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MetaTextKit; path = ../MetaTextKit; sourceTree = ""; }; DB55495F29E01330004AF42A /* DataSourceProvider+UITableViewDataSourcePrefetching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceProvider+UITableViewDataSourcePrefetching.swift"; sourceTree = ""; }; DB56329626DCBE1600FC893F /* StatusThreadViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusThreadViewController.swift; sourceTree = ""; }; DB56329926DCBE7300FC893F /* StatusThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusThreadViewModel.swift; sourceTree = ""; }; @@ -2049,7 +2048,6 @@ DBDA8E1524FCF8A3006750DC = { isa = PBXGroup; children = ( - DB55495E29DFFCBC004AF42A /* MetaTextKit */, DB33E5B42719637400EC2225 /* CoverFlowStackCollectionViewLayout */, DB51DC372716AEF000A0D8FB /* TabBarPager */, DB43239D251491EB004FAAEC /* TwidereSDK */, From 6fedfcfc58e022e1e72dd29ccf9b5e8a282f8afc Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 13 Apr 2023 15:56:44 +0800 Subject: [PATCH 056/128] feat: use the SwiftUI to layout UserView --- .../Extension/CoreDataStack/Feed.swift | 83 +- .../Notification/NotificationObject.swift | 20 +- .../Notification/NotificationRecord.swift | 15 +- .../Content/NotificationView+ViewModel.swift | 140 ++ .../TwidereUI/Content/NotificationView.swift | 53 + .../TwidereUI/Content/StatusHeaderView.swift | 4 - .../Content/StatusView+ViewModel.swift | 32 +- .../TwidereUI/Content/StatusView.swift | 13 +- .../Content/UserContentView+ViewModel.swift | 154 +- .../Content/UserView+Configuration.swift | 370 ++--- .../Content/UserView+ViewModel.swift | 626 +++++--- .../Sources/TwidereUI/Content/UserView.swift | 1283 ++++++++++------- .../ComposeContent/ComposeContentView.swift | 13 - .../MentionPickViewController.swift | 54 +- .../MentionPickViewModel+Diffable.swift | 88 +- .../TableSlideTableViewCell.swift | 0 .../TableViewCheckmarkTableViewCell.swift | 0 .../TableViewEntryTableViewCell.swift | 0 .../TableViewPlainCell.swift | 0 .../TableViewSwitchTableViewCell.swift | 0 .../TableViewTextFieldTableViewCell.swift | 0 ...eMiddleLoaderTableViewCell+ViewModel.swift | 4 +- .../TimelineMiddleLoaderTableViewCell.swift | 2 +- .../NotificationTableViewCell.swift | 57 + .../Status/StatusTableViewCell.swift | 50 + .../StatusViewTableViewCellDelegate.swift | 38 +- .../User/UserAccountStyleTableViewCell.swift | 42 +- .../UserAddListMemberStyleTableViewCell.swift | 40 +- .../UserFriendshipStyleTableViewCell.swift | 40 +- .../UserListMemberStyleTableViewCell.swift | 40 +- .../UserMentionPickStyleTableViewCell.swift | 20 +- .../UserNotificationStyleTableViewCell.swift | 40 +- .../UserRelationshipStyleTableViewCell.swift | 40 +- .../User/UserTableViewCell+ViewModel.swift | 58 +- .../User/UserTableViewCell.swift | 30 +- .../User/UserViewTableViewCellDelegate.swift | 37 +- .../Utility/SizeDimensionPreferenceKey.swift | 19 + TwidereX.xcodeproj/project.pbxproj | 12 - .../xcshareddata/swiftpm/Package.resolved | 9 + TwidereX/Coordinator/SceneCoordinator.swift | 15 +- .../Misc/History/HistorySection.swift | 3 +- .../Notification/NotificationSection.swift | 83 +- TwidereX/Diffable/Status/StatusSection.swift | 3 +- TwidereX/Diffable/User/UserItem.swift | 2 +- TwidereX/Diffable/User/UserSection.swift | 138 +- .../Facade/DataSourceFacade+Friendship.swift | 5 +- ...ider+StatusViewTableViewCellDelegate.swift | 110 +- ...taSourceProvider+UITableViewDelegate.swift | 21 +- ...ovider+UserViewTableViewCellDelegate.swift | 272 ++-- .../List/AccountListViewController.swift | 2 +- .../List/AccountListViewModel+Diffable.swift | 6 +- .../StatusHistoryViewModel+Diffable.swift | 4 - .../User/UserHistoryViewModel+Diffable.swift | 4 - .../ListUser/ListUserViewController.swift | 106 +- .../ListUser/ListUserViewModel+Diffable.swift | 8 +- ...ineViewController+DataSourceProvider.swift | 10 +- .../NotificationTimelineViewController.swift | 27 +- ...tificationTimelineViewModel+Diffable.swift | 15 +- ...ionTimelineViewModel+LoadOldestState.swift | 5 +- .../FollowingListViewModel+Diffable.swift | 8 +- .../User/SearchUserViewController.swift | 4 +- .../User/SearchUserViewModel+Diffable.swift | 10 +- .../StatusTableViewCell+ViewModel.swift | 128 -- .../TableViewCell/StatusTableViewCell.swift | 116 -- ...eadViewController+DataSourceProvider.swift | 2 +- .../StatusThreadViewModel+Diffable.swift | 6 +- .../StatusThread/StatusThreadViewModel.swift | 4 +- .../TimelineViewModel+LoadOldestState.swift | 3 +- .../Base/Common/TimelineViewModel.swift | 3 +- .../Base/List/ListTimelineViewModel.swift | 9 +- .../Timeline/Home/HomeTimelineViewModel.swift | 5 +- 71 files changed, 2526 insertions(+), 2137 deletions(-) create mode 100644 TwidereSDK/Sources/TwidereUI/Content/NotificationView+ViewModel.swift create mode 100644 TwidereSDK/Sources/TwidereUI/Content/NotificationView.swift rename TwidereSDK/Sources/TwidereUI/TableViewCell/{TableView => Common}/TableSlideTableViewCell.swift (100%) rename TwidereSDK/Sources/TwidereUI/TableViewCell/{TableView => Common}/TableViewCheckmarkTableViewCell.swift (100%) rename TwidereSDK/Sources/TwidereUI/TableViewCell/{TableView => Common}/TableViewEntryTableViewCell.swift (100%) rename TwidereSDK/Sources/TwidereUI/TableViewCell/{TableView => Common}/TableViewPlainCell.swift (100%) rename TwidereSDK/Sources/TwidereUI/TableViewCell/{TableView => Common}/TableViewSwitchTableViewCell.swift (100%) rename TwidereSDK/Sources/TwidereUI/TableViewCell/{TableView => Common}/TableViewTextFieldTableViewCell.swift (100%) create mode 100644 TwidereSDK/Sources/TwidereUI/TableViewCell/Notification/NotificationTableViewCell.swift create mode 100644 TwidereSDK/Sources/TwidereUI/TableViewCell/Status/StatusTableViewCell.swift rename {TwidereX/Scene/Share/View/TableViewCell => TwidereSDK/Sources/TwidereUI/TableViewCell/Status}/StatusViewTableViewCellDelegate.swift (63%) create mode 100644 TwidereSDK/Sources/TwidereUI/Utility/SizeDimensionPreferenceKey.swift delete mode 100644 TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell+ViewModel.swift delete mode 100644 TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift diff --git a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/Feed.swift b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/Feed.swift index 75c9f7bf..3486c59b 100644 --- a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/Feed.swift +++ b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/Feed.swift @@ -11,47 +11,12 @@ import CoreDataStack extension Feed { public enum Content { - case twitter(TwitterStatus) - case mastodon(MastodonStatus) - case mastodonNotification(MastodonNotification) - case none - } - - public var content: Content { - switch kind { - case .home: - if let status = twitterStatus { - return .twitter(status) - } else if let status = mastodonStatus { - return .mastodon(status) - } else { - return .none - } - case .notificationAll, .notificationMentions: - if let status = twitterStatus { - return .twitter(status) - } else if let status = mastodonStatus { - assertionFailure("The status should nest in mastodonNotification") - return .mastodon(status) - } else if let notification = mastodonNotification { - return .mastodonNotification(notification) - } else { - return .none - } - case .none: - return .none - } - } -} - -extension Feed { - public enum ObjectContent { case status(StatusObject) case notification(NotificationObject) case none } - public var objectContent: ObjectContent { + public var content: Content { switch kind { case .home: if let status = twitterStatus { @@ -63,16 +28,9 @@ extension Feed { } case .notificationAll, .notificationMentions: if let status = twitterStatus { - return .status(.twitter(object: status)) - } else if let status = mastodonStatus { - assertionFailure("The status should nest in mastodonNotification") - return .status(.mastodon(object: status)) + return .notification(.twitter(object: status)) } else if let notification = mastodonNotification { - if let status = notification.status { - return .status(.mastodon(object: status)) - } else { - return .notification(.mastodon(object: notification)) - } + return .notification(.mastodon(object: notification)) } else { return .none } @@ -80,39 +38,4 @@ extension Feed { return .none } } - -} - -extension Feed { - public var statusObject: StatusObject? { - switch acct { - case .none: - return nil - case .twitter: - guard let status = twitterStatus else { return nil } - return .twitter(object: status) - case .mastodon: - if let status = mastodonStatus { - return .mastodon(object: status) - } else if let notification = mastodonNotification, - let status = notification.status { - return .mastodon(object: status) - } else { - return nil - } - } - } - - @available(*, deprecated, message: "") - public var notificationObject: NotificationObject? { - switch acct { - case .none: - return nil - case .twitter: - return nil - case .mastodon: - guard let notification = mastodonNotification else { return nil } - return .mastodon(object: notification) - } - } } diff --git a/TwidereSDK/Sources/TwidereCore/Model/Notification/NotificationObject.swift b/TwidereSDK/Sources/TwidereCore/Model/Notification/NotificationObject.swift index 7b22d7df..1ef29bab 100644 --- a/TwidereSDK/Sources/TwidereCore/Model/Notification/NotificationObject.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/Notification/NotificationObject.swift @@ -9,15 +9,31 @@ import Foundation import CoreDataStack -public enum NotificationObject { +public enum NotificationObject: Hashable { + case twitter(object: TwitterStatus) case mastodon(object: MastodonNotification) } +extension NotificationObject { + public var asRecord: NotificationRecord { + switch self { + case .twitter(let object): + return .twitter(record: object.asRecrod) + case .mastodon(let object): + return .mastodon(record: object.asRecrod) + } + } +} + extension NotificationObject { public var status: StatusObject? { switch self { + case .twitter(let object): + let status = object + return .twitter(object: status) case .mastodon(let object): - return object.status.flatMap { .mastodon(object: $0) } + guard let status = object.status else { return nil } + return .mastodon(object: status) } // end swich } // end func } diff --git a/TwidereSDK/Sources/TwidereCore/Model/Notification/NotificationRecord.swift b/TwidereSDK/Sources/TwidereCore/Model/Notification/NotificationRecord.swift index 37565da1..e491933c 100644 --- a/TwidereSDK/Sources/TwidereCore/Model/Notification/NotificationRecord.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/Notification/NotificationRecord.swift @@ -11,15 +11,19 @@ import CoreData import CoreDataStack public enum NotificationRecord: Hashable { + case twitter(record: ManagedObjectRecord) case mastodon(record: ManagedObjectRecord) } extension NotificationRecord { public func object(in managedObjectContext: NSManagedObjectContext) -> NotificationObject? { switch self { + case .twitter(let record): + guard let object = record.object(in: managedObjectContext) else { return nil } + return .twitter(object: object) case .mastodon(let record): - guard let notification = record.object(in: managedObjectContext) else { return nil } - return .mastodon(object: notification) + guard let object = record.object(in: managedObjectContext) else { return nil } + return .mastodon(object: object) } } } @@ -29,9 +33,12 @@ extension NotificationRecord { return await managedObjectContext.perform { guard let object = self.object(in: managedObjectContext) else { return nil } switch object { + case .twitter(let object): + let status = object + return .twitter(record: status.asRecrod) case .mastodon(let object): - guard let objectID = object.status?.objectID else { return nil } - return .mastodon(record: .init(objectID: objectID)) + guard let status = object.status else { return nil } + return .mastodon(record: status.asRecrod) } } } // end func diff --git a/TwidereSDK/Sources/TwidereUI/Content/NotificationView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/NotificationView+ViewModel.swift new file mode 100644 index 00000000..9c4ed017 --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Content/NotificationView+ViewModel.swift @@ -0,0 +1,140 @@ +// +// NotificationView+ViewModel.swift +// +// +// Created by MainasuK on 2023/4/11. +// + +import os.log +import SwiftUI +import Combine +import CoreDataStack + +extension NotificationView { + public class ViewModel: ObservableObject { + + let logger = Logger(subsystem: "StatusView", category: "ViewModel") + + @Published public var viewLayoutFrame = ViewLayoutFrame() + + + // input + public let notification: NotificationObject + public let authContext: AuthContext? + + // output + + // user + @Published public var userViewModel: UserView.ViewModel? + + // status + @Published public var statusViewModel: StatusView.ViewModel? + + // header + @Published var notificationHeaderViewModel: StatusHeaderView.ViewModel? + + public init( + notification: NotificationObject, + authContext: AuthContext?, + viewLayoutFramePublisher: Published.Publisher?, + statusViewModel: StatusView.ViewModel?, + userViewModel: UserView.ViewModel? + ) { + self.notification = notification + self.authContext = authContext + self.statusViewModel = statusViewModel + self.userViewModel = userViewModel + // end init + + viewLayoutFramePublisher?.assign(to: &$viewLayoutFrame) + } + + } +} + +extension NotificationView.ViewModel { + public convenience init( + notification: NotificationObject, + authContext: AuthContext?, + statusViewDelegate: StatusViewDelegate?, + userViewDelegate: UserViewDelegate?, + viewLayoutFramePublisher: Published.Publisher? + ) { + switch notification { + case .twitter(let status): + self.init( + notification: notification, + authContext: authContext, + viewLayoutFramePublisher: viewLayoutFramePublisher, + statusViewModel: .init( + status: status, + authContext: authContext, + kind: .timeline, + delegate: statusViewDelegate, + parentViewModel: nil, + viewLayoutFramePublisher: viewLayoutFramePublisher + ), + userViewModel: nil + ) + case .mastodon(let notification): + self.init( + notification: notification, + authContext: authContext, + statusViewDelegate: statusViewDelegate, + userViewDelegate: userViewDelegate, + viewLayoutFramePublisher: viewLayoutFramePublisher + ) + } + } // end init +} + +extension NotificationView.ViewModel { + public convenience init( + notification: MastodonNotification, + authContext: AuthContext?, + statusViewDelegate: StatusViewDelegate?, + userViewDelegate: UserViewDelegate?, + viewLayoutFramePublisher: Published.Publisher? + ) { + self.init( + notification: .mastodon(object: notification), + authContext: authContext, + viewLayoutFramePublisher: viewLayoutFramePublisher, + statusViewModel: { + guard let status = notification.status else { return nil } + return StatusView.ViewModel( + status: status, + authContext: authContext, + kind: .timeline, + delegate: statusViewDelegate, + parentViewModel: nil, + viewLayoutFramePublisher: viewLayoutFramePublisher + ) + }(), + userViewModel: { + guard notification.status == nil else { return nil } + let ViewModel = UserView.ViewModel( + user: notification.account, + authContext: authContext, + kind: .notification(.mastodon(object: notification)), + delegate: userViewDelegate + ) + return ViewModel + }() + ) + + // header + let _info = NotificationHeaderInfo( + type: notification.notificationType, + user: notification.account + ) + if let info = _info { + let _notificationHeaderViewModel = StatusHeaderView.ViewModel( + image: info.iconImage, + label: info.textMetaContent + ) + _notificationHeaderViewModel.hasHangingAvatar = true + self.notificationHeaderViewModel = _notificationHeaderViewModel + } + } // end init +} diff --git a/TwidereSDK/Sources/TwidereUI/Content/NotificationView.swift b/TwidereSDK/Sources/TwidereUI/Content/NotificationView.swift new file mode 100644 index 00000000..3e7af058 --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Content/NotificationView.swift @@ -0,0 +1,53 @@ +// +// NotificationView.swift +// +// +// Created by MainasuK on 2023/4/11. +// + +import SwiftUI + +public struct NotificationView: View { + + static var verticalMargin: CGFloat = 8 + + @ObservedObject public private(set) var viewModel: ViewModel + + @Environment(\.dynamicTypeSize) var dynamicTypeSize + + public init(viewModel: NotificationView.ViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + VStack(spacing: .zero) { + // header + if let notificationHeaderViewModel = viewModel.notificationHeaderViewModel { + StatusHeaderView(viewModel: notificationHeaderViewModel) + .padding(.top, NotificationView.verticalMargin) + .allowsHitTesting(false) + } + // status + if let statusViewModel = viewModel.statusViewModel { + StatusView(viewModel: statusViewModel) + } else { + if let userViewModel = viewModel.userViewModel { + UserView(viewModel: userViewModel) + } + Color.clear + .frame(height: NotificationView.verticalMargin) + .overlay { + HStack(spacing: StatusView.hangingAvatarButtonTrailingSpacing) { + Color.clear.frame(width: StatusView.hangingAvatarButtonDimension) + VStack(spacing: .zero) { + Spacer() + Divider() + Color.clear.frame(height: 1) + } + } + } // end overlay + } // end if … else + } + } + +} diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift index c7336df3..463d345a 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift @@ -11,10 +11,6 @@ import TwidereLocalization import Meta import Kingfisher -protocol StatusHeaderViewDelegate: AnyObject { - func viewDidPressed(_ viewModel: StatusHeaderView.ViewModel) -} - public struct StatusHeaderView: View { static var iconImageTrailingSpacing: CGFloat { 4.0 } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index 68e268ad..b841d4cc 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -34,7 +34,8 @@ extension StatusView { @Published public var quoteViewModel: StatusView.ViewModel? // input - public let status: StatusRecord? + public let status: StatusObject + public let author: UserObject public let authContext: AuthContext? public let kind: Kind public weak var delegate: StatusViewDelegate? @@ -164,13 +165,15 @@ extension StatusView { @Published public var isBottomConversationLinkLineViewDisplay = false private init( - status: StatusRecord?, + status: StatusObject, + author: UserObject, authContext: AuthContext?, kind: Kind, delegate: StatusViewDelegate?, viewLayoutFramePublisher: Published.Publisher? ) { self.status = status + self.author = author self.authContext = authContext self.kind = kind self.delegate = delegate @@ -955,27 +958,16 @@ extension StatusView.ViewModel { viewLayoutFramePublisher: Published.Publisher? ) { switch feed.content { - case .twitter(let status): - self.init( - status: status, - authContext: authContext, - kind: .timeline, - delegate: delegate, - parentViewModel: nil, - viewLayoutFramePublisher: viewLayoutFramePublisher - ) - case .mastodon(let status): + case .status(let object): self.init( - status: status, + status: object, authContext: authContext, kind: .timeline, delegate: delegate, - parentViewModel: nil, viewLayoutFramePublisher: viewLayoutFramePublisher ) - case .mastodonNotification(let notification): - return nil - case .none: + default: + assertionFailure("should use other View & ViewModel") return nil } } // end init @@ -1020,7 +1012,8 @@ extension StatusView.ViewModel { viewLayoutFramePublisher: Published.Publisher? ) { self.init( - status: .twitter(record: status.asRecrod), + status: .twitter(object: status), + author: .twitter(object: status.author), authContext: authContext, kind: status.repost != nil ? .repost : kind, delegate: delegate, @@ -1181,7 +1174,8 @@ extension StatusView.ViewModel { viewLayoutFramePublisher: Published.Publisher? ) { self.init( - status: .mastodon(record: status.asRecrod), + status: .mastodon(object: status), + author: .mastodon(object: status.author), authContext: authContext, kind: status.repost != nil ? .repost : kind, delegate: delegate, diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index 20f187ff..fbcb1f69 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -19,18 +19,15 @@ import TwidereCore public protocol StatusViewDelegate: AnyObject { // func statusView(_ statusView: StatusView, headerDidPressed header: UIView) -// func statusView(_ statusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) -// func statusView(_ statusView: StatusView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) -// -// func statusView(_ statusView: StatusView, expandContentButtonDidPressed button: UIButton) + // avatar + func statusView(_ viewModel: StatusView.ViewModel, userAvatarButtonDidPressed user: UserRecord) + + // spoiler func statusView(_ viewModel: StatusView.ViewModel, toggleContentDisplay isReveal: Bool) // meta func statusView(_ viewModel: StatusView.ViewModel, textViewDidSelectMeta meta: Meta) -// func statusView(_ statusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) -// func statusView(_ statusView: StatusView, quoteStatusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) - // media func statusView(_ viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel) func statusView(_ viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel, previewActionContext: ContextMenuInteractionPreviewActionContext) @@ -357,7 +354,7 @@ extension StatusView { var avatarButton: some View { Button { - + viewModel.delegate?.statusView(viewModel, userAvatarButtonDidPressed: viewModel.author.asRecord) } label: { let dimension: CGFloat = { switch viewModel.kind { diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserContentView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/UserContentView+ViewModel.swift index 5cbda61c..cd5941c2 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserContentView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserContentView+ViewModel.swift @@ -38,7 +38,7 @@ extension UserContentView { self.accessoryType = accessoryType // end init - configure() + // configure() } } } @@ -52,81 +52,81 @@ extension UserContentView.ViewModel { } -extension UserContentView.ViewModel { - - func configure() { - assert(Thread.isMainThread) - - switch user { - case .twitter(let user): - configure(user: user) - case .mastodon(let user): - configure(user: user) - } - } - -} +//extension UserContentView.ViewModel { +// +// func configure() { +// assert(Thread.isMainThread) +// +// switch user { +// case .twitter(let user): +// configure(user: user) +// case .mastodon(let user): +// configure(user: user) +// } +// } +// +//} -extension UserContentView.ViewModel { - private func configure(user: TwitterUser) { - // platform - platform = .twitter - // avatar - user.publisher(for: \.profileImageURL) - .map { _ in user.avatarImageURL() } - .assign(to: &$avatarImageURL) - // author name - user.publisher(for: \.name) - .map { PlaintextMetaContent(string: $0) } - .assign(to: &$name) - // author username - user.publisher(for: \.username) - .map { PlaintextMetaContent(string: "@" + $0) } - .assign(to: &$username) - // acct - user.publisher(for: \.username) - .map { PlaintextMetaContent(string: "@" + $0) } - .assign(to: &$acct) - // protected - user.publisher(for: \.protected) - .assign(to: &$protected) - } -} +//extension UserContentView.ViewModel { +// private func configure(user: TwitterUser) { +// // platform +// platform = .twitter +// // avatar +// user.publisher(for: \.profileImageURL) +// .map { _ in user.avatarImageURL() } +// .assign(to: &$avatarImageURL) +// // author name +// user.publisher(for: \.name) +// .map { PlaintextMetaContent(string: $0) } +// .assign(to: &$name) +// // author username +// user.publisher(for: \.username) +// .map { PlaintextMetaContent(string: "@" + $0) } +// .assign(to: &$username) +// // acct +// user.publisher(for: \.username) +// .map { PlaintextMetaContent(string: "@" + $0) } +// .assign(to: &$acct) +// // protected +// user.publisher(for: \.protected) +// .assign(to: &$protected) +// } +//} -extension UserContentView.ViewModel { - private func configure(user: MastodonUser) { - // platform - platform = .mastodon - // avatar - Publishers.CombineLatest3( - UserDefaults.shared.publisher(for: \.preferredStaticAvatar), - user.publisher(for: \.avatar), - user.publisher(for: \.avatarStatic) - ) - .map { preferredStaticAvatar, avatar, avatarStatic in - let string = preferredStaticAvatar ? (avatarStatic ?? avatar) : avatar - return string.flatMap { URL(string: $0) } - } - .assign(to: &$avatarImageURL) - // author name - Publishers.CombineLatest( - user.publisher(for: \.displayName), - user.publisher(for: \.emojis) - ) - .map { name, _ -> MetaContent in - user.nameMetaContent ?? PlaintextMetaContent(string: name) - } - .assign(to: &$name) - // author username - user.publisher(for: \.acct) - .map { PlaintextMetaContent(string: "@" + $0) } - .assign(to: &$username) - // acct - user.publisher(for: \.acct) - .map { _ in PlaintextMetaContent(string: "@" + user.acctWithDomain) } - .assign(to: &$acct) - // protected - user.publisher(for: \.locked) - .assign(to: &$protected) - } -} +//extension UserContentView.ViewModel { +// private func configure(user: MastodonUser) { +// // platform +// platform = .mastodon +// // avatar +// Publishers.CombineLatest3( +// UserDefaults.shared.publisher(for: \.preferredStaticAvatar), +// user.publisher(for: \.avatar), +// user.publisher(for: \.avatarStatic) +// ) +// .map { preferredStaticAvatar, avatar, avatarStatic in +// let string = preferredStaticAvatar ? (avatarStatic ?? avatar) : avatar +// return string.flatMap { URL(string: $0) } +// } +// .assign(to: &$avatarImageURL) +// // author name +// Publishers.CombineLatest( +// user.publisher(for: \.displayName), +// user.publisher(for: \.emojis) +// ) +// .map { name, _ -> MetaContent in +// user.nameMetaContent ?? PlaintextMetaContent(string: name) +// } +// .assign(to: &$name) +// // author username +// user.publisher(for: \.acct) +// .map { PlaintextMetaContent(string: "@" + $0) } +// .assign(to: &$username) +// // acct +// user.publisher(for: \.acct) +// .map { _ in PlaintextMetaContent(string: "@" + user.acctWithDomain) } +// .assign(to: &$acct) +// // protected +// user.publisher(for: \.locked) +// .assign(to: &$protected) +// } +//} diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView+Configuration.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView+Configuration.swift index 53a7b5f1..296083b4 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView+Configuration.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView+Configuration.swift @@ -14,188 +14,188 @@ import TwidereAsset import Meta import MastodonSDK -extension UserView { - public struct ConfigurationContext { - public let authContext: AuthContext - public let listMembershipViewModel: ListMembershipViewModel? - - public init( - authContext: AuthContext, - listMembershipViewModel: ListMembershipViewModel? - ) { - self.authContext = authContext - self.listMembershipViewModel = listMembershipViewModel - } - } -} - -extension UserView { - public func configure( - user: UserObject, - me: UserObject?, - notification: NotificationObject?, - configurationContext: ConfigurationContext - ) { - switch user { - case .twitter(let user): - configure(twitterUser: user) - case .mastodon(let user): - configure(mastodonUser: user) - } - - if let notification = notification { - configure(notification: notification) - } - - viewModel.relationshipViewModel.user = user - viewModel.relationshipViewModel.me = me - - viewModel.listMembershipViewModel = configurationContext.listMembershipViewModel - if let listMembershipViewModel = configurationContext.listMembershipViewModel { - listMembershipViewModel.$ownerUserIdentifier - .assign(to: \.listOwnerUserIdentifier, on: viewModel) - .store(in: &disposeBag) - } - - // accessory - switch style { - case .addListMember: - guard let listMembershipViewModel = configurationContext.listMembershipViewModel else { - assertionFailure() - break - } - let userRecord = user.asRecord - listMembershipViewModel.$members - .map { members in members.contains(userRecord) } - .assign(to: \.isListMember, on: viewModel) - .store(in: &disposeBag) - listMembershipViewModel.$workingMembers - .map { members in members.contains(userRecord) } - .assign(to: \.isListMemberCandidate, on: viewModel) - .store(in: &disposeBag) - default: - break - } - } - - public func configure(notification: NotificationObject) { - switch notification { - case .mastodon(let notification): - configure(mastodonNotification: notification) - } - } -} - -extension UserView { - private func configure(twitterUser user: TwitterUser) { - // platform - viewModel.platform = .twitter - // userIdentifier - viewModel.userIdentifier = .twitter(.init(id: user.id)) - // userAuthenticationContext - viewModel.userAuthenticationContext = user.twitterAuthentication.flatMap { - AuthenticationContext(authenticationIndex: $0.authenticationIndex, secret: AppSecret.default.secret) - } - // avatar - user.publisher(for: \.profileImageURL) - .map { _ in user.avatarImageURL() } - .assign(to: \.avatarImageURL, on: viewModel) - .store(in: &disposeBag) - // author name - user.publisher(for: \.name) - .map { PlaintextMetaContent(string: $0) } - .assign(to: \.name, on: viewModel) - .store(in: &disposeBag) - // author username - user.publisher(for: \.username) - .map { $0 as String? } - .assign(to: \.username, on: viewModel) - .store(in: &disposeBag) - // protected - user.publisher(for: \.protected) - .assign(to: \.protected, on: viewModel) - .store(in: &disposeBag) - // followersCount - user.publisher(for: \.followersCount) - .map { Int($0) } - .assign(to: \.followerCount, on: viewModel) - .store(in: &disposeBag) - } -} - -extension UserView { - private func configure(mastodonUser user: MastodonUser) { - // platform - viewModel.platform = .mastodon - // userIdentifier - viewModel.userIdentifier = .mastodon(.init(domain: user.domain, id: user.id)) - // userAuthenticationContext - viewModel.userAuthenticationContext = user.mastodonAuthentication.flatMap { - AuthenticationContext(authenticationIndex: $0.authenticationIndex, secret: AppSecret.default.secret) - } - // avatar - Publishers.CombineLatest3( - UserDefaults.shared.publisher(for: \.preferredStaticAvatar), - user.publisher(for: \.avatar), - user.publisher(for: \.avatarStatic) - ) - .map { preferredStaticAvatar, avatar, avatarStatic in - let string = preferredStaticAvatar ? (avatarStatic ?? avatar) : avatar - return string.flatMap { URL(string: $0) } - } - .assign(to: \.avatarImageURL, on: viewModel) - .store(in: &disposeBag) - // author name - Publishers.CombineLatest( - user.publisher(for: \.displayName), - user.publisher(for: \.emojis) - ) - .map { _ in user.nameMetaContent } - .assign(to: \.name, on: viewModel) - .store(in: &disposeBag) - // author username - user.publisher(for: \.acct) - .map { _ in user.acctWithDomain as String? } - .assign(to: \.username, on: viewModel) - .store(in: &disposeBag) - // protected - user.publisher(for: \.locked) - .assign(to: \.protected, on: viewModel) - .store(in: &disposeBag) - // followersCount - user.publisher(for: \.followersCount) - .map { Int($0) } - .assign(to: \.followerCount, on: viewModel) - .store(in: &disposeBag) - } - - private func configure(mastodonNotification notification: MastodonNotification) { - let user = notification.account - let type = notification.notificationType - Publishers.CombineLatest( - user.publisher(for: \.displayName), - user.publisher(for: \.emojis) - ) - .map { _ in - guard let info = NotificationHeaderInfo(type: type, user: user) else { - return .none - } - return ViewModel.Header.notification(info: info) - } - .assign(to: \.header, on: viewModel) - .store(in: &disposeBag) - - switch type { - case .followRequest: - setFollowRequestControlDisplay() - default: - break - } - - notification.publisher(for: \.isFollowRequestBusy) - .assign(to: \.isFollowRequestBusy, on: viewModel) - .store(in: &disposeBag) - } -} - - +//extension UserView { +// public struct ConfigurationContext { +// public let authContext: AuthContext +// public let listMembershipViewModel: ListMembershipViewModel? +// +// public init( +// authContext: AuthContext, +// listMembershipViewModel: ListMembershipViewModel? +// ) { +// self.authContext = authContext +// self.listMembershipViewModel = listMembershipViewModel +// } +// } +//} +// +//extension UserView { +// public func configure( +// user: UserObject, +// me: UserObject?, +// notification: NotificationObject?, +// configurationContext: ConfigurationContext +// ) { +// switch user { +// case .twitter(let user): +// configure(twitterUser: user) +// case .mastodon(let user): +// configure(mastodonUser: user) +// } +// +//// if let notification = notification { +//// configure(notification: notification) +//// } +// +// viewModel.relationshipViewModel.user = user +// viewModel.relationshipViewModel.me = me +// +// viewModel.listMembershipViewModel = configurationContext.listMembershipViewModel +// if let listMembershipViewModel = configurationContext.listMembershipViewModel { +// listMembershipViewModel.$ownerUserIdentifier +// .assign(to: \.listOwnerUserIdentifier, on: viewModel) +// .store(in: &disposeBag) +// } +// +// // accessory +// switch style { +// case .addListMember: +// guard let listMembershipViewModel = configurationContext.listMembershipViewModel else { +// assertionFailure() +// break +// } +// let userRecord = user.asRecord +// listMembershipViewModel.$members +// .map { members in members.contains(userRecord) } +// .assign(to: \.isListMember, on: viewModel) +// .store(in: &disposeBag) +// listMembershipViewModel.$workingMembers +// .map { members in members.contains(userRecord) } +// .assign(to: \.isListMemberCandidate, on: viewModel) +// .store(in: &disposeBag) +// default: +// break +// } +// } +// +//// public func configure(notification: NotificationObject) { +//// switch notification { +//// case .mastodon(let notification): +//// configure(mastodonNotification: notification) +//// } +//// } +//} +// +//extension UserView { +// private func configure(twitterUser user: TwitterUser) { +// // platform +// viewModel.platform = .twitter +// // userIdentifier +// viewModel.userIdentifier = .twitter(.init(id: user.id)) +// // userAuthenticationContext +// viewModel.userAuthenticationContext = user.twitterAuthentication.flatMap { +// AuthenticationContext(authenticationIndex: $0.authenticationIndex, secret: AppSecret.default.secret) +// } +// // avatar +// user.publisher(for: \.profileImageURL) +// .map { _ in user.avatarImageURL() } +// .assign(to: \.avatarImageURL, on: viewModel) +// .store(in: &disposeBag) +// // author name +// user.publisher(for: \.name) +// .map { PlaintextMetaContent(string: $0) } +// .assign(to: \.name, on: viewModel) +// .store(in: &disposeBag) +// // author username +// user.publisher(for: \.username) +// .map { $0 as String? } +// .assign(to: \.username, on: viewModel) +// .store(in: &disposeBag) +// // protected +// user.publisher(for: \.protected) +// .assign(to: \.protected, on: viewModel) +// .store(in: &disposeBag) +// // followersCount +// user.publisher(for: \.followersCount) +// .map { Int($0) } +// .assign(to: \.followerCount, on: viewModel) +// .store(in: &disposeBag) +// } +//} +// +//extension UserView { +// private func configure(mastodonUser user: MastodonUser) { +// // platform +// viewModel.platform = .mastodon +// // userIdentifier +// viewModel.userIdentifier = .mastodon(.init(domain: user.domain, id: user.id)) +// // userAuthenticationContext +// viewModel.userAuthenticationContext = user.mastodonAuthentication.flatMap { +// AuthenticationContext(authenticationIndex: $0.authenticationIndex, secret: AppSecret.default.secret) +// } +// // avatar +// Publishers.CombineLatest3( +// UserDefaults.shared.publisher(for: \.preferredStaticAvatar), +// user.publisher(for: \.avatar), +// user.publisher(for: \.avatarStatic) +// ) +// .map { preferredStaticAvatar, avatar, avatarStatic in +// let string = preferredStaticAvatar ? (avatarStatic ?? avatar) : avatar +// return string.flatMap { URL(string: $0) } +// } +// .assign(to: \.avatarImageURL, on: viewModel) +// .store(in: &disposeBag) +// // author name +// Publishers.CombineLatest( +// user.publisher(for: \.displayName), +// user.publisher(for: \.emojis) +// ) +// .map { _ in user.nameMetaContent } +// .assign(to: \.name, on: viewModel) +// .store(in: &disposeBag) +// // author username +// user.publisher(for: \.acct) +// .map { _ in user.acctWithDomain as String? } +// .assign(to: \.username, on: viewModel) +// .store(in: &disposeBag) +// // protected +// user.publisher(for: \.locked) +// .assign(to: \.protected, on: viewModel) +// .store(in: &disposeBag) +// // followersCount +// user.publisher(for: \.followersCount) +// .map { Int($0) } +// .assign(to: \.followerCount, on: viewModel) +// .store(in: &disposeBag) +// } +// +// private func configure(mastodonNotification notification: MastodonNotification) { +// let user = notification.account +// let type = notification.notificationType +// Publishers.CombineLatest( +// user.publisher(for: \.displayName), +// user.publisher(for: \.emojis) +// ) +// .map { _ in +// guard let info = NotificationHeaderInfo(type: type, user: user) else { +// return .none +// } +// return ViewModel.Header.notification(info: info) +// } +// .assign(to: \.header, on: viewModel) +// .store(in: &disposeBag) +// +// switch type { +// case .followRequest: +// setFollowRequestControlDisplay() +// default: +// break +// } +// +// notification.publisher(for: \.isFollowRequestBusy) +// .assign(to: \.isFollowRequestBusy, on: viewModel) +// .store(in: &disposeBag) +// } +//} +// +// diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift index 69440859..c13440d7 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift @@ -17,253 +17,435 @@ import MastodonSDK extension UserView { public final class ViewModel: ObservableObject { + var disposeBag = Set() var observations = Set() let relationshipViewModel = RelationshipViewModel() - @Published public var platform: Platform = .none - @Published public var authenticationContext: AuthenticationContext? // me - @Published public var userAuthenticationContext: AuthenticationContext? - - @Published public var header: Header = .none - - @Published public var userIdentifier: UserIdentifier? = nil - @Published public var avatarImageURL: URL? - @Published public var avatarBadge: AvatarBadge = .none - // TODO: verified | bot + // input + public let user: UserObject + public let authContext: AuthContext? + public let kind: Kind + public weak var delegate: UserViewDelegate? - @Published public var name: MetaContent? = PlaintextMetaContent(string: " ") - @Published public var username: String? + public private(set) var notification: NotificationObject? - @Published public var protected: Bool = false + // output - @Published public var followerCount: Int? + // user + @Published public var avatarURL: URL? + @Published public var name: MetaContent = PlaintextMetaContent(string: "") + @Published public var username: String = "" +// @Published public var platform: Platform = .none +// @Published public var authenticationContext: AuthenticationContext? // me +// @Published public var userAuthenticationContext: AuthenticationContext? +// +// @Published public var header: Header = .none +// +// @Published public var userIdentifier: UserIdentifier? = nil +// @Published public var avatarImageURL: URL? +// @Published public var avatarBadge: AvatarBadge = .none +// // TODO: verified | bot +// +// @Published public var name: MetaContent? = PlaintextMetaContent(string: " ") +// @Published public var username: String? +// +// @Published public var protected: Bool = false +// +// @Published public var followerCount: Int? +// + @Published public var isFollowRequestActionDisplay = false @Published public var isFollowRequestBusy = false +// +// public var listMembershipViewModel: ListMembershipViewModel? +// @Published public var listOwnerUserIdentifier: UserIdentifier? = nil +// @Published public var isListMember = false +// @Published public var isListMemberCandidate = false // a.k.a isBusy +// @Published public var isMyList = false +// +// @Published public var badgeCount: Int = 0 +// +// public enum Header { +// case none +// case notification(info: NotificationHeaderInfo) +// } + + private init( + object user: UserObject, + authContext: AuthContext?, + kind: Kind, + delegate: UserViewDelegate? + ) { + self.user = user + self.authContext = authContext + self.kind = kind + self.delegate = delegate + // end init + + switch kind { + case .notification(let notification): + self.notification = notification + default: + break + } + + switch notification { + case .twitter: + break + case .mastodon(let notification): + self.isFollowRequestActionDisplay = notification.notificationType == .followRequest + notification.publisher(for: \.isFollowRequestBusy) + .assign(to: &$isFollowRequestBusy) + default: + break + } +// // isMyList +// Publishers.CombineLatest( +// $authenticationContext, +// $listOwnerUserIdentifier +// ) +// .map { authenticationContext, userIdentifier -> Bool in +// guard let authenticationContext = authenticationContext else { return false } +// guard let userIdentifier = userIdentifier else { return false } +// return authenticationContext.userIdentifier == userIdentifier +// } +// .assign(to: &$isMyList) +// // badge count +// $userAuthenticationContext +// .map { authenticationContext -> Int in +// switch authenticationContext { +// case .twitter: +// return 0 +// case .mastodon(let authenticationContext): +// let accessToken = authenticationContext.authorization.accessToken +// let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) +// return count +// case .none: +// return 0 +// } +// } +// .assign(to: &$badgeCount) + } + } +} + +extension UserView.ViewModel { + public enum Kind: Hashable { + // headline: name | lock + // subheadline: username + // accessory: [ badge | menu ] + case account - public var listMembershipViewModel: ListMembershipViewModel? - @Published public var listOwnerUserIdentifier: UserIdentifier? = nil - @Published public var isListMember = false - @Published public var isListMemberCandidate = false // a.k.a isBusy - @Published public var isMyList = false + // headline: name | lock | username + // subheadline: follower count + // accessory: follow button + case relationship - @Published public var badgeCount: Int = 0 + // headline: name | lock + // subheadline: username + // accessory: action button + case friendship - public enum Header { - case none - case notification(info: NotificationHeaderInfo) - } + // header: notification + // headline: name | lock | username + // subheadline: follower count + // accessory: [ followRquest accept and reject button ] + case notification(NotificationObject) - public enum AvatarBadge { - case none - case platform - case user // verified | bot - } + // headline: name | lock + // subheadline: username + // accessory: checkmark button + case mentionPick - func prepareForReuse() { - avatarImageURL = nil - isFollowRequestBusy = false - } + // headline: name | lock | username + // subheadline: follower count + // accessory: membership menu + case listMember - init() { - // isMyList - Publishers.CombineLatest( - $authenticationContext, - $listOwnerUserIdentifier - ) - .map { authenticationContext, userIdentifier -> Bool in - guard let authenticationContext = authenticationContext else { return false } - guard let userIdentifier = userIdentifier else { return false } - return authenticationContext.userIdentifier == userIdentifier - } - .assign(to: &$isMyList) - // badge count - $userAuthenticationContext - .map { authenticationContext -> Int in - switch authenticationContext { - case .twitter: - return 0 - case .mastodon(let authenticationContext): - let accessToken = authenticationContext.authorization.accessToken - let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) - return count - case .none: - return 0 - } - } - .assign(to: &$badgeCount) - } + // headline: name | lock | username + // subheadline: follower count + // accessory: membership button + case addListMember + } + + public enum AvatarBadge { + case none + case platform + case user // verified | bot + } + + public enum MenuAction: Hashable { + case signOut + case remove } } extension UserView.ViewModel { - public func bind(userView: UserView) { - // avatar - $avatarImageURL - .sink { url in - let configuration = AvatarImageView.Configuration(url: url) - userView.authorProfileAvatarView.avatarButton.avatarImageView.configure(configuration: configuration) - } - .store(in: &disposeBag) - Publishers.CombineLatest( - $avatarBadge, - $platform - ) - .sink { avatarBadge, platform in - switch avatarBadge { - case .none: - userView.authorProfileAvatarView.badge = .none - case .platform: - userView.authorProfileAvatarView.badge = { - switch platform { - case .none: return .none - case .twitter: return .circle(.twitter) - case .mastodon: return .circle(.mastodon) - } - }() - case .user: - userView.authorProfileAvatarView.badge = .none - } + var verticalMargin: CGFloat { + switch kind { + case .notification: return .zero + default: return 12.0 } - .store(in: &disposeBag) - // badge - // TODO: - // header - $header - .sink { header in - switch header { - case .none: - return - case .notification(let info): - userView.headerIconImageView.image = info.iconImage - userView.headerIconImageView.tintColor = info.iconImageTintColor - userView.headerTextLabel.setupAttributes(style: UserView.headerTextLabelStyle) - userView.headerTextLabel.configure(content: info.textMetaContent) - userView.setHeaderDisplay() - } - } - .store(in: &disposeBag) - // name - $name - .sink { content in - guard let content = content else { - userView.nameLabel.reset() - return - } - userView.nameLabel.configure(content: content) - } - .store(in: &disposeBag) - // username - $username - .map { username in - return username.flatMap { "@\($0)" } ?? " " - } - .assign(to: \.text, on: userView.usernameLabel) - .store(in: &disposeBag) - // protected - $protected - .map { !$0 } - .assign(to: \.isHidden, on: userView.lockImageView) - .store(in: &disposeBag) - // follower count - $followerCount - .sink { followerCount in - let count = followerCount.flatMap { String($0) } ?? "-" - userView.followerCountLabel.text = L10n.Common.Controls.ProfileDashboard.followers + ": " + count - } - .store(in: &disposeBag) - // relationship - relationshipViewModel.$optionSet - .map { $0?.relationship(except: [.muting]) } - .sink { relationship in - guard let relationship = relationship else { return } - userView.friendshipButton.configure(relationship: relationship) - userView.friendshipButton.isHidden = relationship == .isMyself - } - .store(in: &disposeBag) + } + + var isSeparateLineDisplay: Bool { + switch kind { + case .notification: return false + default: return true + } + } +} +//extension UserView.ViewModel { +// public func bind(userView: UserView) { +// // avatar +// $avatarImageURL +// .sink { url in +// let configuration = AvatarImageView.Configuration(url: url) +// userView.authorProfileAvatarView.avatarButton.avatarImageView.configure(configuration: configuration) +// } +// .store(in: &disposeBag) +// Publishers.CombineLatest( +// $avatarBadge, +// $platform +// ) +// .sink { avatarBadge, platform in +// switch avatarBadge { +// case .none: +// userView.authorProfileAvatarView.badge = .none +// case .platform: +// userView.authorProfileAvatarView.badge = { +// switch platform { +// case .none: return .none +// case .twitter: return .circle(.twitter) +// case .mastodon: return .circle(.mastodon) +// } +// }() +// case .user: +// userView.authorProfileAvatarView.badge = .none +// } +// } +// .store(in: &disposeBag) +// // badge +// // TODO: +// // header +// $header +// .sink { header in +// switch header { +// case .none: +// return +// case .notification(let info): +// userView.headerIconImageView.image = info.iconImage +// userView.headerIconImageView.tintColor = info.iconImageTintColor +// userView.headerTextLabel.setupAttributes(style: UserView.headerTextLabelStyle) +// userView.headerTextLabel.configure(content: info.textMetaContent) +// userView.setHeaderDisplay() +// } +// } +// .store(in: &disposeBag) +// // name +// $name +// .sink { content in +// guard let content = content else { +// userView.nameLabel.reset() +// return +// } +// userView.nameLabel.configure(content: content) +// } +// .store(in: &disposeBag) +// // username +// $username +// .map { username in +// return username.flatMap { "@\($0)" } ?? " " +// } +// .assign(to: \.text, on: userView.usernameLabel) +// .store(in: &disposeBag) +// // protected +// $protected +// .map { !$0 } +// .assign(to: \.isHidden, on: userView.lockImageView) +// .store(in: &disposeBag) +// // follower count +// $followerCount +// .sink { followerCount in +// let count = followerCount.flatMap { String($0) } ?? "-" +// userView.followerCountLabel.text = L10n.Common.Controls.ProfileDashboard.followers + ": " + count +// } +// .store(in: &disposeBag) +// // relationship +// relationshipViewModel.$optionSet +// .map { $0?.relationship(except: [.muting]) } +// .sink { relationship in +// guard let relationship = relationship else { return } +// userView.friendshipButton.configure(relationship: relationship) +// userView.friendshipButton.isHidden = relationship == .isMyself +// } +// .store(in: &disposeBag) +// +// // accessory +// switch userView.style { +// case .account: +// $badgeCount +// .sink { count in +// let count = max(0, min(count, 50)) +// userView.badgeImageView.image = UIImage(systemName: "\(count).circle.fill")?.withRenderingMode(.alwaysTemplate) +// userView.badgeImageView.isHidden = count == 0 +// } +// .store(in: &disposeBag) +// userView.menuButton.showsMenuAsPrimaryAction = true +// userView.menuButton.menu = { +// let children = [ +// UIAction( +// title: L10n.Common.Controls.Actions.signOut, +// image: UIImage(systemName: "person.crop.circle.badge.minus"), +// attributes: .destructive, +// state: .off +// ) { [weak userView] _ in +// guard let userView = userView else { return } +// userView.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): sign out user…") +// userView.delegate?.userView(userView, menuActionDidPressed: .signOut, menuButton: userView.menuButton) +// } +// ] +// return UIMenu(title: "", image: nil, options: [], children: children) +// }() +// +// case .notification: +// $isFollowRequestBusy +// .sink { isFollowRequestBusy in +// userView.acceptFollowRequestButton.isHidden = isFollowRequestBusy +// userView.rejectFollowRequestButton.isHidden = isFollowRequestBusy +// userView.activityIndicatorView.isHidden = !isFollowRequestBusy +// userView.activityIndicatorView.startAnimating() +// } +// .store(in: &disposeBag) +// +// case .listMember: +// userView.menuButton.showsMenuAsPrimaryAction = true +// userView.menuButton.menu = { +// let children = [ +// UIAction( +// title: L10n.Common.Controls.Actions.remove, +// image: UIImage(systemName: "minus.circle"), +// attributes: .destructive, +// state: .off +// ) { [weak userView] _ in +// guard let userView = userView else { return } +// userView.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): remove user…") +// userView.delegate?.userView(userView, menuActionDidPressed: .remove, menuButton: userView.menuButton) +// } +// ] +// return UIMenu(title: "", image: nil, options: [], children: children) +// }() +// $isMyList +// .map { !$0 } +// .assign(to: \.isHidden, on: userView.menuButton) +// .store(in: &disposeBag) +// case .addListMember: +// Publishers.CombineLatest( +// $isListMember, +// $isListMemberCandidate +// ) +// .receive(on: DispatchQueue.main) +// .sink { [weak userView] isMember, isMemberCandidate in +// guard let userView = userView else { return } +// let image = isMember ? UIImage(systemName: "minus.circle") : UIImage(systemName: "plus.circle") +// let tintColor = isMember ? UIColor.systemRed : Asset.Colors.hightLight.color +// userView.membershipButton.setImage(image, for: .normal) +// userView.membershipButton.tintColor = tintColor +// +// userView.membershipButton.alpha = isMemberCandidate ? 0 : 1 +// userView.activityIndicatorView.isHidden = !isMemberCandidate +// userView.activityIndicatorView.startAnimating() +// } +// .store(in: &disposeBag) +// +// default: +// userView.menuButton.showsMenuAsPrimaryAction = true +// userView.menuButton.menu = nil +// } +// } +// +//} - // accessory - switch userView.style { - case .account: - $badgeCount - .sink { count in - let count = max(0, min(count, 50)) - userView.badgeImageView.image = UIImage(systemName: "\(count).circle.fill")?.withRenderingMode(.alwaysTemplate) - userView.badgeImageView.isHidden = count == 0 - } - .store(in: &disposeBag) - userView.menuButton.showsMenuAsPrimaryAction = true - userView.menuButton.menu = { - let children = [ - UIAction( - title: L10n.Common.Controls.Actions.signOut, - image: UIImage(systemName: "person.crop.circle.badge.minus"), - attributes: .destructive, - state: .off - ) { [weak userView] _ in - guard let userView = userView else { return } - userView.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): sign out user…") - userView.delegate?.userView(userView, menuActionDidPressed: .signOut, menuButton: userView.menuButton) - } - ] - return UIMenu(title: "", image: nil, options: [], children: children) - }() - - case .notification: - $isFollowRequestBusy - .sink { isFollowRequestBusy in - userView.acceptFollowRequestButton.isHidden = isFollowRequestBusy - userView.rejectFollowRequestButton.isHidden = isFollowRequestBusy - userView.activityIndicatorView.isHidden = !isFollowRequestBusy - userView.activityIndicatorView.startAnimating() - } - .store(in: &disposeBag) - - case .listMember: - userView.menuButton.showsMenuAsPrimaryAction = true - userView.menuButton.menu = { - let children = [ - UIAction( - title: L10n.Common.Controls.Actions.remove, - image: UIImage(systemName: "minus.circle"), - attributes: .destructive, - state: .off - ) { [weak userView] _ in - guard let userView = userView else { return } - userView.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): remove user…") - userView.delegate?.userView(userView, menuActionDidPressed: .remove, menuButton: userView.menuButton) - } - ] - return UIMenu(title: "", image: nil, options: [], children: children) - }() - $isMyList - .map { !$0 } - .assign(to: \.isHidden, on: userView.menuButton) - .store(in: &disposeBag) - case .addListMember: - Publishers.CombineLatest( - $isListMember, - $isListMemberCandidate +extension UserView.ViewModel { + public convenience init?( + user: UserObject?, + authContext: AuthContext?, + kind: Kind, + delegate: UserViewDelegate? + ) { + switch user { + case .twitter(let object): + self.init( + user: object, + authContext: authContext, + kind: kind, + delegate: delegate + ) + case .mastodon(let object): + self.init( + user: object, + authContext: authContext, + kind: kind, + delegate: delegate ) - .receive(on: DispatchQueue.main) - .sink { [weak userView] isMember, isMemberCandidate in - guard let userView = userView else { return } - let image = isMember ? UIImage(systemName: "minus.circle") : UIImage(systemName: "plus.circle") - let tintColor = isMember ? UIColor.systemRed : Asset.Colors.hightLight.color - userView.membershipButton.setImage(image, for: .normal) - userView.membershipButton.tintColor = tintColor - - userView.membershipButton.alpha = isMemberCandidate ? 0 : 1 - userView.activityIndicatorView.isHidden = !isMemberCandidate - userView.activityIndicatorView.startAnimating() - } - .store(in: &disposeBag) - default: - userView.menuButton.showsMenuAsPrimaryAction = true - userView.menuButton.menu = nil + return nil } + // end init + } +} + +extension UserView.ViewModel { + public convenience init( + user: TwitterUser, + authContext: AuthContext?, + kind: Kind, + delegate: UserViewDelegate? + ) { + self.init( + object: .twitter(object: user), + authContext: authContext, + kind: kind, + delegate: delegate + ) + // end init + + // user + user.publisher(for: \.profileImageURL) + .map { _ in user.avatarImageURL() } + .assign(to: &$avatarURL) + user.publisher(for: \.name) + .map { PlaintextMetaContent(string: $0) } + .assign(to: &$name) + user.publisher(for: \.username) + .assign(to: &$username) } + public convenience init( + user: MastodonUser, + authContext: AuthContext?, + kind: Kind, + delegate: UserViewDelegate? + ) { + self.init( + object: .mastodon(object: user), + authContext: authContext, + kind: kind, + delegate: delegate + ) + // end init + + // user + user.publisher(for: \.avatar) + .compactMap { $0.flatMap { URL(string: $0) } } + .assign(to: &$avatarURL) + user.publisher(for: \.displayName) + .compactMap { _ in user.nameMetaContent } + .assign(to: &$name) + user.publisher(for: \.username) + .map { _ in user.acctWithDomain } + .assign(to: &$username) + } } diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift index 420f7661..5b1ac1d6 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift @@ -12,578 +12,799 @@ import Combine import MetaTextKit import MetaLabel import TwidereCore +import Kingfisher -protocol UserViewDelegate: AnyObject { - // func userView(_ userView: UserView, authorAvatarButtonDidPressed button: AvatarButton) - func userView(_ userView: UserView, menuActionDidPressed action: UserView.MenuAction, menuButton button: UIButton) - func userView(_ userView: UserView, friendshipButtonDidPressed button: UIButton) - func userView(_ userView: UserView, membershipButtonDidPressed button: UIButton) - func userView(_ userView: UserView, acceptFollowReqeustButtonDidPressed button: UIButton) - func userView(_ userView: UserView, rejectFollowReqeustButtonDidPressed button: UIButton) +public protocol UserViewDelegate: AnyObject { + func userView(_ viewModel: UserView.ViewModel, userAvatarButtonDidPressed user: UserRecord) + func userView(_ viewModel: UserView.ViewModel, menuActionDidPressed action: UserView.ViewModel.MenuAction) +// func userView(_ userView: UserView, friendshipButtonDidPressed button: UIButton) +// func userView(_ userView: UserView, membershipButtonDidPressed button: UIButton) + func userView(_ viewModel: UserView.ViewModel, followReqeustButtonDidPressed user: UserRecord, accept: Bool) } -public final class UserView: UIView { +public struct UserView: View { - let logger = Logger(subsystem: "UserView", category: "UI") + @ObservedObject public private(set) var viewModel: ViewModel - public static let avatarImageViewSize = CGSize(width: 44, height: 44) - - private var _disposeBag = Set() // which lifetime same to view scope - var disposeBag = Set() // clear when reuse - - weak var delegate: UserViewDelegate? - - private(set) var style: Style? - - public private(set) lazy var viewModel: ViewModel = { - let viewModel = ViewModel() - viewModel.bind(userView: self) - return viewModel - }() - - // container - public let containerStackView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .vertical - stackView.spacing = 8 - return stackView - }() - - public static var contentStackViewSpacing: CGFloat = 10 - public let contentStackView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .horizontal - stackView.spacing = UserView.contentStackViewSpacing - stackView.alignment = .center - return stackView - }() - - public let infoContainerStackView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .vertical - stackView.distribution = .fillEqually - return stackView - }() - - public let accessoryContainerView: UIStackView = { - let stackView = UIStackView() - stackView.axis = .horizontal - stackView.alignment = .firstBaseline - stackView.spacing = 8 - return stackView - }() - - // header - public let headerContainerView = UIView() - public let headerIconImageView = UIImageView() - public static var headerTextLabelStyle: TextStyle { .statusHeader } - public let headerTextLabel = MetaLabel(style: .statusHeader) - - // avatar - public let authorProfileAvatarView: ProfileAvatarView = { - let profileAvatarView = ProfileAvatarView() - profileAvatarView.setup(dimension: .inline) - return profileAvatarView - }() - - // name - public let nameLabel = MetaLabel(style: .userAuthorName) - - // username - public let usernameLabel = PlainLabel(style: .userAuthorUsername) - - // lock - public let lockImageView: UIImageView = { - let imageView = UIImageView() - imageView.tintColor = .secondaryLabel - imageView.contentMode = .scaleAspectFill - imageView.image = Asset.ObjectTools.lockMiniInline.image.withRenderingMode(.alwaysTemplate) - return imageView - }() - - // followerCount - public let followerCountLabel = PlainLabel(style: .userDescription) - - // friendship control - public let friendshipButton: FriendshipButton = { - let button = FriendshipButton() - button.titleFont = UIFontMetrics(forTextStyle: .headline) - .scaledFont(for: UIFont.systemFont(ofSize: 13, weight: .semibold)) - return button - }() - - // checkmark control - public let checkmarkButton: UIButton = { - let button = UIButton() - button.setImage(UIImage(systemName: "checkmark.circle.fill"), for: .normal) - button.tintColor = Asset.Colors.hightLight.color - return button - }() - - // menu control - public let menuButton: HitTestExpandedButton = { - let button = HitTestExpandedButton() - button.setImage(UIImage(systemName: "ellipsis.circle"), for: .normal) - button.tintColor = Asset.Colors.hightLight.color - return button - }() - - // add/remove control - public let membershipButton: HitTestExpandedButton = { - let button = HitTestExpandedButton() - button.setImage(UIImage(systemName: "plus.circle"), for: .normal) - button.tintColor = Asset.Colors.hightLight.color // FIXME: tint color - return button - }() - - // follow request - public let followRequestControlContainerView = UIStackView() - - public private(set) lazy var acceptFollowRequestButton: HitTestExpandedButton = { - let button = HitTestExpandedButton() - button.setImage(Asset.Indices.checkmarkCircle.image.withRenderingMode(.alwaysTemplate), for: .normal) - button.tintColor = Asset.Colors.hightLight.color // FIXME: tint color - button.addTarget(self, action: #selector(UserView.acceptFollowRequestButtonDidPressed(_:)), for: .touchUpInside) - button.accessibilityLabel = L10n.Common.Notification.FollowRequestAction.approve - return button - }() - - public private(set) lazy var rejectFollowRequestButton: HitTestExpandedButton = { - let button = HitTestExpandedButton() - button.setImage(Asset.Indices.xmarkCircle.image.withRenderingMode(.alwaysTemplate), for: .normal) - button.tintColor = .secondaryLabel - button.addTarget(self, action: #selector(UserView.rejectFollowRequestButtonDidPressed(_:)), for: .touchUpInside) - button.accessibilityLabel = L10n.Common.Notification.FollowRequestAction.deny - return button - }() - - // activity indicator - public let activityIndicatorView: UIActivityIndicatorView = { - let activityIndicatorView = UIActivityIndicatorView(style: .medium) - activityIndicatorView.hidesWhenStopped = true - activityIndicatorView.startAnimating() - return activityIndicatorView - }() - - // badge - public let badgeImageView: UIImageView = { - let imageView = UIImageView() - imageView.contentMode = .scaleAspectFit - imageView.tintColor = .label - return imageView - }() - - public func prepareForReuse() { - disposeBag.removeAll() - viewModel.prepareForReuse() - authorProfileAvatarView.avatarButton.avatarImageView.cancelTask() - Style.prepareForReuse(userView: self) - } - - public override init(frame: CGRect) { - super.init(frame: frame) - _init() - } - - public required init?(coder: NSCoder) { - super.init(coder: coder) - _init() + public init(viewModel: UserView.ViewModel) { + self.viewModel = viewModel } -} - -extension UserView { - public enum MenuAction: Hashable { - case signOut - case remove + public var body: some View { + HStack(alignment: .center, spacing: .zero) { + // avatar + avatarButton + .padding(.trailing, StatusView.hangingAvatarButtonTrailingSpacing) + // info + VStackLayout(alignment: .leading, spacing: .zero) { + headlineView + subheadlineView + } + .frame(alignment: .leading) + Spacer() + // accessory view + accessoryView + } // end HStack + .padding(.vertical, viewModel.verticalMargin) + .overlay { + if viewModel.isSeparateLineDisplay { + HStack(spacing: .zero) { + Color.clear.frame(width: StatusView.hangingAvatarButtonDimension + StatusView.hangingAvatarButtonTrailingSpacing) + VStack(spacing: .zero) { + Spacer() + Divider() + Color.clear.frame(height: 1) + } + } // end HStack + } // end if + } // end .overlay } - } extension UserView { - private func _init() { - containerStackView.translatesAutoresizingMaskIntoConstraints = false - addSubview(containerStackView) - NSLayoutConstraint.activate([ - containerStackView.topAnchor.constraint(equalTo: topAnchor), - containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor), - trailingAnchor.constraint(equalTo: containerStackView.trailingAnchor), - bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor), - ]) - - // container:: V - [ header container | content ] - containerStackView.addArrangedSubview(headerContainerView) - containerStackView.addArrangedSubview(contentStackView) - - // content: H - [ user avatar | info container | accessory container ] - authorProfileAvatarView.translatesAutoresizingMaskIntoConstraints = false - contentStackView.addArrangedSubview(authorProfileAvatarView) - NSLayoutConstraint.activate([ - authorProfileAvatarView.widthAnchor.constraint(equalToConstant: UserView.avatarImageViewSize.width).priority(.required - 1), - authorProfileAvatarView.heightAnchor.constraint(equalToConstant: UserView.avatarImageViewSize.height).priority(.required - 1), - ]) - contentStackView.addArrangedSubview(infoContainerStackView) - contentStackView.addArrangedSubview(accessoryContainerView) - - authorProfileAvatarView.isUserInteractionEnabled = false - nameLabel.isUserInteractionEnabled = false - usernameLabel.isUserInteractionEnabled = false - followerCountLabel.isUserInteractionEnabled = false - - membershipButton.addTarget(self, action: #selector(UserView.membershipButtonDidPressed(_:)), for: .touchUpInside) - - #if DEBUG - nameLabel.configure(content: PlaintextMetaContent(string: "Name")) - usernameLabel.text = "@username" - followerCountLabel.text = "1000 Followers" - #endif + var allowsAvatarButtonHitTesting: Bool { + switch viewModel.kind { + case .account: return false + default: return true + } } - public func setup(style: Style) { - guard self.style == nil else { - assertionFailure("Should only setup once") - return + var avatarButton: some View { + Button { + viewModel.delegate?.userView(viewModel, userAvatarButtonDidPressed: viewModel.user.asRecord) + } label: { + let dimension: CGFloat = StatusView.hangingAvatarButtonDimension + KFImage(viewModel.avatarURL) + .placeholder { progress in + Color(uiColor: .placeholderText) + } + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: dimension, height: dimension) + .clipShape(Circle()) } - self.style = style - style.layout(userView: self) - Style.prepareForReuse(userView: self) - } -} - -extension UserView { - - @objc private func membershipButtonDidPressed(_ sender: UIButton) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - delegate?.userView(self, membershipButtonDidPressed: sender) + .buttonStyle(.borderless) + .allowsHitTesting(allowsAvatarButtonHitTesting) } - @objc private func acceptFollowRequestButtonDidPressed(_ sender: UIButton) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - delegate?.userView(self, acceptFollowReqeustButtonDidPressed: sender) + var nameLabel: some View { + LabelRepresentable( + metaContent: viewModel.name, + textStyle: .statusAuthorName, + setupLabel: { label in + label.setContentHuggingPriority(.defaultHigh, for: .horizontal) + label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + } + ) + .fixedSize(horizontal: false, vertical: true) } - - @objc private func rejectFollowRequestButtonDidPressed(_ sender: UIButton) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - delegate?.userView(self, rejectFollowReqeustButtonDidPressed: sender) + + var usernameLabel: some View { + LabelRepresentable( + metaContent: { + guard !viewModel.username.isEmpty else { return PlaintextMetaContent(string: "") } + return PlaintextMetaContent(string: "@" + viewModel.username) + }(), + textStyle: .statusAuthorUsername, + setupLabel: { label in + label.setContentHuggingPriority(.defaultHigh, for: .horizontal) + label.setContentCompressionResistancePriority(.defaultLow - 100, for: .horizontal) + } + ) + .fixedSize(horizontal: false, vertical: true) } -} + var menuView: some View { + Menu { + switch viewModel.kind { + case .account: + // sign out + Button(role: .destructive) { + viewModel.delegate?.userView(viewModel, menuActionDidPressed: .signOut) + } label: { + Label { + Text(L10n.Common.Controls.Actions.signOut) + } icon: { + Image(systemName: "person.crop.circle.badge.minus") + } + } -extension UserView { - public enum Style { - // headline: name | lock - // subheadline: username - // accessory: [ badge | menu ] - case account - - // headline: name | lock | username - // subheadline: follower count - // accessory: follow button - case relationship - - // headline: name | lock - // subheadline: username - // accessory: action button - case friendship - - // header: notification - // headline: name | lock | username - // subheadline: follower count - // accessory: [ followRquest accept and reject button ] - case notification - - // headline: name | lock - // subheadline: username - // accessory: checkmark button - case mentionPick - - // headline: name | lock | username - // subheadline: follower count - // accessory: membership menu - case listMember - - // headline: name | lock | username - // subheadline: follower count - // accessory: membership button - case addListMember - - public func layout(userView: UserView) { - switch self { - case .account: layoutAccount(userView: userView) - case .relationship: layoutRelationship(userView: userView) - case .friendship: layoutFriendship(userView: userView) - case .notification: layoutNotification(userView: userView) - case .mentionPick: layoutMentionPick(userView: userView) - case .listMember: layoutListMember(userView: userView) - case .addListMember: layoutAddListMember(userView: userView) + default: + EmptyView() } + } label: { + Image(systemName: "ellipsis.circle") + .padding() } - - public static func prepareForReuse(userView: UserView) { - userView.headerContainerView.isHidden = true - userView.followRequestControlContainerView.isHidden = true - } - } -} - -extension UserView.Style { - - // headline: name | lock | username - // subheadline: follower count - private func layoutRelationshipBase(userView: UserView) { - let headlineStackView = UIStackView() - userView.infoContainerStackView.addArrangedSubview(headlineStackView) - headlineStackView.axis = .horizontal - headlineStackView.spacing = 6 - headlineStackView.addArrangedSubview(userView.nameLabel) - userView.lockImageView.translatesAutoresizingMaskIntoConstraints = false - headlineStackView.addArrangedSubview(userView.lockImageView) - NSLayoutConstraint.activate([ - userView.lockImageView.heightAnchor.constraint(equalTo: userView.nameLabel.heightAnchor).priority(.required - 10), - ]) - userView.lockImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) - userView.lockImageView.setContentCompressionResistancePriority(.required - 10, for: .horizontal) - headlineStackView.addArrangedSubview(UIView()) // padding - - userView.infoContainerStackView.addArrangedSubview(userView.usernameLabel) } - // FIXME: update layout - func layoutAccount(userView: UserView) { - let headlineStackView = UIStackView() - userView.infoContainerStackView.addArrangedSubview(headlineStackView) - headlineStackView.axis = .horizontal - headlineStackView.spacing = 6 - headlineStackView.addArrangedSubview(userView.nameLabel) - userView.lockImageView.translatesAutoresizingMaskIntoConstraints = false - headlineStackView.addArrangedSubview(userView.lockImageView) - NSLayoutConstraint.activate([ - userView.lockImageView.heightAnchor.constraint(equalTo: userView.nameLabel.heightAnchor).priority(.required - 10), - ]) - userView.lockImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) - userView.lockImageView.setContentCompressionResistancePriority(.required - 10, for: .horizontal) - headlineStackView.addArrangedSubview(UIView()) // padding - - userView.infoContainerStackView.addArrangedSubview(userView.usernameLabel) - - userView.accessoryContainerView.addArrangedSubview(userView.badgeImageView) - userView.accessoryContainerView.addArrangedSubview(userView.menuButton) - userView.badgeImageView.setContentHuggingPriority(.required - 2, for: .horizontal) - userView.badgeImageView.setContentCompressionResistancePriority(.required - 1, for: .vertical) - userView.menuButton.setContentHuggingPriority(.required - 1, for: .horizontal) - - userView.setNeedsLayout() - } - - // FIXME: update layout - func layoutRelationship(userView: UserView) { - layoutRelationshipBase(userView: userView) - - userView.friendshipButton.translatesAutoresizingMaskIntoConstraints = false - userView.accessoryContainerView.addArrangedSubview(userView.friendshipButton) - NSLayoutConstraint.activate([ -// userView.friendshipButton.heightAnchor.constraint(equalToConstant: 34).priority(.required - 1), - userView.friendshipButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 80).priority(.required - 1), - ]) - userView.friendshipButton.setContentHuggingPriority(.required - 10, for: .horizontal) - userView.friendshipButton.setContentCompressionResistancePriority(.required - 10, for: .horizontal) - - userView.setNeedsLayout() - } - - func layoutFriendship(userView: UserView) { - // headline - let headlineStackView = UIStackView() - userView.infoContainerStackView.addArrangedSubview(headlineStackView) - headlineStackView.axis = .horizontal - headlineStackView.spacing = 6 - headlineStackView.addArrangedSubview(userView.nameLabel) - userView.lockImageView.translatesAutoresizingMaskIntoConstraints = false - headlineStackView.addArrangedSubview(userView.lockImageView) - NSLayoutConstraint.activate([ - userView.lockImageView.heightAnchor.constraint(equalTo: userView.nameLabel.heightAnchor).priority(.required - 10), - ]) - userView.lockImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) - userView.lockImageView.setContentCompressionResistancePriority(.required - 10, for: .horizontal) - headlineStackView.addArrangedSubview(userView.usernameLabel) - headlineStackView.addArrangedSubview(UIView()) // padding - - // subheadline - userView.infoContainerStackView.addArrangedSubview(userView.followerCountLabel) - - // TODO: menu - - userView.setNeedsLayout() + var membershipButton: some View { + Button { +// switch + } label: { +// let systemName = +// Image(systemName: "ellipsis.circle") + } } - func layoutNotification(userView: UserView) { - userView.headerIconImageView.translatesAutoresizingMaskIntoConstraints = false - userView.headerTextLabel.translatesAutoresizingMaskIntoConstraints = false - userView.headerContainerView.addSubview(userView.headerIconImageView) - userView.headerContainerView.addSubview(userView.headerTextLabel) - NSLayoutConstraint.activate([ - userView.headerTextLabel.topAnchor.constraint(equalTo: userView.headerContainerView.topAnchor), - userView.headerTextLabel.bottomAnchor.constraint(equalTo: userView.headerContainerView.bottomAnchor), - userView.headerTextLabel.trailingAnchor.constraint(equalTo: userView.headerContainerView.trailingAnchor), - userView.headerIconImageView.centerYAnchor.constraint(equalTo: userView.headerTextLabel.centerYAnchor), - userView.headerIconImageView.heightAnchor.constraint(equalTo: userView.headerTextLabel.heightAnchor, multiplier: 1.0).priority(.required - 1), - userView.headerIconImageView.widthAnchor.constraint(equalTo: userView.headerIconImageView.heightAnchor, multiplier: 1.0).priority(.required - 1), - userView.headerTextLabel.leadingAnchor.constraint(equalTo: userView.headerIconImageView.trailingAnchor, constant: 4), - // align to author name below - ]) - userView.headerTextLabel.setContentHuggingPriority(.required - 10, for: .vertical) - userView.headerIconImageView.setContentHuggingPriority(.defaultLow, for: .vertical) - userView.headerIconImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) - - layoutRelationshipBase(userView: userView) - - // set header label align to author name - NSLayoutConstraint.activate([ - userView.headerTextLabel.leadingAnchor.constraint(equalTo: userView.authorProfileAvatarView.trailingAnchor, constant: UserView.contentStackViewSpacing), - ]) - - // follow request button - userView.accessoryContainerView.addArrangedSubview(userView.followRequestControlContainerView) - userView.followRequestControlContainerView.axis = .horizontal - userView.followRequestControlContainerView.spacing = 20 - userView.followRequestControlContainerView.isHidden = true - - userView.followRequestControlContainerView.addArrangedSubview(userView.acceptFollowRequestButton) - userView.followRequestControlContainerView.addArrangedSubview(userView.rejectFollowRequestButton) - userView.acceptFollowRequestButton.setContentHuggingPriority(.required - 1, for: .horizontal) - userView.acceptFollowRequestButton.setContentCompressionResistancePriority(.required - 1, for: .horizontal) - userView.rejectFollowRequestButton.setContentHuggingPriority(.required - 1, for: .horizontal) - userView.rejectFollowRequestButton.setContentCompressionResistancePriority(.required - 1, for: .horizontal) - - userView.accessoryContainerView.addArrangedSubview(userView.activityIndicatorView) - userView.activityIndicatorView.setContentHuggingPriority(.required - 1, for: .horizontal) - userView.activityIndicatorView.setContentCompressionResistancePriority(.required - 1, for: .horizontal) - userView.activityIndicatorView.isHidden = true - - userView.setNeedsLayout() + var followRequestActionView: some View { + HStack(spacing: .zero) { + Button { + guard !viewModel.isFollowRequestBusy else { return } + viewModel.delegate?.userView(viewModel, followReqeustButtonDidPressed: viewModel.user.asRecord, accept: true) + } label: { + Image(uiImage: Asset.Indices.checkmarkCircle.image.withRenderingMode(.alwaysTemplate)) + .padding() + } + Button { + guard !viewModel.isFollowRequestBusy else { return } + viewModel.delegate?.userView(viewModel, followReqeustButtonDidPressed: viewModel.user.asRecord, accept: false) + } label: { + Image(uiImage: Asset.Indices.xmarkCircle.image.withRenderingMode(.alwaysTemplate)) + .padding() + } + .tint(.secondary) + } // end HStack + .opacity(viewModel.isFollowRequestBusy ? 0 : 1) + .overlay(alignment: .trailing) { + if viewModel.isFollowRequestBusy { + ProgressView() + .progressViewStyle(.circular) + } + } } +} - func layoutMentionPick(userView: UserView) { - let headlineStackView = UIStackView() - userView.infoContainerStackView.addArrangedSubview(headlineStackView) - headlineStackView.axis = .horizontal - headlineStackView.spacing = 6 - headlineStackView.addArrangedSubview(userView.nameLabel) - userView.lockImageView.translatesAutoresizingMaskIntoConstraints = false - headlineStackView.addArrangedSubview(userView.lockImageView) - NSLayoutConstraint.activate([ - userView.lockImageView.heightAnchor.constraint(equalTo: userView.nameLabel.heightAnchor).priority(.required - 10), - ]) - userView.lockImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) - userView.lockImageView.setContentCompressionResistancePriority(.required - 10, for: .horizontal) - headlineStackView.addArrangedSubview(UIView()) // padding - - - userView.infoContainerStackView.addArrangedSubview(userView.usernameLabel) - - userView.accessoryContainerView.addArrangedSubview(userView.checkmarkButton) - userView.checkmarkButton.setContentHuggingPriority(.required - 1, for: .horizontal) - userView.checkmarkButton.setContentCompressionResistancePriority(.required - 1, for: .horizontal) - - userView.accessoryContainerView.addArrangedSubview(userView.activityIndicatorView) - userView.activityIndicatorView.setContentHuggingPriority(.required - 1, for: .horizontal) - userView.activityIndicatorView.setContentCompressionResistancePriority(.required - 1, for: .horizontal) - userView.activityIndicatorView.isHidden = true - - userView.setNeedsLayout() +extension UserView { + var headlineView: some View { + Group { + switch viewModel.kind { + case .account: + nameLabel + case .relationship: + nameLabel + case .friendship: + nameLabel + case .notification: + nameLabel + case .mentionPick: + nameLabel + case .listMember: + nameLabel + case .addListMember: + nameLabel + } + } // end Group } - func layoutAddListMember(userView: UserView) { - layoutRelationshipBase(userView: userView) - - userView.membershipButton.translatesAutoresizingMaskIntoConstraints = false - userView.accessoryContainerView.addArrangedSubview(userView.membershipButton) - NSLayoutConstraint.activate([ - userView.membershipButton.widthAnchor.constraint(equalToConstant: 44), - userView.membershipButton.heightAnchor.constraint(equalToConstant: 44).priority(.required - 1), - ]) - userView.membershipButton.setContentHuggingPriority(.required - 1, for: .horizontal) - userView.membershipButton.setContentCompressionResistancePriority(.required - 1, for: .horizontal) - - userView.activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false - userView.accessoryContainerView.addSubview(userView.activityIndicatorView) - NSLayoutConstraint.activate([ - userView.activityIndicatorView.centerXAnchor.constraint(equalTo: userView.membershipButton.centerXAnchor), - userView.activityIndicatorView.centerYAnchor.constraint(equalTo: userView.membershipButton.centerYAnchor), - ]) - userView.activityIndicatorView.isHidden = true + var subheadlineView: some View { + Group { + switch viewModel.kind { + case .account: + usernameLabel + case .relationship: + usernameLabel + case .friendship: + usernameLabel + case .notification: + usernameLabel + case .mentionPick: + usernameLabel + case .listMember: + usernameLabel + case .addListMember: + usernameLabel + } + } // end Group } - func layoutListMember(userView: UserView) { - layoutRelationshipBase(userView: userView) - - userView.menuButton.translatesAutoresizingMaskIntoConstraints = false - userView.accessoryContainerView.addArrangedSubview(userView.menuButton) - userView.menuButton.setContentHuggingPriority(.required - 1, for: .horizontal) - userView.menuButton.setContentCompressionResistancePriority(.required - 1, for: .horizontal) + var accessoryView: some View { + Group { + switch viewModel.kind { + case .account: + menuView + case .relationship: + EmptyView() + case .friendship: + EmptyView() + case .notification: + if viewModel.isFollowRequestActionDisplay { + followRequestActionView + } + case .mentionPick: + EmptyView() + case .listMember: + EmptyView() + case .addListMember: + EmptyView() + } + } // end Group } - } -extension UserView { - public func setHeaderDisplay() { - headerContainerView.isHidden = false - } - - public func setFollowRequestControlDisplay() { - followRequestControlContainerView.isHidden = false - } -} +//public final class UserView: UIView { +// +// let logger = Logger(subsystem: "UserView", category: "UI") +// +// public static let avatarImageViewSize = CGSize(width: 44, height: 44) +// +// private var _disposeBag = Set() // which lifetime same to view scope +// var disposeBag = Set() // clear when reuse +// +// weak var delegate: UserViewDelegate? +// +// private(set) var style: Style? +// +// public private(set) lazy var viewModel: ViewModel = { +// let viewModel = ViewModel() +// viewModel.bind(userView: self) +// return viewModel +// }() +// +// // container +// public let containerStackView: UIStackView = { +// let stackView = UIStackView() +// stackView.axis = .vertical +// stackView.spacing = 8 +// return stackView +// }() +// +// public static var contentStackViewSpacing: CGFloat = 10 +// public let contentStackView: UIStackView = { +// let stackView = UIStackView() +// stackView.axis = .horizontal +// stackView.spacing = UserView.contentStackViewSpacing +// stackView.alignment = .center +// return stackView +// }() +// +// public let infoContainerStackView: UIStackView = { +// let stackView = UIStackView() +// stackView.axis = .vertical +// stackView.distribution = .fillEqually +// return stackView +// }() +// +// public let accessoryContainerView: UIStackView = { +// let stackView = UIStackView() +// stackView.axis = .horizontal +// stackView.alignment = .firstBaseline +// stackView.spacing = 8 +// return stackView +// }() +// +// // header +// public let headerContainerView = UIView() +// public let headerIconImageView = UIImageView() +// public static var headerTextLabelStyle: TextStyle { .statusHeader } +// public let headerTextLabel = MetaLabel(style: .statusHeader) +// +// // avatar +// public let authorProfileAvatarView: ProfileAvatarView = { +// let profileAvatarView = ProfileAvatarView() +// profileAvatarView.setup(dimension: .inline) +// return profileAvatarView +// }() +// +// // name +// public let nameLabel = MetaLabel(style: .userAuthorName) +// +// // username +// public let usernameLabel = PlainLabel(style: .userAuthorUsername) +// +// // lock +// public let lockImageView: UIImageView = { +// let imageView = UIImageView() +// imageView.tintColor = .secondaryLabel +// imageView.contentMode = .scaleAspectFill +// imageView.image = Asset.ObjectTools.lockMiniInline.image.withRenderingMode(.alwaysTemplate) +// return imageView +// }() +// +// // followerCount +// public let followerCountLabel = PlainLabel(style: .userDescription) +// +// // friendship control +// public let friendshipButton: FriendshipButton = { +// let button = FriendshipButton() +// button.titleFont = UIFontMetrics(forTextStyle: .headline) +// .scaledFont(for: UIFont.systemFont(ofSize: 13, weight: .semibold)) +// return button +// }() +// +// // checkmark control +// public let checkmarkButton: UIButton = { +// let button = UIButton() +// button.setImage(UIImage(systemName: "checkmark.circle.fill"), for: .normal) +// button.tintColor = Asset.Colors.hightLight.color +// return button +// }() +// +// // menu control +// public let menuButton: HitTestExpandedButton = { +// let button = HitTestExpandedButton() +// button.setImage(UIImage(systemName: "ellipsis.circle"), for: .normal) +// button.tintColor = Asset.Colors.hightLight.color +// return button +// }() +// +// // add/remove control +// public let membershipButton: HitTestExpandedButton = { +// let button = HitTestExpandedButton() +// button.setImage(UIImage(systemName: "plus.circle"), for: .normal) +// button.tintColor = Asset.Colors.hightLight.color // FIXME: tint color +// return button +// }() +// +// // follow request +// public let followRequestControlContainerView = UIStackView() +// +// public private(set) lazy var acceptFollowRequestButton: HitTestExpandedButton = { +// let button = HitTestExpandedButton() +// button.setImage(Asset.Indices.checkmarkCircle.image.withRenderingMode(.alwaysTemplate), for: .normal) +// button.tintColor = Asset.Colors.hightLight.color // FIXME: tint color +// button.addTarget(self, action: #selector(UserView.acceptFollowRequestButtonDidPressed(_:)), for: .touchUpInside) +// button.accessibilityLabel = L10n.Common.Notification.FollowRequestAction.approve +// return button +// }() +// +// public private(set) lazy var rejectFollowRequestButton: HitTestExpandedButton = { +// let button = HitTestExpandedButton() +// button.setImage(Asset.Indices.xmarkCircle.image.withRenderingMode(.alwaysTemplate), for: .normal) +// button.tintColor = .secondaryLabel +// button.addTarget(self, action: #selector(UserView.rejectFollowRequestButtonDidPressed(_:)), for: .touchUpInside) +// button.accessibilityLabel = L10n.Common.Notification.FollowRequestAction.deny +// return button +// }() +// +// // activity indicator +// public let activityIndicatorView: UIActivityIndicatorView = { +// let activityIndicatorView = UIActivityIndicatorView(style: .medium) +// activityIndicatorView.hidesWhenStopped = true +// activityIndicatorView.startAnimating() +// return activityIndicatorView +// }() +// +// // badge +// public let badgeImageView: UIImageView = { +// let imageView = UIImageView() +// imageView.contentMode = .scaleAspectFit +// imageView.tintColor = .label +// return imageView +// }() +// +// public func prepareForReuse() { +// disposeBag.removeAll() +// viewModel.prepareForReuse() +// authorProfileAvatarView.avatarButton.avatarImageView.cancelTask() +// Style.prepareForReuse(userView: self) +// } +// +// public override init(frame: CGRect) { +// super.init(frame: frame) +// _init() +// } +// +// public required init?(coder: NSCoder) { +// super.init(coder: coder) +// _init() +// } +// +//} +// +//extension UserView { +// public enum MenuAction: Hashable { +// case signOut +// case remove +// } +// +//} + +//extension UserView { +// private func _init() { +// containerStackView.translatesAutoresizingMaskIntoConstraints = false +// addSubview(containerStackView) +// NSLayoutConstraint.activate([ +// containerStackView.topAnchor.constraint(equalTo: topAnchor), +// containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor), +// trailingAnchor.constraint(equalTo: containerStackView.trailingAnchor), +// bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor), +// ]) +// +// // container:: V - [ header container | content ] +// containerStackView.addArrangedSubview(headerContainerView) +// containerStackView.addArrangedSubview(contentStackView) +// +// // content: H - [ user avatar | info container | accessory container ] +// authorProfileAvatarView.translatesAutoresizingMaskIntoConstraints = false +// contentStackView.addArrangedSubview(authorProfileAvatarView) +// NSLayoutConstraint.activate([ +// authorProfileAvatarView.widthAnchor.constraint(equalToConstant: UserView.avatarImageViewSize.width).priority(.required - 1), +// authorProfileAvatarView.heightAnchor.constraint(equalToConstant: UserView.avatarImageViewSize.height).priority(.required - 1), +// ]) +// contentStackView.addArrangedSubview(infoContainerStackView) +// contentStackView.addArrangedSubview(accessoryContainerView) +// +// authorProfileAvatarView.isUserInteractionEnabled = false +// nameLabel.isUserInteractionEnabled = false +// usernameLabel.isUserInteractionEnabled = false +// followerCountLabel.isUserInteractionEnabled = false +// +// membershipButton.addTarget(self, action: #selector(UserView.membershipButtonDidPressed(_:)), for: .touchUpInside) +// +// #if DEBUG +// nameLabel.configure(content: PlaintextMetaContent(string: "Name")) +// usernameLabel.text = "@username" +// followerCountLabel.text = "1000 Followers" +// #endif +// } +// +// public func setup(style: Style) { +// guard self.style == nil else { +// assertionFailure("Should only setup once") +// return +// } +// self.style = style +// style.layout(userView: self) +// Style.prepareForReuse(userView: self) +// } +//} +// +//extension UserView { +// +// @objc private func membershipButtonDidPressed(_ sender: UIButton) { +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") +// delegate?.userView(self, membershipButtonDidPressed: sender) +// } +// +// @objc private func acceptFollowRequestButtonDidPressed(_ sender: UIButton) { +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") +// delegate?.userView(self, acceptFollowReqeustButtonDidPressed: sender) +// } +// +// @objc private func rejectFollowRequestButtonDidPressed(_ sender: UIButton) { +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") +// delegate?.userView(self, rejectFollowReqeustButtonDidPressed: sender) +// } +// +//} +// +//extension UserView { +// public enum Style { +// // headline: name | lock +// // subheadline: username +// // accessory: [ badge | menu ] +// case account +// +// // headline: name | lock | username +// // subheadline: follower count +// // accessory: follow button +// case relationship +// +// // headline: name | lock +// // subheadline: username +// // accessory: action button +// case friendship +// +// // header: notification +// // headline: name | lock | username +// // subheadline: follower count +// // accessory: [ followRquest accept and reject button ] +// case notification +// +// // headline: name | lock +// // subheadline: username +// // accessory: checkmark button +// case mentionPick +// +// // headline: name | lock | username +// // subheadline: follower count +// // accessory: membership menu +// case listMember +// +// // headline: name | lock | username +// // subheadline: follower count +// // accessory: membership button +// case addListMember +// +// public func layout(userView: UserView) { +// switch self { +// case .account: layoutAccount(userView: userView) +// case .relationship: layoutRelationship(userView: userView) +// case .friendship: layoutFriendship(userView: userView) +// case .notification: layoutNotification(userView: userView) +// case .mentionPick: layoutMentionPick(userView: userView) +// case .listMember: layoutListMember(userView: userView) +// case .addListMember: layoutAddListMember(userView: userView) +// } +// } +// +// public static func prepareForReuse(userView: UserView) { +// userView.headerContainerView.isHidden = true +// userView.followRequestControlContainerView.isHidden = true +// } +// } +//} +// +//extension UserView.Style { +// +// // headline: name | lock | username +// // subheadline: follower count +// private func layoutRelationshipBase(userView: UserView) { +// let headlineStackView = UIStackView() +// userView.infoContainerStackView.addArrangedSubview(headlineStackView) +// headlineStackView.axis = .horizontal +// headlineStackView.spacing = 6 +// headlineStackView.addArrangedSubview(userView.nameLabel) +// userView.lockImageView.translatesAutoresizingMaskIntoConstraints = false +// headlineStackView.addArrangedSubview(userView.lockImageView) +// NSLayoutConstraint.activate([ +// userView.lockImageView.heightAnchor.constraint(equalTo: userView.nameLabel.heightAnchor).priority(.required - 10), +// ]) +// userView.lockImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) +// userView.lockImageView.setContentCompressionResistancePriority(.required - 10, for: .horizontal) +// headlineStackView.addArrangedSubview(UIView()) // padding +// +// userView.infoContainerStackView.addArrangedSubview(userView.usernameLabel) +// } +// +// // FIXME: update layout +// func layoutAccount(userView: UserView) { +// let headlineStackView = UIStackView() +// userView.infoContainerStackView.addArrangedSubview(headlineStackView) +// headlineStackView.axis = .horizontal +// headlineStackView.spacing = 6 +// headlineStackView.addArrangedSubview(userView.nameLabel) +// userView.lockImageView.translatesAutoresizingMaskIntoConstraints = false +// headlineStackView.addArrangedSubview(userView.lockImageView) +// NSLayoutConstraint.activate([ +// userView.lockImageView.heightAnchor.constraint(equalTo: userView.nameLabel.heightAnchor).priority(.required - 10), +// ]) +// userView.lockImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) +// userView.lockImageView.setContentCompressionResistancePriority(.required - 10, for: .horizontal) +// headlineStackView.addArrangedSubview(UIView()) // padding +// +// userView.infoContainerStackView.addArrangedSubview(userView.usernameLabel) +// +// userView.accessoryContainerView.addArrangedSubview(userView.badgeImageView) +// userView.accessoryContainerView.addArrangedSubview(userView.menuButton) +// userView.badgeImageView.setContentHuggingPriority(.required - 2, for: .horizontal) +// userView.badgeImageView.setContentCompressionResistancePriority(.required - 1, for: .vertical) +// userView.menuButton.setContentHuggingPriority(.required - 1, for: .horizontal) +// +// userView.setNeedsLayout() +// } +// +// // FIXME: update layout +// func layoutRelationship(userView: UserView) { +// layoutRelationshipBase(userView: userView) +// +// userView.friendshipButton.translatesAutoresizingMaskIntoConstraints = false +// userView.accessoryContainerView.addArrangedSubview(userView.friendshipButton) +// NSLayoutConstraint.activate([ +//// userView.friendshipButton.heightAnchor.constraint(equalToConstant: 34).priority(.required - 1), +// userView.friendshipButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 80).priority(.required - 1), +// ]) +// userView.friendshipButton.setContentHuggingPriority(.required - 10, for: .horizontal) +// userView.friendshipButton.setContentCompressionResistancePriority(.required - 10, for: .horizontal) +// +// userView.setNeedsLayout() +// } +// +// func layoutFriendship(userView: UserView) { +// // headline +// let headlineStackView = UIStackView() +// userView.infoContainerStackView.addArrangedSubview(headlineStackView) +// headlineStackView.axis = .horizontal +// headlineStackView.spacing = 6 +// headlineStackView.addArrangedSubview(userView.nameLabel) +// userView.lockImageView.translatesAutoresizingMaskIntoConstraints = false +// headlineStackView.addArrangedSubview(userView.lockImageView) +// NSLayoutConstraint.activate([ +// userView.lockImageView.heightAnchor.constraint(equalTo: userView.nameLabel.heightAnchor).priority(.required - 10), +// ]) +// userView.lockImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) +// userView.lockImageView.setContentCompressionResistancePriority(.required - 10, for: .horizontal) +// headlineStackView.addArrangedSubview(userView.usernameLabel) +// headlineStackView.addArrangedSubview(UIView()) // padding +// +// // subheadline +// userView.infoContainerStackView.addArrangedSubview(userView.followerCountLabel) +// +// // TODO: menu +// +// userView.setNeedsLayout() +// } +// +// func layoutNotification(userView: UserView) { +// userView.headerIconImageView.translatesAutoresizingMaskIntoConstraints = false +// userView.headerTextLabel.translatesAutoresizingMaskIntoConstraints = false +// userView.headerContainerView.addSubview(userView.headerIconImageView) +// userView.headerContainerView.addSubview(userView.headerTextLabel) +// NSLayoutConstraint.activate([ +// userView.headerTextLabel.topAnchor.constraint(equalTo: userView.headerContainerView.topAnchor), +// userView.headerTextLabel.bottomAnchor.constraint(equalTo: userView.headerContainerView.bottomAnchor), +// userView.headerTextLabel.trailingAnchor.constraint(equalTo: userView.headerContainerView.trailingAnchor), +// userView.headerIconImageView.centerYAnchor.constraint(equalTo: userView.headerTextLabel.centerYAnchor), +// userView.headerIconImageView.heightAnchor.constraint(equalTo: userView.headerTextLabel.heightAnchor, multiplier: 1.0).priority(.required - 1), +// userView.headerIconImageView.widthAnchor.constraint(equalTo: userView.headerIconImageView.heightAnchor, multiplier: 1.0).priority(.required - 1), +// userView.headerTextLabel.leadingAnchor.constraint(equalTo: userView.headerIconImageView.trailingAnchor, constant: 4), +// // align to author name below +// ]) +// userView.headerTextLabel.setContentHuggingPriority(.required - 10, for: .vertical) +// userView.headerIconImageView.setContentHuggingPriority(.defaultLow, for: .vertical) +// userView.headerIconImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) +// +// layoutRelationshipBase(userView: userView) +// +// // set header label align to author name +// NSLayoutConstraint.activate([ +// userView.headerTextLabel.leadingAnchor.constraint(equalTo: userView.authorProfileAvatarView.trailingAnchor, constant: UserView.contentStackViewSpacing), +// ]) +// +// // follow request button +// userView.accessoryContainerView.addArrangedSubview(userView.followRequestControlContainerView) +// userView.followRequestControlContainerView.axis = .horizontal +// userView.followRequestControlContainerView.spacing = 20 +// userView.followRequestControlContainerView.isHidden = true +// +// userView.followRequestControlContainerView.addArrangedSubview(userView.acceptFollowRequestButton) +// userView.followRequestControlContainerView.addArrangedSubview(userView.rejectFollowRequestButton) +// userView.acceptFollowRequestButton.setContentHuggingPriority(.required - 1, for: .horizontal) +// userView.acceptFollowRequestButton.setContentCompressionResistancePriority(.required - 1, for: .horizontal) +// userView.rejectFollowRequestButton.setContentHuggingPriority(.required - 1, for: .horizontal) +// userView.rejectFollowRequestButton.setContentCompressionResistancePriority(.required - 1, for: .horizontal) +// +// userView.accessoryContainerView.addArrangedSubview(userView.activityIndicatorView) +// userView.activityIndicatorView.setContentHuggingPriority(.required - 1, for: .horizontal) +// userView.activityIndicatorView.setContentCompressionResistancePriority(.required - 1, for: .horizontal) +// userView.activityIndicatorView.isHidden = true +// +// userView.setNeedsLayout() +// } +// +// func layoutMentionPick(userView: UserView) { +// let headlineStackView = UIStackView() +// userView.infoContainerStackView.addArrangedSubview(headlineStackView) +// headlineStackView.axis = .horizontal +// headlineStackView.spacing = 6 +// headlineStackView.addArrangedSubview(userView.nameLabel) +// userView.lockImageView.translatesAutoresizingMaskIntoConstraints = false +// headlineStackView.addArrangedSubview(userView.lockImageView) +// NSLayoutConstraint.activate([ +// userView.lockImageView.heightAnchor.constraint(equalTo: userView.nameLabel.heightAnchor).priority(.required - 10), +// ]) +// userView.lockImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) +// userView.lockImageView.setContentCompressionResistancePriority(.required - 10, for: .horizontal) +// headlineStackView.addArrangedSubview(UIView()) // padding +// +// +// userView.infoContainerStackView.addArrangedSubview(userView.usernameLabel) +// +// userView.accessoryContainerView.addArrangedSubview(userView.checkmarkButton) +// userView.checkmarkButton.setContentHuggingPriority(.required - 1, for: .horizontal) +// userView.checkmarkButton.setContentCompressionResistancePriority(.required - 1, for: .horizontal) +// +// userView.accessoryContainerView.addArrangedSubview(userView.activityIndicatorView) +// userView.activityIndicatorView.setContentHuggingPriority(.required - 1, for: .horizontal) +// userView.activityIndicatorView.setContentCompressionResistancePriority(.required - 1, for: .horizontal) +// userView.activityIndicatorView.isHidden = true +// +// userView.setNeedsLayout() +// } +// +// func layoutAddListMember(userView: UserView) { +// layoutRelationshipBase(userView: userView) +// +// userView.membershipButton.translatesAutoresizingMaskIntoConstraints = false +// userView.accessoryContainerView.addArrangedSubview(userView.membershipButton) +// NSLayoutConstraint.activate([ +// userView.membershipButton.widthAnchor.constraint(equalToConstant: 44), +// userView.membershipButton.heightAnchor.constraint(equalToConstant: 44).priority(.required - 1), +// ]) +// userView.membershipButton.setContentHuggingPriority(.required - 1, for: .horizontal) +// userView.membershipButton.setContentCompressionResistancePriority(.required - 1, for: .horizontal) +// +// userView.activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false +// userView.accessoryContainerView.addSubview(userView.activityIndicatorView) +// NSLayoutConstraint.activate([ +// userView.activityIndicatorView.centerXAnchor.constraint(equalTo: userView.membershipButton.centerXAnchor), +// userView.activityIndicatorView.centerYAnchor.constraint(equalTo: userView.membershipButton.centerYAnchor), +// ]) +// userView.activityIndicatorView.isHidden = true +// } +// +// func layoutListMember(userView: UserView) { +// layoutRelationshipBase(userView: userView) +// +// userView.menuButton.translatesAutoresizingMaskIntoConstraints = false +// userView.accessoryContainerView.addArrangedSubview(userView.menuButton) +// userView.menuButton.setContentHuggingPriority(.required - 1, for: .horizontal) +// userView.menuButton.setContentCompressionResistancePriority(.required - 1, for: .horizontal) +// } +// +//} +// +//extension UserView { +// public func setHeaderDisplay() { +// headerContainerView.isHidden = false +// } +// +// public func setFollowRequestControlDisplay() { +// followRequestControlContainerView.isHidden = false +// } +//} #if DEBUG import SwiftUI struct UserView_Preview: PreviewProvider { static var previews: some View { - Group { - UIViewPreview { - let userView = UserView() - userView.setup(style: .account) - return userView - } - .previewLayout(.fixed(width: 375, height: 48)) - .previewDisplayName("Account") - UIViewPreview { - let userView = UserView() - userView.setup(style: .relationship) - return userView - } - .previewLayout(.fixed(width: 375, height: 48)) - .previewDisplayName("Relationship") - UIViewPreview { - let userView = UserView() - userView.setup(style: .friendship) - return userView - } - .previewLayout(.fixed(width: 375, height: 48)) - .previewDisplayName("Friendship") - UIViewPreview { - let userView = UserView() - userView.setup(style: .notification) - return userView - } - .previewLayout(.fixed(width: 375, height: 48)) - .previewDisplayName("Notification") - UIViewPreview { - let userView = UserView() - userView.setup(style: .mentionPick) - return userView - } - .previewLayout(.fixed(width: 375, height: 48)) - .previewDisplayName("MentionPick") - UIViewPreview { - let userView = UserView() - userView.setup(style: .addListMember) - return userView - } - .previewLayout(.fixed(width: 375, height: 48)) - .previewDisplayName("AddListMember") - } + EmptyView() +// Group { +// UIViewPreview { +// let userView = UserView() +// userView.setup(style: .account) +// return userView +// } +// .previewLayout(.fixed(width: 375, height: 48)) +// .previewDisplayName("Account") +// UIViewPreview { +// let userView = UserView() +// userView.setup(style: .relationship) +// return userView +// } +// .previewLayout(.fixed(width: 375, height: 48)) +// .previewDisplayName("Relationship") +// UIViewPreview { +// let userView = UserView() +// userView.setup(style: .friendship) +// return userView +// } +// .previewLayout(.fixed(width: 375, height: 48)) +// .previewDisplayName("Friendship") +// UIViewPreview { +// let userView = UserView() +// userView.setup(style: .notification) +// return userView +// } +// .previewLayout(.fixed(width: 375, height: 48)) +// .previewDisplayName("Notification") +// UIViewPreview { +// let userView = UserView() +// userView.setup(style: .mentionPick) +// return userView +// } +// .previewLayout(.fixed(width: 375, height: 48)) +// .previewDisplayName("MentionPick") +// UIViewPreview { +// let userView = UserView() +// userView.setup(style: .addListMember) +// return userView +// } +// .previewLayout(.fixed(width: 375, height: 48)) +// .previewDisplayName("AddListMember") +// } } } #endif diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift index 9344ddcc..33680088 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift @@ -397,19 +397,6 @@ extension ComposeContentView { } -private extension ComposeContentView { - struct SizeDimensionPreferenceKey: PreferenceKey { - static let defaultValue: CGFloat = 0 - - static func reduce( - value: inout CGFloat, - nextValue: () -> CGFloat - ) { - value = max(value, nextValue()) - } - } -} - // MARK: - TypeIdentifiedItemProvider extension PollComposeItem.Option: TypeIdentifiedItemProvider { public static var typeIdentifier: String { diff --git a/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewController.swift b/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewController.swift index e5549b8c..9d9cd87d 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewController.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewController.swift @@ -49,12 +49,12 @@ extension MentionPickViewController { ]) tableView.delegate = self - viewModel.setupDiffableDataSource( - for: tableView, - configuration: MentionPickViewModel.DataSourceConfiguration( - userTableViewCellDelegate: self - ) - ) +// viewModel.setupDiffableDataSource( +// for: tableView, +// configuration: MentionPickViewModel.DataSourceConfiguration( +// userTableViewCellDelegate: self +// ) +// ) } } @@ -100,24 +100,24 @@ extension MentionPickViewController: UITableViewDelegate { } // MARK: - UserTableViewCellDelegate -extension MentionPickViewController: UserViewTableViewCellDelegate { - public func tableViewCell(_ cell: UITableViewCell, userView: UserView, menuActionDidPressed action: UserView.MenuAction, menuButton button: UIButton) { - // do nothing - } - - public func tableViewCell(_ cell: UITableViewCell, userView: UserView, friendshipButtonDidPressed button: UIButton) { - // do nothing - } - - public func tableViewCell(_ cell: UITableViewCell, userView: UserView, membershipButtonDidPressed button: UIButton) { - // do nothing - } - - public func tableViewCell(_ cell: UITableViewCell, userView: UserView, acceptFollowReqeustButtonDidPressed button: UIButton) { - // do nothing - } - - public func tableViewCell(_ cell: UITableViewCell, userView: UserView, rejectFollowReqeustButtonDidPressed button: UIButton) { - // do nothing - } -} +//extension MentionPickViewController: UserViewTableViewCellDelegate { +// public func tableViewCell(_ cell: UITableViewCell, userView: UserView, menuActionDidPressed action: UserView.MenuAction, menuButton button: UIButton) { +// // do nothing +// } +// +// public func tableViewCell(_ cell: UITableViewCell, userView: UserView, friendshipButtonDidPressed button: UIButton) { +// // do nothing +// } +// +// public func tableViewCell(_ cell: UITableViewCell, userView: UserView, membershipButtonDidPressed button: UIButton) { +// // do nothing +// } +// +// public func tableViewCell(_ cell: UITableViewCell, userView: UserView, acceptFollowReqeustButtonDidPressed button: UIButton) { +// // do nothing +// } +// +// public func tableViewCell(_ cell: UITableViewCell, userView: UserView, rejectFollowReqeustButtonDidPressed button: UIButton) { +// // do nothing +// } +//} diff --git a/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewModel+Diffable.swift b/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewModel+Diffable.swift index 8066caba..556a4942 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewModel+Diffable.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewModel+Diffable.swift @@ -18,56 +18,56 @@ extension MentionPickViewModel { for tableView: UITableView, configuration: DataSourceConfiguration ) { - tableView.register(UserMentionPickStyleTableViewCell.self, forCellReuseIdentifier: String(describing: UserMentionPickStyleTableViewCell.self)) - - diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserMentionPickStyleTableViewCell.self), for: indexPath) as! UserMentionPickStyleTableViewCell - MentionPickViewModel.configure( - cell: cell, - item: item, - configuration: configuration - ) - return cell - } - - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.primary]) - snapshot.appendItems([primaryItem], toSection: .primary) - if !secondaryItems.isEmpty { - snapshot.appendSections([.secondary]) - snapshot.appendItems(secondaryItems, toSection: .secondary) - } - diffableDataSource?.apply(snapshot) +// tableView.register(UserMentionPickStyleTableViewCell.self, forCellReuseIdentifier: String(describing: UserMentionPickStyleTableViewCell.self)) +// +// diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in +// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserMentionPickStyleTableViewCell.self), for: indexPath) as! UserMentionPickStyleTableViewCell +// MentionPickViewModel.configure( +// cell: cell, +// item: item, +// configuration: configuration +// ) +// return cell +// } +// +// var snapshot = NSDiffableDataSourceSnapshot() +// snapshot.appendSections([.primary]) +// snapshot.appendItems([primaryItem], toSection: .primary) +// if !secondaryItems.isEmpty { +// snapshot.appendSections([.secondary]) +// snapshot.appendItems(secondaryItems, toSection: .secondary) +// } +// diffableDataSource?.apply(snapshot) } } extension MentionPickViewModel { // FIXME: use UserRecord bind view - static func configure( - cell: UserMentionPickStyleTableViewCell, - item: Item, - configuration: DataSourceConfiguration - ) { - switch item { - case .twitterUser(let username, let attribute): - cell.userView.viewModel.platform = .twitter - cell.userView.viewModel.avatarImageURL = attribute.avatarImageURL - cell.userView.viewModel.name = attribute.name.flatMap { PlaintextMetaContent(string: $0) } - cell.userView.viewModel.username = "@" + username - - cell.userView.activityIndicatorView.isHidden = attribute.state == .finish - cell.userView.checkmarkButton.isHidden = attribute.state == .loading - - if attribute.selected { - cell.userView.checkmarkButton.setImage(UIImage(systemName: "checkmark.circle.fill"), for: .normal) - } else { - cell.userView.checkmarkButton.setImage(UIImage(systemName: "circle"), for: .normal) - } - cell.selectionStyle = attribute.disabled ? .none : .default - cell.userView.checkmarkButton.tintColor = attribute.disabled ? .systemGray : (attribute.selected ? Asset.Colors.hightLight.color : .systemGray) - } // end switch - } +// static func configure( +// cell: UserMentionPickStyleTableViewCell, +// item: Item, +// configuration: DataSourceConfiguration +// ) { +// switch item { +// case .twitterUser(let username, let attribute): +// cell.userView.viewModel.platform = .twitter +// cell.userView.viewModel.avatarImageURL = attribute.avatarImageURL +// cell.userView.viewModel.name = attribute.name.flatMap { PlaintextMetaContent(string: $0) } +// cell.userView.viewModel.username = "@" + username +// +// cell.userView.activityIndicatorView.isHidden = attribute.state == .finish +// cell.userView.checkmarkButton.isHidden = attribute.state == .loading +// +// if attribute.selected { +// cell.userView.checkmarkButton.setImage(UIImage(systemName: "checkmark.circle.fill"), for: .normal) +// } else { +// cell.userView.checkmarkButton.setImage(UIImage(systemName: "circle"), for: .normal) +// } +// cell.selectionStyle = attribute.disabled ? .none : .default +// cell.userView.checkmarkButton.tintColor = attribute.disabled ? .systemGray : (attribute.selected ? Asset.Colors.hightLight.color : .systemGray) +// } // end switch +// } } diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/TableView/TableSlideTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/Common/TableSlideTableViewCell.swift similarity index 100% rename from TwidereSDK/Sources/TwidereUI/TableViewCell/TableView/TableSlideTableViewCell.swift rename to TwidereSDK/Sources/TwidereUI/TableViewCell/Common/TableSlideTableViewCell.swift diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/TableView/TableViewCheckmarkTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/Common/TableViewCheckmarkTableViewCell.swift similarity index 100% rename from TwidereSDK/Sources/TwidereUI/TableViewCell/TableView/TableViewCheckmarkTableViewCell.swift rename to TwidereSDK/Sources/TwidereUI/TableViewCell/Common/TableViewCheckmarkTableViewCell.swift diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/TableView/TableViewEntryTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/Common/TableViewEntryTableViewCell.swift similarity index 100% rename from TwidereSDK/Sources/TwidereUI/TableViewCell/TableView/TableViewEntryTableViewCell.swift rename to TwidereSDK/Sources/TwidereUI/TableViewCell/Common/TableViewEntryTableViewCell.swift diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/TableView/TableViewPlainCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/Common/TableViewPlainCell.swift similarity index 100% rename from TwidereSDK/Sources/TwidereUI/TableViewCell/TableView/TableViewPlainCell.swift rename to TwidereSDK/Sources/TwidereUI/TableViewCell/Common/TableViewPlainCell.swift diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/TableView/TableViewSwitchTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/Common/TableViewSwitchTableViewCell.swift similarity index 100% rename from TwidereSDK/Sources/TwidereUI/TableViewCell/TableView/TableViewSwitchTableViewCell.swift rename to TwidereSDK/Sources/TwidereUI/TableViewCell/Common/TableViewSwitchTableViewCell.swift diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/TableView/TableViewTextFieldTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/Common/TableViewTextFieldTableViewCell.swift similarity index 100% rename from TwidereSDK/Sources/TwidereUI/TableViewCell/TableView/TableViewTextFieldTableViewCell.swift rename to TwidereSDK/Sources/TwidereUI/TableViewCell/Common/TableViewTextFieldTableViewCell.swift diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/Loader/TimelineMiddleLoaderTableViewCell+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/Loader/TimelineMiddleLoaderTableViewCell+ViewModel.swift index 9745bbf5..ab4ff8f5 100644 --- a/TwidereSDK/Sources/TwidereUI/TableViewCell/Loader/TimelineMiddleLoaderTableViewCell+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/Loader/TimelineMiddleLoaderTableViewCell+ViewModel.swift @@ -12,10 +12,10 @@ import SwiftUI import CoreDataStack extension TimelineMiddleLoaderTableViewCell { - class ViewModel { + public class ViewModel { var disposeBag = Set() - @Published var isFetching = false + @Published public var isFetching = false } func configure( diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/Loader/TimelineMiddleLoaderTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/Loader/TimelineMiddleLoaderTableViewCell.swift index 129c837c..b1d49955 100644 --- a/TwidereSDK/Sources/TwidereUI/TableViewCell/Loader/TimelineMiddleLoaderTableViewCell.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/Loader/TimelineMiddleLoaderTableViewCell.swift @@ -18,7 +18,7 @@ public final class TimelineMiddleLoaderTableViewCell: TimelineLoaderTableViewCel weak var delegate: TimelineMiddleLoaderTableViewCellDelegate? - private(set) lazy var viewModel: ViewModel = { + public private(set) lazy var viewModel: ViewModel = { let viewModel = ViewModel() viewModel.bind(cell: self) return viewModel diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/Notification/NotificationTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/Notification/NotificationTableViewCell.swift new file mode 100644 index 00000000..d63abe9c --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/Notification/NotificationTableViewCell.swift @@ -0,0 +1,57 @@ +// +// NotificationTableViewCell.swift +// +// +// Created by MainasuK on 2023/4/11. +// + +import os.log +import UIKit + +public final class NotificationTableViewCell: UITableViewCell { + + let logger = Logger(subsystem: "StatusTableViewCell", category: "View") + + public weak var statusViewTableViewCellDelegate: StatusViewTableViewCellDelegate? + public weak var userViewTableViewCellDelegate: UserViewTableViewCellDelegate? + public var viewModel: NotificationView.ViewModel? + + public override func prepareForReuse() { + super.prepareForReuse() + + contentConfiguration = nil + statusViewTableViewCellDelegate = nil + userViewTableViewCellDelegate = nil + } + + public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + _init() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension NotificationTableViewCell { + + private func _init() { + selectionStyle = .none + } + +} + +// MARK: - StatusViewContainerTableViewCell +extension NotificationTableViewCell: StatusViewContainerTableViewCell { } + +// MARK: - StatusViewDelegate +extension NotificationTableViewCell: StatusViewDelegate { } + +// MARK: - UserViewContainerTableViewCell +extension NotificationTableViewCell: UserViewContainerTableViewCell { } + +// MARK: - UserViewDelegate +extension NotificationTableViewCell: UserViewDelegate { } diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/Status/StatusTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/Status/StatusTableViewCell.swift new file mode 100644 index 00000000..d9881e2f --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/Status/StatusTableViewCell.swift @@ -0,0 +1,50 @@ +// +// StatusTableViewCell.swift +// StatusTableViewCell +// +// Created by Cirno MainasuK on 2021-8-20. +// Copyright © 2021 Twidere. All rights reserved. +// + +import os.log +import UIKit +import Combine + +public class StatusTableViewCell: UITableViewCell { + + let logger = Logger(subsystem: "StatusTableViewCell", category: "View") + + public weak var statusViewTableViewCellDelegate: StatusViewTableViewCellDelegate? + + public override func prepareForReuse() { + super.prepareForReuse() + + contentConfiguration = nil + statusViewTableViewCellDelegate = nil + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension StatusTableViewCell { + + private func _init() { + selectionStyle = .none + } + +} + +// MARK: - StatusViewContainerTableViewCell +extension StatusTableViewCell: StatusViewContainerTableViewCell { } + +// MARK: - StatusViewDelegate +extension StatusTableViewCell: StatusViewDelegate { } diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/Status/StatusViewTableViewCellDelegate.swift similarity index 63% rename from TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift rename to TwidereSDK/Sources/TwidereUI/TableViewCell/Status/StatusViewTableViewCellDelegate.swift index 31a87447..407dae7a 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/Status/StatusViewTableViewCellDelegate.swift @@ -13,18 +13,18 @@ import Meta // sourcery: protocolName = "StatusViewDelegate" // sourcery: replaceOf = "statusView(viewModel" -// sourcery: replaceWith = "delegate?.tableViewCell(self, viewModel: viewModel" -protocol StatusViewContainerTableViewCell: UITableViewCell, AutoGenerateProtocolRelayDelegate { - var delegate: StatusViewTableViewCellDelegate? { get } - var viewModel: StatusView.ViewModel? { get } +// sourcery: replaceWith = "statusViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel" +public protocol StatusViewContainerTableViewCell: UITableViewCell, AutoGenerateProtocolRelayDelegate { + var statusViewTableViewCellDelegate: StatusViewTableViewCellDelegate? { get } } // MARK: - AutoGenerateProtocolDelegate // sourcery: protocolName = "StatusViewDelegate" // sourcery: replaceOf = "statusView(_" // sourcery: replaceWith = "func tableViewCell(_ cell: UITableViewCell," -protocol StatusViewTableViewCellDelegate: AutoGenerateProtocolDelegate { +public protocol StatusViewTableViewCellDelegate: AutoGenerateProtocolDelegate { // sourcery:inline:StatusViewTableViewCellDelegate.AutoGenerateProtocolDelegate + func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, userAvatarButtonDidPressed user: UserRecord) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, toggleContentDisplay isReveal: Bool) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, textViewDidSelectMeta meta: Meta) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel) @@ -41,50 +41,54 @@ protocol StatusViewTableViewCellDelegate: AutoGenerateProtocolDelegate { // MARK: - AutoGenerateProtocolDelegate // Protocol Extension -extension StatusViewDelegate where Self: StatusViewContainerTableViewCell { +public extension StatusViewDelegate where Self: StatusViewContainerTableViewCell { // sourcery:inline:StatusViewContainerTableViewCell.AutoGenerateProtocolRelayDelegate + func statusView(_ viewModel: StatusView.ViewModel, userAvatarButtonDidPressed user: UserRecord) { + statusViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, userAvatarButtonDidPressed: user) + } + func statusView(_ viewModel: StatusView.ViewModel, toggleContentDisplay isReveal: Bool) { - delegate?.tableViewCell(self, viewModel: viewModel, toggleContentDisplay: isReveal) + statusViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, toggleContentDisplay: isReveal) } func statusView(_ viewModel: StatusView.ViewModel, textViewDidSelectMeta meta: Meta) { - delegate?.tableViewCell(self, viewModel: viewModel, textViewDidSelectMeta: meta) + statusViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, textViewDidSelectMeta: meta) } func statusView(_ viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel) { - delegate?.tableViewCell(self, viewModel: viewModel, previewActionForMediaViewModel: mediaViewModel) + statusViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, previewActionForMediaViewModel: mediaViewModel) } func statusView(_ viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel, previewActionContext: ContextMenuInteractionPreviewActionContext) { - delegate?.tableViewCell(self, viewModel: viewModel, previewActionForMediaViewModel: mediaViewModel, previewActionContext: previewActionContext) + statusViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, previewActionForMediaViewModel: mediaViewModel, previewActionContext: previewActionContext) } func statusView(_ viewModel: StatusView.ViewModel, toggleContentWarningOverlayDisplay isReveal: Bool) { - delegate?.tableViewCell(self, viewModel: viewModel, toggleContentWarningOverlayDisplay: isReveal) + statusViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, toggleContentWarningOverlayDisplay: isReveal) } func statusView(_ viewModel: StatusView.ViewModel, pollVoteActionForViewModel pollViewModel: PollView.ViewModel) { - delegate?.tableViewCell(self, viewModel: viewModel, pollVoteActionForViewModel: pollViewModel) + statusViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, pollVoteActionForViewModel: pollViewModel) } func statusView(_ viewModel: StatusView.ViewModel, pollUpdateIfNeedsForViewModel pollViewModel: PollView.ViewModel) { - delegate?.tableViewCell(self, viewModel: viewModel, pollUpdateIfNeedsForViewModel: pollViewModel) + statusViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, pollUpdateIfNeedsForViewModel: pollViewModel) } func statusView(_ viewModel: StatusView.ViewModel, pollViewModel: PollView.ViewModel, pollOptionDidSelectForViewModel optionViewModel: PollOptionView.ViewModel) { - delegate?.tableViewCell(self, viewModel: viewModel, pollViewModel: pollViewModel, pollOptionDidSelectForViewModel: optionViewModel) + statusViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, pollViewModel: pollViewModel, pollOptionDidSelectForViewModel: optionViewModel) } func statusView(_ viewModel: StatusView.ViewModel, statusMetricViewModel: StatusMetricView.ViewModel, statusMetricButtonDidPressed action: StatusMetricView.Action) { - delegate?.tableViewCell(self, viewModel: viewModel, statusMetricViewModel: statusMetricViewModel, statusMetricButtonDidPressed: action) + statusViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, statusMetricViewModel: statusMetricViewModel, statusMetricButtonDidPressed: action) } func statusView(_ viewModel: StatusView.ViewModel, statusToolbarViewModel: StatusToolbarView.ViewModel, statusToolbarButtonDidPressed action: StatusToolbarView.Action) { - delegate?.tableViewCell(self, viewModel: viewModel, statusToolbarViewModel: statusToolbarViewModel, statusToolbarButtonDidPressed: action) + statusViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, statusToolbarViewModel: statusToolbarViewModel, statusToolbarButtonDidPressed: action) } func statusView(_ viewModel: StatusView.ViewModel, viewHeightDidChange: Void) { - delegate?.tableViewCell(self, viewModel: viewModel, viewHeightDidChange: viewHeightDidChange) + statusViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, viewHeightDidChange: viewHeightDidChange) } // sourcery:end } diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserAccountStyleTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserAccountStyleTableViewCell.swift index f8b91a6c..b1599b4a 100644 --- a/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserAccountStyleTableViewCell.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserAccountStyleTableViewCell.swift @@ -7,24 +7,24 @@ import UIKit -public final class UserAccountStyleTableViewCell: UserTableViewCell { - - let separator = SeparatorLineView() - - public override func _init() { - super._init() - - userView.setup(style: .account) - userView.viewModel.avatarBadge = .platform - - separator.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(separator) - NSLayoutConstraint.activate([ - separator.leadingAnchor.constraint(equalTo: userView.nameLabel.leadingAnchor), - separator.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), - separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - separator.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), - ]) - } - -} +//public final class UserAccountStyleTableViewCell: UserTableViewCell { +// +// let separator = SeparatorLineView() +// +// public override func _init() { +// super._init() +// +// userView.setup(style: .account) +// userView.viewModel.avatarBadge = .platform +// +// separator.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(separator) +// NSLayoutConstraint.activate([ +// separator.leadingAnchor.constraint(equalTo: userView.nameLabel.leadingAnchor), +// separator.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), +// separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// separator.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), +// ]) +// } +// +//} diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserAddListMemberStyleTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserAddListMemberStyleTableViewCell.swift index 06c04b4c..03b4a4c7 100644 --- a/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserAddListMemberStyleTableViewCell.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserAddListMemberStyleTableViewCell.swift @@ -7,23 +7,23 @@ import UIKit -public final class UserAddListMemberStyleTableViewCell: UserTableViewCell { - - let separator = SeparatorLineView() - - public override func _init() { - super._init() - - userView.setup(style: .addListMember) - - separator.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(separator) - NSLayoutConstraint.activate([ - separator.leadingAnchor.constraint(equalTo: userView.nameLabel.leadingAnchor), - separator.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), - separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - separator.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), - ]) - } - -} +//public final class UserAddListMemberStyleTableViewCell: UserTableViewCell { +// +// let separator = SeparatorLineView() +// +// public override func _init() { +// super._init() +// +// userView.setup(style: .addListMember) +// +// separator.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(separator) +// NSLayoutConstraint.activate([ +// separator.leadingAnchor.constraint(equalTo: userView.nameLabel.leadingAnchor), +// separator.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), +// separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// separator.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), +// ]) +// } +// +//} diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserFriendshipStyleTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserFriendshipStyleTableViewCell.swift index 74a45a3a..0435e14a 100644 --- a/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserFriendshipStyleTableViewCell.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserFriendshipStyleTableViewCell.swift @@ -7,23 +7,23 @@ import UIKit -public final class UserFriendshipStyleTableViewCell: UserTableViewCell { - - let separator = SeparatorLineView() - - public override func _init() { - super._init() - - userView.setup(style: .friendship) - - separator.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(separator) - NSLayoutConstraint.activate([ - separator.leadingAnchor.constraint(equalTo: userView.nameLabel.leadingAnchor), - separator.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), - separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - separator.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), - ]) - } - -} +//public final class UserFriendshipStyleTableViewCell: UserTableViewCell { +// +// let separator = SeparatorLineView() +// +// public override func _init() { +// super._init() +// +// userView.setup(style: .friendship) +// +// separator.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(separator) +// NSLayoutConstraint.activate([ +// separator.leadingAnchor.constraint(equalTo: userView.nameLabel.leadingAnchor), +// separator.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), +// separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// separator.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), +// ]) +// } +// +//} diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserListMemberStyleTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserListMemberStyleTableViewCell.swift index 4e3a0ce0..4dfecc6d 100644 --- a/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserListMemberStyleTableViewCell.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserListMemberStyleTableViewCell.swift @@ -7,23 +7,23 @@ import UIKit -public final class UserListMemberStyleTableViewCell: UserTableViewCell { - - let separator = SeparatorLineView() - - public override func _init() { - super._init() - - userView.setup(style: .listMember) - - separator.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(separator) - NSLayoutConstraint.activate([ - separator.leadingAnchor.constraint(equalTo: userView.nameLabel.leadingAnchor), - separator.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), - separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - separator.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), - ]) - } - -} +//public final class UserListMemberStyleTableViewCell: UserTableViewCell { +// +// let separator = SeparatorLineView() +// +// public override func _init() { +// super._init() +// +// userView.setup(style: .listMember) +// +// separator.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(separator) +// NSLayoutConstraint.activate([ +// separator.leadingAnchor.constraint(equalTo: userView.nameLabel.leadingAnchor), +// separator.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), +// separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// separator.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), +// ]) +// } +// +//} diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserMentionPickStyleTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserMentionPickStyleTableViewCell.swift index f6823aed..179af1e2 100644 --- a/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserMentionPickStyleTableViewCell.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserMentionPickStyleTableViewCell.swift @@ -7,13 +7,13 @@ import UIKit -public final class UserMentionPickStyleTableViewCell: UserTableViewCell { - - - public override func _init() { - super._init() - - userView.setup(style: .mentionPick) - } - -} +//public final class UserMentionPickStyleTableViewCell: UserTableViewCell { +// +// +// public override func _init() { +// super._init() +// +// userView.setup(style: .mentionPick) +// } +// +//} diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserNotificationStyleTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserNotificationStyleTableViewCell.swift index 0c551382..96556594 100644 --- a/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserNotificationStyleTableViewCell.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserNotificationStyleTableViewCell.swift @@ -8,23 +8,23 @@ import UIKit -public final class UserNotificationStyleTableViewCell: UserTableViewCell { - - let separator = SeparatorLineView() - - public override func _init() { - super._init() - - userView.setup(style: .notification) - - separator.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(separator) - NSLayoutConstraint.activate([ - separator.leadingAnchor.constraint(equalTo: userView.nameLabel.leadingAnchor), - separator.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), - separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - separator.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), - ]) - } - -} +//public final class UserNotificationStyleTableViewCell: UserTableViewCell { +// +// let separator = SeparatorLineView() +// +// public override func _init() { +// super._init() +// +// userView.setup(style: .notification) +// +// separator.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(separator) +// NSLayoutConstraint.activate([ +// separator.leadingAnchor.constraint(equalTo: userView.nameLabel.leadingAnchor), +// separator.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), +// separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// separator.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), +// ]) +// } +// +//} diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserRelationshipStyleTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserRelationshipStyleTableViewCell.swift index dab0dfa8..18ffdaea 100644 --- a/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserRelationshipStyleTableViewCell.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserRelationshipStyleTableViewCell.swift @@ -8,23 +8,23 @@ import UIKit -public final class UserRelationshipStyleTableViewCell: UserTableViewCell { - - let separator = SeparatorLineView() - - public override func _init() { - super._init() - - userView.setup(style: .relationship) - - separator.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(separator) - NSLayoutConstraint.activate([ - separator.leadingAnchor.constraint(equalTo: userView.nameLabel.leadingAnchor), - separator.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), - separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - separator.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), - ]) - } - -} +//public final class UserRelationshipStyleTableViewCell: UserTableViewCell { +// +// let separator = SeparatorLineView() +// +// public override func _init() { +// super._init() +// +// userView.setup(style: .relationship) +// +// separator.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(separator) +// NSLayoutConstraint.activate([ +// separator.leadingAnchor.constraint(equalTo: userView.nameLabel.leadingAnchor), +// separator.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), +// separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// separator.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), +// ]) +// } +// +//} diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserTableViewCell+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserTableViewCell+ViewModel.swift index 4d7a0dc1..72120058 100644 --- a/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserTableViewCell+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserTableViewCell+ViewModel.swift @@ -11,36 +11,36 @@ import CoreDataStack import TwidereCore extension UserTableViewCell { - public final class ViewModel { - public let user: UserObject - public let me: UserObject? - public let notification: NotificationObject? - - public init( - user: UserObject, - me: UserObject?, - notification: NotificationObject? - ) { - self.user = user - self.me = me - self.notification = notification - } - } +// public final class ViewModel { +// public let user: UserObject +// public let me: UserObject? +// public let notification: NotificationObject? +// +// public init( +// user: UserObject, +// me: UserObject?, +// notification: NotificationObject? +// ) { +// self.user = user +// self.me = me +// self.notification = notification +// } +// } } extension UserTableViewCell { - public func configure( - viewModel: ViewModel, - configurationContext: UserView.ConfigurationContext, - delegate: UserViewTableViewCellDelegate? - ) { - userView.configure( - user: viewModel.user, - me: viewModel.me, - notification: viewModel.notification, - configurationContext: configurationContext - ) - - self.delegate = delegate - } +// public func configure( +// viewModel: ViewModel, +// configurationContext: UserView.ConfigurationContext, +// delegate: UserViewTableViewCellDelegate? +// ) { +// userView.configure( +// user: viewModel.user, +// me: viewModel.me, +// notification: viewModel.notification, +// configurationContext: configurationContext +// ) +// +// self.delegate = delegate +// } } diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserTableViewCell.swift index 3021a8c8..83aeff8e 100644 --- a/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserTableViewCell.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserTableViewCell.swift @@ -11,21 +11,16 @@ import UIKit import Combine public class UserTableViewCell: UITableViewCell { - - var disposeBag = Set() - - let logger = Logger(subsystem: "UserTableViewCell", category: "View") - public let userView = UserView() - - public weak var delegate: UserViewTableViewCellDelegate? - + let logger = Logger(subsystem: "UserTableViewCell", category: "View") + + public weak var userViewTableViewCellDelegate: UserViewTableViewCellDelegate? + public override func prepareForReuse() { super.prepareForReuse() - userView.prepareForReuse() - disposeBag.removeAll() - delegate = nil + contentConfiguration = nil + userViewTableViewCellDelegate = nil } public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -39,16 +34,7 @@ public class UserTableViewCell: UITableViewCell { } func _init() { - userView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(userView) - NSLayoutConstraint.activate([ - userView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20), - userView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - userView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), - contentView.bottomAnchor.constraint(equalTo: userView.bottomAnchor, constant: 16).priority(.defaultHigh), - ]) - - userView.delegate = self + // selectionStyle = .none } } @@ -56,5 +42,5 @@ public class UserTableViewCell: UITableViewCell { // MARK: - UserViewContainerTableViewCell extension UserTableViewCell: UserViewContainerTableViewCell { } -// MARK: - +// MARK: - UserViewDelegate extension UserTableViewCell: UserViewDelegate { } diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserViewTableViewCellDelegate.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserViewTableViewCellDelegate.swift index ec846b06..451633dc 100644 --- a/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserViewTableViewCellDelegate.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserViewTableViewCellDelegate.swift @@ -8,11 +8,10 @@ import UIKit // sourcery: protocolName = "UserViewDelegate" -// sourcery: replaceOf = "userView(userView" -// sourcery: replaceWith = "delegate?.tableViewCell(self, userView: userView" +// sourcery: replaceOf = "userView(viewModel" +// sourcery: replaceWith = "userViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel" public protocol UserViewContainerTableViewCell: UITableViewCell, AutoGenerateProtocolRelayDelegate { - var delegate: UserViewTableViewCellDelegate? { get } - var userView: UserView { get } + var userViewTableViewCellDelegate: UserViewTableViewCellDelegate? { get } } @@ -22,36 +21,26 @@ public protocol UserViewContainerTableViewCell: UITableViewCell, AutoGeneratePro // sourcery: replaceWith = "func tableViewCell(_ cell: UITableViewCell," public protocol UserViewTableViewCellDelegate: AutoGenerateProtocolDelegate { // sourcery:inline:UserViewTableViewCellDelegate.AutoGenerateProtocolDelegate - func tableViewCell(_ cell: UITableViewCell, userView: UserView, menuActionDidPressed action: UserView.MenuAction, menuButton button: UIButton) - func tableViewCell(_ cell: UITableViewCell, userView: UserView, friendshipButtonDidPressed button: UIButton) - func tableViewCell(_ cell: UITableViewCell, userView: UserView, membershipButtonDidPressed button: UIButton) - func tableViewCell(_ cell: UITableViewCell, userView: UserView, acceptFollowReqeustButtonDidPressed button: UIButton) - func tableViewCell(_ cell: UITableViewCell, userView: UserView, rejectFollowReqeustButtonDidPressed button: UIButton) + func tableViewCell(_ cell: UITableViewCell, viewModel: UserView.ViewModel, userAvatarButtonDidPressed user: UserRecord) + func tableViewCell(_ cell: UITableViewCell, viewModel: UserView.ViewModel, menuActionDidPressed action: UserView.ViewModel.MenuAction) + func tableViewCell(_ cell: UITableViewCell, viewModel: UserView.ViewModel, followReqeustButtonDidPressed user: UserRecord, accept: Bool) // sourcery:end } // MARK: - AutoGenerateProtocolDelegate // Protocol Extension -extension UserViewDelegate where Self: UserViewContainerTableViewCell { +public extension UserViewDelegate where Self: UserViewContainerTableViewCell { // sourcery:inline:UserViewContainerTableViewCell.AutoGenerateProtocolRelayDelegate - func userView(_ userView: UserView, menuActionDidPressed action: UserView.MenuAction, menuButton button: UIButton) { - delegate?.tableViewCell(self, userView: userView, menuActionDidPressed: action, menuButton: button) + func userView(_ viewModel: UserView.ViewModel, userAvatarButtonDidPressed user: UserRecord) { + userViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, userAvatarButtonDidPressed: user) } - func userView(_ userView: UserView, friendshipButtonDidPressed button: UIButton) { - delegate?.tableViewCell(self, userView: userView, friendshipButtonDidPressed: button) + func userView(_ viewModel: UserView.ViewModel, menuActionDidPressed action: UserView.ViewModel.MenuAction) { + userViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, menuActionDidPressed: action) } - func userView(_ userView: UserView, membershipButtonDidPressed button: UIButton) { - delegate?.tableViewCell(self, userView: userView, membershipButtonDidPressed: button) - } - - func userView(_ userView: UserView, acceptFollowReqeustButtonDidPressed button: UIButton) { - delegate?.tableViewCell(self, userView: userView, acceptFollowReqeustButtonDidPressed: button) - } - - func userView(_ userView: UserView, rejectFollowReqeustButtonDidPressed button: UIButton) { - delegate?.tableViewCell(self, userView: userView, rejectFollowReqeustButtonDidPressed: button) + func userView(_ viewModel: UserView.ViewModel, followReqeustButtonDidPressed user: UserRecord, accept: Bool) { + userViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, followReqeustButtonDidPressed: user, accept: accept) } // sourcery:end diff --git a/TwidereSDK/Sources/TwidereUI/Utility/SizeDimensionPreferenceKey.swift b/TwidereSDK/Sources/TwidereUI/Utility/SizeDimensionPreferenceKey.swift new file mode 100644 index 00000000..29aa39a8 --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Utility/SizeDimensionPreferenceKey.swift @@ -0,0 +1,19 @@ +// +// SizeDimensionPreferenceKey.swift +// +// +// Created by MainasuK on 2023/4/10. +// + +import SwiftUI + +public struct SizeDimensionPreferenceKey: PreferenceKey { + public static let defaultValue: CGFloat = 0 + + public static func reduce( + value: inout CGFloat, + nextValue: () -> CGFloat + ) { + value = max(value, nextValue()) + } +} diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 0b8564b8..744ac04e 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -84,13 +84,11 @@ DB2D36F927D5E74D00C1FBE0 /* CompositeListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2D36F827D5E74D00C1FBE0 /* CompositeListViewModel.swift */; }; DB2EBBF0255D368200956CAA /* TableViewEntryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2EBBEF255D368200956CAA /* TableViewEntryRow.swift */; }; DB2FFF3E258B78B0003DBC19 /* AVPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2FFF3D258B78B0003DBC19 /* AVPlayer.swift */; }; - DB30ADDC26CFC7EE00B2D2BE /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB30ADDB26CFC7EE00B2D2BE /* StatusTableViewCell.swift */; }; DB30ADDD26CFD3CC00B2D2BE /* HomeTimelineViewController+DebugAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB2513D2599DBAB0064A876 /* HomeTimelineViewController+DebugAction.swift */; }; DB33A4A925A319A0003CED7D /* ActionToolbarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB33A4A825A319A0003CED7D /* ActionToolbarContainer.swift */; }; DB36F35E257F74C10028F81E /* ScrollViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB36F35D257F74C00028F81E /* ScrollViewContainer.swift */; }; DB37F6A0274B556B0081603F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB37F69F274B556B0081603F /* Assets.xcassets */; }; DB3B905F26E8A9650010F64C /* StatusThreadViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3B905E26E8A9650010F64C /* StatusThreadViewController+DataSourceProvider.swift */; }; - DB3B906126E8AB480010F64C /* StatusViewTableViewCellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3B906026E8AB480010F64C /* StatusViewTableViewCellDelegate.swift */; }; DB3B906326E8BBD70010F64C /* ProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3B906226E8BBD70010F64C /* ProfileHeaderView.swift */; }; DB3B906526E8BF1F0010F64C /* ProfileHeaderView+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3B906426E8BF1F0010F64C /* ProfileHeaderView+ViewModel.swift */; }; DB3B906726E8CD6D0010F64C /* ProfileHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3B906626E8CD6D0010F64C /* ProfileHeaderViewModel.swift */; }; @@ -164,7 +162,6 @@ DB5FB0102727FCC5006520FA /* SearchUserViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB67617A254C0279006C6798 /* SearchUserViewModel.swift */; }; DB5FB0112727FD02006520FA /* SearchUserViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBDDE72D254C084A0057CF8E /* SearchUserViewModel+Diffable.swift */; }; DB5FB0122727FD4A006520FA /* SearchUserViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB578CD7254C0FB500745336 /* SearchUserViewModel+State.swift */; }; - DB5FD9B326D8D94600CF5439 /* StatusTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5FD9B226D8D94600CF5439 /* StatusTableViewCell+ViewModel.swift */; }; DB5FD9B726D8EFF700CF5439 /* UserBriefInfoView+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5FD9B626D8EFF700CF5439 /* UserBriefInfoView+ViewModel.swift */; }; DB657A7B25AD574D001339B6 /* UIBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB657A7A25AD574D001339B6 /* UIBarButtonItem.swift */; }; DB66DB8D2823A7C80071F5F3 /* SecondaryTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66DB8C2823A7C80071F5F3 /* SecondaryTabBarController.swift */; }; @@ -543,14 +540,12 @@ DB2D36F827D5E74D00C1FBE0 /* CompositeListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositeListViewModel.swift; sourceTree = ""; }; DB2EBBEF255D368200956CAA /* TableViewEntryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewEntryRow.swift; sourceTree = ""; }; DB2FFF3D258B78B0003DBC19 /* AVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayer.swift; sourceTree = ""; }; - DB30ADDB26CFC7EE00B2D2BE /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = ""; }; DB33A4A825A319A0003CED7D /* ActionToolbarContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionToolbarContainer.swift; sourceTree = ""; }; DB33E5B42719637400EC2225 /* CoverFlowStackCollectionViewLayout */ = {isa = PBXFileReference; lastKnownFileType = folder; path = CoverFlowStackCollectionViewLayout; sourceTree = ""; }; DB36F35D257F74C00028F81E /* ScrollViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewContainer.swift; sourceTree = ""; }; DB36F375257F79DB0028F81E /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; DB37F69F274B556B0081603F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; DB3B905E26E8A9650010F64C /* StatusThreadViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusThreadViewController+DataSourceProvider.swift"; sourceTree = ""; }; - DB3B906026E8AB480010F64C /* StatusViewTableViewCellDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusViewTableViewCellDelegate.swift; sourceTree = ""; }; DB3B906226E8BBD70010F64C /* ProfileHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderView.swift; sourceTree = ""; }; DB3B906426E8BF1F0010F64C /* ProfileHeaderView+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileHeaderView+ViewModel.swift"; sourceTree = ""; }; DB3B906626E8CD6D0010F64C /* ProfileHeaderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderViewModel.swift; sourceTree = ""; }; @@ -622,7 +617,6 @@ DB5BF12427F5A5C1002A3EF5 /* Account+Fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Fetch.swift"; sourceTree = ""; }; DB5BF12C27F5BAD5002A3EF5 /* AuthenticationIndex+Fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AuthenticationIndex+Fetch.swift"; sourceTree = ""; }; DB5F1C132886AE2F00978F38 /* TwidereXTests+Issue92.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TwidereXTests+Issue92.swift"; sourceTree = ""; }; - DB5FD9B226D8D94600CF5439 /* StatusTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusTableViewCell+ViewModel.swift"; sourceTree = ""; }; DB5FD9B626D8EFF700CF5439 /* UserBriefInfoView+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserBriefInfoView+ViewModel.swift"; sourceTree = ""; }; DB60C1A82762394E00628235 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; }; DB657A7A25AD574D001339B6 /* UIBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIBarButtonItem.swift; sourceTree = ""; }; @@ -1821,9 +1815,6 @@ DB97D1E7256CBA5D0056F8C2 /* TableViewCell */ = { isa = PBXGroup; children = ( - DB3B906026E8AB480010F64C /* StatusViewTableViewCellDelegate.swift */, - DB30ADDB26CFC7EE00B2D2BE /* StatusTableViewCell.swift */, - DB5FD9B226D8D94600CF5439 /* StatusTableViewCell+ViewModel.swift */, DB5632AF26DCED1300FC893F /* StatusThreadRootTableViewCell.swift */, DB5632B126DCF1CD00FC893F /* StatusThreadRootTableViewCell+ViewModel.swift */, DB25C4C7277994B800EC1435 /* CenterFootnoteLabelTableViewCell.swift */, @@ -2953,13 +2944,11 @@ DB44245F285A49CD0095AECF /* HashtagTimelineViewModel+Diffable.swift in Sources */, DB51DC412717FCAA00A0D8FB /* StatusMediaGalleryCollectionCell.swift in Sources */, DB9B3251285735F200AC818D /* GridTimelineViewController.swift in Sources */, - DB3B906126E8AB480010F64C /* StatusViewTableViewCellDelegate.swift in Sources */, DB46D11A27DB26FF003B8BA1 /* ListUserViewController.swift in Sources */, DB1E48122772CE380074F6A0 /* SearchViewModel.swift in Sources */, DB76A662275F65FF00A50673 /* DataSourceFacade+Model.swift in Sources */, DB2EBBF0255D368200956CAA /* TableViewEntryRow.swift in Sources */, DB580EB7288187BD00BC4A0F /* MastodonNotificationSectionViewModel.swift in Sources */, - DB30ADDC26CFC7EE00B2D2BE /* StatusTableViewCell.swift in Sources */, DB8761CB2745530200BA7EE2 /* CoverFlowStackSection.swift in Sources */, DB8AC0F825401BA200E636BE /* UIViewController.swift in Sources */, DB434888251DFE2D005B599F /* ProfileBannerStatusView.swift in Sources */, @@ -3011,7 +3000,6 @@ DB2C873C274F4B7D00CE0398 /* DeveloperView.swift in Sources */, DB2C8742274F4B7D00CE0398 /* AboutViewController.swift in Sources */, DB442465285AD8660095AECF /* ListStatusTimelineViewModel+Diffable.swift in Sources */, - DB5FD9B326D8D94600CF5439 /* StatusTableViewCell+ViewModel.swift in Sources */, DB2C0BDA27DF14950033FC94 /* EditListViewController.swift in Sources */, DBA210342759D7A8000B7CB2 /* FriendshipListViewModel.swift in Sources */, DBC747E1259DBD5400787EEF /* AvatarBarButtonItem.swift in Sources */, diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index 047a4eb8..26d5ab46 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -136,6 +136,15 @@ "version": "7.6.2" } }, + { + "package": "MetaTextKit", + "repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git", + "state": { + "branch": null, + "revision": "addb2b4875a2a5712e1c50add410c560a9cf42f2", + "version": "4.5.2" + } + }, { "package": "Pageboy", "repositoryURL": "https://github.com/uias/Pageboy", diff --git a/TwidereX/Coordinator/SceneCoordinator.swift b/TwidereX/Coordinator/SceneCoordinator.swift index 884ad560..044cace8 100644 --- a/TwidereX/Coordinator/SceneCoordinator.swift +++ b/TwidereX/Coordinator/SceneCoordinator.swift @@ -117,14 +117,21 @@ extension SceneCoordinator { extension SceneCoordinator { @MainActor - func setup() { + func setup(authentication record: ManagedObjectRecord? = nil) { let rootViewController: UIViewController do { // check AuthContext - let request = AuthenticationIndex.sortedFetchRequest - request.fetchLimit = 1 - let _authenticationIndex = try context.managedObjectContext.fetch(request).first + let _authenticationIndex: AuthenticationIndex? = try { + if let index = record?.object(in: context.managedObjectContext) { + return index + } else { + let request = AuthenticationIndex.sortedFetchRequest + request.fetchLimit = 1 + let result = try context.managedObjectContext.fetch(request).first + return result + } + }() guard let authenticationIndex = _authenticationIndex, let authContext = AuthContext(authenticationIndex: authenticationIndex) else { diff --git a/TwidereX/Diffable/Misc/History/HistorySection.swift b/TwidereX/Diffable/Misc/History/HistorySection.swift index 9dba71d6..7f1af493 100644 --- a/TwidereX/Diffable/Misc/History/HistorySection.swift +++ b/TwidereX/Diffable/Misc/History/HistorySection.swift @@ -25,7 +25,6 @@ extension HistorySection { struct Configuration { weak var statusViewTableViewCellDelegate: StatusViewTableViewCellDelegate? weak var userViewTableViewCellDelegate: UserViewTableViewCellDelegate? - let userViewConfigurationContext: UserView.ConfigurationContext let viewLayoutFramePublisher: Published.Publisher? } @@ -35,7 +34,7 @@ extension HistorySection { configuration: Configuration ) -> UITableViewDiffableDataSource { tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self)) - tableView.register(UserRelationshipStyleTableViewCell.self, forCellReuseIdentifier: String(describing: UserRelationshipStyleTableViewCell.self)) +// tableView.register(UserRelationshipStyleTableViewCell.self, forCellReuseIdentifier: String(describing: UserRelationshipStyleTableViewCell.self)) let diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in // data source should dispatch in main thread diff --git a/TwidereX/Diffable/Misc/Notification/NotificationSection.swift b/TwidereX/Diffable/Misc/Notification/NotificationSection.swift index 49f333b0..5f628f26 100644 --- a/TwidereX/Diffable/Misc/Notification/NotificationSection.swift +++ b/TwidereX/Diffable/Misc/Notification/NotificationSection.swift @@ -8,6 +8,7 @@ import UIKit +import SwiftUI enum NotificationSection: Hashable { case main @@ -18,30 +19,50 @@ extension NotificationSection { struct Configuration { weak var statusViewTableViewCellDelegate: StatusViewTableViewCellDelegate? weak var userViewTableViewCellDelegate: UserViewTableViewCellDelegate? - let userViewConfigurationContext: UserView.ConfigurationContext let viewLayoutFramePublisher: Published.Publisher? } static func diffableDataSource( tableView: UITableView, context: AppContext, + authContext: AuthContext, configuration: Configuration ) -> UITableViewDiffableDataSource { return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in // data source should dispatch in main thread assert(Thread.isMainThread) - return UITableViewCell() + tableView.register(NotificationTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationTableViewCell.self)) + // tableView.register(UserNotificationStyleTableViewCell.self, forCellReuseIdentifier: String(describing: UserNotificationStyleTableViewCell.self)) + tableView.register(TimelineMiddleLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self)) + tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) // configure cell with item -// switch item { -// case .feed(let record): -// return context.managedObjectContext.performAndWait { -// guard let feed = record.object(in: context.managedObjectContext) else { -// assertionFailure() -// return UITableViewCell() -// } -// + switch item { + case .feed(let record): + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationTableViewCell.self), for: indexPath) as! NotificationTableViewCell + cell.statusViewTableViewCellDelegate = configuration.statusViewTableViewCellDelegate + cell.userViewTableViewCellDelegate = configuration.userViewTableViewCellDelegate + context.managedObjectContext.performAndWait { + guard let feed = record.object(in: context.managedObjectContext) else { return } + guard case let .notification(notification) = feed.content else { return } + let viewModel = NotificationView.ViewModel( + notification: notification, + authContext: authContext, + statusViewDelegate: cell, + userViewDelegate: cell, + viewLayoutFramePublisher: configuration.viewLayoutFramePublisher + ) + cell.contentConfiguration = UIHostingConfiguration { + NotificationView(viewModel: viewModel) + } + .margins(.vertical, 0) // remove vertical margins + } + return cell + + default: + return UITableViewCell() + } // end switch // switch feed.objectContent { // case .status: // let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell @@ -80,7 +101,7 @@ extension NotificationSection { // return UITableViewCell() // } // } // end return context.managedObjectContext.performAndWait -// + // case .feedLoader: // let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell // cell.viewModel.isFetching = true @@ -91,37 +112,37 @@ extension NotificationSection { // cell.activityIndicatorView.startAnimating() // return cell // } // end switch - } - } + } // end return + } // end func } extension NotificationSection { - static func configure( - tableView: UITableView, - cell: StatusTableViewCell, - viewModel: StatusTableViewCell.ViewModel, - configuration: Configuration - ) { +// static func configure( +// tableView: UITableView, +// cell: StatusTableViewCell, +// viewModel: StatusTableViewCell.ViewModel, +// configuration: Configuration +// ) { // cell.configure( // tableView: tableView, // viewModel: viewModel, // configurationContext: configuration.statusViewConfigurationContext, // delegate: configuration.statusViewTableViewCellDelegate // ) - } +// } - static func configure( - cell: UserTableViewCell, - viewModel: UserTableViewCell.ViewModel, - configuration: Configuration - ) { - cell.configure( - viewModel: viewModel, - configurationContext: configuration.userViewConfigurationContext, - delegate: configuration.userViewTableViewCellDelegate - ) - } +// static func configure( +// cell: UserTableViewCell, +// viewModel: UserTableViewCell.ViewModel, +// configuration: Configuration +// ) { +// cell.configure( +// viewModel: viewModel, +// configurationContext: configuration.userViewConfigurationContext, +// delegate: configuration.userViewTableViewCellDelegate +// ) +// } } diff --git a/TwidereX/Diffable/Status/StatusSection.swift b/TwidereX/Diffable/Status/StatusSection.swift index a42c2d92..4a3eb3c8 100644 --- a/TwidereX/Diffable/Status/StatusSection.swift +++ b/TwidereX/Diffable/Status/StatusSection.swift @@ -48,7 +48,7 @@ extension StatusSection { switch item { case .feed(let record): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell - cell.delegate = configuration.statusViewTableViewCellDelegate + cell.statusViewTableViewCellDelegate = configuration.statusViewTableViewCellDelegate context.managedObjectContext.performAndWait { guard let feed = record.object(in: context.managedObjectContext) else { return } let _viewModel = StatusView.ViewModel( @@ -65,6 +65,7 @@ extension StatusSection { .margins(.vertical, 0) // remove vertical margins } return cell + case .feedLoader(let record): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell // context.managedObjectContext.performAndWait { diff --git a/TwidereX/Diffable/User/UserItem.swift b/TwidereX/Diffable/User/UserItem.swift index 07e8c151..312cf338 100644 --- a/TwidereX/Diffable/User/UserItem.swift +++ b/TwidereX/Diffable/User/UserItem.swift @@ -12,6 +12,6 @@ import TwidereCore enum UserItem: Hashable { case authenticationIndex(record: ManagedObjectRecord) - case user(record: UserRecord, style: UserView.Style) + case user(record: UserRecord, kind: UserView.ViewModel.Kind) case bottomLoader } diff --git a/TwidereX/Diffable/User/UserSection.swift b/TwidereX/Diffable/User/UserSection.swift index 0b009044..38823309 100644 --- a/TwidereX/Diffable/User/UserSection.swift +++ b/TwidereX/Diffable/User/UserSection.swift @@ -7,6 +7,7 @@ // import UIKit +import SwiftUI import Combine import CoreDataStack @@ -18,7 +19,7 @@ extension UserSection { struct Configuration { weak var userViewTableViewCellDelegate: UserViewTableViewCellDelegate? - let userViewConfigurationContext: UserView.ConfigurationContext + // let userViewConfigurationContext: UserView.ConfigurationContext } static func diffableDataSource( @@ -27,106 +28,95 @@ extension UserSection { authContext: AuthContext, configuration: Configuration ) -> UITableViewDiffableDataSource { - let cellTypes = [ - UserAccountStyleTableViewCell.self, - UserRelationshipStyleTableViewCell.self, - UserFriendshipStyleTableViewCell.self, - UserMentionPickStyleTableViewCell.self, - UserNotificationStyleTableViewCell.self, - UserListMemberStyleTableViewCell.self, - UserAddListMemberStyleTableViewCell.self, - TimelineBottomLoaderTableViewCell.self, - ] - - cellTypes.forEach { type in - tableView.register(type, forCellReuseIdentifier: String(describing: type)) - } - + tableView.register(UserTableViewCell.self, forCellReuseIdentifier: String(describing: UserTableViewCell.self)) + tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) + return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in // data source should dispatch in main thread assert(Thread.isMainThread) + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell + cell.userViewTableViewCellDelegate = configuration.userViewTableViewCellDelegate + // configure cell with item switch item { case .authenticationIndex(let record): - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserAccountStyleTableViewCell.self), for: indexPath) as! UserAccountStyleTableViewCell context.managedObjectContext.performAndWait { guard let authenticationIndex = record.object(in: context.managedObjectContext) else { return } guard let me = authenticationIndex.user else { return } - let viewModel = UserTableViewCell.ViewModel( + let _viewModel = UserView.ViewModel( user: me, - me: me, - notification: nil - ) - configure( - cell: cell, - viewModel: viewModel, - configuration: configuration + authContext: authContext, + kind: .account, + delegate: cell ) + guard let viewModel = _viewModel else { return } + cell.contentConfiguration = UIHostingConfiguration { + UserView(viewModel: viewModel) + } + .margins(.vertical, 0) // remove vertical margins } - return cell - case .user(let record, let style): - let cell = dequeueReusableCell(tableView: tableView, indexPath: indexPath, style: style) + case .user(let record, let kind): context.managedObjectContext.performAndWait { guard let user = record.object(in: context.managedObjectContext) else { return } - let authenticationContext = authContext.authenticationContext - let me = authenticationContext.user(in: context.managedObjectContext) - let viewModel = UserTableViewCell.ViewModel( + let _viewModel = UserView.ViewModel( user: user, - me: me, - notification: nil - ) - configure( - cell: cell, - viewModel: viewModel, - configuration: configuration + authContext: authContext, + kind: kind, + delegate: cell ) + guard let viewModel = _viewModel else { return } + cell.contentConfiguration = UIHostingConfiguration { + UserView(viewModel: viewModel) + } + .margins(.vertical, 0) // remove vertical margins } - return cell case .bottomLoader: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell cell.activityIndicatorView.startAnimating() return cell - } + } // end switch + + return cell } - } + } // end func } extension UserSection { - static func dequeueReusableCell( - tableView: UITableView, - indexPath: IndexPath, - style: UserView.Style - ) -> UserTableViewCell { - switch style { - case .account: - return tableView.dequeueReusableCell(withIdentifier: String(describing: UserAccountStyleTableViewCell.self), for: indexPath) as! UserAccountStyleTableViewCell - case .relationship: - return tableView.dequeueReusableCell(withIdentifier: String(describing: UserRelationshipStyleTableViewCell.self), for: indexPath) as! UserRelationshipStyleTableViewCell - case .friendship: - return tableView.dequeueReusableCell(withIdentifier: String(describing: UserFriendshipStyleTableViewCell.self), for: indexPath) as! UserFriendshipStyleTableViewCell - case .notification: - return tableView.dequeueReusableCell(withIdentifier: String(describing: UserNotificationStyleTableViewCell.self), for: indexPath) as! UserNotificationStyleTableViewCell - case .mentionPick: - return tableView.dequeueReusableCell(withIdentifier: String(describing: UserMentionPickStyleTableViewCell.self), for: indexPath) as! UserMentionPickStyleTableViewCell - case .listMember: - return tableView.dequeueReusableCell(withIdentifier: String(describing: UserListMemberStyleTableViewCell.self), for: indexPath) as! UserListMemberStyleTableViewCell - case .addListMember: - return tableView.dequeueReusableCell(withIdentifier: String(describing: UserAddListMemberStyleTableViewCell.self), for: indexPath) as! UserAddListMemberStyleTableViewCell - } - } +// static func dequeueReusableCell( +// tableView: UITableView, +// indexPath: IndexPath, +// style: UserView.Style +// ) -> UserTableViewCell { +// switch style { +// case .account: +// return tableView.dequeueReusableCell(withIdentifier: String(describing: UserAccountStyleTableViewCell.self), for: indexPath) as! UserAccountStyleTableViewCell +// case .relationship: +// return tableView.dequeueReusableCell(withIdentifier: String(describing: UserRelationshipStyleTableViewCell.self), for: indexPath) as! UserRelationshipStyleTableViewCell +// case .friendship: +// return tableView.dequeueReusableCell(withIdentifier: String(describing: UserFriendshipStyleTableViewCell.self), for: indexPath) as! UserFriendshipStyleTableViewCell +// case .notification: +// return tableView.dequeueReusableCell(withIdentifier: String(describing: UserNotificationStyleTableViewCell.self), for: indexPath) as! UserNotificationStyleTableViewCell +// case .mentionPick: +// return tableView.dequeueReusableCell(withIdentifier: String(describing: UserMentionPickStyleTableViewCell.self), for: indexPath) as! UserMentionPickStyleTableViewCell +// case .listMember: +// return tableView.dequeueReusableCell(withIdentifier: String(describing: UserListMemberStyleTableViewCell.self), for: indexPath) as! UserListMemberStyleTableViewCell +// case .addListMember: +// return tableView.dequeueReusableCell(withIdentifier: String(describing: UserAddListMemberStyleTableViewCell.self), for: indexPath) as! UserAddListMemberStyleTableViewCell +// } +// } - static func configure( - cell: UserTableViewCell, - viewModel: UserTableViewCell.ViewModel, - configuration: Configuration - ) { - cell.configure( - viewModel: viewModel, - configurationContext: configuration.userViewConfigurationContext, - delegate: configuration.userViewTableViewCellDelegate - ) - } +// static func configure( +// cell: UserTableViewCell, +// viewModel: UserTableViewCell.ViewModel, +// configuration: Configuration +// ) { +// cell.configure( +// viewModel: viewModel, +// configurationContext: configuration.userViewConfigurationContext, +// delegate: configuration.userViewTableViewCellDelegate +// ) +// } } diff --git a/TwidereX/Protocol/Facade/DataSourceFacade+Friendship.swift b/TwidereX/Protocol/Facade/DataSourceFacade+Friendship.swift index 41a19c74..f2308ee1 100644 --- a/TwidereX/Protocol/Facade/DataSourceFacade+Friendship.swift +++ b/TwidereX/Protocol/Facade/DataSourceFacade+Friendship.swift @@ -62,7 +62,7 @@ extension DataSourceFacade { do { switch (notification, authenticationContext) { - case (_, .twitter): + case (.twitter, .twitter): assertionFailure("Twitter notification has no entry for follow request") return case (.mastodon(let notification), .mastodon(let authenticationContext)): @@ -77,6 +77,9 @@ extension DataSourceFacade { }(), authenticationContext: authenticationContext ) + default: + assertionFailure() + return } // end switch await notificationFeedbackGenerator.notificationOccurred(.success) diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift index 4ebf153d..c6c39a28 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift @@ -38,54 +38,18 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC // MARK: - avatar button extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { - -// func tableViewCell( -// _ cell: UITableViewCell, -// statusView: StatusView, -// authorAvatarButtonDidPressed button: AvatarButton -// ) { -// Task { -// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) -// guard let item = await item(from: source) else { -// assertionFailure() -// return -// } -// guard let status = await item.status(in: self.context.managedObjectContext) else { -// assertionFailure("only works for status data provider") -// return -// } -// await DataSourceFacade.coordinateToProfileScene( -// provider: self, -// target: .status, -// status: status -// ) -// } -// } -// -// func tableViewCell( -// _ cell: UITableViewCell, -// statusView: StatusView, -// quoteStatusView: StatusView, -// authorAvatarButtonDidPressed button: AvatarButton -// ) { -// Task { -// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) -// guard let item = await item(from: source) else { -// assertionFailure() -// return -// } -// guard let status = await item.status(in: self.context.managedObjectContext) else { -// assertionFailure("only works for status data provider") -// return -// } -// await DataSourceFacade.coordinateToProfileScene( -// provider: self, -// target: .quote, -// status: status -// ) -// } -// } - + func tableViewCell( + _ cell: UITableViewCell, + viewModel: TwidereUI.StatusView.ViewModel, + userAvatarButtonDidPressed user: TwidereCore.UserRecord + ) { + Task { + await DataSourceFacade.coordinateToProfileScene( + provider: self, + user: user + ) + } // end Task + } } // MARK: - content @@ -136,10 +100,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, toggleContentDisplay isReveal: Bool) { Task { @MainActor in - guard let status = viewModel.status else { - assertionFailure() - return - } + let status = viewModel.status.asRecord try await DataSourceFacade.responseToToggleContentSensitiveAction( provider: self, @@ -151,10 +112,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, textViewDidSelectMeta meta: Meta) { Task { @MainActor in - guard let status = viewModel.status else { - assertionFailure() - return - } + let status = viewModel.status.asRecord await DataSourceFacade.responseToMetaText( provider: self, @@ -175,10 +133,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel ) { Task { - guard let status = viewModel.status else { - assertionFailure() - return - } + let status = viewModel.status.asRecord await DataSourceFacade.coordinateToMediaPreviewScene( provider: self, @@ -196,10 +151,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel, previewActionContext: ContextMenuInteractionPreviewActionContext ) { - guard let status = viewModel.status else { - assertionFailure() - return - } + let status = viewModel.status.asRecord DataSourceFacade.coordinateToMediaPreviewScene( provider: self, @@ -257,10 +209,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC toggleContentWarningOverlayDisplay isReveal: Bool ) { Task { - guard let status = viewModel.status else { - assertionFailure() - return - } + let status = viewModel.status.asRecord try await DataSourceFacade.responseToToggleMediaSensitiveAction( provider: self, target: .status, @@ -298,10 +247,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC pollVoteActionForViewModel pollViewModel: PollView.ViewModel ) { Task { - guard let status = viewModel.status else { - assertionFailure() - return - } + let status = viewModel.status.asRecord try await DataSourceFacade.responseToStatusPollVote( provider: self, @@ -320,10 +266,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Poll] not needs update. skip") return } - guard let status = viewModel.status else { - assertionFailure() - return - } + let status = viewModel.status.asRecord try await DataSourceFacade.responseToStatusPollUpdate( provider: self, @@ -340,10 +283,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC pollOptionDidSelectForViewModel optionViewModel: PollOptionView.ViewModel ) { Task { - guard let status = viewModel.status else { - assertionFailure() - return - } + let status = viewModel.status.asRecord await DataSourceFacade.responseToStatusPollOption( provider: self, @@ -426,10 +366,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC statusMetricButtonDidPressed action: StatusMetricView.Action ) { Task { - guard let status = viewModel.status else { - assertionFailure() - return - } + let status = viewModel.status.asRecord // TODO: } // end Task } @@ -444,10 +381,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC statusToolbarButtonDidPressed action: StatusToolbarView.Action ) { Task { - guard let status = viewModel.status else { - assertionFailure() - return - } + let status = viewModel.status.asRecord await DataSourceFacade.responseToStatusToolbar( provider: self, viewModel: viewModel, diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift index 45881fc1..4ff91370 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -35,14 +35,19 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid user: user ) case .notification(let notification): - let managedObjectContext = self.context.managedObjectContext - guard let object = notification.object(in: managedObjectContext) else { - assertionFailure() - return - } - switch object { + switch notification { + case .twitter(let status): + await DataSourceFacade.coordinateToStatusThreadScene( + provider: self, + kind: .status(.twitter(record: status)) + ) case .mastodon(let notification): - if let status = notification.status { + let managedObjectContext = self.context.managedObjectContext + guard let object = notification.object(in: managedObjectContext) else { + assertionFailure() + return + } + if let status = object.status { await DataSourceFacade.coordinateToStatusThreadScene( provider: self, kind: .status(.mastodon(record: status.asRecrod)) @@ -50,7 +55,7 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid } else { await DataSourceFacade.coordinateToProfileScene( provider: self, - user: .mastodon(record: notification.account.asRecrod) + user: .mastodon(record: object.account.asRecrod) ) } } diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift index eb31ebfc..3bd1d7bf 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift @@ -9,159 +9,213 @@ import UIKit import SwiftMessages +// MARK: - avatar button +extension UserViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { + func tableViewCell( + _ cell: UITableViewCell, + viewModel: UserView.ViewModel, + userAvatarButtonDidPressed user: UserRecord + ) { + Task { + await DataSourceFacade.coordinateToProfileScene( + provider: self, + user: user + ) + } // end Task + } +} + + // MARK: - menu button extension UserViewTableViewCellDelegate where Self: DataSourceProvider { - func tableViewCell( _ cell: UITableViewCell, - userView: UserView, - menuActionDidPressed action: UserView.MenuAction, - menuButton button: UIButton + viewModel: UserView.ViewModel, + menuActionDidPressed action: UserView.ViewModel.MenuAction ) { switch action { case .signOut: - // TODO: move to view controller Task { - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { - assertionFailure() - return - } - guard case let .user(user) = item else { - assertionFailure("only works for user data") - return - } try await DataSourceFacade.responseToUserSignOut( - dependency: self, - user: user + dependency: self, + user: viewModel.user.asRecord ) } // end Task case .remove: assertionFailure("Override in view controller") - } // end swtich + } } + +// func tableViewCell( +// _ cell: UITableViewCell, +// userView: UserView, +// menuActionDidPressed action: UserView.MenuAction, +// menuButton button: UIButton +// ) { +// switch action { +// case .signOut: +// // TODO: move to view controller +// Task { +// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) +// guard let item = await item(from: source) else { +// assertionFailure() +// return +// } +// guard case let .user(user) = item else { +// assertionFailure("only works for user data") +// return +// } +// try await DataSourceFacade.responseToUserSignOut( +// dependency: self, +// user: user +// ) +// } // end Task +// case .remove: +// assertionFailure("Override in view controller") +// } // end swtich +// } } // MARK: - friendship button extension UserViewTableViewCellDelegate where Self: DataSourceProvider { - func tableViewCell( - _ cell: UITableViewCell, - userView: UserView, - friendshipButtonDidPressed button: UIButton - ) { - assertionFailure("TODO") - } +// func tableViewCell( +// _ cell: UITableViewCell, +// userView: UserView, +// friendshipButtonDidPressed button: UIButton +// ) { +// assertionFailure("TODO") +// } } // MARK: - membership extension UserViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { - func tableViewCell( - _ cell: UITableViewCell, - userView: UserView, - membershipButtonDidPressed button: UIButton - ) { - guard !userView.viewModel.isListMemberCandidate else { - return - } - - Task { @MainActor in - let authenticationContext = self.authContext.authenticationContext - - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { - assertionFailure() - return - } - guard case let .user(user) = item else { - assertionFailure("only works for user data") - return - } - - guard let listMembershipViewModel = userView.viewModel.listMembershipViewModel else { - assertionFailure() - return - } - - do { - if userView.viewModel.isListMember { - try await listMembershipViewModel.remove(user: user, authenticationContext: authenticationContext) - } else { - try await listMembershipViewModel.add(user: user, authenticationContext: authenticationContext) - } - } catch { - var config = SwiftMessages.defaultConfig - config.duration = .seconds(seconds: 3) - config.interactiveHide = true - let bannerView = NotificationBannerView() - bannerView.configure(style: .warning) - bannerView.titleLabel.text = L10n.Common.Alerts.FailedToAddListMember.title - bannerView.messageLabel.text = error.localizedDescription - SwiftMessages.show(config: config, view: bannerView) - } - } // end Task - } +// func tableViewCell( +// _ cell: UITableViewCell, +// userView: UserView, +// membershipButtonDidPressed button: UIButton +// ) { +// guard !userView.viewModel.isListMemberCandidate else { +// return +// } +// +// Task { @MainActor in +// let authenticationContext = self.authContext.authenticationContext +// +// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) +// guard let item = await item(from: source) else { +// assertionFailure() +// return +// } +// guard case let .user(user) = item else { +// assertionFailure("only works for user data") +// return +// } +// +// guard let listMembershipViewModel = userView.viewModel.listMembershipViewModel else { +// assertionFailure() +// return +// } +// +// do { +// if userView.viewModel.isListMember { +// try await listMembershipViewModel.remove(user: user, authenticationContext: authenticationContext) +// } else { +// try await listMembershipViewModel.add(user: user, authenticationContext: authenticationContext) +// } +// } catch { +// var config = SwiftMessages.defaultConfig +// config.duration = .seconds(seconds: 3) +// config.interactiveHide = true +// let bannerView = NotificationBannerView() +// bannerView.configure(style: .warning) +// bannerView.titleLabel.text = L10n.Common.Alerts.FailedToAddListMember.title +// bannerView.messageLabel.text = error.localizedDescription +// SwiftMessages.show(config: config, view: bannerView) +// } +// } // end Task +// } } // MARK: - follow request extension UserViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { - - func tableViewCell( - _ cell: UITableViewCell, - userView: UserView, - acceptFollowReqeustButtonDidPressed button: UIButton - ) { - Task { - let authenticationContext = self.authContext.authenticationContext - - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { - assertionFailure() - return - } - guard case let .notification(notification) = item else { - assertionFailure("only works for notification data") - return - } - - try await DataSourceFacade.responseToUserFollowRequestAction( - dependency: self, - notification: notification, - query: .accept, - authenticationContext: authenticationContext - ) - } // end Task - } - func tableViewCell( _ cell: UITableViewCell, - userView: UserView, - rejectFollowReqeustButtonDidPressed button: UIButton + viewModel: UserView.ViewModel, + followReqeustButtonDidPressed user: UserRecord, + accept: Bool ) { Task { - let authenticationContext = self.authContext.authenticationContext - - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { + guard let notification = viewModel.notification?.asRecord else { assertionFailure() return } - guard case let .notification(notification) = item else { - assertionFailure("only works for notification data") - return - } try await DataSourceFacade.responseToUserFollowRequestAction( dependency: self, notification: notification, - query: .reject, - authenticationContext: authenticationContext + query: accept ? .accept : .reject, + authenticationContext: self.authContext.authenticationContext ) } // end Task } +// func tableViewCell( +// _ cell: UITableViewCell, +// userView: UserView, +// acceptFollowReqeustButtonDidPressed button: UIButton +// ) { +// Task { +// let authenticationContext = self.authContext.authenticationContext +// +// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) +// guard let item = await item(from: source) else { +// assertionFailure() +// return +// } +// guard case let .notification(notification) = item else { +// assertionFailure("only works for notification data") +// return +// } +// +// try await DataSourceFacade.responseToUserFollowRequestAction( +// dependency: self, +// notification: notification, +// query: .accept, +// authenticationContext: authenticationContext +// ) +// } // end Task +// } +// +// func tableViewCell( +// _ cell: UITableViewCell, +// userView: UserView, +// rejectFollowReqeustButtonDidPressed button: UIButton +// ) { +// Task { +// let authenticationContext = self.authContext.authenticationContext +// +// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) +// guard let item = await item(from: source) else { +// assertionFailure() +// return +// } +// guard case let .notification(notification) = item else { +// assertionFailure("only works for notification data") +// return +// } +// +// try await DataSourceFacade.responseToUserFollowRequestAction( +// dependency: self, +// notification: notification, +// query: .reject, +// authenticationContext: authenticationContext +// ) +// } // end Task +// } + } diff --git a/TwidereX/Scene/Account/List/AccountListViewController.swift b/TwidereX/Scene/Account/List/AccountListViewController.swift index 8602e861..a7a7758a 100644 --- a/TwidereX/Scene/Account/List/AccountListViewController.swift +++ b/TwidereX/Scene/Account/List/AccountListViewController.swift @@ -117,7 +117,7 @@ extension AccountListViewController: UITableViewDelegate { do { let isActive = try await self.context.authenticationService.activeAuthenticationIndex(record: record) guard isActive else { return } - self.coordinator.setup() + self.coordinator.setup(authentication: record) } catch { // handle error assertionFailure(error.localizedDescription) diff --git a/TwidereX/Scene/Account/List/AccountListViewModel+Diffable.swift b/TwidereX/Scene/Account/List/AccountListViewModel+Diffable.swift index 3df0efc2..c322e0da 100644 --- a/TwidereX/Scene/Account/List/AccountListViewModel+Diffable.swift +++ b/TwidereX/Scene/Account/List/AccountListViewModel+Diffable.swift @@ -23,11 +23,7 @@ extension AccountListViewModel { context: context, authContext: authContext, configuration: UserSection.Configuration( - userViewTableViewCellDelegate: userViewTableViewCellDelegate, - userViewConfigurationContext: .init( - authContext: authContext, - listMembershipViewModel: nil - ) + userViewTableViewCellDelegate: userViewTableViewCellDelegate ) ) diff --git a/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift b/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift index 29a088ad..9fe3a4f1 100644 --- a/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift +++ b/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift @@ -21,10 +21,6 @@ extension StatusHistoryViewModel { configuration: .init( statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, userViewTableViewCellDelegate: nil, - userViewConfigurationContext: .init( - authContext: authContext, - listMembershipViewModel: nil - ), viewLayoutFramePublisher: $viewLayoutFrame ) ) diff --git a/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift b/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift index 3bc1aff5..3e5fbf65 100644 --- a/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift +++ b/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift @@ -21,10 +21,6 @@ extension UserHistoryViewModel { configuration: .init( statusViewTableViewCellDelegate: nil, userViewTableViewCellDelegate: userViewTableViewCellDelegate, - userViewConfigurationContext: .init( - authContext: authContext, - listMembershipViewModel: nil - ), viewLayoutFramePublisher: $viewLayoutFrame ) ) diff --git a/TwidereX/Scene/List/ListUser/ListUserViewController.swift b/TwidereX/Scene/List/ListUser/ListUserViewController.swift index 42891947..92f90c14 100644 --- a/TwidereX/Scene/List/ListUser/ListUserViewController.swift +++ b/TwidereX/Scene/List/ListUser/ListUserViewController.swift @@ -134,59 +134,59 @@ extension ListUserViewController: UITableViewDelegate, AutoGenerateTableViewDele // MARK: - UserViewTableViewCellDelegate extension ListUserViewController: UserViewTableViewCellDelegate { - func tableViewCell( - _ cell: UITableViewCell, - userView: UserView, - menuActionDidPressed action: UserView.MenuAction, - menuButton button: UIButton - ) { - switch action { - case .remove: - Task { - let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) - guard let item = await item(from: source) else { - assertionFailure() - return - } - guard case let .user(user) = item else { - assertionFailure("only works for status data provider") - return - } - - let authenticationContext = self.viewModel.authContext.authenticationContext - - do { - let list = self.viewModel.kind.list - _ = try await self.context.apiService.removeListMember( - list: list, - user: user, - authenticationContext: authenticationContext - ) - await self.viewModel.update(user: user, action: .remove) - - var config = SwiftMessages.defaultConfig - config.duration = .seconds(seconds: 3) - config.interactiveHide = true - let bannerView = NotificationBannerView() - bannerView.configure(style: .success) - bannerView.titleLabel.text = L10n.Common.Alerts.ListMemberRemoved.title - bannerView.messageLabel.isHidden = true - SwiftMessages.show(config: config, view: bannerView) - } catch { - var config = SwiftMessages.defaultConfig - config.duration = .seconds(seconds: 3) - config.interactiveHide = true - let bannerView = NotificationBannerView() - bannerView.configure(style: .warning) - bannerView.titleLabel.text = L10n.Common.Alerts.FailedToRemoveListMember.title - bannerView.messageLabel.text = L10n.Common.Alerts.FailedToRemoveListMember.message - SwiftMessages.show(config: config, view: bannerView) - } - } // end Task - default: - assertionFailure() - } // end swtich - } +// func tableViewCell( +// _ cell: UITableViewCell, +// userView: UserView, +// menuActionDidPressed action: UserView.MenuAction, +// menuButton button: UIButton +// ) { +// switch action { +// case .remove: +// Task { +// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) +// guard let item = await item(from: source) else { +// assertionFailure() +// return +// } +// guard case let .user(user) = item else { +// assertionFailure("only works for status data provider") +// return +// } +// +// let authenticationContext = self.viewModel.authContext.authenticationContext +// +// do { +// let list = self.viewModel.kind.list +// _ = try await self.context.apiService.removeListMember( +// list: list, +// user: user, +// authenticationContext: authenticationContext +// ) +// await self.viewModel.update(user: user, action: .remove) +// +// var config = SwiftMessages.defaultConfig +// config.duration = .seconds(seconds: 3) +// config.interactiveHide = true +// let bannerView = NotificationBannerView() +// bannerView.configure(style: .success) +// bannerView.titleLabel.text = L10n.Common.Alerts.ListMemberRemoved.title +// bannerView.messageLabel.isHidden = true +// SwiftMessages.show(config: config, view: bannerView) +// } catch { +// var config = SwiftMessages.defaultConfig +// config.duration = .seconds(seconds: 3) +// config.interactiveHide = true +// let bannerView = NotificationBannerView() +// bannerView.configure(style: .warning) +// bannerView.titleLabel.text = L10n.Common.Alerts.FailedToRemoveListMember.title +// bannerView.messageLabel.text = L10n.Common.Alerts.FailedToRemoveListMember.message +// SwiftMessages.show(config: config, view: bannerView) +// } +// } // end Task +// default: +// assertionFailure() +// } // end swtich +// } } // MARK: - ListMembershipViewModelDelegate diff --git a/TwidereX/Scene/List/ListUser/ListUserViewModel+Diffable.swift b/TwidereX/Scene/List/ListUser/ListUserViewModel+Diffable.swift index 51619179..cf21595d 100644 --- a/TwidereX/Scene/List/ListUser/ListUserViewModel+Diffable.swift +++ b/TwidereX/Scene/List/ListUser/ListUserViewModel+Diffable.swift @@ -20,11 +20,7 @@ extension ListUserViewModel { context: context, authContext: authContext, configuration: UserSection.Configuration( - userViewTableViewCellDelegate: userViewTableViewCellDelegate, - userViewConfigurationContext: .init( - authContext: authContext, - listMembershipViewModel: listMembershipViewModel - ) + userViewTableViewCellDelegate: userViewTableViewCellDelegate ) ) @@ -36,7 +32,7 @@ extension ListUserViewModel { snapshot.appendSections([.main]) - let items = records.map { UserItem.user(record: $0, style: .listMember) } + let items = records.map { UserItem.user(record: $0, kind: .listMember) } snapshot.appendItems(items, toSection: .main) let currentState = await self.stateMachine.currentState diff --git a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift index aaa2f4ce..faee41e1 100644 --- a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift +++ b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift @@ -28,12 +28,10 @@ extension NotificationTimelineViewController: DataSourceProvider { guard let feed = record.object(in: managedObjectContext) else { return nil } let content = feed.content switch content { - case .twitter(let status): - return .status(.twitter(record: .init(objectID: status.objectID))) - case .mastodon(let status): - return .status(.mastodon(record: .init(objectID: status.objectID))) - case .mastodonNotification(let notification): - return .notification(.mastodon(record: .init(objectID: notification.objectID))) + case .status(let object): + return .status(object.asRecord) + case .notification(let object): + return .notification(object.asRecord) case .none: return nil } diff --git a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift index a2343a77..5c196eba 100644 --- a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift +++ b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift @@ -30,10 +30,6 @@ final class NotificationTimelineViewController: UIViewController, NeedsDependenc private(set) lazy var tableView: UITableView = { let tableView = UITableView() - tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self)) - tableView.register(UserNotificationStyleTableViewCell.self, forCellReuseIdentifier: String(describing: UserNotificationStyleTableViewCell.self)) - tableView.register(TimelineMiddleLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self)) - tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) tableView.backgroundColor = .systemBackground tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .none @@ -119,6 +115,29 @@ extension NotificationTimelineViewController { } } } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + viewModel.viewLayoutFrame.update(view: view) + } + + override func viewSafeAreaInsetsDidChange() { + super.viewSafeAreaInsetsDidChange() + + viewModel.viewLayoutFrame.update(view: view) + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + coordinator.animate {[weak self] _ in + guard let self = self else { return } + self.viewModel.viewLayoutFrame.update(view: self.view) + } completion: { _ in + // do nothing + } + } } extension NotificationTimelineViewController { diff --git a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift index 875eb763..fe2229ab 100644 --- a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift @@ -23,15 +23,12 @@ extension NotificationTimelineViewModel { let configuration = NotificationSection.Configuration( statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, userViewTableViewCellDelegate: userViewTableViewCellDelegate, - userViewConfigurationContext: .init( - authContext: authContext, - listMembershipViewModel: nil - ), viewLayoutFramePublisher: $viewLayoutFrame ) diffableDataSource = NotificationSection.diffableDataSource( tableView: tableView, context: context, + authContext: authContext, configuration: configuration ) @@ -179,7 +176,11 @@ extension NotificationTimelineViewModel { // fetch data do { - switch (feed.content, authenticationContext) { + guard case let .notification(object) = feed.content else { + assertionFailure() + throw AppError.implicit(.badRequest) + } + switch (object, authenticationContext) { case (.twitter(let status), .twitter(let authenticationContext)): let query = Twitter.API.Statuses.Timeline.TimelineQuery( count: 20, @@ -190,13 +191,13 @@ extension NotificationTimelineViewModel { authenticationContext: authenticationContext ) - case (.mastodonNotification(let mastodonNotification), .mastodon(let authenticationContext)): + case (.mastodon(let notification), .mastodon(let authenticationContext)): guard case let .mastodon(timelineScope) = scope else { throw AppError.implicit(.badRequest) } _ = try await context.apiService.mastodonNotificationTimeline( query: .init( - maxID: mastodonNotification.id, + maxID: notification.id, types: timelineScope.includeTypes, excludeTypes: timelineScope.excludeTypes ), diff --git a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift index a1e5a476..c8aa8f0d 100644 --- a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift +++ b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift @@ -68,7 +68,8 @@ extension NotificationTimelineViewModel.LoadOldestState { let managedObjectContext = viewModel.context.managedObjectContext let _input: NotificationFetchViewModel.Input? = try await managedObjectContext.perform { guard let feed = lsatFeedRecord.object(in: managedObjectContext) else { return nil } - switch (feed.content, authenticationContext) { + guard case let .notification(object) = feed.content else { return nil } + switch (object, authenticationContext) { case (.twitter(let status), .twitter(let authenticationContext)): return NotificationFetchViewModel.Input.twitter(.init( authenticationContext: authenticationContext, @@ -76,7 +77,7 @@ extension NotificationTimelineViewModel.LoadOldestState { count: 20 )) - case (.mastodonNotification(let notification), .mastodon(let authenticationContext)): + case (.mastodon(let notification), .mastodon(let authenticationContext)): guard case let .mastodon(timelineScope) = viewModel.scope else { throw AppError.implicit(.badRequest) } diff --git a/TwidereX/Scene/Profile/FriendshipList/FollowingListViewModel+Diffable.swift b/TwidereX/Scene/Profile/FriendshipList/FollowingListViewModel+Diffable.swift index b7f80056..858653ea 100644 --- a/TwidereX/Scene/Profile/FriendshipList/FollowingListViewModel+Diffable.swift +++ b/TwidereX/Scene/Profile/FriendshipList/FollowingListViewModel+Diffable.swift @@ -18,11 +18,7 @@ extension FriendshipListViewModel { tableView: UITableView ) { let configuration = UserSection.Configuration( - userViewTableViewCellDelegate: nil, - userViewConfigurationContext: .init( - authContext: authContext, - listMembershipViewModel: nil - ) + userViewTableViewCellDelegate: nil ) diffableDataSource = UserSection.diffableDataSource( @@ -56,7 +52,7 @@ extension FriendshipListViewModel { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) let newItems: [UserItem] = records.map { - .user(record: $0, style: .friendship) + .user(record: $0, kind: .friendship) } snapshot.appendItems(newItems, toSection: .main) return snapshot diff --git a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewController.swift b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewController.swift index 01bf425f..995d0530 100644 --- a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewController.swift +++ b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewController.swift @@ -26,8 +26,8 @@ final class SearchUserViewController: UIViewController, NeedsDependency { lazy var tableView: UITableView = { let tableView = UITableView() - tableView.register(UserRelationshipStyleTableViewCell.self, forCellReuseIdentifier: String(describing: UserRelationshipStyleTableViewCell.self)) - tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) +// tableView.register(UserRelationshipStyleTableViewCell.self, forCellReuseIdentifier: String(describing: UserRelationshipStyleTableViewCell.self)) +// tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .none return tableView diff --git a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+Diffable.swift b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+Diffable.swift index d47e9a1c..3fbf33a4 100644 --- a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+Diffable.swift +++ b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+Diffable.swift @@ -23,11 +23,7 @@ extension SearchUserViewModel { context: context, authContext: authContext, configuration: UserSection.Configuration( - userViewTableViewCellDelegate: userViewTableViewCellDelegate, - userViewConfigurationContext: .init( - authContext: authContext, - listMembershipViewModel: listMembershipViewModel - ) + userViewTableViewCellDelegate: userViewTableViewCellDelegate ) ) @@ -57,9 +53,9 @@ extension SearchUserViewModel { let newItems: [UserItem] = records.map { record in switch self.kind { case .friendship: - return .user(record: record, style: .relationship) + return .user(record: record, kind: .relationship) case .listMember: - return .user(record: record, style: .addListMember) + return .user(record: record, kind: .addListMember) } // end switch } snapshot.appendItems(newItems, toSection: .main) diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell+ViewModel.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell+ViewModel.swift deleted file mode 100644 index 6b4238fe..00000000 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell+ViewModel.swift +++ /dev/null @@ -1,128 +0,0 @@ -// -// StatusTableViewCell+ViewModel.swift -// StatusTableViewCell+ViewModel -// -// Created by Cirno MainasuK on 2021-8-27. -// Copyright © 2021 Twidere. All rights reserved. -// - -import UIKit -import Combine -import SwiftUI -import CoreDataStack - -extension StatusTableViewCell { - - final class ViewModel { - enum Value { - case feed(Feed) - case statusObject(StatusObject) - case twitterStatus(TwitterStatus) - case mastodonStatus(MastodonStatus) - } - - let value: Value - - init(value: Value) { - self.value = value - } - } - -// func configure( -// tableView: UITableView, -// viewModel: ViewModel, -// configurationContext: StatusView.ConfigurationContext, -// delegate: StatusViewTableViewCellDelegate? -// ) { -// if statusView.frame == .zero { -// // set status view width -// statusView.frame.size.width = tableView.readableContentGuide.layoutFrame.width -// let contentMaxLayoutWidth = statusView.contentMaxLayoutWidth -// statusView.quoteStatusView?.frame.size.width = contentMaxLayoutWidth -// // set preferredMaxLayoutWidth for content -// statusView.spoilerContentTextView.preferredMaxLayoutWidth = contentMaxLayoutWidth -// statusView.contentTextView.preferredMaxLayoutWidth = contentMaxLayoutWidth -// statusView.quoteStatusView?.contentTextView.preferredMaxLayoutWidth = statusView.quoteStatusView?.contentMaxLayoutWidth -// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): did layout for new cell") -// } -// -// switch viewModel.value { -// case .feed(let feed): -// statusView.configure( -// feed: feed, -// configurationContext: configurationContext -// ) -// configureSeparator(style: feed.hasMore ? .edge : .inset) -// case .statusObject(let object): -// statusView.configure( -// statusObject: object, -// configurationContext: configurationContext -// ) -// configureSeparator(style: .inset) -// case .twitterStatus(let status): -// statusView.configure( -// status: status, -// configurationContext: configurationContext -// ) -// configureSeparator(style: .inset) -// case .mastodonStatus(let status): -// statusView.configure( -// status: status, -// notification: nil, -// configurationContext: configurationContext -// ) -// configureSeparator(style: .inset) -// } -// -// self.delegate = delegate -// -// statusView.viewModel.$isContentReveal -// .removeDuplicates() -// .dropFirst() -// .receive(on: DispatchQueue.main) -// .sink { [weak tableView, weak self] _ in -// guard let tableView = tableView else { return } -// guard let _ = self else { return } -// UIView.setAnimationsEnabled(false) -// tableView.beginUpdates() -// tableView.endUpdates() -// UIView.setAnimationsEnabled(true) -// } -// .store(in: &disposeBag) -// } - -} - - -extension StatusTableViewCell { - enum SeparatorStyle { - case edge - case inset - } - - func configureSeparator(style: SeparatorStyle) { -// separator.removeFromSuperview() -// separator.removeConstraints(separator.constraints) -// -// switch style { -// case .edge: -// separator.translatesAutoresizingMaskIntoConstraints = false -// contentView.addSubview(separator) -// NSLayoutConstraint.activate([ -// separator.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), -// separator.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), -// separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), -// separator.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), -// ]) -// case .inset: -// separator.translatesAutoresizingMaskIntoConstraints = false -// contentView.addSubview(separator) -// NSLayoutConstraint.activate([ -// separator.leadingAnchor.constraint(equalTo: statusView.toolbar.leadingAnchor), -// separator.trailingAnchor.constraint(equalTo: statusView.toolbar.trailingAnchor), -// separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), -// separator.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), -// ]) -// } - } -} diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift deleted file mode 100644 index e8db74bf..00000000 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift +++ /dev/null @@ -1,116 +0,0 @@ -// -// StatusTableViewCell.swift -// StatusTableViewCell -// -// Created by Cirno MainasuK on 2021-8-20. -// Copyright © 2021 Twidere. All rights reserved. -// - -import os.log -import UIKit -import Combine - -class StatusTableViewCell: UITableViewCell { - - private var _disposeBag = Set() - var disposeBag = Set() - - let logger = Logger(subsystem: "StatusTableViewCell", category: "View") - - weak var delegate: StatusViewTableViewCellDelegate? - var viewModel: StatusView.ViewModel? - - override func prepareForReuse() { - super.prepareForReuse() - - contentConfiguration = nil - delegate = nil - viewModel = nil - disposeBag.removeAll() - } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension StatusTableViewCell { - - private func _init() { - selectionStyle = .none - -// statusView.translatesAutoresizingMaskIntoConstraints = false -// contentView.addSubview(statusView) -// NSLayoutConstraint.activate([ -// statusView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), -// statusView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), -// statusView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), -// statusView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), -// ]) -// statusView.setup(style: .inline) -// statusView.toolbar.setup(style: .inline) -// -// topConversationLinkLineView.translatesAutoresizingMaskIntoConstraints = false -// contentView.addSubview(topConversationLinkLineView) -// NSLayoutConstraint.activate([ -// topConversationLinkLineView.topAnchor.constraint(equalTo: contentView.topAnchor), -// topConversationLinkLineView.centerXAnchor.constraint(equalTo: statusView.authorAvatarButton.centerXAnchor), -// topConversationLinkLineView.widthAnchor.constraint(equalToConstant: 1), -// statusView.authorAvatarButton.topAnchor.constraint(equalTo: topConversationLinkLineView.bottomAnchor, constant: 2), -// ]) -// topConversationLinkLineView.isHidden = true -// -// bottomConversationLinkLineView.translatesAutoresizingMaskIntoConstraints = false -// contentView.addSubview(bottomConversationLinkLineView) -// NSLayoutConstraint.activate([ -// bottomConversationLinkLineView.topAnchor.constraint(equalTo: statusView.authorAvatarButton.bottomAnchor, constant: 2), -// bottomConversationLinkLineView.centerXAnchor.constraint(equalTo: statusView.authorAvatarButton.centerXAnchor), -// bottomConversationLinkLineView.widthAnchor.constraint(equalToConstant: 1), -// contentView.bottomAnchor.constraint(equalTo: bottomConversationLinkLineView.bottomAnchor), -// ]) -// bottomConversationLinkLineView.isHidden = true -// -// statusView.delegate = self - - // a11y -// isAccessibilityElement = true -// statusView.viewModel.$groupedAccessibilityLabel -// .receive(on: DispatchQueue.main) -// .sink { [weak self] accessibilityLabel in -// guard let self = self else { return } -// self.accessibilityLabel = accessibilityLabel -// } -// .store(in: &_disposeBag) - } - -// override func accessibilityActivate() -> Bool { -// delegate?.tableViewCell(self, statusView: statusView, accessibilityActivate: Void()) -// return true -// } - -} - -extension StatusTableViewCell { - -// func setTopConversationLinkLineViewDisplay() { -// topConversationLinkLineView.isHidden = false -// } -// -// func setBottomConversationLinkLineViewDisplay() { -// bottomConversationLinkLineView.isHidden = false -// } - -} - -// MARK: - StatusViewContainerTableViewCell -extension StatusTableViewCell: StatusViewContainerTableViewCell { } - -// MARK: - StatusViewDelegate -extension StatusTableViewCell: StatusViewDelegate { } diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewController+DataSourceProvider.swift b/TwidereX/Scene/StatusThread/StatusThreadViewController+DataSourceProvider.swift index c0af79ef..1953be81 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewController+DataSourceProvider.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewController+DataSourceProvider.swift @@ -22,7 +22,7 @@ extension StatusThreadViewController: DataSourceProvider { switch item { case .root: - guard let status = viewModel.statusViewModel?.status else { return nil } + guard let status = viewModel.statusViewModel?.status.asRecord else { return nil } return .status(status) case .status(let status): return .status(status) diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift index 4a8940df..24208f1c 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift @@ -28,7 +28,7 @@ extension StatusThreadViewModel { switch item { case .status(let record): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell - cell.delegate = statusViewTableViewCellDelegate + cell.statusViewTableViewCellDelegate = statusViewTableViewCellDelegate self.context.managedObjectContext.performAndWait { guard let status = record.object(in: self.context.managedObjectContext) else { return } let viewModel = StatusView.ViewModel( @@ -52,7 +52,7 @@ extension StatusThreadViewModel { guard let viewModel = self.statusViewModel else { return UITableViewCell() } - cell.delegate = statusViewTableViewCellDelegate + cell.statusViewTableViewCellDelegate = statusViewTableViewCellDelegate self.updateConversationRootLink(viewModel: viewModel) cell.contentConfiguration = UIHostingConfiguration { StatusView(viewModel: viewModel) @@ -212,7 +212,7 @@ extension StatusThreadViewModel { } private func updateConversationRootLink(viewModel: StatusView.ViewModel) { - guard let record = viewModel.status else { return } + let record = viewModel.status.asRecord guard let linkConfiguration = conversationLinkConfiguration[record] else { return } viewModel.isTopConversationLinkLineViewDisplay = linkConfiguration.isTopLinkDisplay diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift index 5504f844..519d20b9 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift @@ -313,7 +313,7 @@ extension StatusThreadViewModel { isLoadTop = true defer { isLoadTop = false } - guard let status = self.statusViewModel?.status else { return } + guard let status = self.statusViewModel?.status.asRecord else { return } guard case .value(let cursor) = topCursor else { return } try await fetchConversation(status: status, cursor: .value(cursor)) } @@ -324,7 +324,7 @@ extension StatusThreadViewModel { isLoadBottom = true defer { isLoadBottom = false } - guard let status = self.statusViewModel?.status else { return } + guard let status = self.statusViewModel?.status.asRecord else { return } guard case .value(let cursor) = bottomCursor else { return } try await fetchConversation(status: status, cursor: .value(cursor)) } diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift index 86e630ae..354a3d8e 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift @@ -107,7 +107,8 @@ extension TimelineViewModel.LoadOldestState { case .home: guard let record = viewModel.feedFetchedResultsController.records.last else { return nil } guard let feed = record.object(in: managedObjectContext) else { return nil } - return feed.statusObject?.asRecord + guard case let .status(status) = feed.content else { return nil } + return status.asRecord case .public, .hashtag, .list, .search, .user: guard let status = viewModel.statusRecordFetchedResultController.records.last else { return nil } return status diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift index 3e089f10..23bd88f7 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift @@ -149,7 +149,8 @@ extension TimelineViewModel { let anchor: StatusRecord? = { guard let record = feedFetchedResultsController.records.first else { return nil } guard let feed = record.object(in: managedObjectContext) else { return nil } - return feed.statusObject?.asRecord + guard case let .status(status) = feed.content else { return nil } + return status.asRecord }() return .top(anchor: anchor) case .public, .hashtag, .list: diff --git a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift index b2dd60b0..c97750e6 100644 --- a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift @@ -45,7 +45,10 @@ extension ListTimelineViewModel { @MainActor func loadMore(item: StatusItem) async { - guard case let .feedLoader(record) = item else { return } + guard case let .feedLoader(record) = item else { + assertionFailure() + return + } guard let diffableDataSource = diffableDataSource else { return } var snapshot = diffableDataSource.snapshot() @@ -55,7 +58,7 @@ extension ListTimelineViewModel { let key = "LoadMore@\(record.objectID)#\(UUID().uuidString)" guard let feed = record.object(in: managedObjectContext) else { return } - guard let statusObject = feed.statusObject else { return } + guard case let .status(status) = feed.content else { return } // keep transient property alive managedObjectContext.cache(feed, key: key) @@ -81,7 +84,7 @@ extension ListTimelineViewModel { managedObjectContext: managedObjectContext, authenticationContext: authenticationContext, kind: kind, - position: .middle(anchor: statusObject.asRecord), + position: .middle(anchor: status.asRecord), filter: StatusFetchViewModel.Timeline.Filter(rule: .empty) ) let input = try await StatusFetchViewModel.Timeline.prepare(fetchContext: fetchContext) diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel.swift index 3364438d..4f4601eb 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel.swift @@ -60,9 +60,10 @@ extension HomeTimelineViewModel { var sinceID: String? { guard let first = feedFetchedResultsController.records.first, - let object = first.object(in: context.managedObjectContext)?.statusObject + let feed = first.object(in: context.managedObjectContext), + case let .status(status) = feed.content else { return nil } - return object.id + return status.id } } From eb642fe812fab1b4a0485ad3ee415d5b636ff250 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 19 Apr 2023 17:24:13 +0800 Subject: [PATCH 057/128] feat: migrate the search and profile scene to SwiftUI --- .../Example/Base.lproj/Main.storyboard | 24 -- ...erFlowStackCollectionViewLayoutTests.swift | 11 - .../contents.xcworkspacedata | 0 .../Example/Example.xcodeproj/project.pbxproj | 26 +- .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcschemes/Example - RTL.xcscheme | 0 .../xcshareddata/xcschemes/Example.xcscheme | 0 .../Example/Example/AppDelegate.swift | 0 .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../Example/Assets.xcassets/Contents.json | 0 .../Base.lproj/LaunchScreen.storyboard | 0 .../Example/Base.lproj/Main.storyboard | 107 +++++++ .../Example/Example/Info.plist | 0 .../Example/Example/SceneDelegate.swift | 0 .../Example/SwiftUIViewController.swift | 279 ++++++++++++++++++ .../Example/Example/UIKitViewController.swift | 8 +- .../Package.swift | 14 +- .../README.md | 0 .../CoverFlowStackCollectionViewLayout.swift | 0 .../CoverFlowStackLayoutAttributes.swift | 0 .../CoverFlowStackScrollView.swift | 67 +++++ ...erFlowStackCollectionViewLayoutTests.swift | 7 + TwidereSDK/Package.swift | 2 + .../TwidereUI/Button/FollowButton.swift | 69 +++++ .../Container/MediaGridContainerView.swift | 22 +- .../Container/MediaStackContainerView.swift | 248 ++++++++++++++++ .../Content/MediaMetaIndicatorView.swift | 38 +++ .../Content/MediaView+ViewModel.swift | 43 +-- .../Content/UserView+ViewModel.swift | 23 +- .../Sources/TwidereUI/Content/UserView.swift | 13 +- .../Status/StatusTableViewCell.swift | 14 + TwidereX.xcodeproj/project.pbxproj | 11 +- .../Status/StatusMediaGallerySection.swift | 32 +- TwidereX/Diffable/Status/StatusSection.swift | 48 ++- .../List/AccountListViewController.swift | 1 - .../AccountListTableViewCell+ViewModel.swift | 150 +++++----- .../List/View/AccountListTableViewCell.swift | 132 ++++----- .../FollowingListViewModel+Diffable.swift | 2 +- .../SearchResult/SearchResultViewModel.swift | 2 +- .../User/SearchUserViewModel+Diffable.swift | 4 +- .../User/SearchUserViewModel+State.swift | 2 +- .../User/SearchUserViewModel.swift | 2 +- .../StatusMediaGalleryCollectionCell.swift | 89 +++--- 45 files changed, 1136 insertions(+), 361 deletions(-) delete mode 100644 CoverFlowStackCollectionViewLayout/Example/Example/Base.lproj/Main.storyboard delete mode 100644 CoverFlowStackCollectionViewLayout/Tests/CoverFlowStackCollectionViewLayoutTests/CoverFlowStackCollectionViewLayoutTests.swift rename {CoverFlowStackCollectionViewLayout/Example/Example.xcodeproj/project.xcworkspace => CoverFlowStackLayout/.swiftpm/xcode/package.xcworkspace}/contents.xcworkspacedata (100%) rename {CoverFlowStackCollectionViewLayout => CoverFlowStackLayout}/Example/Example.xcodeproj/project.pbxproj (89%) create mode 100644 CoverFlowStackLayout/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename {CoverFlowStackCollectionViewLayout => CoverFlowStackLayout}/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {CoverFlowStackCollectionViewLayout => CoverFlowStackLayout}/Example/Example.xcodeproj/xcshareddata/xcschemes/Example - RTL.xcscheme (100%) rename {CoverFlowStackCollectionViewLayout => CoverFlowStackLayout}/Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme (100%) rename {CoverFlowStackCollectionViewLayout => CoverFlowStackLayout}/Example/Example/AppDelegate.swift (100%) rename {CoverFlowStackCollectionViewLayout => CoverFlowStackLayout}/Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename {CoverFlowStackCollectionViewLayout => CoverFlowStackLayout}/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {CoverFlowStackCollectionViewLayout => CoverFlowStackLayout}/Example/Example/Assets.xcassets/Contents.json (100%) rename {CoverFlowStackCollectionViewLayout => CoverFlowStackLayout}/Example/Example/Base.lproj/LaunchScreen.storyboard (100%) create mode 100644 CoverFlowStackLayout/Example/Example/Base.lproj/Main.storyboard rename {CoverFlowStackCollectionViewLayout => CoverFlowStackLayout}/Example/Example/Info.plist (100%) rename {CoverFlowStackCollectionViewLayout => CoverFlowStackLayout}/Example/Example/SceneDelegate.swift (100%) create mode 100644 CoverFlowStackLayout/Example/Example/SwiftUIViewController.swift rename CoverFlowStackCollectionViewLayout/Example/Example/ViewController.swift => CoverFlowStackLayout/Example/Example/UIKitViewController.swift (92%) rename {CoverFlowStackCollectionViewLayout => CoverFlowStackLayout}/Package.swift (74%) rename {CoverFlowStackCollectionViewLayout => CoverFlowStackLayout}/README.md (100%) rename {CoverFlowStackCollectionViewLayout => CoverFlowStackLayout}/Sources/CoverFlowStackCollectionViewLayout/CoverFlowStackCollectionViewLayout.swift (100%) rename {CoverFlowStackCollectionViewLayout => CoverFlowStackLayout}/Sources/CoverFlowStackCollectionViewLayout/CoverFlowStackLayoutAttributes.swift (100%) create mode 100644 CoverFlowStackLayout/Sources/CoverFlowStackScrollView/CoverFlowStackScrollView.swift create mode 100644 CoverFlowStackLayout/Tests/CoverFlowStackCollectionViewLayoutTests/CoverFlowStackCollectionViewLayoutTests.swift create mode 100644 TwidereSDK/Sources/TwidereUI/Button/FollowButton.swift create mode 100644 TwidereSDK/Sources/TwidereUI/Container/MediaStackContainerView.swift create mode 100644 TwidereSDK/Sources/TwidereUI/Content/MediaMetaIndicatorView.swift diff --git a/CoverFlowStackCollectionViewLayout/Example/Example/Base.lproj/Main.storyboard b/CoverFlowStackCollectionViewLayout/Example/Example/Base.lproj/Main.storyboard deleted file mode 100644 index 25a76385..00000000 --- a/CoverFlowStackCollectionViewLayout/Example/Example/Base.lproj/Main.storyboard +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/CoverFlowStackCollectionViewLayout/Tests/CoverFlowStackCollectionViewLayoutTests/CoverFlowStackCollectionViewLayoutTests.swift b/CoverFlowStackCollectionViewLayout/Tests/CoverFlowStackCollectionViewLayoutTests/CoverFlowStackCollectionViewLayoutTests.swift deleted file mode 100644 index 8e258121..00000000 --- a/CoverFlowStackCollectionViewLayout/Tests/CoverFlowStackCollectionViewLayoutTests/CoverFlowStackCollectionViewLayoutTests.swift +++ /dev/null @@ -1,11 +0,0 @@ -import XCTest -@testable import CoverFlowStackCollectionViewLayout - -final class CoverFlowStackCollectionViewLayoutTests: XCTestCase { - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(CoverFlowStackCollectionViewLayout().text, "Hello, World!") - } -} diff --git a/CoverFlowStackCollectionViewLayout/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/CoverFlowStackLayout/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata similarity index 100% rename from CoverFlowStackCollectionViewLayout/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to CoverFlowStackLayout/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/CoverFlowStackCollectionViewLayout/Example/Example.xcodeproj/project.pbxproj b/CoverFlowStackLayout/Example/Example.xcodeproj/project.pbxproj similarity index 89% rename from CoverFlowStackCollectionViewLayout/Example/Example.xcodeproj/project.pbxproj rename to CoverFlowStackLayout/Example/Example.xcodeproj/project.pbxproj index dc97f0b7..2497204a 100644 --- a/CoverFlowStackCollectionViewLayout/Example/Example.xcodeproj/project.pbxproj +++ b/CoverFlowStackLayout/Example/Example.xcodeproj/project.pbxproj @@ -9,23 +9,25 @@ /* Begin PBXBuildFile section */ DB33E59D27193C9600EC2225 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB33E59C27193C9600EC2225 /* AppDelegate.swift */; }; DB33E59F27193C9600EC2225 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB33E59E27193C9600EC2225 /* SceneDelegate.swift */; }; - DB33E5A127193C9600EC2225 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB33E5A027193C9600EC2225 /* ViewController.swift */; }; DB33E5A427193C9600EC2225 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DB33E5A227193C9600EC2225 /* Main.storyboard */; }; DB33E5A627193C9900EC2225 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB33E5A527193C9900EC2225 /* Assets.xcassets */; }; DB33E5A927193C9900EC2225 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DB33E5A727193C9900EC2225 /* LaunchScreen.storyboard */; }; - DB33E5B327193CEA00EC2225 /* CoverFlowStackCollectionViewLayout in Frameworks */ = {isa = PBXBuildFile; productRef = DB33E5B227193CEA00EC2225 /* CoverFlowStackCollectionViewLayout */; }; + DB3E0D9829ED51650077EE8B /* SwiftUIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E0D9729ED51650077EE8B /* SwiftUIViewController.swift */; }; + DBE399DB29EFE448008FA278 /* CoverFlowStackLayout in Frameworks */ = {isa = PBXBuildFile; productRef = DBE399DA29EFE448008FA278 /* CoverFlowStackLayout */; }; + DBE3DC4429ED518F0054DC25 /* UIKitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE3DC4329ED518F0054DC25 /* UIKitViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ DB33E59927193C9600EC2225 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; DB33E59C27193C9600EC2225 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; DB33E59E27193C9600EC2225 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - DB33E5A027193C9600EC2225 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; DB33E5A327193C9600EC2225 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; DB33E5A527193C9900EC2225 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; DB33E5A827193C9900EC2225 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; DB33E5AA27193C9900EC2225 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - DB33E5B027193CDB00EC2225 /* CoverFlowStackCollectionViewLayout */ = {isa = PBXFileReference; lastKnownFileType = folder; name = CoverFlowStackCollectionViewLayout; path = ..; sourceTree = ""; }; + DB3E0D9729ED51650077EE8B /* SwiftUIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIViewController.swift; sourceTree = ""; }; + DBE399D929EFE429008FA278 /* CoverFlowStackLayout */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = CoverFlowStackLayout; path = ..; sourceTree = ""; }; + DBE3DC4329ED518F0054DC25 /* UIKitViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKitViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -33,7 +35,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB33E5B327193CEA00EC2225 /* CoverFlowStackCollectionViewLayout in Frameworks */, + DBE399DB29EFE448008FA278 /* CoverFlowStackLayout in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -43,7 +45,7 @@ DB33E59027193C9600EC2225 = { isa = PBXGroup; children = ( - DB33E5B027193CDB00EC2225 /* CoverFlowStackCollectionViewLayout */, + DBE399D929EFE429008FA278 /* CoverFlowStackLayout */, DB33E59B27193C9600EC2225 /* Example */, DB33E59A27193C9600EC2225 /* Products */, DB33E5B127193CEA00EC2225 /* Frameworks */, @@ -63,7 +65,8 @@ children = ( DB33E59C27193C9600EC2225 /* AppDelegate.swift */, DB33E59E27193C9600EC2225 /* SceneDelegate.swift */, - DB33E5A027193C9600EC2225 /* ViewController.swift */, + DBE3DC4329ED518F0054DC25 /* UIKitViewController.swift */, + DB3E0D9729ED51650077EE8B /* SwiftUIViewController.swift */, DB33E5A227193C9600EC2225 /* Main.storyboard */, DB33E5A527193C9900EC2225 /* Assets.xcassets */, DB33E5A727193C9900EC2225 /* LaunchScreen.storyboard */, @@ -96,7 +99,7 @@ ); name = Example; packageProductDependencies = ( - DB33E5B227193CEA00EC2225 /* CoverFlowStackCollectionViewLayout */, + DBE399DA29EFE448008FA278 /* CoverFlowStackLayout */, ); productName = Example; productReference = DB33E59927193C9600EC2225 /* Example.app */; @@ -153,7 +156,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DB33E5A127193C9600EC2225 /* ViewController.swift in Sources */, + DB3E0D9829ED51650077EE8B /* SwiftUIViewController.swift in Sources */, + DBE3DC4429ED518F0054DC25 /* UIKitViewController.swift in Sources */, DB33E59D27193C9600EC2225 /* AppDelegate.swift in Sources */, DB33E59F27193C9600EC2225 /* SceneDelegate.swift in Sources */, ); @@ -377,9 +381,9 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ - DB33E5B227193CEA00EC2225 /* CoverFlowStackCollectionViewLayout */ = { + DBE399DA29EFE448008FA278 /* CoverFlowStackLayout */ = { isa = XCSwiftPackageProductDependency; - productName = CoverFlowStackCollectionViewLayout; + productName = CoverFlowStackLayout; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/CoverFlowStackLayout/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/CoverFlowStackLayout/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/CoverFlowStackLayout/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/CoverFlowStackCollectionViewLayout/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/CoverFlowStackLayout/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from CoverFlowStackCollectionViewLayout/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to CoverFlowStackLayout/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/CoverFlowStackCollectionViewLayout/Example/Example.xcodeproj/xcshareddata/xcschemes/Example - RTL.xcscheme b/CoverFlowStackLayout/Example/Example.xcodeproj/xcshareddata/xcschemes/Example - RTL.xcscheme similarity index 100% rename from CoverFlowStackCollectionViewLayout/Example/Example.xcodeproj/xcshareddata/xcschemes/Example - RTL.xcscheme rename to CoverFlowStackLayout/Example/Example.xcodeproj/xcshareddata/xcschemes/Example - RTL.xcscheme diff --git a/CoverFlowStackCollectionViewLayout/Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme b/CoverFlowStackLayout/Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme similarity index 100% rename from CoverFlowStackCollectionViewLayout/Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme rename to CoverFlowStackLayout/Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme diff --git a/CoverFlowStackCollectionViewLayout/Example/Example/AppDelegate.swift b/CoverFlowStackLayout/Example/Example/AppDelegate.swift similarity index 100% rename from CoverFlowStackCollectionViewLayout/Example/Example/AppDelegate.swift rename to CoverFlowStackLayout/Example/Example/AppDelegate.swift diff --git a/CoverFlowStackCollectionViewLayout/Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json b/CoverFlowStackLayout/Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from CoverFlowStackCollectionViewLayout/Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json rename to CoverFlowStackLayout/Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/CoverFlowStackCollectionViewLayout/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json b/CoverFlowStackLayout/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from CoverFlowStackCollectionViewLayout/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json rename to CoverFlowStackLayout/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/CoverFlowStackCollectionViewLayout/Example/Example/Assets.xcassets/Contents.json b/CoverFlowStackLayout/Example/Example/Assets.xcassets/Contents.json similarity index 100% rename from CoverFlowStackCollectionViewLayout/Example/Example/Assets.xcassets/Contents.json rename to CoverFlowStackLayout/Example/Example/Assets.xcassets/Contents.json diff --git a/CoverFlowStackCollectionViewLayout/Example/Example/Base.lproj/LaunchScreen.storyboard b/CoverFlowStackLayout/Example/Example/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from CoverFlowStackCollectionViewLayout/Example/Example/Base.lproj/LaunchScreen.storyboard rename to CoverFlowStackLayout/Example/Example/Base.lproj/LaunchScreen.storyboard diff --git a/CoverFlowStackLayout/Example/Example/Base.lproj/Main.storyboard b/CoverFlowStackLayout/Example/Example/Base.lproj/Main.storyboard new file mode 100644 index 00000000..1c91519f --- /dev/null +++ b/CoverFlowStackLayout/Example/Example/Base.lproj/Main.storyboard @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CoverFlowStackCollectionViewLayout/Example/Example/Info.plist b/CoverFlowStackLayout/Example/Example/Info.plist similarity index 100% rename from CoverFlowStackCollectionViewLayout/Example/Example/Info.plist rename to CoverFlowStackLayout/Example/Example/Info.plist diff --git a/CoverFlowStackCollectionViewLayout/Example/Example/SceneDelegate.swift b/CoverFlowStackLayout/Example/Example/SceneDelegate.swift similarity index 100% rename from CoverFlowStackCollectionViewLayout/Example/Example/SceneDelegate.swift rename to CoverFlowStackLayout/Example/Example/SceneDelegate.swift diff --git a/CoverFlowStackLayout/Example/Example/SwiftUIViewController.swift b/CoverFlowStackLayout/Example/Example/SwiftUIViewController.swift new file mode 100644 index 00000000..674dbc0d --- /dev/null +++ b/CoverFlowStackLayout/Example/Example/SwiftUIViewController.swift @@ -0,0 +1,279 @@ +// +// SwiftUIViewController.swift +// Example +// +// Created by MainasuK on 2023/4/17. +// + +import UIKit +import SwiftUI +import CoverFlowStackScrollView + +final class SwiftUIViewController: UIViewController { + +} + +extension SwiftUIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + title = "SwiftUI" + + let hostingController = UIHostingController(rootView: ContentView()) + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + hostingController.willMove(toParent: self) + view.addSubview(hostingController.view) + NSLayoutConstraint.activate([ + hostingController.view.topAnchor.constraint(equalTo: view.topAnchor), + hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } + +} + +public struct ContentView: View { + + @StateObject var viewModel = ViewModel() + + public var body: some View { + GeometryReader { root in + let dimension = min(root.size.width, root.size.height) + CoverFlowStackScrollView { + HStack(spacing: .zero) { + ForEach(Array(viewModel.colors.enumerated()), id: \.0) { index, color in + GeometryReader { geo in + let transformAttribute = viewModel.transformAttribute(at: index) + ZStack { + VStack { + Text("Number \(index)") + .font(.largeTitle) + Text("\(viewModel.frame(at: index).debugDescription)") + .font(.caption) + } + .frame( + width: transformAttribute.transformFrame.width, + height: transformAttribute.transformFrame.height + ) + .background(Color(uiColor: color)) + .offset( + x: transformAttribute.offsetX, + y: transformAttribute.offsetY + ) + } + .frame(width: dimension, height: dimension) + } + .frame(width: dimension, height: dimension) + .zIndex(Double(999 - index)) + } + } + } contentOffsetDidUpdate: { contentOffset in + viewModel.contentOffset = contentOffset + } contentSizeDidUpdate: { contentSize in + viewModel.contentSize = contentSize + } // end ScrollView + .overlay(alignment: .top) { + VStack { + Text("\(viewModel.progress)") + .font(.title) + Text("viewPort: \(viewModel.viewPortRect().debugDescription)") + .font(.caption) + Text("viewPort maxX: \(viewModel.viewPortRect().maxX)") + .font(.caption) + } + } + + } // end GeometryReader + } // end body + +} + +extension ContentView { + class ViewModel: ObservableObject { + + // input + @Published var colors: [UIColor] = [] + + @Published var contentOffset: CGFloat = .zero + @Published var contentSize: CGSize = .zero + + // output + var progress: CGFloat { + return abs(contentOffset) / contentSize.width + } + + init() { + colors = (0..<4).map { i in + return [.systemRed, .systemGreen, .systemBlue][i % 3] + } + } + } // end class +} + +extension ContentView.ViewModel { + func frame(at index: Int) -> CGRect { + let count = colors.count + guard count > 0 else { return .zero } + let width = contentSize.width / CGFloat(count) + let minX = CGFloat(index) * width + let frame = CGRect( + x: minX, + y: 0, + width: width, + height: contentSize.height + ) + return frame + } + + func viewPortRect() -> CGRect { + let count = colors.count + guard count > 0 else { return .zero } + let width = contentSize.width / CGFloat(count) + let rect = CGRect( + origin: .init(x: -contentOffset, y: 0), + size: .init(width: width, height: contentSize.height) + ) + return rect + } + + struct TransformAttribute { + + let originalFrame: CGRect + let transformFrame: CGRect + let zIndex: Int + let alpha: CGFloat + + init( + originalFrame: CGRect, + transformFrame: CGRect, + zIndex: Int, + alpha: CGFloat + ) { + self.originalFrame = originalFrame + self.transformFrame = transformFrame + self.zIndex = zIndex + self.alpha = alpha + } + + var offsetX: CGFloat { + return (transformFrame.minX - originalFrame.minX) + (transformFrame.width - originalFrame.width) / 2 + //return transformFrame.origin.x - originalFrame.origin.x + } + + var offsetY: CGFloat { + return .zero // (transformFrame.height - originalFrame.height) / 2 + //return transformFrame.origin.y - originalFrame.origin.y + } + } + + var sizeScaleRatio: CGFloat { 0.8 } + var trailingMarginRatio: CGFloat { 0.1 } + + func transformAttribute(at index: Int) -> TransformAttribute { + let originalFrame = frame(at: index) + let viewPortRect = self.viewPortRect() + + // calculate constants + let endFrameSize = CGSize( + width: viewPortRect.width * (1 - trailingMarginRatio), + height: viewPortRect.height + ) + let startFrameSize = CGSize( + width: endFrameSize.width * sizeScaleRatio, + height: endFrameSize.height * sizeScaleRatio + ) + + if originalFrame.minX <= viewPortRect.minX { + // A: top most cover + // set frame + return TransformAttribute( + originalFrame: originalFrame, + transformFrame: CGRect( + x: originalFrame.origin.x, + y: originalFrame.origin.y, + width: endFrameSize.width, + height: endFrameSize.height + ), + zIndex: Int.max - index, + alpha: 1 + ) + } else if originalFrame.minX <= viewPortRect.maxX { + // B: middle cover + // timing curve + let offset = viewPortRect.maxX - originalFrame.minX + let t = offset / viewPortRect.width + let timingCurve = easeInOutInterpolation(progress: t) + // get current scale ratio + let scaleRatio: CGFloat = { + let start = sizeScaleRatio + let end: CGFloat = 1 + return lerp(v0: start, v1: end, t: timingCurve) + }() + // set height + let height = endFrameSize.height * scaleRatio + // pin offsetY + let topMargin = (viewPortRect.height - height) / 2 + // set width + let width = endFrameSize.width * scaleRatio + // set offsetX + let end = viewPortRect.origin.x + let start = viewPortRect.maxX - width + let minX = lerp(v0: start, v1: end, t: timingCurve) + // set alpha + let alpha = lerp(v0: 0.5, v1: 1, t: timingCurve) + return TransformAttribute( + originalFrame: originalFrame, + transformFrame: CGRect( + x: minX, + y: topMargin - (originalFrame.height - endFrameSize.height) / 2, + width: width, + height: height + ), + zIndex: Int.max - index, + alpha: alpha + ) + } else { + // C: bottom cover + // timing curve + let offset = originalFrame.minX - viewPortRect.maxX + let t = 1 - (offset / viewPortRect.width) + // set height + let height = startFrameSize.height + // pin offsetY + let topMargin = (viewPortRect.height - height) / 2 + // set width + let width = startFrameSize.width + // set offsetX + let minX = viewPortRect.maxX - width + // set alpha + let alpha = lerp(v0: 0, v1: 0.5, t: t) + return TransformAttribute( + originalFrame: originalFrame, + transformFrame: CGRect( + x: minX, + y: topMargin, + width: width, + height: height + ), + zIndex: Int.max - index, + alpha: alpha + ) + } + } +} + +// ref: +// - https://stackoverflow.com/questions/13462001/ease-in-and-ease-out-animation-formula +// - https://math.stackexchange.com/questions/121720/ease-in-out-function/121755#121755 +// for a = 2 +func easeInOutInterpolation(progress t: CGFloat) -> CGFloat { + let sqt = t * t + return sqt / (2.0 * (sqt - t) + 1.0) +} + +// linear interpolation +func lerp(v0: CGFloat, v1: CGFloat, t: CGFloat) -> CGFloat { + return (1 - t) * v0 + (t * v1) +} diff --git a/CoverFlowStackCollectionViewLayout/Example/Example/ViewController.swift b/CoverFlowStackLayout/Example/Example/UIKitViewController.swift similarity index 92% rename from CoverFlowStackCollectionViewLayout/Example/Example/ViewController.swift rename to CoverFlowStackLayout/Example/Example/UIKitViewController.swift index 6fd750bc..1361e443 100644 --- a/CoverFlowStackCollectionViewLayout/Example/Example/ViewController.swift +++ b/CoverFlowStackLayout/Example/Example/UIKitViewController.swift @@ -1,5 +1,5 @@ // -// ViewController.swift +// UIKitViewController.swift // Example // // Created by Cirno MainasuK on 2021-10-15. @@ -8,7 +8,7 @@ import UIKit import CoverFlowStackCollectionViewLayout -class ViewController: UIViewController { +class UIKitViewController: UIViewController { var colors: [UIColor] = (0..<20).map { i in return [.systemRed, .systemGreen, .systemBlue][i % 3] @@ -26,6 +26,8 @@ class ViewController: UIViewController { super.viewDidLoad() // Do any additional setup after loading the view. + title = "UIKit" + collectionView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(collectionView) NSLayoutConstraint.activate([ @@ -64,7 +66,7 @@ extension CollectionViewCell { } // MARK: - UICollectionViewDataSource -extension ViewController: UICollectionViewDataSource { +extension UIKitViewController: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return colors.count } diff --git a/CoverFlowStackCollectionViewLayout/Package.swift b/CoverFlowStackLayout/Package.swift similarity index 74% rename from CoverFlowStackCollectionViewLayout/Package.swift rename to CoverFlowStackLayout/Package.swift index 6a53d946..ca812cb3 100644 --- a/CoverFlowStackCollectionViewLayout/Package.swift +++ b/CoverFlowStackLayout/Package.swift @@ -4,13 +4,16 @@ import PackageDescription let package = Package( - name: "CoverFlowStackCollectionViewLayout", - platforms: [.iOS(.v10)], + name: "CoverFlowStackLayout", + platforms: [.iOS(.v13)], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( - name: "CoverFlowStackCollectionViewLayout", - targets: ["CoverFlowStackCollectionViewLayout"]), + name: "CoverFlowStackLayout", + targets: [ + "CoverFlowStackCollectionViewLayout", + "CoverFlowStackScrollView", + ]), ], dependencies: [ // Dependencies declare other packages that this package depends on. @@ -22,6 +25,9 @@ let package = Package( .target( name: "CoverFlowStackCollectionViewLayout", dependencies: []), + .target( + name: "CoverFlowStackScrollView", + dependencies: []), .testTarget( name: "CoverFlowStackCollectionViewLayoutTests", dependencies: ["CoverFlowStackCollectionViewLayout"]), diff --git a/CoverFlowStackCollectionViewLayout/README.md b/CoverFlowStackLayout/README.md similarity index 100% rename from CoverFlowStackCollectionViewLayout/README.md rename to CoverFlowStackLayout/README.md diff --git a/CoverFlowStackCollectionViewLayout/Sources/CoverFlowStackCollectionViewLayout/CoverFlowStackCollectionViewLayout.swift b/CoverFlowStackLayout/Sources/CoverFlowStackCollectionViewLayout/CoverFlowStackCollectionViewLayout.swift similarity index 100% rename from CoverFlowStackCollectionViewLayout/Sources/CoverFlowStackCollectionViewLayout/CoverFlowStackCollectionViewLayout.swift rename to CoverFlowStackLayout/Sources/CoverFlowStackCollectionViewLayout/CoverFlowStackCollectionViewLayout.swift diff --git a/CoverFlowStackCollectionViewLayout/Sources/CoverFlowStackCollectionViewLayout/CoverFlowStackLayoutAttributes.swift b/CoverFlowStackLayout/Sources/CoverFlowStackCollectionViewLayout/CoverFlowStackLayoutAttributes.swift similarity index 100% rename from CoverFlowStackCollectionViewLayout/Sources/CoverFlowStackCollectionViewLayout/CoverFlowStackLayoutAttributes.swift rename to CoverFlowStackLayout/Sources/CoverFlowStackCollectionViewLayout/CoverFlowStackLayoutAttributes.swift diff --git a/CoverFlowStackLayout/Sources/CoverFlowStackScrollView/CoverFlowStackScrollView.swift b/CoverFlowStackLayout/Sources/CoverFlowStackScrollView/CoverFlowStackScrollView.swift new file mode 100644 index 00000000..1912928b --- /dev/null +++ b/CoverFlowStackLayout/Sources/CoverFlowStackScrollView/CoverFlowStackScrollView.swift @@ -0,0 +1,67 @@ +// +// CoverFlowStackScrollView.swift +// +// +// Created by MainasuK on 2023/4/17. +// + +import SwiftUI +import UIKit + + +// Seealso: Example.SwiftUIViewController +public struct CoverFlowStackScrollView: View { + + let id = UUID() + let content: () -> Content + let contentOffsetDidUpdate: (CGFloat) -> Void + let contentSizeDidUpdate: (CGSize) -> Void + + public init( + @ViewBuilder _ content: @escaping () -> Content, + contentOffsetDidUpdate: @escaping (CGFloat) -> Void, + contentSizeDidUpdate: @escaping (CGSize) -> Void + ) { + self.content = content + self.contentOffsetDidUpdate = contentOffsetDidUpdate + self.contentSizeDidUpdate = contentSizeDidUpdate + } + + public var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + offsetReader + content() + .background(GeometryReader{ proxy in + Color.clear.preference(key: SizePreferenceKey.self, value: proxy.size) + .onPreferenceChange(SizePreferenceKey.self) { size in + contentSizeDidUpdate(size) + } + }) + } + .coordinateSpace(name: id.uuidString) + .onPreferenceChange(OffsetPreferenceKey.self) { offset in + contentOffsetDidUpdate(offset) + } + } + + var offsetReader: some View { + GeometryReader { proxy in + Color.clear + .preference( + key: OffsetPreferenceKey.self, + value: proxy.frame(in: .named(id.uuidString)).minX + ) + } + .frame(height: .leastNonzeroMagnitude) + } +} + +private struct OffsetPreferenceKey: PreferenceKey { + static var defaultValue: CGFloat = .zero + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { } +} + +private struct SizePreferenceKey: PreferenceKey { + static var defaultValue: CGSize = .zero + static func reduce(value: inout CGSize, nextValue: () -> CGSize) { } +} diff --git a/CoverFlowStackLayout/Tests/CoverFlowStackCollectionViewLayoutTests/CoverFlowStackCollectionViewLayoutTests.swift b/CoverFlowStackLayout/Tests/CoverFlowStackCollectionViewLayoutTests/CoverFlowStackCollectionViewLayoutTests.swift new file mode 100644 index 00000000..96aa1764 --- /dev/null +++ b/CoverFlowStackLayout/Tests/CoverFlowStackCollectionViewLayoutTests/CoverFlowStackCollectionViewLayoutTests.swift @@ -0,0 +1,7 @@ +import XCTest +@testable import CoverFlowStackCollectionViewLayout + +final class CoverFlowStackCollectionViewLayoutTests: XCTestCase { + func testSmoke() throws { + } +} diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index 89d3ef22..7ae5cee4 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -53,6 +53,7 @@ let package = Package( .package(url: "https://github.com/tid-kijyun/Kanna.git", from: "5.2.7"), .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"), .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"), + .package(name: "CoverFlowStackLayout", path: "../CoverFlowStackLayout"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -147,6 +148,7 @@ let package = Package( .product(name: "FPSIndicator", package: "FPSIndicator"), .product(name: "Floaty", package: "Floaty"), .product(name: "Tabman", package: "Tabman"), + .product(name: "CoverFlowStackLayout", package: "CoverFlowStackLayout"), ] ), ] diff --git a/TwidereSDK/Sources/TwidereUI/Button/FollowButton.swift b/TwidereSDK/Sources/TwidereUI/Button/FollowButton.swift new file mode 100644 index 00000000..7942f8a6 --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Button/FollowButton.swift @@ -0,0 +1,69 @@ +// +// FollowButton.swift +// +// +// Created by MainasuK on 2023/4/13. +// + +import SwiftUI +import CoreDataStack + +public struct FollowButton: View { + + @ObservedObject public var viewModel: ViewModel + + public var body: some View { + Button { + + } label: { + Text("Follow") + } + .buttonStyle(.borderless) + } +} + +extension FollowButton { + public class ViewModel: ObservableObject { + + // input + public let user: UserObject + public let authContext: AuthContext + + // output + + + public init( + user: UserObject, + authContext: AuthContext + ) { + self.user = user + self.authContext = authContext + // end init + } + + } +} + +extension FollowButton.ViewModel { + public convenience init( + user: TwitterUser, + authContext: AuthContext + ) { + self.init( + user: .twitter(object: user), + authContext: authContext + ) + // end init + } + + public convenience init( + user: MastodonUser, + authContext: AuthContext + ) { + self.init( + user: .mastodon(object: user), + authContext: authContext + ) + // end init + } +} diff --git a/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift b/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift index bd34016f..7108e03a 100644 --- a/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift +++ b/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift @@ -203,25 +203,9 @@ extension MediaGridContainerView { } }) .overlay(alignment: .bottom) { - HStack { - let viewModel = viewModels[index] - Spacer() - Group { - if viewModel.mediaKind == .animatedGIF { - Text("GIF") - } else if let durationText = viewModel.durationText { - Text("\(Image(systemName: "play.fill")) \(durationText)") - } - } - .foregroundColor(Color(uiColor: .label)) - .font(.system(.footnote, design: .default, weight: .medium)) - .padding(.horizontal, 5) - .padding(.vertical, 3) - .background(.thinMaterial) - .cornerRadius(4) - } - .padding(EdgeInsets(top: 0, leading: 11, bottom: 8, trailing: 11)) - .allowsHitTesting(false) + MediaMetaIndicatorView(viewModel: viewModels[index]) + .padding(EdgeInsets(top: 0, leading: 11, bottom: 8, trailing: 11)) + .allowsHitTesting(false) } .overlay( RoundedRectangle(cornerRadius: MediaGridContainerView.cornerRadius) diff --git a/TwidereSDK/Sources/TwidereUI/Container/MediaStackContainerView.swift b/TwidereSDK/Sources/TwidereUI/Container/MediaStackContainerView.swift new file mode 100644 index 00000000..e66d9715 --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Container/MediaStackContainerView.swift @@ -0,0 +1,248 @@ +// +// MediaStackContainerView.swift +// +// +// Created by MainasuK on 2023/4/17. +// + +import SwiftUI +import Kingfisher +import CoverFlowStackScrollView + +public struct MediaStackContainerView: View { + + @ObservedObject public private(set) var viewModel: ViewModel + + public init(viewModel: MediaStackContainerView.ViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + GeometryReader { root in + let dimension = min(root.size.width, root.size.height) + CoverFlowStackScrollView { + HStack(spacing: .zero) { + ForEach(Array(viewModel.items.enumerated()), id: \.0) { index, item in + GeometryReader { geo in + let transformAttribute = viewModel.transformAttribute(at: index) + ZStack { + MediaView(viewModel: item) + .frame( + width: transformAttribute.transformFrame.width, + height: transformAttribute.transformFrame.height + ) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .overlay(alignment: .bottom) { + MediaMetaIndicatorView(viewModel: item) + .padding(EdgeInsets(top: 0, leading: 11, bottom: 8, trailing: 11)) + .allowsHitTesting(false) + } + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color(uiColor: .placeholderText).opacity(0.5), lineWidth: 1) + ) + .offset( + x: transformAttribute.offsetX, + y: transformAttribute.offsetY + ) + } + .frame(width: dimension, height: dimension) + } + .frame(width: dimension, height: dimension) + .zIndex(Double(999 - index)) + } + } // HStack + } contentOffsetDidUpdate: { contentOffset in + viewModel.contentOffset = contentOffset + } contentSizeDidUpdate: { contentSize in + viewModel.contentSize = contentSize + } // end ScrollView + } // end GeometryReader + } +} + +extension MediaStackContainerView { + public class ViewModel: ObservableObject { + + // input + let items: [MediaView.ViewModel] + @Published var contentOffset: CGFloat = .zero + @Published var contentSize: CGSize = .zero + + // output + var progress: CGFloat { + return abs(contentOffset) / contentSize.width + } + + public init(items: [MediaView.ViewModel]) { + self.items = items + // end init + } + + } +} + +extension MediaStackContainerView.ViewModel { + func frame(at index: Int) -> CGRect { + let count = items.count + guard count > 0 else { return .zero } + let width = contentSize.width / CGFloat(count) + let minX = CGFloat(index) * width + let frame = CGRect( + x: minX, + y: 0, + width: width, + height: contentSize.height + ) + return frame + } + + func viewPortRect() -> CGRect { + let count = items.count + guard count > 0 else { return .zero } + let width = contentSize.width / CGFloat(count) + let rect = CGRect( + origin: .init(x: -contentOffset, y: 0), + size: .init(width: width, height: contentSize.height) + ) + return rect + } + + struct TransformAttribute { + let originalFrame: CGRect + let transformFrame: CGRect + let zIndex: Int + let alpha: CGFloat + + init( + originalFrame: CGRect, + transformFrame: CGRect, + zIndex: Int, + alpha: CGFloat + ) { + self.originalFrame = originalFrame + self.transformFrame = transformFrame + self.zIndex = zIndex + self.alpha = alpha + } + + var offsetX: CGFloat { + return (transformFrame.minX - originalFrame.minX) + (transformFrame.width - originalFrame.width) / 2 + //return transformFrame.origin.x - originalFrame.origin.x + } + + var offsetY: CGFloat { + return .zero // (transformFrame.height - originalFrame.height) / 2 + //return transformFrame.origin.y - originalFrame.origin.y + } + } + + var sizeScaleRatio: CGFloat { 0.8 } + var trailingMarginRatio: CGFloat { 0.1 } + + func transformAttribute(at index: Int) -> TransformAttribute { + let originalFrame = frame(at: index) + let viewPortRect = self.viewPortRect() + + // calculate constants + let endFrameSize = CGSize( + width: viewPortRect.width * (1 - trailingMarginRatio), + height: viewPortRect.height + ) + let startFrameSize = CGSize( + width: endFrameSize.width * sizeScaleRatio, + height: endFrameSize.height * sizeScaleRatio + ) + + if originalFrame.minX <= viewPortRect.minX { + // A: top most cover + // set frame + return TransformAttribute( + originalFrame: originalFrame, + transformFrame: CGRect( + x: originalFrame.origin.x, + y: originalFrame.origin.y, + width: endFrameSize.width, + height: endFrameSize.height + ), + zIndex: Int.max - index, + alpha: 1 + ) + } else if originalFrame.minX <= viewPortRect.maxX { + // B: middle cover + // timing curve + let offset = viewPortRect.maxX - originalFrame.minX + let t = offset / viewPortRect.width + let timingCurve = easeInOutInterpolation(progress: t) + // get current scale ratio + let scaleRatio: CGFloat = { + let start = sizeScaleRatio + let end: CGFloat = 1 + return lerp(v0: start, v1: end, t: timingCurve) + }() + // set height + let height = endFrameSize.height * scaleRatio + // pin offsetY + let topMargin = (viewPortRect.height - height) / 2 + // set width + let width = endFrameSize.width * scaleRatio + // set offsetX + let end = viewPortRect.origin.x + let start = viewPortRect.maxX - width + let minX = lerp(v0: start, v1: end, t: timingCurve) + // set alpha + let alpha = lerp(v0: 0.5, v1: 1, t: timingCurve) + return TransformAttribute( + originalFrame: originalFrame, + transformFrame: CGRect( + x: minX, + y: topMargin - (originalFrame.height - endFrameSize.height) / 2, + width: width, + height: height + ), + zIndex: Int.max - index, + alpha: alpha + ) + } else { + // C: bottom cover + // timing curve + let offset = originalFrame.minX - viewPortRect.maxX + let t = 1 - (offset / viewPortRect.width) + // set height + let height = startFrameSize.height + // pin offsetY + let topMargin = (viewPortRect.height - height) / 2 + // set width + let width = startFrameSize.width + // set offsetX + let minX = viewPortRect.maxX - width + // set alpha + let alpha = lerp(v0: 0, v1: 0.5, t: t) + return TransformAttribute( + originalFrame: originalFrame, + transformFrame: CGRect( + x: minX, + y: topMargin, + width: width, + height: height + ), + zIndex: Int.max - index, + alpha: alpha + ) + } + } +} + +// ref: +// - https://stackoverflow.com/questions/13462001/ease-in-and-ease-out-animation-formula +// - https://math.stackexchange.com/questions/121720/ease-in-out-function/121755#121755 +// for a = 2 +func easeInOutInterpolation(progress t: CGFloat) -> CGFloat { + let sqt = t * t + return sqt / (2.0 * (sqt - t) + 1.0) +} + +// linear interpolation +func lerp(v0: CGFloat, v1: CGFloat, t: CGFloat) -> CGFloat { + return (1 - t) * v0 + (t * v1) +} diff --git a/TwidereSDK/Sources/TwidereUI/Content/MediaMetaIndicatorView.swift b/TwidereSDK/Sources/TwidereUI/Content/MediaMetaIndicatorView.swift new file mode 100644 index 00000000..d83c5aaf --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Content/MediaMetaIndicatorView.swift @@ -0,0 +1,38 @@ +// +// MediaMetaIndicatorView.swift +// +// +// Created by MainasuK on 2023/4/19. +// + +import SwiftUI + +public struct MediaMetaIndicatorView: View { + + @ObservedObject public var viewModel: MediaView.ViewModel + + public init(viewModel: MediaView.ViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + HStack { + Spacer() + Group { + if viewModel.mediaKind == .animatedGIF { + Text("GIF") + } else if let durationText = viewModel.durationText { + Text("\(Image(systemName: "play.fill")) \(durationText)") + } + } + .foregroundColor(Color(uiColor: .label)) + .font(.system(.footnote, design: .default, weight: .medium)) + .padding(.horizontal, 5) + .padding(.vertical, 3) + .background(.thinMaterial) + .cornerRadius(4) + } + .padding(EdgeInsets(top: 0, leading: 11, bottom: 8, trailing: 11)) + .allowsHitTesting(false) + } +} diff --git a/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift index 00278aac..4825012c 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift @@ -200,6 +200,15 @@ extension MediaView.ViewModel { //} // extension MediaView.ViewModel { + public static func viewModels(from status: StatusObject) -> [MediaView.ViewModel] { + switch status { + case .twitter(let object): + return viewModels(from: object) + case .mastodon(let object): + return viewModels(from: object) + } + } + public static func viewModels(from status: TwitterStatus) -> [MediaView.ViewModel] { return status.attachments.map { attachment -> MediaView.ViewModel in MediaView.ViewModel( @@ -240,38 +249,4 @@ extension MediaView.ViewModel { ) } } - -// public static func configuration(mastodonStatus status: MastodonStatus) -> [MediaView.Configuration] { -// func videoInfo(from attachment: MastodonAttachment) -> MediaView.Configuration.VideoInfo { -// MediaView.Configuration.VideoInfo( -// aspectRadio: attachment.size, -// assetURL: attachment.assetURL, -// previewURL: attachment.previewURL, -// durationMS: attachment.durationMS -// ) -// } -// -// let status = status.repost ?? status -// return status.attachments.map { attachment -> MediaView.Configuration in -// switch attachment.kind { -// case .image: -// let info = MediaView.Configuration.ImageInfo( -// aspectRadio: attachment.size, -// assetURL: attachment.assetURL, -// downloadURL: attachment.downloadURL -// ) -// return .image(info: info) -// case .video: -// let info = videoInfo(from: attachment) -// return .video(info: info) -// case .gifv: -// let info = videoInfo(from: attachment) -// return .gif(info: info) -// case .audio: -// // TODO: -// let info = videoInfo(from: attachment) -// return .video(info: info) -// } -// } -// } } diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift index c13440d7..284560bf 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift @@ -42,8 +42,6 @@ extension UserView { // @Published public var authenticationContext: AuthenticationContext? // me // @Published public var userAuthenticationContext: AuthenticationContext? // -// @Published public var header: Header = .none -// // @Published public var userIdentifier: UserIdentifier? = nil // @Published public var avatarImageURL: URL? // @Published public var avatarBadge: AvatarBadge = .none @@ -56,8 +54,12 @@ extension UserView { // // @Published public var followerCount: Int? // + // follow request @Published public var isFollowRequestActionDisplay = false @Published public var isFollowRequestBusy = false + + // follow + @Published public var followButtonViewModel: FollowButton.ViewModel? // // public var listMembershipViewModel: ListMembershipViewModel? // @Published public var listOwnerUserIdentifier: UserIdentifier? = nil @@ -84,6 +86,7 @@ extension UserView { self.delegate = delegate // end init + // notification switch kind { case .notification(let notification): self.notification = notification @@ -91,6 +94,7 @@ extension UserView { break } + // follow request switch notification { case .twitter: break @@ -101,6 +105,15 @@ extension UserView { default: break } + + switch kind { + case .search: // follow + if let authContext = authContext { + self.followButtonViewModel = .init(user: user, authContext: authContext) + } + default: + break + } // // isMyList // Publishers.CombineLatest( // $authenticationContext, @@ -141,12 +154,12 @@ extension UserView.ViewModel { // headline: name | lock | username // subheadline: follower count // accessory: follow button - case relationship + case search // headline: name | lock // subheadline: username - // accessory: action button - case friendship + // accessory: none + case friend // header: notification // headline: name | lock | username diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift index 5b1ac1d6..45596093 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift @@ -181,9 +181,9 @@ extension UserView { switch viewModel.kind { case .account: nameLabel - case .relationship: + case .search: nameLabel - case .friendship: + case .friend: nameLabel case .notification: nameLabel @@ -202,9 +202,9 @@ extension UserView { switch viewModel.kind { case .account: usernameLabel - case .relationship: + case .search: usernameLabel - case .friendship: + case .friend: usernameLabel case .notification: usernameLabel @@ -223,9 +223,10 @@ extension UserView { switch viewModel.kind { case .account: menuView - case .relationship: + case .search: + // TODO: follow button EmptyView() - case .friendship: + case .friend: EmptyView() case .notification: if viewModel.isFollowRequestActionDisplay { diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/Status/StatusTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/Status/StatusTableViewCell.swift index d9881e2f..3b4e7655 100644 --- a/TwidereSDK/Sources/TwidereUI/TableViewCell/Status/StatusTableViewCell.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/Status/StatusTableViewCell.swift @@ -16,6 +16,8 @@ public class StatusTableViewCell: UITableViewCell { public weak var statusViewTableViewCellDelegate: StatusViewTableViewCellDelegate? + @Published public private(set) var viewLayoutaFrame = ViewLayoutFrame() + public override func prepareForReuse() { super.prepareForReuse() @@ -33,6 +35,18 @@ public class StatusTableViewCell: UITableViewCell { _init() } + public override func layoutSubviews() { + super.layoutSubviews() + + viewLayoutaFrame.update(view: self) + } + + public override func safeAreaInsetsDidChange() { + super.safeAreaInsetsDidChange() + + viewLayoutaFrame.update(view: self) + } + } extension StatusTableViewCell { diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 744ac04e..f2e3bbfa 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -352,7 +352,6 @@ DBFA471F2859C4AE00C9FF7F /* UserLikeTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFA471E2859C4AE00C9FF7F /* UserLikeTimelineViewController.swift */; }; DBFA47212859C4F300C9FF7F /* UserLikeTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFA47202859C4F300C9FF7F /* UserLikeTimelineViewModel.swift */; }; DBFA4A2025A5924C00D51703 /* ListTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFA4A1F25A5924C00D51703 /* ListTimelineViewModel+Diffable.swift */; }; - DBFADA892872BEDA00B512D6 /* CoverFlowStackCollectionViewLayout in Frameworks */ = {isa = PBXBuildFile; productRef = DBFADA882872BEDA00B512D6 /* CoverFlowStackCollectionViewLayout */; }; DBFADA8B2872BEDE00B512D6 /* TabBarPager in Frameworks */ = {isa = PBXBuildFile; productRef = DBFADA8A2872BEDE00B512D6 /* TabBarPager */; }; DBFCC44725667C620016698E /* UILabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCC44625667C620016698E /* UILabel.swift */; }; DBFCEF1228939C9600EEBFB1 /* UserHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCEF1128939C9600EEBFB1 /* UserHistoryViewController.swift */; }; @@ -541,7 +540,6 @@ DB2EBBEF255D368200956CAA /* TableViewEntryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewEntryRow.swift; sourceTree = ""; }; DB2FFF3D258B78B0003DBC19 /* AVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayer.swift; sourceTree = ""; }; DB33A4A825A319A0003CED7D /* ActionToolbarContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionToolbarContainer.swift; sourceTree = ""; }; - DB33E5B42719637400EC2225 /* CoverFlowStackCollectionViewLayout */ = {isa = PBXFileReference; lastKnownFileType = folder; path = CoverFlowStackCollectionViewLayout; sourceTree = ""; }; DB36F35D257F74C00028F81E /* ScrollViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewContainer.swift; sourceTree = ""; }; DB36F375257F79DB0028F81E /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; DB37F69F274B556B0081603F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -811,6 +809,7 @@ DBDA8E9724FE075E006750DC /* MainTabBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainTabBarController.swift; sourceTree = ""; }; DBDA8E9E24FE0FFF006750DC /* HomeTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineViewController.swift; sourceTree = ""; }; DBDDE72D254C084A0057CF8E /* SearchUserViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchUserViewModel+Diffable.swift"; sourceTree = ""; }; + DBE399D829EFE3A7008FA278 /* CoverFlowStackLayout */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = CoverFlowStackLayout; sourceTree = ""; }; DBE6357928855302001C114B /* PushNotificationScratchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationScratchViewController.swift; sourceTree = ""; }; DBE6357C2885557C001C114B /* PushNotificationScratchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationScratchViewModel.swift; sourceTree = ""; }; DBE6357E288555AE001C114B /* PushNotificationScratchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationScratchView.swift; sourceTree = ""; }; @@ -898,7 +897,6 @@ buildActionMask = 2147483647; files = ( DB05E13D29C321960055BF3F /* TwidereSDK in Frameworks */, - DBFADA892872BEDA00B512D6 /* CoverFlowStackCollectionViewLayout in Frameworks */, DBFADA8B2872BEDE00B512D6 /* TabBarPager in Frameworks */, 44CCAE5E7B79E0C359E367E5 /* Pods_TwidereX.framework in Frameworks */, ); @@ -2039,7 +2037,7 @@ DBDA8E1524FCF8A3006750DC = { isa = PBXGroup; children = ( - DB33E5B42719637400EC2225 /* CoverFlowStackCollectionViewLayout */, + DBE399D829EFE3A7008FA278 /* CoverFlowStackLayout */, DB51DC372716AEF000A0D8FB /* TabBarPager */, DB43239D251491EB004FAAEC /* TwidereSDK */, DBDA8E2024FCF8A3006750DC /* TwidereX */, @@ -2477,7 +2475,6 @@ ); name = TwidereX; packageProductDependencies = ( - DBFADA882872BEDA00B512D6 /* CoverFlowStackCollectionViewLayout */, DBFADA8A2872BEDE00B512D6 /* TabBarPager */, DB05E13C29C321960055BF3F /* TwidereSDK */, ); @@ -4131,10 +4128,6 @@ isa = XCSwiftPackageProductDependency; productName = TwidereSDK; }; - DBFADA882872BEDA00B512D6 /* CoverFlowStackCollectionViewLayout */ = { - isa = XCSwiftPackageProductDependency; - productName = CoverFlowStackCollectionViewLayout; - }; DBFADA8A2872BEDE00B512D6 /* TabBarPager */ = { isa = XCSwiftPackageProductDependency; productName = TabBarPager; diff --git a/TwidereX/Diffable/Status/StatusMediaGallerySection.swift b/TwidereX/Diffable/Status/StatusMediaGallerySection.swift index 3ecfa2ca..66ee487c 100644 --- a/TwidereX/Diffable/Status/StatusMediaGallerySection.swift +++ b/TwidereX/Diffable/Status/StatusMediaGallerySection.swift @@ -7,6 +7,7 @@ // import UIKit +import SwiftUI import CoreData import CoreDataStack @@ -33,12 +34,13 @@ extension StatusMediaGallerySection { assertionFailure() return } - configure( - collectionView: collectionView, - cell: cell, - status: status, - configuration: configuration - ) + let items = MediaView.ViewModel.viewModels(from: status) + let viewModel = MediaStackContainerView.ViewModel(items: items) + cell.contentConfiguration = UIHostingConfiguration { + MediaStackContainerView(viewModel: viewModel) + } + .margins(.vertical, 0) // remove vertical margins + } } @@ -63,14 +65,14 @@ extension StatusMediaGallerySection { extension StatusMediaGallerySection { - static func configure( - collectionView: UICollectionView, - cell: StatusMediaGalleryCollectionCell, - status: StatusObject, - configuration: Configuration - ) { - cell.configure(status: status) - cell.delegate = configuration.statusMediaGalleryCollectionCellDelegate - } +// static func configure( +// collectionView: UICollectionView, +// cell: StatusMediaGalleryCollectionCell, +// status: StatusObject, +// configuration: Configuration +// ) { +// cell.configure(status: status) +// cell.delegate = configuration.statusMediaGalleryCollectionCellDelegate +// } } diff --git a/TwidereX/Diffable/Status/StatusSection.swift b/TwidereX/Diffable/Status/StatusSection.swift index 4a3eb3c8..387b5a17 100644 --- a/TwidereX/Diffable/Status/StatusSection.swift +++ b/TwidereX/Diffable/Status/StatusSection.swift @@ -65,6 +65,25 @@ extension StatusSection { .margins(.vertical, 0) // remove vertical margins } return cell + + case .status(let record): + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell + cell.statusViewTableViewCellDelegate = configuration.statusViewTableViewCellDelegate + context.managedObjectContext.performAndWait { + guard let status = record.object(in: context.managedObjectContext) else { return } + let viewModel = StatusView.ViewModel( + status: status, + authContext: authContext, + delegate: cell, + viewLayoutFramePublisher: cell.$viewLayoutaFrame + ) + + cell.contentConfiguration = UIHostingConfiguration { + StatusView(viewModel: viewModel) + } + .margins(.vertical, 0) // remove vertical margins + } + return cell case .feedLoader(let record): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell @@ -78,34 +97,7 @@ extension StatusSection { // } return cell - case .status(let status): - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell -// setupStatusPollDataSource( -// context: context, -// statusView: cell.statusView, -// configurationContext: configuration.statusViewConfigurationContext -// ) -// context.managedObjectContext.performAndWait { -// switch status { -// case .twitter(let record): -// guard let status = record.object(in: context.managedObjectContext) else { return } -// configure( -// tableView: tableView, -// cell: cell, -// viewModel: StatusTableViewCell.ViewModel(value: .twitterStatus(status)), -// configuration: configuration -// ) -// case .mastodon(let record): -// guard let status = record.object(in: context.managedObjectContext) else { return } -// configure( -// tableView: tableView, -// cell: cell, -// viewModel: StatusTableViewCell.ViewModel(value: .mastodonStatus(status)), -// configuration: configuration -// ) -// } // end switch -// } - return cell + case .topLoader: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell diff --git a/TwidereX/Scene/Account/List/AccountListViewController.swift b/TwidereX/Scene/Account/List/AccountListViewController.swift index a7a7758a..b0e30dc4 100644 --- a/TwidereX/Scene/Account/List/AccountListViewController.swift +++ b/TwidereX/Scene/Account/List/AccountListViewController.swift @@ -31,7 +31,6 @@ final class AccountListViewController: UIViewController, NeedsDependency { }() private(set) lazy var tableView: UITableView = { let tableView = UITableView() - tableView.register(AccountListTableViewCell.self, forCellReuseIdentifier: String(describing: AccountListTableViewCell.self)) tableView.rowHeight = UITableView.automaticDimension tableView.separatorStyle = .none return tableView diff --git a/TwidereX/Scene/Account/List/View/AccountListTableViewCell+ViewModel.swift b/TwidereX/Scene/Account/List/View/AccountListTableViewCell+ViewModel.swift index 620b0eb6..2f338137 100644 --- a/TwidereX/Scene/Account/List/View/AccountListTableViewCell+ViewModel.swift +++ b/TwidereX/Scene/Account/List/View/AccountListTableViewCell+ViewModel.swift @@ -1,78 +1,78 @@ +//// +//// AccountListTableViewCell+ViewModel.swift +//// AccountListTableViewCell+ViewModel +//// +//// Created by Cirno MainasuK on 2021-8-26. +//// Copyright © 2021 Twidere. All rights reserved. +//// // -// AccountListTableViewCell+ViewModel.swift -// AccountListTableViewCell+ViewModel +//import UIKit +//import Combine +//import CoreDataStack +//import MastodonMeta // -// Created by Cirno MainasuK on 2021-8-26. -// Copyright © 2021 Twidere. All rights reserved. +//extension AccountListTableViewCell { +// func configure(authenticationIndex: AuthenticationIndex) { +// if let twitterUser = authenticationIndex.twitterAuthentication?.user { +// configure(twitterUser: twitterUser) +// } else if let mastodonUser = authenticationIndex.mastodonAuthentication?.user { +// configure(mastodonUser: mastodonUser) +// } else { +// assertionFailure() +// } +// } // - -import UIKit -import Combine -import CoreDataStack -import MastodonMeta - -extension AccountListTableViewCell { - func configure(authenticationIndex: AuthenticationIndex) { - if let twitterUser = authenticationIndex.twitterAuthentication?.user { - configure(twitterUser: twitterUser) - } else if let mastodonUser = authenticationIndex.mastodonAuthentication?.user { - configure(mastodonUser: mastodonUser) - } else { - assertionFailure() - } - } - - private func configure(twitterUser user: TwitterUser) { - // badge - userBriefInfoView.viewModel.platform = .twitter - // avatar - user.publisher(for: \.profileImageURL) - .map { _ in user.avatarImageURL() } - .assign(to: \.avatarImageURL, on: userBriefInfoView.viewModel) - .store(in: &disposeBag) - // name - user.publisher(for: \.name) - .map { PlaintextMetaContent(string: $0) } - .assign(to: \.headlineMetaContent, on: userBriefInfoView.viewModel) - .store(in: &disposeBag) - // username - user.publisher(for: \.username) - .map { "@" + $0 } - .map { $0 as String? } - .assign(to: \.subheadlineText, on: userBriefInfoView.viewModel) - .store(in: &disposeBag) - } - - private func configure(mastodonUser user: MastodonUser) { - // badge - userBriefInfoView.viewModel.platform = .mastodon - // avatar - user.publisher(for: \.avatar) - .map { avatar in avatar.flatMap { URL(string: $0) } } - .assign(to: \.avatarImageURL, on: userBriefInfoView.viewModel) - .store(in: &disposeBag) - // name - Publishers.CombineLatest( - user.publisher(for: \.displayName), - user.publisher(for: \.emojis) - ) - .map { _, emojis -> MetaContent? in - let content = MastodonContent(content: user.name, emojis: emojis.asDictionary) - do { - let metaContent = try MastodonMetaContent.convert(document: content) - return metaContent - } catch { - assertionFailure() - return PlaintextMetaContent(string: user.name) - } - } - .assign(to: \.headlineMetaContent, on: userBriefInfoView.viewModel) - .store(in: &disposeBag) - // username - user.publisher(for: \.acct) - .map { _ in user.acctWithDomain } - .map { $0 as String? } - .assign(to: \.subheadlineText, on: userBriefInfoView.viewModel) - .store(in: &disposeBag) - } -} +// private func configure(twitterUser user: TwitterUser) { +// // badge +// userBriefInfoView.viewModel.platform = .twitter +// // avatar +// user.publisher(for: \.profileImageURL) +// .map { _ in user.avatarImageURL() } +// .assign(to: \.avatarImageURL, on: userBriefInfoView.viewModel) +// .store(in: &disposeBag) +// // name +// user.publisher(for: \.name) +// .map { PlaintextMetaContent(string: $0) } +// .assign(to: \.headlineMetaContent, on: userBriefInfoView.viewModel) +// .store(in: &disposeBag) +// // username +// user.publisher(for: \.username) +// .map { "@" + $0 } +// .map { $0 as String? } +// .assign(to: \.subheadlineText, on: userBriefInfoView.viewModel) +// .store(in: &disposeBag) +// } +// +// private func configure(mastodonUser user: MastodonUser) { +// // badge +// userBriefInfoView.viewModel.platform = .mastodon +// // avatar +// user.publisher(for: \.avatar) +// .map { avatar in avatar.flatMap { URL(string: $0) } } +// .assign(to: \.avatarImageURL, on: userBriefInfoView.viewModel) +// .store(in: &disposeBag) +// // name +// Publishers.CombineLatest( +// user.publisher(for: \.displayName), +// user.publisher(for: \.emojis) +// ) +// .map { _, emojis -> MetaContent? in +// let content = MastodonContent(content: user.name, emojis: emojis.asDictionary) +// do { +// let metaContent = try MastodonMetaContent.convert(document: content) +// return metaContent +// } catch { +// assertionFailure() +// return PlaintextMetaContent(string: user.name) +// } +// } +// .assign(to: \.headlineMetaContent, on: userBriefInfoView.viewModel) +// .store(in: &disposeBag) +// // username +// user.publisher(for: \.acct) +// .map { _ in user.acctWithDomain } +// .map { $0 as String? } +// .assign(to: \.subheadlineText, on: userBriefInfoView.viewModel) +// .store(in: &disposeBag) +// } +//} diff --git a/TwidereX/Scene/Account/List/View/AccountListTableViewCell.swift b/TwidereX/Scene/Account/List/View/AccountListTableViewCell.swift index 98cc56f2..b006a387 100644 --- a/TwidereX/Scene/Account/List/View/AccountListTableViewCell.swift +++ b/TwidereX/Scene/Account/List/View/AccountListTableViewCell.swift @@ -1,69 +1,69 @@ +//// +//// AccountListTableViewCell.swift +//// TwidereX +//// +//// Created by Cirno MainasuK on 2020/11/11. +//// Copyright © 2020 Twidere. All rights reserved. +//// // -// AccountListTableViewCell.swift -// TwidereX +//import os.log +//import UIKit +//import Combine +//import TwidereCore // -// Created by Cirno MainasuK on 2020/11/11. -// Copyright © 2020 Twidere. All rights reserved. +//final class AccountListTableViewCell: UITableViewCell { +// +// var disposeBag = Set() +// var observations = Set() +// +// let userBriefInfoView = UserBriefInfoView() +// +// let separatorLine = SeparatorLineView() +// +// override func prepareForReuse() { +// super.prepareForReuse() +// +// disposeBag.removeAll() +// observations.removeAll() +// +// userBriefInfoView.prepareForReuse() +// } // - -import os.log -import UIKit -import Combine -import TwidereCore - -final class AccountListTableViewCell: UITableViewCell { - - var disposeBag = Set() - var observations = Set() - - let userBriefInfoView = UserBriefInfoView() - - let separatorLine = SeparatorLineView() - - override func prepareForReuse() { - super.prepareForReuse() - - disposeBag.removeAll() - observations.removeAll() - - userBriefInfoView.prepareForReuse() - } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension AccountListTableViewCell { - - private func _init() { - userBriefInfoView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(userBriefInfoView) - NSLayoutConstraint.activate([ - userBriefInfoView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20), - userBriefInfoView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - contentView.readableContentGuide.trailingAnchor.constraint(equalTo: userBriefInfoView.trailingAnchor), - contentView.bottomAnchor.constraint(equalTo: userBriefInfoView.bottomAnchor, constant: 16).priority(.defaultHigh), - ]) - - userBriefInfoView.secondaryHeadlineLabel.isHidden = true - userBriefInfoView.followActionButton.isHidden = true - - separatorLine.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(separatorLine) - NSLayoutConstraint.activate([ - separatorLine.leadingAnchor.constraint(equalTo: userBriefInfoView.headlineLabel.leadingAnchor), - separatorLine.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), - separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), - ]) - } - -} +// override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { +// super.init(style: style, reuseIdentifier: reuseIdentifier) +// _init() +// } +// +// required init?(coder: NSCoder) { +// super.init(coder: coder) +// _init() +// } +// +//} +// +//extension AccountListTableViewCell { +// +// private func _init() { +// userBriefInfoView.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(userBriefInfoView) +// NSLayoutConstraint.activate([ +// userBriefInfoView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20), +// userBriefInfoView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), +// contentView.readableContentGuide.trailingAnchor.constraint(equalTo: userBriefInfoView.trailingAnchor), +// contentView.bottomAnchor.constraint(equalTo: userBriefInfoView.bottomAnchor, constant: 16).priority(.defaultHigh), +// ]) +// +// userBriefInfoView.secondaryHeadlineLabel.isHidden = true +// userBriefInfoView.followActionButton.isHidden = true +// +// separatorLine.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(separatorLine) +// NSLayoutConstraint.activate([ +// separatorLine.leadingAnchor.constraint(equalTo: userBriefInfoView.headlineLabel.leadingAnchor), +// separatorLine.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), +// separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), +// ]) +// } +// +//} diff --git a/TwidereX/Scene/Profile/FriendshipList/FollowingListViewModel+Diffable.swift b/TwidereX/Scene/Profile/FriendshipList/FollowingListViewModel+Diffable.swift index 858653ea..6436a673 100644 --- a/TwidereX/Scene/Profile/FriendshipList/FollowingListViewModel+Diffable.swift +++ b/TwidereX/Scene/Profile/FriendshipList/FollowingListViewModel+Diffable.swift @@ -52,7 +52,7 @@ extension FriendshipListViewModel { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) let newItems: [UserItem] = records.map { - .user(record: $0, kind: .friendship) + .user(record: $0, kind: .friend) } snapshot.appendItems(newItems, toSection: .main) return snapshot diff --git a/TwidereX/Scene/Search/SearchResult/SearchResultViewModel.swift b/TwidereX/Scene/Search/SearchResult/SearchResultViewModel.swift index 344d0f75..e61a5549 100644 --- a/TwidereX/Scene/Search/SearchResult/SearchResultViewModel.swift +++ b/TwidereX/Scene/Search/SearchResult/SearchResultViewModel.swift @@ -116,7 +116,7 @@ extension SearchResultViewModel { case .user: let _viewController = SearchUserViewController() - _viewController.viewModel = SearchUserViewModel(context: context, authContext: authContext, kind: .friendship) + _viewController.viewModel = SearchUserViewModel(context: context, authContext: authContext, kind: .search) $searchText.assign(to: &_viewController.viewModel.$searchText) $userIdentifier.assign(to: &_viewController.viewModel.$userIdentifier) viewController = _viewController diff --git a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+Diffable.swift b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+Diffable.swift index 3fbf33a4..2014d64d 100644 --- a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+Diffable.swift +++ b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+Diffable.swift @@ -52,8 +52,8 @@ extension SearchUserViewModel { snapshot.appendSections([.main]) let newItems: [UserItem] = records.map { record in switch self.kind { - case .friendship: - return .user(record: record, kind: .relationship) + case .search: + return .user(record: record, kind: .search) case .listMember: return .user(record: record, kind: .addListMember) } // end switch diff --git a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+State.swift b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+State.swift index 9ce3259c..c1bf055d 100644 --- a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+State.swift +++ b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+State.swift @@ -92,7 +92,7 @@ extension SearchUserViewModel.State { searchText: searchText, following: { switch viewModel.kind { - case .friendship: return false + case .search: return false case .listMember: return true } }(), diff --git a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel.swift b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel.swift index fd67e318..fc6b5153 100644 --- a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel.swift +++ b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel.swift @@ -89,7 +89,7 @@ final class SearchUserViewModel { extension SearchUserViewModel { enum Kind { - case friendship + case search case listMember(list: ListRecord) } } diff --git a/TwidereX/Scene/Share/View/CollectionViewCell/StatusMediaGalleryCollectionCell.swift b/TwidereX/Scene/Share/View/CollectionViewCell/StatusMediaGalleryCollectionCell.swift index 0eeb5d4e..10b91bda 100644 --- a/TwidereX/Scene/Share/View/CollectionViewCell/StatusMediaGalleryCollectionCell.swift +++ b/TwidereX/Scene/Share/View/CollectionViewCell/StatusMediaGalleryCollectionCell.swift @@ -19,15 +19,15 @@ protocol StatusMediaGalleryCollectionCellDelegate: AnyObject { final class StatusMediaGalleryCollectionCell: UICollectionViewCell { let logger = Logger(subsystem: "StatusMediaGalleryCollectionCell", category: "Cell") - - weak var delegate: StatusMediaGalleryCollectionCellDelegate? - - var disposeBag = Set() - private(set) lazy var viewModel: ViewModel = { - let viewModel = ViewModel() - viewModel.bind(cell: self) - return viewModel - }() + +// weak var delegate: StatusMediaGalleryCollectionCellDelegate? +// +// var disposeBag = Set() +// private(set) lazy var viewModel: ViewModel = { +// let viewModel = ViewModel() +// viewModel.bind(cell: self) +// return viewModel +// }() // let sensitiveToggleButtonBlurVisualEffectView: UIVisualEffectView = { // let visualEffectView = UIVisualEffectView(effect: ContentWarningOverlayView.blurVisualEffect) @@ -37,11 +37,11 @@ final class StatusMediaGalleryCollectionCell: UICollectionViewCell { // return visualEffectView // }() // let sensitiveToggleButtonVibrancyVisualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: ContentWarningOverlayView.blurVisualEffect)) - let sensitiveToggleButton: HitTestExpandedButton = { - let button = HitTestExpandedButton(type: .system) - button.setImage(Asset.Human.eyeSlashMini.image.withRenderingMode(.alwaysTemplate), for: .normal) - return button - }() +// let sensitiveToggleButton: HitTestExpandedButton = { +// let button = HitTestExpandedButton(type: .system) +// button.setImage(Asset.Human.eyeSlashMini.image.withRenderingMode(.alwaysTemplate), for: .normal) +// return button +// }() // public let contentWarningOverlayView: ContentWarningOverlayView = { // let overlay = ContentWarningOverlayView() @@ -53,27 +53,28 @@ final class StatusMediaGalleryCollectionCell: UICollectionViewCell { // let mediaView = MediaView() - let collectionViewLayout: CoverFlowStackCollectionViewLayout = { - let layout = CoverFlowStackCollectionViewLayout() - layout.sizeScaleRatio = 0.9 - return layout - }() - private(set) lazy var collectionView: UICollectionView = { - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) - collectionView.backgroundColor = .clear - collectionView.layer.masksToBounds = true -// collectionView.layer.cornerRadius = MediaView.cornerRadius - collectionView.layer.cornerCurve = .continuous - return collectionView - }() - var diffableDataSource: UICollectionViewDiffableDataSource? +// let collectionViewLayout: CoverFlowStackCollectionViewLayout = { +// let layout = CoverFlowStackCollectionViewLayout() +// layout.sizeScaleRatio = 0.9 +// return layout +// }() +// private(set) lazy var collectionView: UICollectionView = { +// let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) +// collectionView.backgroundColor = .clear +// collectionView.layer.masksToBounds = true +//// collectionView.layer.cornerRadius = MediaView.cornerRadius +// collectionView.layer.cornerCurve = .continuous +// return collectionView +// }() +// var diffableDataSource: UICollectionViewDiffableDataSource? override func prepareForReuse() { super.prepareForReuse() - disposeBag.removeAll() + contentConfiguration = nil +// disposeBag.removeAll() // mediaView.prepareForReuse() - diffableDataSource?.applySnapshotUsingReloadData(.init()) +// diffableDataSource?.applySnapshotUsingReloadData(.init()) } override init(frame: CGRect) { @@ -161,17 +162,17 @@ extension StatusMediaGalleryCollectionCell { } -extension StatusMediaGalleryCollectionCell { - @objc private func sensitiveToggleButtonDidPressed(_ sender: UIButton) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") -// delegate?.statusMediaGalleryCollectionCell(self, toggleContentWarningOverlayViewDisplay: contentWarningOverlayView) - } -} - -// MARK: - UICollectionViewDelegate -extension StatusMediaGalleryCollectionCell: UICollectionViewDelegate { - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): did select \(indexPath.debugDescription)") - delegate?.statusMediaGalleryCollectionCell(self, coverFlowCollectionView: collectionView, didSelectItemAt: indexPath) - } -} +//extension StatusMediaGalleryCollectionCell { +// @objc private func sensitiveToggleButtonDidPressed(_ sender: UIButton) { +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") +//// delegate?.statusMediaGalleryCollectionCell(self, toggleContentWarningOverlayViewDisplay: contentWarningOverlayView) +// } +//} +// +//// MARK: - UICollectionViewDelegate +//extension StatusMediaGalleryCollectionCell: UICollectionViewDelegate { +// func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): did select \(indexPath.debugDescription)") +// delegate?.statusMediaGalleryCollectionCell(self, coverFlowCollectionView: collectionView, didSelectItemAt: indexPath) +// } +//} From 87b47b07378afec7251f28516cdfcd23931b501d Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 20 Apr 2023 14:18:45 +0800 Subject: [PATCH 058/128] chore: restore the menu actions for media view --- .../Container/MediaGridContainerView.swift | 68 +++-- .../Content/MediaView+ViewModel.swift | 41 +++ .../TwidereUI/Content/StatusView.swift | 12 +- .../StatusViewTableViewCellDelegate.swift | 11 +- .../Facade/DataSourceFacade+Media.swift | 254 +++++++++++------- ...ider+StatusViewTableViewCellDelegate.swift | 71 +---- 6 files changed, 260 insertions(+), 197 deletions(-) diff --git a/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift b/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift index 7108e03a..f085a4f6 100644 --- a/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift +++ b/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift @@ -20,8 +20,7 @@ public struct MediaGridContainerView: View { public let idealWidth: CGFloat? public let idealHeight: CGFloat // ideal height for grid exclude single media - public let previewAction: (MediaView.ViewModel) -> Void - public let previewActionWithContext: (MediaView.ViewModel, ContextMenuInteractionPreviewActionContext) -> Void + public let handler: (MediaView.ViewModel, MediaView.ViewModel.Action) -> Void public var body: some View { VStack { @@ -158,16 +157,6 @@ public struct MediaGridContainerView: View { } // end body } -extension MediaGridContainerView { - public func contextMenuItems(for viewModel: MediaView.ViewModel) -> some View { - Button { - - } label: { - Label(L10n.Common.Controls.Actions.save, systemImage: "square.and.arrow.down") - } - } -} - extension MediaGridContainerView { private func mediaView(at index: Int, width: CGFloat?, height: CGFloat?) -> some View { @@ -213,7 +202,8 @@ extension MediaGridContainerView { ) .contentShape(Rectangle()) .onTapGesture { - previewAction(viewModels[index]) + let viewModel = viewModels[index] + handler(viewModel, MediaView.ViewModel.Action.preview) } .contextMenu(contextMenuContentPreviewProvider: { let viewModel = viewModels[index] @@ -224,19 +214,54 @@ extension MediaGridContainerView { return previewProvider }, contextMenuActionProvider: { _ in - let children: [UIAction] = [ + let viewModel = viewModels[index] + let shareChildren: [UIAction] = [ UIAction( - title: L10n.Common.Controls.Actions.copy, - image: UIImage(systemName: "doc.on.doc"), + title: MediaView.ViewModel.Action.shareLink.title, + image: MediaView.ViewModel.Action.shareLink.image, attributes: [], state: .off ) { _ in - print("Hi copy") - } + handler(viewModel, .shareLink) + }, + UIAction( + title: MediaView.ViewModel.Action.shareMedia.title, + image: MediaView.ViewModel.Action.shareMedia.image, + attributes: [], + state: .off + ) { _ in + handler(viewModel, .shareMedia) + }, + ] + let children: [UIMenuElement] = [ + UIAction( + title: MediaView.ViewModel.Action.save.title, + image: MediaView.ViewModel.Action.save.image, + attributes: [], + state: .off + ) { _ in + handler(viewModel, .save) + }, + UIAction( + title: MediaView.ViewModel.Action.copy.title, + image: MediaView.ViewModel.Action.copy.image, + attributes: [], + state: .off + ) { _ in + handler(viewModel, .copy) + }, + UIMenu( + title: L10n.Common.Controls.Actions.share, + image: UIImage(systemName: "square.and.arrow.up"), + identifier: nil, + options: [], + children: shareChildren + ), ] return UIMenu(title: "", image: nil, identifier: nil, options: [], children: children) }, previewActionWithContext: { context in - previewActionWithContext(viewModels[index], context) + let viewModel = viewModels[index] + handler(viewModel, MediaView.ViewModel.Action.previewWithContext(context)) }) } // end func @@ -356,10 +381,7 @@ struct MediaGridContainerView_Previews: PreviewProvider { viewModels: Array(viewModels.prefix(i + 1)), idealWidth: 300, idealHeight: 280, - previewAction: { _ in - // do nothing - }, - previewActionWithContext: { _, _ in + handler: { _, _ in // do nothing } ) diff --git a/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift index 4825012c..931df219 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift @@ -98,6 +98,47 @@ extension MediaView.ViewModel { case photo case animatedGIF } + + public enum Action { + case preview + case previewWithContext(ContextMenuInteractionPreviewActionContext) + case save + case copy + case shareLink + case shareMedia + + public var title: String { + switch self { + case .preview: return "Preview" + case .previewWithContext: return "Preview with context" + case .save: return L10n.Common.Controls.Actions.save + case .copy: return L10n.Common.Controls.Actions.copy + case .shareLink: return L10n.Common.Controls.Actions.shareLink + case .shareMedia: return L10n.Common.Controls.Actions.shareMedia + } + } + + public var image: UIImage? { + switch self { + case .preview: return UIImage(systemName: "eye") + case .previewWithContext: return UIImage(systemName: "eye.fill") + case .save: return UIImage(systemName: "square.and.arrow.down") + case .copy: return UIImage(systemName: "doc.on.doc") + case .shareLink: return UIImage(systemName: "link") + case .shareMedia: return UIImage(systemName: "photo") + } + } + } +} + +extension MediaView.ViewModel.MediaKind { + public var resourceType: PHAssetResourceType { + switch self { + case .video: return .video + case .photo: return .photo + case .animatedGIF: return .video + } + } } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index fbcb1f69..d095ff9b 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -29,8 +29,7 @@ public protocol StatusViewDelegate: AnyObject { func statusView(_ viewModel: StatusView.ViewModel, textViewDidSelectMeta meta: Meta) // media - func statusView(_ viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel) - func statusView(_ viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel, previewActionContext: ContextMenuInteractionPreviewActionContext) + func statusView(_ viewModel: StatusView.ViewModel, mediaViewModel mediaViewModel: MediaView.ViewModel, action: MediaView.ViewModel.Action) func statusView(_ viewModel: StatusView.ViewModel, toggleContentWarningOverlayDisplay isReveal: Bool) // poll @@ -132,14 +131,11 @@ public struct StatusView: View { viewModels: viewModel.mediaViewModels, idealWidth: viewModel.contentWidth, idealHeight: 280, - previewAction: { mediaViewModel in - viewModel.delegate?.statusView(viewModel, previewActionForMediaViewModel: mediaViewModel) - }, - previewActionWithContext: { mediaViewModel, context in - viewModel.delegate?.statusView(viewModel, previewActionForMediaViewModel: mediaViewModel, previewActionContext: context) + handler: { mediaViewModel, action in + viewModel.delegate?.statusView(viewModel, mediaViewModel: mediaViewModel, action: action) } ) - .clipShape(RoundedRectangle(cornerRadius: MediaGridContainerView.cornerRadius)) + // .clipShape(RoundedRectangle(cornerRadius: MediaGridContainerView.cornerRadius)) .overlay { ContentWarningOverlayView(isReveal: viewModel.isMediaContentWarningOverlayReveal) { viewModel.delegate?.statusView(viewModel, toggleContentWarningOverlayDisplay: !viewModel.isMediaContentWarningOverlayReveal) diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/Status/StatusViewTableViewCellDelegate.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/Status/StatusViewTableViewCellDelegate.swift index 407dae7a..a06d6e8b 100644 --- a/TwidereSDK/Sources/TwidereUI/TableViewCell/Status/StatusViewTableViewCellDelegate.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/Status/StatusViewTableViewCellDelegate.swift @@ -27,8 +27,7 @@ public protocol StatusViewTableViewCellDelegate: AutoGenerateProtocolDelegate { func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, userAvatarButtonDidPressed user: UserRecord) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, toggleContentDisplay isReveal: Bool) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, textViewDidSelectMeta meta: Meta) - func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel) - func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel, previewActionContext: ContextMenuInteractionPreviewActionContext) + func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, mediaViewModel: MediaView.ViewModel, action: MediaView.ViewModel.Action) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, toggleContentWarningOverlayDisplay isReveal: Bool) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, pollVoteActionForViewModel pollViewModel: PollView.ViewModel) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, pollUpdateIfNeedsForViewModel pollViewModel: PollView.ViewModel) @@ -55,12 +54,8 @@ public extension StatusViewDelegate where Self: StatusViewContainerTableViewCell statusViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, textViewDidSelectMeta: meta) } - func statusView(_ viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel) { - statusViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, previewActionForMediaViewModel: mediaViewModel) - } - - func statusView(_ viewModel: StatusView.ViewModel, previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel, previewActionContext: ContextMenuInteractionPreviewActionContext) { - statusViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, previewActionForMediaViewModel: mediaViewModel, previewActionContext: previewActionContext) + func statusView(_ viewModel: StatusView.ViewModel, mediaViewModel: MediaView.ViewModel, action: MediaView.ViewModel.Action) { + statusViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, mediaViewModel: mediaViewModel, action: action) } func statusView(_ viewModel: StatusView.ViewModel, toggleContentWarningOverlayDisplay isReveal: Bool) { diff --git a/TwidereX/Protocol/Facade/DataSourceFacade+Media.swift b/TwidereX/Protocol/Facade/DataSourceFacade+Media.swift index a494fe74..abf42969 100644 --- a/TwidereX/Protocol/Facade/DataSourceFacade+Media.swift +++ b/TwidereX/Protocol/Facade/DataSourceFacade+Media.swift @@ -10,63 +10,6 @@ import UIKit import AVKit import TwidereCore -extension DataSourceFacade { - - struct MediaPreviewContext { - // let statusView: StatusView - let containerView: ContainerView - let mediaView: MediaView - let index: Int - - enum ContainerView { - case mediaView(MediaView) - case mediaGridContainerView(MediaGridContainerView) - } - - func thumbnails() async -> [UIImage?] { - return [] -// switch containerView { -// case .mediaView(let mediaView): -// let thumbnail = await mediaView.thumbnail() -// return [thumbnail] -// case .mediaGridContainerView(let mediaGridContainerView): -// let thumbnails = await mediaGridContainerView.mediaViews.parallelMap { mediaView in -// return await mediaView.thumbnail() -// } -// return thumbnails -// } - } - } - - static func coordinateToMediaPreviewScene( - provider: DataSourceProvider & AuthContextProvider & MediaPreviewableViewController, - target: StatusTarget, - status: StatusRecord, - mediaPreviewContext: MediaPreviewContext - ) async { -// let _redirectRecord = await DataSourceFacade.status( -// managedObjectContext: provider.context.managedObjectContext, -// status: status, -// target: target -// ) -// guard let redirectRecord = _redirectRecord else { return } -// -// await coordinateToMediaPreviewScene( -// provider: provider, -// status: redirectRecord, -// mediaPreviewContext: mediaPreviewContext -// ) -// -// Task { -// await recordStatusHistory( -// denpendency: provider, -// status: status -// ) -// } // end Task - } - -} - extension DataSourceFacade { @MainActor @@ -83,42 +26,7 @@ extension DataSourceFacade { return } let thumbnails = statusViewModel.mediaViewModels.map { $0.thumbnail } - - // use standard video player -// if let first = attachments.first, first.kind == .video || first.kind == .audio { -// Task { @MainActor [weak provider] in -// guard let provider = provider else { return } -// // workaround Twitter Video assertURL missing from V2 API issue -// var assetURL: URL -// if let url = first.assetURL { -// assetURL = url -// } else if case let .twitter(record) = status { -// let _statusID: String? = await provider.context.managedObjectContext.perform { -// let status = record.object(in: provider.context.managedObjectContext) -// return status?.id -// } -// guard let statusID = _statusID, -// case let .twitter(authenticationContext) = provider.authContext.authenticationContext -// else { return } -// -// let _response = try? await provider.context.apiService.twitterStatusV1(statusIDs: [statusID], authenticationContext: authenticationContext) -// guard let status = _response?.value.first, -// let url = status.extendedEntities?.media?.first?.assetURL.flatMap({ URL(string: $0) }) -// else { return } -// assetURL = url -// } else { -// assertionFailure() -// return -// } -// let playerViewController = AVPlayerViewController() -// playerViewController.player = AVPlayer(url: assetURL) -// playerViewController.player?.play() -// playerViewController.delegate = provider.context.playerService -// provider.present(playerViewController, animated: true, completion: nil) -// } // end Task -// return -// } - + // note: // previewActionContext will automatically dismiss with fade animation style previewActionContext?.animator.preferredCommitStyle = .dismiss @@ -187,3 +95,163 @@ extension DataSourceFacade { } } + +extension DataSourceFacade { + + @MainActor + static func responseToMediaViewAction( + provider: DataSourceProvider & AuthContextProvider & MediaPreviewableViewController, + statusViewModel: StatusView.ViewModel, + mediaViewModel: MediaView.ViewModel, + action: MediaView.ViewModel.Action + ) { + switch action { + case .preview: + assert(Thread.isMainThread) + let status = statusViewModel.status.asRecord + DataSourceFacade.coordinateToMediaPreviewScene( + provider: provider, + status: status, + statusViewModel: statusViewModel, + mediaViewModel: mediaViewModel + ) + case .previewWithContext(let previewActionContext): + assert(Thread.isMainThread) + let status = statusViewModel.status.asRecord + DataSourceFacade.coordinateToMediaPreviewScene( + provider: provider, + status: status, + statusViewModel: statusViewModel, + mediaViewModel: mediaViewModel, + previewActionContext: previewActionContext + ) + case .save: + Task { + await responseToMediaViewSaveAction( + provider: provider, + mediaViewModel: mediaViewModel + ) + } // end Task + case .copy: + Task { + await responseToMediaViewCopyAction( + provider: provider, + mediaViewModel: mediaViewModel + ) + } // end Task + case .shareLink: + Task { + await responseToMediaViewShareLinkAction( + provider: provider, + mediaViewModel: mediaViewModel + ) + } // end Task + case .shareMedia: + Task { + await responseToMediaViewShareMediaAction( + provider: provider, + mediaViewModel: mediaViewModel + ) + } // end Task + } // end switch + } + +} + +extension DataSourceFacade { + + @MainActor + static func responseToMediaViewSaveAction( + provider: DataSourceProvider & AuthContextProvider & MediaPreviewableViewController, + mediaViewModel: MediaView.ViewModel + ) async { + guard let assetURL = mediaViewModel.downloadURL else { return } + + let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) + let notificationFeedbackGenerator = UINotificationFeedbackGenerator() + + do { + impactFeedbackGenerator.impactOccurred() + try await provider.context.photoLibraryService.save( + source: .remote(url: assetURL), + resourceType: mediaViewModel.mediaKind.resourceType + ) + provider.context.photoLibraryService.presentSuccessNotification(title: L10n.Common.Alerts.PhotoSaved.title) + notificationFeedbackGenerator.notificationOccurred(.success) + } catch { + provider.context.photoLibraryService.presentFailureNotification( + error: error, + title: L10n.Common.Alerts.PhotoSaveFail.title, + message: L10n.Common.Alerts.PhotoSaveFail.message + ) + notificationFeedbackGenerator.notificationOccurred(.error) + } + } + + @MainActor + static func responseToMediaViewCopyAction( + provider: DataSourceProvider & AuthContextProvider & MediaPreviewableViewController, + mediaViewModel: MediaView.ViewModel + ) async { + guard let assetURL = mediaViewModel.downloadURL else { return } + + let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) + let notificationFeedbackGenerator = UINotificationFeedbackGenerator() + + do { + impactFeedbackGenerator.impactOccurred() + try await provider.context.photoLibraryService.copy( + source: .remote(url: assetURL), + resourceType: mediaViewModel.mediaKind.resourceType + ) + provider.context.photoLibraryService.presentSuccessNotification(title: L10n.Common.Alerts.PhotoCopied.title) + notificationFeedbackGenerator.notificationOccurred(.success) + } catch { + provider.context.photoLibraryService.presentFailureNotification( + error: error, + title: L10n.Common.Alerts.PhotoCopied.title, + message: L10n.Common.Alerts.PhotoCopyFail.message + ) + notificationFeedbackGenerator.notificationOccurred(.error) + } + } + + @MainActor + static func responseToMediaViewShareLinkAction( + provider: DataSourceProvider & AuthContextProvider & MediaPreviewableViewController, + mediaViewModel: MediaView.ViewModel + ) async { + guard let assetURL = mediaViewModel.downloadURL else { return } + + let applicationActivities: [UIActivity] = [ + SafariActivity(sceneCoordinator: provider.coordinator) + ] + let activityViewController = UIActivityViewController( + activityItems: [assetURL], + applicationActivities: applicationActivities + ) + activityViewController.popoverPresentationController?.sourceRect = mediaViewModel.frameInWindow + provider.present(activityViewController, animated: true, completion: nil) + } + + @MainActor + static func responseToMediaViewShareMediaAction( + provider: DataSourceProvider & AuthContextProvider & MediaPreviewableViewController, + mediaViewModel: MediaView.ViewModel + ) async { + guard let assetURL = mediaViewModel.downloadURL else { return } + guard let url = try? await provider.context.photoLibraryService.file(from: .remote(url: assetURL)) else { + return + } + + let applicationActivities: [UIActivity] = [ + SafariActivity(sceneCoordinator: provider.coordinator) + ] + let activityViewController = UIActivityViewController( + activityItems: [url], + applicationActivities: applicationActivities + ) + activityViewController.popoverPresentationController?.sourceRect = mediaViewModel.frameInWindow + provider.present(activityViewController, animated: true, completion: nil) + } +} diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift index c6c39a28..b089a00e 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift @@ -127,82 +127,23 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC // MARK: - media extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider & MediaPreviewableViewController { - func tableViewCell( - _ cell: UITableViewCell, - viewModel: StatusView.ViewModel, - previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel - ) { - Task { - let status = viewModel.status.asRecord - - await DataSourceFacade.coordinateToMediaPreviewScene( - provider: self, - status: status, - statusViewModel: viewModel, - mediaViewModel: mediaViewModel - ) - } // end Task - } - + @MainActor func tableViewCell( _ cell: UITableViewCell, viewModel: StatusView.ViewModel, - previewActionForMediaViewModel mediaViewModel: MediaView.ViewModel, - previewActionContext: ContextMenuInteractionPreviewActionContext + mediaViewModel: MediaView.ViewModel, + action: MediaView.ViewModel.Action ) { - let status = viewModel.status.asRecord - - DataSourceFacade.coordinateToMediaPreviewScene( + assert(Thread.isMainThread) + DataSourceFacade.responseToMediaViewAction( provider: self, - status: status, statusViewModel: viewModel, mediaViewModel: mediaViewModel, - previewActionContext: previewActionContext + action: action ) } -// func tableViewCell( -// _ cell: UITableViewCell, -// statusView: StatusView, -// mediaGridContainerView containerView: MediaGridContainerView, -// didTapMediaView mediaView: MediaView, -// at index: Int -// ) { -// -// } -// -// func tableViewCell( -// _ cell: UITableViewCell, -// statusView: StatusView, -// quoteStatusView: StatusView, -// mediaGridContainerView containerView: MediaGridContainerView, -// didTapMediaView mediaView: MediaView, -// at index: Int -// ) { -// Task { -// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) -// guard let item = await item(from: source) else { -// assertionFailure() -// return -// } -// guard let status = await item.status(in: self.context.managedObjectContext) else { -// assertionFailure("only works for status data provider") -// return -// } -// await DataSourceFacade.coordinateToMediaPreviewScene( -// provider: self, -// target: .quote, -// status: status, -// mediaPreviewContext: DataSourceFacade.MediaPreviewContext( -// containerView: .mediaGridContainerView(containerView), -// mediaView: mediaView, -// index: index -// ) -// ) -// } -// } - func tableViewCell( _ cell: UITableViewCell, viewModel: StatusView.ViewModel, From 2360bd19bf31448731088e206bcde665d59767af Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 20 Apr 2023 16:13:21 +0800 Subject: [PATCH 059/128] chore: restore the prototype view in settings scene --- .../Content/StatusView+ViewModel.swift | 188 +++++++++++++++++- .../TwidereUI/Content/StatusView.swift | 3 +- .../Content/UserView+ViewModel.swift | 16 +- .../Sources/TwidereUI/Content/UserView.swift | 13 ++ .../Facade/DataSourceFacade+Media.swift | 4 +- ...ider+StatusViewTableViewCellDelegate.swift | 19 +- .../AccountPreferenceView.swift | 7 +- .../AccountPreferenceViewModel.swift | 7 + .../DisplayPreferenceView.swift | 5 +- .../DisplayPreferenceViewController.swift | 18 ++ .../DisplayPreferenceViewModel.swift | 5 +- .../Scene/Setting/List/SettingListView.swift | 7 +- .../List/SettingListViewController.swift | 3 +- .../Setting/List/SettingListViewModel.swift | 10 +- ...eadViewController+DataSourceProvider.swift | 2 +- .../StatusThreadViewModel+Diffable.swift | 2 +- .../StatusThread/StatusThreadViewModel.swift | 4 +- 17 files changed, 275 insertions(+), 38 deletions(-) diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index b841d4cc..a8154665 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -34,14 +34,16 @@ extension StatusView { @Published public var quoteViewModel: StatusView.ViewModel? // input - public let status: StatusObject - public let author: UserObject + public let status: StatusObject? + public let author: UserObject? public let authContext: AuthContext? public let kind: Kind public weak var delegate: StatusViewDelegate? weak var parentViewModel: StatusView.ViewModel? + @Published public var addtionalHorizontalMargin: CGFloat = 0.0 + // output // header @@ -258,6 +260,18 @@ extension StatusView { .map { $0 } .assign(to: &$translateButtonPreference) } + + private init( + viewLayoutFramePublisher: Published.Publisher? + ) { + self.status = nil + self.author = nil + self.authContext = nil + self.kind = .timeline + // end init + + viewLayoutFramePublisher?.assign(to: &$viewLayoutFrame) + } } } @@ -891,7 +905,7 @@ extension StatusView.ViewModel { var containerWidth: CGFloat { let width: CGFloat = { - var width = parentViewModel?.containerWidth ?? viewLayoutFrame.readableContentLayoutFrame.width + var width = parentViewModel?.containerWidth ?? (viewLayoutFrame.readableContentLayoutFrame.width - addtionalHorizontalMargin) width -= containerMargin return width }() @@ -1331,3 +1345,171 @@ extension StatusView.ViewModel { } } } + +extension StatusView.ViewModel { + public static func prototype( + viewLayoutFramePublisher: Published.Publisher? + ) -> StatusView.ViewModel { + let viewModel = StatusView.ViewModel(viewLayoutFramePublisher: viewLayoutFramePublisher) + + viewModel.addtionalHorizontalMargin = 20 + + viewModel.avatarURL = URL(string: "https://pbs.twimg.com/profile_images/809741368134234112/htSiXXAU_400x400.jpg") + viewModel.authorName = PlaintextMetaContent(string: "Twidere") + viewModel.authorUsernme = "TwidereProject" + viewModel.content = TwitterMetaContent.convert( + document: TwitterContent(content: L10n.Scene.Settings.Display.Preview.thankForUsingTwidereX, urlEntities: []), + urlMaximumLength: 16, + twitterTextProvider: SwiftTwitterTextProvider() + ) + + return viewModel +// +// if let repost = status.repost { +// let _repostViewModel = StatusView.ViewModel( +// status: repost, +// authContext: authContext, +// kind: kind, +// delegate: delegate, +// parentViewModel: self, +// viewLayoutFramePublisher: viewLayoutFramePublisher +// ) +// repostViewModel = _repostViewModel +// +// // header - repost +// let _statusHeaderViewModel = StatusHeaderView.ViewModel( +// image: Asset.Media.repeat.image.withRenderingMode(.alwaysTemplate), +// label: { +// let name = status.author.name +// let userRepostText = L10n.Common.Controls.Status.userBoosted(name) +// let text = MastodonContent(content: userRepostText, emojis: status.author.emojis.asDictionary) +// let label = MastodonMetaContent.convert(text: text) +// return label +// }() +// ) +// _statusHeaderViewModel.hasHangingAvatar = _repostViewModel.hasHangingAvatar +// _repostViewModel.statusHeaderViewModel = _statusHeaderViewModel +// } +// +// // author +// status.author.publisher(for: \.avatar) +// .compactMap { $0.flatMap { URL(string: $0) } } +// .assign(to: &$avatarURL) +// status.author.publisher(for: \.displayName) +// .compactMap { _ in status.author.nameMetaContent } +// .assign(to: &$authorName) +// status.author.publisher(for: \.username) +// .map { _ in status.author.acct } +// .assign(to: &$authorUsernme) +// authorUserIdentifier = .mastodon(.init(domain: status.author.domain, id: status.author.id)) +// +// // visibility +// visibility = status.visibility +// +// // timestamp +// switch kind { +// case .conversationRoot: +// break +// default: +// timestampLabelViewModel = TimestampLabelView.ViewModel(timestamp: status.createdAt) +// } +// +// // spoiler content +// if let spoilerText = status.spoilerText, !spoilerText.isEmpty { +// do { +// let content = MastodonContent(content: spoilerText, emojis: status.emojis.asDictionary) +// let metaContent = try MastodonMetaContent.convert(document: content, useParagraphMark: true) +// self.spoilerContent = metaContent +// } catch { +// assertionFailure(error.localizedDescription) +// self.spoilerContent = nil +// } +// } +// +// // content +// do { +// let content = MastodonContent(content: status.content, emojis: status.emojis.asDictionary) +// let metaContent = try MastodonMetaContent.convert(document: content, useParagraphMark: true) +// self.content = metaContent +// } catch { +// assertionFailure(error.localizedDescription) +// self.content = PlaintextMetaContent(string: "") +// } +// +// // language +// status.publisher(for: \.language) +// .assign(to: &$language) +// +// // content warning +// isContentSensitiveToggled = status.isContentSensitiveToggled +// status.publisher(for: \.isContentSensitiveToggled) +// .receive(on: DispatchQueue.main) +// .assign(to: \.isContentSensitiveToggled, on: self) +// .store(in: &disposeBag) +// +// // media +// mediaViewModels = MediaView.ViewModel.viewModels(from: status) +// +// // poll +// if let poll = status.poll { +// self.pollViewModel = PollView.ViewModel( +// authContext: authContext, +// poll: .mastodon(object: poll) +// ) +// } +// +// // media content warning +// isMediaSensitive = status.isMediaSensitive +// isMediaSensitiveToggled = status.isMediaSensitiveToggled +// status.publisher(for: \.isMediaSensitiveToggled) +// .receive(on: DispatchQueue.main) +// .assign(to: \.isMediaSensitiveToggled, on: self) +// .store(in: &disposeBag) +// +// // toolbar +// toolbarViewModel.platform = .mastodon +// status.publisher(for: \.replyCount) +// .map { Int($0) } +// .assign(to: &toolbarViewModel.$replyCount) +// status.publisher(for: \.repostCount) +// .map { Int($0) } +// .assign(to: &toolbarViewModel.$repostCount) +// status.publisher(for: \.likeCount) +// .map { Int($0) } +// .assign(to: &toolbarViewModel.$likeCount) +// if case let .mastodon(authenticationContext) = authContext?.authenticationContext { +// status.publisher(for: \.likeBy) +// .map { users -> Bool in +// let ids = users.map { $0.id } +// return ids.contains(authenticationContext.userID) +// } +// .assign(to: &toolbarViewModel.$isLiked) +// status.publisher(for: \.repostBy) +// .map { users -> Bool in +// let ids = users.map { $0.id } +// return ids.contains(authenticationContext.userID) +// } +// .assign(to: &toolbarViewModel.$isReposted) +// } else { +// // do nothing +// } +// +// // metric +// switch kind { +// case .conversationRoot: +// let _metricViewModel = StatusMetricView.ViewModel(platform: .mastodon, timestamp: status.createdAt) +// metricViewModel = _metricViewModel +// status.publisher(for: \.replyCount) +// .map { Int($0) } +// .assign(to: &_metricViewModel.$replyCount) +// status.publisher(for: \.repostCount) +// .map { Int($0) } +// .assign(to: &_metricViewModel.$repostCount) +// status.publisher(for: \.likeCount) +// .map { Int($0) } +// .assign(to: &_metricViewModel.$likeCount) +// default: +// break +// } + } +} diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index d095ff9b..7cc51384 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -350,7 +350,8 @@ extension StatusView { var avatarButton: some View { Button { - viewModel.delegate?.statusView(viewModel, userAvatarButtonDidPressed: viewModel.author.asRecord) + guard let author = viewModel.author?.asRecord else { return } + viewModel.delegate?.statusView(viewModel, userAvatarButtonDidPressed: author) } label: { let dimension: CGFloat = { switch viewModel.kind { diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift index 284560bf..6c9820c0 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift @@ -181,6 +181,16 @@ extension UserView.ViewModel { // subheadline: follower count // accessory: membership button case addListMember + + // headline: name | lock + // subheadline: username + // accessory: disclosureIndicator + case settingAccountSection + + // headline: name | lock + // subheadline: username + // accessory: none + case plain } public enum AvatarBadge { @@ -205,8 +215,10 @@ extension UserView.ViewModel { var isSeparateLineDisplay: Bool { switch kind { - case .notification: return false - default: return true + case .notification: return false + case .settingAccountSection: return false + case .plain: return false + default: return true } } } diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift index 45596093..d24fbc28 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift @@ -193,6 +193,10 @@ extension UserView { nameLabel case .addListMember: nameLabel + case .settingAccountSection: + nameLabel + case .plain: + nameLabel } } // end Group } @@ -214,6 +218,10 @@ extension UserView { usernameLabel case .addListMember: usernameLabel + case .settingAccountSection: + usernameLabel + case .plain: + usernameLabel } } // end Group } @@ -238,6 +246,11 @@ extension UserView { EmptyView() case .addListMember: EmptyView() + case .settingAccountSection: + Image(systemName: "chevron.right") + .foregroundColor(Color(.secondaryLabel)) + case .plain: + EmptyView() } } // end Group } diff --git a/TwidereX/Protocol/Facade/DataSourceFacade+Media.swift b/TwidereX/Protocol/Facade/DataSourceFacade+Media.swift index abf42969..570e9f8a 100644 --- a/TwidereX/Protocol/Facade/DataSourceFacade+Media.swift +++ b/TwidereX/Protocol/Facade/DataSourceFacade+Media.swift @@ -108,7 +108,7 @@ extension DataSourceFacade { switch action { case .preview: assert(Thread.isMainThread) - let status = statusViewModel.status.asRecord + guard let status = statusViewModel.status?.asRecord else { return } DataSourceFacade.coordinateToMediaPreviewScene( provider: provider, status: status, @@ -117,7 +117,7 @@ extension DataSourceFacade { ) case .previewWithContext(let previewActionContext): assert(Thread.isMainThread) - let status = statusViewModel.status.asRecord + guard let status = statusViewModel.status?.asRecord else { return } DataSourceFacade.coordinateToMediaPreviewScene( provider: provider, status: status, diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift index b089a00e..4ed055fd 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift @@ -100,7 +100,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, toggleContentDisplay isReveal: Bool) { Task { @MainActor in - let status = viewModel.status.asRecord + guard let status = viewModel.status?.asRecord else { return } try await DataSourceFacade.responseToToggleContentSensitiveAction( provider: self, @@ -112,7 +112,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, textViewDidSelectMeta meta: Meta) { Task { @MainActor in - let status = viewModel.status.asRecord + guard let status = viewModel.status?.asRecord else { return } await DataSourceFacade.responseToMetaText( provider: self, @@ -150,7 +150,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC toggleContentWarningOverlayDisplay isReveal: Bool ) { Task { - let status = viewModel.status.asRecord + guard let status = viewModel.status?.asRecord else { return } try await DataSourceFacade.responseToToggleMediaSensitiveAction( provider: self, target: .status, @@ -188,8 +188,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC pollVoteActionForViewModel pollViewModel: PollView.ViewModel ) { Task { - let status = viewModel.status.asRecord - + guard let status = viewModel.status?.asRecord else { return } try await DataSourceFacade.responseToStatusPollVote( provider: self, status: status @@ -207,8 +206,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Poll] not needs update. skip") return } - let status = viewModel.status.asRecord - + guard let status = viewModel.status?.asRecord else { return } try await DataSourceFacade.responseToStatusPollUpdate( provider: self, status: status @@ -224,8 +222,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC pollOptionDidSelectForViewModel optionViewModel: PollOptionView.ViewModel ) { Task { - let status = viewModel.status.asRecord - + guard let status = viewModel.status?.asRecord else { return } await DataSourceFacade.responseToStatusPollOption( provider: self, target: .status, @@ -307,7 +304,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC statusMetricButtonDidPressed action: StatusMetricView.Action ) { Task { - let status = viewModel.status.asRecord + guard let status = viewModel.status?.asRecord else { return } // TODO: } // end Task } @@ -322,7 +319,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC statusToolbarButtonDidPressed action: StatusToolbarView.Action ) { Task { - let status = viewModel.status.asRecord + guard let status = viewModel.status?.asRecord else { return } await DataSourceFacade.responseToStatusToolbar( provider: self, viewModel: viewModel, diff --git a/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceView.swift b/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceView.swift index 159b2bdb..5828d0da 100644 --- a/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceView.swift +++ b/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceView.swift @@ -52,10 +52,9 @@ struct AccountPreferenceView: View { List { // user header section Section { - UserContentView(viewModel: .init( - user: viewModel.user, - accessoryType: .none - )) + if let userViewModel = viewModel.userViewModel { + UserView(viewModel: userViewModel) + } } // notification section if let viewModel = viewModel.mastodonNotificationSectionViewModel { diff --git a/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceViewModel.swift b/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceViewModel.swift index 3236dd4c..950beb44 100644 --- a/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceViewModel.swift +++ b/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceViewModel.swift @@ -29,6 +29,7 @@ final class AccountPreferenceViewModel: ObservableObject { @Published var isMentionEnabled = true // output + @Published var userViewModel: UserView.ViewModel? let listEntryPublisher = PassthroughSubject() init( @@ -42,6 +43,12 @@ final class AccountPreferenceViewModel: ObservableObject { // end init setupNotificationSource() + userViewModel = UserView.ViewModel( + user: user, + authContext: authContext, + kind: .plain, + delegate: nil + ) } deinit { diff --git a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceView.swift b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceView.swift index 3f5a6244..dc2ad93a 100644 --- a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceView.swift +++ b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceView.swift @@ -14,12 +14,14 @@ struct DisplayPreferenceView: View { @ObservedObject var viewModel: DisplayPreferenceViewModel - @State var timelineStatusViewHeight: CGFloat = .zero @State var threadStatusViewHeight: CGFloat = .zero var body: some View { List { Section { + StatusView(viewModel: StatusView.ViewModel.prototype( + viewLayoutFramePublisher: viewModel.$viewLayoutFrame + )) // PrototypeStatusViewRepresentable( // style: .timeline, // configurationContext: StatusView.ConfigurationContext( @@ -29,7 +31,6 @@ struct DisplayPreferenceView: View { // ), // height: $timelineStatusViewHeight // ) -// .frame(height: timelineStatusViewHeight) } header: { Text(verbatim: L10n.Scene.Settings.Display.SectionHeader.preview) .textCase(nil) diff --git a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewController.swift b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewController.swift index cdf3903c..50afb7d8 100644 --- a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewController.swift +++ b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewController.swift @@ -58,10 +58,28 @@ extension DisplayPreferenceViewController { public override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() + viewModel.viewLayoutFrame.update(view: view) if viewModel.viewSize != view.frame.size { viewModel.viewSize = view.frame.size } } + + override func viewSafeAreaInsetsDidChange() { + super.viewSafeAreaInsetsDidChange() + + viewModel.viewLayoutFrame.update(view: view) + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + coordinator.animate {[weak self] _ in + guard let self = self else { return } + self.viewModel.viewLayoutFrame.update(view: self.view) + } completion: { _ in + // do nothing + } + } } diff --git a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewModel.swift b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewModel.swift index ff0a338f..f960bd21 100644 --- a/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewModel.swift +++ b/TwidereX/Scene/Setting/DisplayPreference/DisplayPreferenceViewModel.swift @@ -17,10 +17,12 @@ final class DisplayPreferenceViewModel: ObservableObject { // MARK: - layout @Published var viewSize: CGSize = .zero + @Published public var viewLayoutFrame = ViewLayoutFrame() // input let authContext: AuthContext - + lazy var statusViewModel = StatusView.ViewModel.prototype(viewLayoutFramePublisher: $viewLayoutFrame) + // avatar @Published var avatarStyle = UserDefaults.shared.avatarStyle @@ -30,6 +32,7 @@ final class DisplayPreferenceViewModel: ObservableObject { // output @Published var authenticationContext: AuthenticationContext? + init(authContext: AuthContext) { self.authContext = authContext diff --git a/TwidereX/Scene/Setting/List/SettingListView.swift b/TwidereX/Scene/Setting/List/SettingListView.swift index 61b98b16..f43ea315 100644 --- a/TwidereX/Scene/Setting/List/SettingListView.swift +++ b/TwidereX/Scene/Setting/List/SettingListView.swift @@ -85,11 +85,8 @@ struct SettingListView: View { @ViewBuilder var accountView: some View { - if let user = viewModel.user { - UserContentView(viewModel: .init( - user: user, - accessoryType: .disclosureIndicator - )) + if let userViewModel = viewModel.userViewModel { + UserView(viewModel: userViewModel) } else { EmptyView() } diff --git a/TwidereX/Scene/Setting/List/SettingListViewController.swift b/TwidereX/Scene/Setting/List/SettingListViewController.swift index dab54619..d8a4dd6f 100644 --- a/TwidereX/Scene/Setting/List/SettingListViewController.swift +++ b/TwidereX/Scene/Setting/List/SettingListViewController.swift @@ -49,7 +49,8 @@ extension SettingListViewController { guard let self = self else { return } switch entry.type { case .account: - guard let user = self.viewModel.user else { return } + let authContext = self.viewModel.authContext + guard let user = authContext.authenticationContext.user(in: self.context.managedObjectContext) else { return } let accountPreferenceViewModel = AccountPreferenceViewModel( context: self.context, diff --git a/TwidereX/Scene/Setting/List/SettingListViewModel.swift b/TwidereX/Scene/Setting/List/SettingListViewModel.swift index 68aaafff..3e951010 100644 --- a/TwidereX/Scene/Setting/List/SettingListViewModel.swift +++ b/TwidereX/Scene/Setting/List/SettingListViewModel.swift @@ -28,7 +28,7 @@ final class SettingListViewModel: ObservableObject { let settingListEntryPublisher = PassthroughSubject() // account - @Published var user: UserObject? + @Published var userViewModel: UserView.ViewModel? // App Icon @Published var alternateIconNamePreference = UserDefaults.shared.alternateIconNamePreference @@ -55,6 +55,12 @@ final class SettingListViewModel: ObservableObject { extension SettingListViewModel { @MainActor func setupAccountSource() async { - user = authContext.authenticationContext.user(in: context.managedObjectContext) + guard let user = authContext.authenticationContext.user(in: context.managedObjectContext) else { return } + userViewModel = UserView.ViewModel( + user: user, + authContext: authContext, + kind: .settingAccountSection, + delegate: nil + ) } } diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewController+DataSourceProvider.swift b/TwidereX/Scene/StatusThread/StatusThreadViewController+DataSourceProvider.swift index 1953be81..9ebe9542 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewController+DataSourceProvider.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewController+DataSourceProvider.swift @@ -22,7 +22,7 @@ extension StatusThreadViewController: DataSourceProvider { switch item { case .root: - guard let status = viewModel.statusViewModel?.status.asRecord else { return nil } + guard let status = viewModel.statusViewModel?.status?.asRecord else { return nil } return .status(status) case .status(let status): return .status(status) diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift index 24208f1c..6c727763 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift @@ -212,7 +212,7 @@ extension StatusThreadViewModel { } private func updateConversationRootLink(viewModel: StatusView.ViewModel) { - let record = viewModel.status.asRecord + guard let record = viewModel.status?.asRecord else { return } guard let linkConfiguration = conversationLinkConfiguration[record] else { return } viewModel.isTopConversationLinkLineViewDisplay = linkConfiguration.isTopLinkDisplay diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift index 519d20b9..cbf83323 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift @@ -313,7 +313,7 @@ extension StatusThreadViewModel { isLoadTop = true defer { isLoadTop = false } - guard let status = self.statusViewModel?.status.asRecord else { return } + guard let status = self.statusViewModel?.status?.asRecord else { return } guard case .value(let cursor) = topCursor else { return } try await fetchConversation(status: status, cursor: .value(cursor)) } @@ -324,7 +324,7 @@ extension StatusThreadViewModel { isLoadBottom = true defer { isLoadBottom = false } - guard let status = self.statusViewModel?.status.asRecord else { return } + guard let status = self.statusViewModel?.status?.asRecord else { return } guard case .value(let cursor) = bottomCursor else { return } try await fetchConversation(status: status, cursor: .value(cursor)) } From c129e4ce6a01924b49ae188c4fc721d9bc642588 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 20 Apr 2023 17:06:09 +0800 Subject: [PATCH 060/128] chore: restore the avatar style --- .../Content/StatusView+ViewModel.swift | 26 +++++++--- .../TwidereUI/Content/StatusView.swift | 33 ++++++++----- .../Content/UserView+ViewModel.swift | 6 +++ .../Sources/TwidereUI/Content/UserView.swift | 3 +- .../TwidereUI/Shape/AvatarClipShape.swift | 48 +++++++++++++++++++ 5 files changed, 98 insertions(+), 18 deletions(-) create mode 100644 TwidereSDK/Sources/TwidereUI/Shape/AvatarClipShape.swift diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index a8154665..6db6f0b3 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -51,6 +51,8 @@ extension StatusView { // author @Published public var avatarURL: URL? + @Published public var avatarStyle = UserDefaults.shared.avatarStyle + @Published public var authorName: MetaContent = PlaintextMetaContent(string: "") @Published public var authorUsernme = "" @Published public var authorUserIdentifier: UserIdentifier? @@ -253,12 +255,8 @@ extension StatusView { // } // } // .assign(to: &$isRepostEnabled) - - toolbarViewModel.style = kind == .conversationRoot ? .plain : .inline - - UserDefaults.shared.publisher(for: \.translateButtonPreference) - .map { $0 } - .assign(to: &$translateButtonPreference) + + setupBinding() } private init( @@ -271,6 +269,22 @@ extension StatusView { // end init viewLayoutFramePublisher?.assign(to: &$viewLayoutFrame) + + setupBinding() + } + + private func setupBinding() { + // avatar style + UserDefaults.shared.publisher(for: \.avatarStyle) + .assign(to: &$avatarStyle) + + // translate button + UserDefaults.shared.publisher(for: \.translateButtonPreference) + .map { $0 } + .assign(to: &$translateButtonPreference) + + // toolbar + toolbarViewModel.style = kind == .conversationRoot ? .plain : .inline } } } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index 7cc51384..02219116 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -169,7 +169,7 @@ public struct StatusView: View { if let location = viewModel.location { HStack { Image(uiImage: Asset.ObjectTools.mappinMini.image.withRenderingMode(.alwaysTemplate)) - Text(location + location + location + location) + Text(location) Spacer() } .foregroundColor(.secondary) @@ -348,27 +348,38 @@ extension StatusView { } // end HStack } + var avatarButtonClipShape: any Shape { + switch viewModel.avatarStyle { + case .circle: + return Circle() + case .roundedSquare: + return RoundedRectangle(cornerRadius: avatarButtonDimension / 4) + } + } + + var avatarButtonDimension: CGFloat { + switch viewModel.kind { + case .quote: + return inlineAvatarButtonDimension + default: + return StatusView.hangingAvatarButtonDimension + } + } + var avatarButton: some View { Button { guard let author = viewModel.author?.asRecord else { return } viewModel.delegate?.statusView(viewModel, userAvatarButtonDidPressed: author) } label: { - let dimension: CGFloat = { - switch viewModel.kind { - case .quote: - return inlineAvatarButtonDimension - default: - return StatusView.hangingAvatarButtonDimension - } - }() KFImage(viewModel.avatarURL) .placeholder { progress in Color(uiColor: .placeholderText) } .resizable() .aspectRatio(contentMode: .fill) - .frame(width: dimension, height: dimension) - .clipShape(Circle()) + .frame(width: avatarButtonDimension, height: avatarButtonDimension) + .clipShape(AvatarClipShape(avatarStyle: viewModel.avatarStyle)) + .animation(.easeInOut, value: viewModel.avatarStyle) } .buttonStyle(.borderless) } diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift index 6c9820c0..c1a1ac32 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift @@ -35,6 +35,8 @@ extension UserView { // user @Published public var avatarURL: URL? + @Published public var avatarStyle = UserDefaults.shared.avatarStyle + @Published public var name: MetaContent = PlaintextMetaContent(string: "") @Published public var username: String = "" @@ -114,6 +116,10 @@ extension UserView { default: break } + + // avatar style + UserDefaults.shared.publisher(for: \.avatarStyle) + .assign(to: &$avatarStyle) // // isMyList // Publishers.CombineLatest( // $authenticationContext, diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift index d24fbc28..b6981a88 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift @@ -81,7 +81,8 @@ extension UserView { .resizable() .aspectRatio(contentMode: .fill) .frame(width: dimension, height: dimension) - .clipShape(Circle()) + .clipShape(AvatarClipShape(avatarStyle: viewModel.avatarStyle)) + .animation(.easeInOut, value: viewModel.avatarStyle) } .buttonStyle(.borderless) .allowsHitTesting(allowsAvatarButtonHitTesting) diff --git a/TwidereSDK/Sources/TwidereUI/Shape/AvatarClipShape.swift b/TwidereSDK/Sources/TwidereUI/Shape/AvatarClipShape.swift new file mode 100644 index 00000000..4bdc16f3 --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Shape/AvatarClipShape.swift @@ -0,0 +1,48 @@ +// +// AvatarClipShape.swift +// +// +// Created by MainasuK on 2023/4/20. +// + +import SwiftUI + +public struct AvatarClipShape: Shape, Animatable { + var avatarStyle: UserDefaults.AvatarStyle + var progress: CGFloat + + public var animatableData: CGFloat { + get { + switch avatarStyle { + case .circle: return 0.0 + case .roundedSquare: return 1.0 + } + } + set { progress = newValue } + } + + public init(avatarStyle: UserDefaults.AvatarStyle) { + self.avatarStyle = avatarStyle + self.progress = { + switch avatarStyle { + case .circle: return 0.0 + case .roundedSquare: return 1.0 + } + }() + // end init + } + + public func path(in rect: CGRect) -> Path { + let cornerRadius = lerp(v0: rect.width / 2, v1: rect.width / 4, t: progress) + return RoundedRectangle(cornerRadius: cornerRadius).path(in: rect) + } + +} + +extension AvatarClipShape { + + // linear interpolation + func lerp(v0: CGFloat, v1: CGFloat, t: CGFloat) -> CGFloat { + return (1 - t) * v0 + (t * v1) + } +} From 9c61dc930e61a35e68bf67e4ecfaf2893b08eac1 Mon Sep 17 00:00:00 2001 From: CMK Date: Sun, 23 Apr 2023 20:28:27 +0800 Subject: [PATCH 061/128] feat: make profile user and media timeline accessible under the basic API --- TwidereSDK/Package.swift | 20 +- .../CoreDataStack/TwitterStatus.swift | 2 - .../Extension/TwitterStatus+Property.swift | 5 +- .../Twitter/Persistence+Twitter.swift | 10 +- .../APIService+Status+Conversation.swift | 46 +- .../APIService/APIService+Status+List.swift | 13 +- .../APIService/APIService+Status+Search.swift | 13 +- .../Timeline/APIService+Timeline+Home.swift | 44 +- .../Timeline/APIService+Timeline+Like.swift | 38 +- .../Timeline/APIService+Timeline+User.swift | 35 +- .../Timeline/APIService+Timeline.swift | 428 +++++++++--------- .../TwidereCore/State/AuthContext.swift | 41 -- .../StatusFetchViewModel+Timeline+Home.swift | 5 +- .../StatusFetchViewModel+Timeline+User.swift | 6 +- .../Twitter+API+Error+InternalError.swift | 26 -- .../Twitter+API+Error+ResponseError.swift | 21 - .../Twitter+API+Error+TwitterAPIError.swift | 86 ---- .../TwitterSDK/API/Twitter+API+Guest.swift | 74 --- .../API/Twitter+API+OAuth+AccessToken.swift | 128 ------ .../API/Twitter+API+OAuth+RequestToken.swift | 301 ------------ .../TwitterSDK/API/Twitter+API+OAuth.swift | 195 -------- .../Sources/TwitterSDK/API/Twitter+API.swift | 327 ------------- .../API/V1/Twitter+API+Account.swift | 32 -- .../API/V1/Twitter+API+Application.swift | 30 -- .../TwitterSDK/API/V1/Twitter+API+Block.swift | 59 --- .../API/V1/Twitter+API+Favorites.swift | 109 ----- .../API/V1/Twitter+API+Friendships.swift | 106 ----- .../TwitterSDK/API/V1/Twitter+API+Geo.swift | 67 --- .../TwitterSDK/API/V1/Twitter+API+List.swift | 107 ----- .../API/V1/Twitter+API+Lookup.swift | 56 --- .../API/V1/Twitter+API+Media+Metadata.swift | 71 --- .../TwitterSDK/API/V1/Twitter+API+Media.swift | 252 ----------- .../TwitterSDK/API/V1/Twitter+API+Mute.swift | 59 --- .../API/V1/Twitter+API+SavedSearch.swift | 104 ----- .../API/V1/Twitter+API+Search.swift | 57 --- .../V1/Twitter+API+Statuses+Timeline.swift | 165 ------- .../API/V1/Twitter+API+Statuses.swift | 162 ------- .../TwitterSDK/API/V1/Twitter+API+Trend.swift | 95 ---- .../API/V1/Twitter+API+Users+Search.swift | 59 --- .../TwitterSDK/API/V1/Twitter+API+Users.swift | 54 --- .../API/V2/Twitter+API+V2+List+Member.swift | 105 ----- .../API/V2/Twitter+API+V2+List.swift | 318 ------------- .../API/V2/Twitter+API+V2+Lookup.swift | 88 ---- .../Twitter+API+V2+OAuth2+AccessToken.swift | 101 ----- .../V2/Twitter+API+V2+OAuth2+Authorize.swift | 257 ----------- .../API/V2/Twitter+API+V2+OAuth2.swift | 68 --- .../API/V2/Twitter+API+V2+Search.swift | 130 ------ .../API/V2/Twitter+API+V2+Status+Delete.swift | 47 -- .../API/V2/Twitter+API+V2+Status+List.swift | 95 ---- .../V2/Twitter+API+V2+Status+Timeline.swift | 244 ---------- .../API/V2/Twitter+API+V2+Status.swift | 147 ------ .../API/V2/Twitter+API+V2+User+Block.swift | 101 ----- .../API/V2/Twitter+API+V2+User+Follow.swift | 218 --------- .../API/V2/Twitter+API+V2+User+Like.swift | 99 ---- .../API/V2/Twitter+API+V2+User+List.swift | 243 ---------- .../API/V2/Twitter+API+V2+User+Lookup.swift | 257 ----------- .../API/V2/Twitter+API+V2+User+Mute.swift | 99 ---- .../API/V2/Twitter+API+V2+User+Retweet.swift | 98 ---- .../API/V2/Twitter+API+V2+User+Timeline.swift | 211 --------- .../API/V2/Twitter+API+V2+User.swift | 12 - .../TwitterSDK/API/V2/Twitter+API+V2.swift | 12 - .../Twitter+AuthorizationContext+OAuth.swift | 79 ---- .../Twitter+AuthorizationContext+OAuth2.swift | 35 -- .../Twitter+AuthorizationContext.swift | 15 - .../TwitterSDK/Entity/Twitter+Entity.swift | 10 - .../V1/Twitter+Entity+Coordinates.swift | 17 - .../V1/Twitter+Entity+ExtendedEntities.swift | 173 ------- .../V1/Twitter+Entity+Internal+Tweet.swift | 99 ---- .../Entity/V1/Twitter+Entity+Internal.swift | 12 - .../Entity/V1/Twitter+Entity+List.swift | 32 -- .../Entity/V1/Twitter+Entity+Place.swift | 35 -- .../V1/Twitter+Entity+QuotedStatus.swift | 13 - .../V1/Twitter+Entity+RateLimitStatus.swift | 38 -- .../V1/Twitter+Entity+Relationship.swift | 99 ---- .../V1/Twitter+Entity+RetweetedStatus.swift | 13 - .../V1/Twitter+Entity+SavedSearch.swift | 26 -- .../V1/Twitter+Entity+Trend+Place.swift | 53 --- .../Entity/V1/Twitter+Entity+Trend.swift | 36 -- .../V1/Twitter+Entity+Tweet+Entities.swift | 123 ----- .../Entity/V1/Twitter+Entity+Tweet.swift | 105 ----- .../V1/Twitter+Entity+User+Entities.swift | 48 -- .../Entity/V1/Twitter+Entity+User.swift | 144 ------ .../V2/Twitter+Entity+V2+Entities.swift | 70 --- .../Entity/V2/Twitter+Entity+V2+List.swift | 35 -- ...witter+Entity+V2+Media+PublicMetrics.swift | 18 - .../Entity/V2/Twitter+Entity+V2+Media.swift | 40 -- .../Entity/V2/Twitter+Entity+V2+Place.swift | 32 -- .../Twitter+Entity+V2+ReferencedTweet.swift | 35 -- .../Twitter+Entity+V2+Tweet+Attachments.swift | 20 - .../V2/Twitter+Entity+V2+Tweet+Geo.swift | 35 -- .../V2/Twitter+Entity+V2+Tweet+Poll.swift | 42 -- ...witter+Entity+V2+Tweet+PublicMetrics.swift | 24 - ...witter+Entity+V2+Tweet+ReplySettings.swift | 16 - .../Entity/V2/Twitter+Entity+V2+Tweet.swift | 61 --- ...Twitter+Entity+V2+User+PublicMetrics.swift | 24 - .../Entity/V2/Twitter+Entity+V2+User.swift | 89 ---- .../V2/Twitter+Entity+V2+Withheld.swift | 20 - TwidereSDK/Sources/TwitterSDK/Helper.swift | 47 -- .../Sources/TwitterSDK/Request/Query.swift | 28 -- .../Request/Twitter+Request+Expansions.swift | 30 -- .../Request/Twitter+Request+ListFields.swift | 26 -- .../Request/Twitter+Request+MediaFields.swift | 32 -- .../Request/Twitter+Request+PlaceFields.swift | 28 -- .../Request/Twitter+Request+PollFields.swift | 25 - .../Request/Twitter+Request+TweetFields.swift | 40 -- .../Request/Twitter+Request+UserFields.swift | 34 -- .../TwitterSDK/Request/Twitter+Request.swift | 97 ---- .../Response/Twitter+Response.swift | 89 ---- .../V2/Twitter+Response+V2+ContentError.swift | 28 -- .../V2/Twitter+Response+V2+DictContent.swift | 91 ---- TwidereSDK/Sources/TwitterSDK/Twitter.swift | 19 - .../TwitterSDKTests/TwitterSDKTests.swift | 33 -- TwidereX.xcodeproj/project.pbxproj | 2 + ...tificationTimelineViewModel+Diffable.swift | 2 + .../StatusThread/StatusThreadViewModel.swift | 41 +- 115 files changed, 310 insertions(+), 8606 deletions(-) delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/Error/Twitter+API+Error+InternalError.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/Error/Twitter+API+Error+ResponseError.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/Error/Twitter+API+Error+TwitterAPIError.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Guest.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/Twitter+API+OAuth+AccessToken.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/Twitter+API+OAuth+RequestToken.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/Twitter+API+OAuth.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/Twitter+API.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Account.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Application.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Block.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Favorites.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Friendships.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Geo.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+List.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Lookup.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Media+Metadata.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Media.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Mute.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+SavedSearch.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Search.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Statuses+Timeline.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Statuses.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Trend.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Users+Search.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Users.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+List+Member.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+List.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Lookup.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+OAuth2+AccessToken.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+OAuth2+Authorize.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+OAuth2.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Search.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Status+Delete.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Status+List.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Status+Timeline.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Status.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Block.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Follow.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Like.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+List.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Lookup.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Mute.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Retweet.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Timeline.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/AuthorizationContext/Twitter+AuthorizationContext+OAuth.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/AuthorizationContext/Twitter+AuthorizationContext+OAuth2.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/AuthorizationContext/Twitter+AuthorizationContext.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Coordinates.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+ExtendedEntities.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Internal+Tweet.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Internal.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+List.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Place.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+QuotedStatus.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+RateLimitStatus.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Relationship.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+RetweetedStatus.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+SavedSearch.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Trend+Place.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Trend.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Tweet+Entities.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Tweet.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+User+Entities.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+User.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Entities.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+List.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Media+PublicMetrics.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Media.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Place.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+ReferencedTweet.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Tweet+Attachments.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Tweet+Geo.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Tweet+Poll.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Tweet+PublicMetrics.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Tweet+ReplySettings.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Tweet.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+User+PublicMetrics.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+User.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Withheld.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Helper.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Request/Query.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+Expansions.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+ListFields.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+MediaFields.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+PlaceFields.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+PollFields.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+TweetFields.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+UserFields.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Response/Twitter+Response.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Response/V2/Twitter+Response+V2+ContentError.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Response/V2/Twitter+Response+V2+DictContent.swift delete mode 100644 TwidereSDK/Sources/TwitterSDK/Twitter.swift delete mode 100644 TwidereSDK/Sources/TwitterSDKTests/TwitterSDKTests.swift diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index 7ae5cee4..5c50c10b 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -14,7 +14,6 @@ let package = Package( .library( name: "TwidereSDK", targets: [ - "TwitterSDK", "MastodonSDK", "CoreDataStack", "TwidereAsset", @@ -52,6 +51,7 @@ let package = Package( .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.6.0"), .package(url: "https://github.com/tid-kijyun/Kanna.git", from: "5.2.7"), .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"), + .package(url: "https://github.com/TwidereProject/TwitterSDK.git", from: "0.1.0"), .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"), .package(name: "CoverFlowStackLayout", path: "../CoverFlowStackLayout"), ], @@ -67,20 +67,6 @@ let package = Package( "Template/Stencil" ] ), - .target( - name: "TwitterSDK", - dependencies: [ - .product(name: "SwiftyJSON", package: "SwiftyJSON"), - .product(name: "NIOHTTP1", package: "swift-nio"), - ] - ), - .testTarget( - name: "TwitterSDKTests", - dependencies: [ - "TwitterSDK", - .product(name: "CommonOSLog", package: "CommonOSLog"), - ] - ), .target( name: "MastodonSDK", dependencies: [ @@ -96,10 +82,10 @@ let package = Package( .target( name: "TwidereCommon", dependencies: [ - "TwitterSDK", "MastodonSDK", .product(name: "KeychainAccess", package: "KeychainAccess"), .product(name: "ArkanaKeys", package: "ArkanaKeys"), + .product(name: "TwitterSDK", package: "TwitterSDK"), ], exclude: [ "Template/AutoGenerateProtocolDelegate.swifttemplate", @@ -113,7 +99,6 @@ let package = Package( "TwidereAsset", "TwidereCommon", "TwidereLocalization", - "TwitterSDK", "MastodonSDK", "CoreDataStack", .product(name: "CommonOSLog", package: "CommonOSLog"), @@ -125,6 +110,7 @@ let package = Package( .product(name: "DateToolsSwift", package: "DateTools"), .product(name: "CryptoSwift", package: "CryptoSwift"), .product(name: "Kanna", package: "Kanna"), + .product(name: "TwitterSDK", package: "TwitterSDK"), .product(name: "CollectionConcurrencyKit", package: "CollectionConcurrencyKit"), ] ), diff --git a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterStatus.swift b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterStatus.swift index c44a8276..b350b999 100644 --- a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterStatus.swift +++ b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterStatus.swift @@ -38,8 +38,6 @@ extension TwitterStatus { continue } } - - text = text.replacingOccurrences(of: shortURL, with: expandedURL) } return text } diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Extension/TwitterStatus+Property.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Extension/TwitterStatus+Property.swift index 36af73a9..a19066a4 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Extension/TwitterStatus+Property.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Extension/TwitterStatus+Property.swift @@ -196,7 +196,10 @@ extension Twitter.Entity.V2.Media { guard let kind = attachmentKind else { return nil } guard let width = width, let height = height - else { return nil } + else { + assertionFailure() + return nil + } return TwitterAttachment( kind: kind, size: CGSize(width: width, height: height), diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+Twitter.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+Twitter.swift index 9de13c69..0d15f64d 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+Twitter.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+Twitter.swift @@ -62,7 +62,10 @@ extension Persistence.Twitter { for status in context.dictionary.tweetDict.values { guard let authorID = status.authorID, let author = context.dictionary.userDict[authorID] - else { continue } + else { + assertionFailure() + continue + } var repost: Persistence.TwitterStatus.PersistContextV2.Entity? var replyTo: Persistence.TwitterStatus.PersistContextV2.Entity? @@ -71,7 +74,10 @@ extension Persistence.Twitter { for referencedTweet in status.referencedTweets ?? [] { guard let type = referencedTweet.type, let statusID = referencedTweet.id - else { continue } + else { + assertionFailure() + continue + } guard let status = context.dictionary.tweetDict[statusID], let authorID = status.authorID, let author = context.dictionary.userDict[authorID] diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Conversation.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Conversation.swift index 1686f1e0..272df39a 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Conversation.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Conversation.swift @@ -5,30 +5,58 @@ // Created by MainasuK on 2023/3/28. // +import os.log import Foundation import CoreDataStack import TwitterSDK +import func QuartzCore.CACurrentMediaTime extension APIService { public func twitterStatusConversation( conversationRootStatusID: Twitter.Entity.V2.Tweet.ID, - query: Twitter.API.V2.Status.Timeline.TimelineQuery, - guestAuthentication: Twitter.API.Guest.GuestAuthorization, + query: Twitter.API.V2.Status.Timeline.ConvsersationQuery, authenticationContext: TwitterAuthenticationContext ) async throws -> Twitter.Response.Content { let response = try await Twitter.API.V2.Status.Timeline.conversation( session: URLSession(configuration: .ephemeral), statusID: conversationRootStatusID, query: query, - authorization: guestAuthentication + authorization: authenticationContext.authorization ) - let statusIDs = response.value.globalObjects.tweets.map { $0.idStr } - - _ = try await twitterStatus( - statusIDs: statusIDs, - authenticationContext: authenticationContext - ) + #if DEBUG + // log time cost + let start = CACurrentMediaTime() + defer { + // log rate limit + response.logRateLimit() + + let end = CACurrentMediaTime() + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: persist cost %.2fs", ((#file as NSString).lastPathComponent), #line, #function, end - start) + } + #endif + + let managedObjectContext = backgroundManagedObjectContext + try await managedObjectContext.performChanges { + let content = response.value + let dictionary = Twitter.Response.V2.DictContent( + tweets: content.includes?.tweets ?? [], + users: content.includes?.users ?? [], + media: content.includes?.media ?? [], + places: content.includes?.places ?? [], + polls: content.includes?.polls ?? [] + ) + let me = authenticationContext.authenticationRecord.object(in: managedObjectContext)?.user + + _ = Persistence.Twitter.persist( + in: managedObjectContext, + context: Persistence.Twitter.PersistContextV2( + dictionary: dictionary, + me: me, + networkDate: response.networkDate + ) + ) + } // end .performChanges { … } return response } diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+List.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+List.swift index 235b3ac5..d1598b2a 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+List.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+List.swift @@ -56,13 +56,7 @@ extension APIService { query: query, authorization: authenticationContext.authorization ) - - let statusIDs: [Twitter.Entity.V2.Tweet.ID] = response.value.map { $0.idStr } - let _lookupResponse = try? await twitterBatchLookupV2( - statusIDs: statusIDs, - authenticationContext: authenticationContext - ) - + #if DEBUG // log time cost let start = CACurrentMediaTime() @@ -93,11 +87,6 @@ extension APIService { ) statusArray.append(result.status) } // end for in - - // amend the v2 only properties - if let lookupResponse = _lookupResponse, let me = me { - lookupResponse.update(statuses: statusArray, me: me) - } } return response diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Search.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Search.swift index 07fcfce7..7dffce2e 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Search.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Search.swift @@ -54,13 +54,7 @@ extension APIService { query: query, authorization: authenticationContext.authorization ) - - let statusIDs: [Twitter.Entity.V2.Tweet.ID] = (response.value.statuses ?? []).map { $0.idStr } - let _lookupResponse = try? await twitterBatchLookupV2( - statusIDs: statusIDs, - authenticationContext: authenticationContext - ) - + #if DEBUG // log time cost let start = CACurrentMediaTime() @@ -91,11 +85,6 @@ extension APIService { ) statusArray.append(result.status) } // end for in - - // amend the v2 only properties - if let lookupResponse = _lookupResponse, let me = me { - lookupResponse.update(statuses: statusArray, me: me) - } } return response diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Home.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Home.swift index 07be62bb..e88f1bcb 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Home.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Home.swift @@ -118,7 +118,8 @@ extension APIService { sinceID: sinceID, untilID: nil, paginationToken: nextToken, - maxResults: query.maxResults + maxResults: query.maxResults, + onlyMedia: query.onlyMedia ), authorization: authenticationContext.authorization ) @@ -128,18 +129,6 @@ extension APIService { return TwitterHomeTimelineTaskResult.content(response) } } - // fetch lookup - group.addTask { - self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch lookup") - let responses = await self.twitterBatchLookupResponses( - content: response.value, - authenticationContext: authenticationContext - ) - #if DEBUG - response.logRateLimit(category: "HomeLookup") - #endif - return TwitterHomeTimelineTaskResult.lookup(responses) - } case .lookup: break case .persist: @@ -169,27 +158,16 @@ extension APIService { let statusArray = statusRecords.compactMap { $0.object(in: managedObjectContext) } assert(statusArray.count == statusRecords.count) - - // amend the v2 missing properties - if let me = me { - var batchLookupResponse = TwitterBatchLookupResponse() - for lookupResult in lookupResults { - for status in lookupResult.value { - batchLookupResponse.lookupDict[status.idStr] = status - } - } - batchLookupResponse.update(statuses: statusArray, me: me) - } - - // locate anchor status - let anchorStatus: TwitterStatus? = { - guard let untilID = query.untilID else { return nil } - let request = TwitterStatus.sortedFetchRequest - request.predicate = TwitterStatus.predicate(id: untilID) - request.fetchLimit = 1 - return try? managedObjectContext.fetch(request).first - }() + // locate anchor status + let anchorStatus: TwitterStatus? = { + guard let untilID = query.untilID else { return nil } + let request = TwitterStatus.sortedFetchRequest + request.predicate = TwitterStatus.predicate(id: untilID) + request.fetchLimit = 1 + return try? managedObjectContext.fetch(request).first + }() + // update hasMore flag for anchor status let acct = Feed.Acct.twitter(userID: authenticationContext.userID) if let anchorStatus = anchorStatus, diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Like.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Like.swift index cd002269..a028b6d8 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Like.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Like.swift @@ -28,22 +28,7 @@ extension APIService { query: query, authorization: authenticationContext.authorization ) - - let statusIDs: [Twitter.Entity.Tweet.ID] = { - var ids: [Twitter.Entity.Tweet.ID] = [] - if let statuses = response.value.data { - ids.append(contentsOf: statuses.map { $0.id }) - } - if let statuses = response.value.includes?.tweets { - ids.append(contentsOf: statuses.map { $0.id }) - } - return Array(Set(ids)) - }() - let _lookupResponse = try? await twitterBatchLookup( - statusIDs: statusIDs, - authenticationContext: authenticationContext - ) - + #if DEBUG // log time cost let start = CACurrentMediaTime() @@ -70,7 +55,7 @@ extension APIService { let me = authenticationContext.authenticationRecord.object(in: managedObjectContext)?.user // persist [TwitterStatus] - let statusArray = Persistence.Twitter.persist( + _ = Persistence.Twitter.persist( in: managedObjectContext, context: Persistence.Twitter.PersistContextV2( dictionary: dictionary, @@ -78,12 +63,6 @@ extension APIService { networkDate: response.networkDate ) ) - - // amend the v2 missing properties - if let lookupResponse = _lookupResponse, let me = me { - lookupResponse.update(statuses: statusArray, me: me) - } - } // end try await managedObjectContext.performChanges return response @@ -98,13 +77,7 @@ extension APIService { query: query, authorization: authenticationContext.authorization ) - - let statusIDs: [Twitter.Entity.V2.Tweet.ID] = response.value.map { $0.idStr } - let _lookupResponse = try? await twitterBatchLookupV2( - statusIDs: statusIDs, - authenticationContext: authenticationContext - ) - + #if DEBUG // log time cost let start = CACurrentMediaTime() @@ -137,11 +110,6 @@ extension APIService { ) statusArray.append(result.status) } - - // amend the v2 only properties - if let lookupResponse = _lookupResponse, let me = me { - lookupResponse.update(statuses: statusArray, me: me) - } } return response diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+User.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+User.swift index 6a6e13ff..020e2189 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+User.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+User.swift @@ -28,21 +28,6 @@ extension APIService { authorization: authenticationContext.authorization ) - let statusIDs: [Twitter.Entity.Tweet.ID] = { - var ids: [Twitter.Entity.Tweet.ID] = [] - if let statuses = response.value.data { - ids.append(contentsOf: statuses.map { $0.id }) - } - if let statuses = response.value.includes?.tweets { - ids.append(contentsOf: statuses.map { $0.id }) - } - return Array(Set(ids)) - }() - let _lookupResponse = try? await twitterBatchLookup( - statusIDs: statusIDs, - authenticationContext: authenticationContext - ) - #if DEBUG // log time cost let start = CACurrentMediaTime() @@ -69,7 +54,7 @@ extension APIService { let me = authenticationContext.authenticationRecord.object(in: managedObjectContext)?.user // persist [TwitterStatus] - let statusArray = Persistence.Twitter.persist( + _ = Persistence.Twitter.persist( in: managedObjectContext, context: Persistence.Twitter.PersistContextV2( dictionary: dictionary, @@ -77,11 +62,6 @@ extension APIService { networkDate: response.networkDate ) ) - - // amend the v2 missing properties - if let lookupResponse = _lookupResponse, let me = me { - lookupResponse.update(statuses: statusArray, me: me) - } } // end try await managedObjectContext.performChanges return response @@ -96,13 +76,7 @@ extension APIService { query: query, authorization: authenticationContext.authorization ) - - let statusIDs: [Twitter.Entity.V2.Tweet.ID] = response.value.map { $0.idStr } - let _lookupResponse = try? await twitterBatchLookupV2( - statusIDs: statusIDs, - authenticationContext: authenticationContext - ) - + #if DEBUG // log time cost let start = CACurrentMediaTime() @@ -135,11 +109,6 @@ extension APIService { ) statusArray.append(result.status) } - - // amend the v2 only properties - if let lookupResponse = _lookupResponse, let me = me { - lookupResponse.update(statuses: statusArray, me: me) - } } return response diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline.swift index 81955e96..127bf5ac 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline.swift @@ -127,218 +127,218 @@ extension APIService { } // end func } -// Fetch v1 API again to update v2 missing properies -extension APIService { - - public struct TwitterBatchLookupResponse { - let logger = Logger(subsystem: "APIService", category: "TwitterBatchLookupResponse") - - public var lookupDict: [Twitter.Entity.Tweet.ID: Twitter.Entity.Tweet] = [:] - - public func update(status: TwitterStatus, me: TwitterUser) { - guard let lookupStatus = lookupDict[status.id] else { return } - - // like state - lookupStatus.favorited.flatMap { - status.update(isLike: $0, by: me) - } - // repost state - lookupStatus.retweeted.flatMap { - status.update(isRepost: $0, by: me) - } - // media - if let twitterAttachments = lookupStatus.twitterAttachments { - // gif - let isGIF = twitterAttachments.contains(where: { $0.kind == .animatedGIF }) - if isGIF { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fix GIF missing") - status.update(attachments: twitterAttachments) - return - } - // media missing bug - if status.attachments.isEmpty { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fix media missing") - status.update(attachments: twitterAttachments) - return - } - } - } - - public func update(statuses: [TwitterStatus], me: TwitterUser) { - for status in statuses { - update(status: status, me: me) - } - } - } - - public func twitterBatchLookupResponses( - statusIDs: [Twitter.Entity.Tweet.ID], - authenticationContext: TwitterAuthenticationContext - ) async -> [Twitter.Response.Content<[Twitter.Entity.Tweet]>] { - let chunks = stride(from: 0, to: statusIDs.count, by: 100).map { - statusIDs[$0.. Twitter.Response.Content<[Twitter.Entity.Tweet]>? in - let query = Twitter.API.Lookup.LookupQuery(ids: Array(chunk)) - let response = try? await Twitter.API.Lookup.tweets( - session: self.session, - query: query, - authorization: authenticationContext.authorization - ) - return response - } - - return _responses.compactMap { $0 } - } - - public func twitterBatchLookupResponses( - content: Twitter.API.V2.User.Timeline.HomeContent, - authenticationContext: TwitterAuthenticationContext - ) async -> [Twitter.Response.Content<[Twitter.Entity.Tweet]>] { - let statusIDs: [Twitter.Entity.Tweet.ID] = { - var ids: [Twitter.Entity.Tweet.ID] = [] - ids.append(contentsOf: content.data?.map { $0.id } ?? []) - ids.append(contentsOf: content.includes?.tweets?.map { $0.id } ?? []) - return ids - }() - - let responses = await twitterBatchLookupResponses( - statusIDs: statusIDs, - authenticationContext: authenticationContext - ) - - return responses - } - - public func twitterBatchLookup( - statusIDs: [Twitter.Entity.Tweet.ID], - authenticationContext: TwitterAuthenticationContext - ) async throws -> TwitterBatchLookupResponse { - let responses = await twitterBatchLookupResponses( - statusIDs: statusIDs, - authenticationContext: authenticationContext - ) - - var lookupDict: [Twitter.Entity.Tweet.ID: Twitter.Entity.Tweet] = [:] - for response in responses { - for status in response.value { - lookupDict[status.idStr] = status - } - } - - return .init(lookupDict: lookupDict) - } - -} - -// Fetch v2 API again to update v2 only properies -extension APIService { - - public struct TwitterBatchLookupResponseV2 { - let logger = Logger(subsystem: "APIService", category: "TwitterBatchLookupResponseV2") - - let dictionary: Twitter.Response.V2.DictContent - - public func update(status: TwitterStatus, me: TwitterUser) { - guard let lookupStatus = dictionary.tweetDict[status.id] else { return } - guard let managedObjectContext = status.managedObjectContext else { return } - - let now = Date() - - // poll - if let poll = dictionary.poll(for: lookupStatus) { - let result = Persistence.TwitterPoll.createOrMerge( - in: managedObjectContext, - context: .init( - entity: poll, - me: me, - networkDate: now - ) - ) - status.attach(poll: result.poll) - } - - // reply settings - if let value = lookupStatus.replySettings { - let replySettings = TwitterReplySettings(value: value.rawValue) - status.update(replySettings: replySettings) - } - } - - public func update(statuses: [TwitterStatus], me: TwitterUser) { - for status in statuses { - update(status: status, me: me) - } - } - } - - public func twitterBatchLookupResponsesV2( - statusIDs: [Twitter.Entity.V2.Tweet.ID], - authenticationContext: TwitterAuthenticationContext - ) async -> [Twitter.Response.Content] { - let chunks = stride(from: 0, to: statusIDs.count, by: 100).map { - statusIDs[$0.. Twitter.Response.Content? in - let response = try? await Twitter.API.V2.Lookup.statuses( - session: self.session, - query: .init(statusIDs: Array(chunk)), - authorization: authenticationContext.authorization - ) - return response - } - - return _responses.compactMap { $0 } - } - - public func twitterBatchLookupV2( - statusIDs: [Twitter.Entity.V2.Tweet.ID], - authenticationContext: TwitterAuthenticationContext - ) async throws -> TwitterBatchLookupResponseV2 { - let responses = await twitterBatchLookupResponsesV2( - statusIDs: statusIDs, - authenticationContext: authenticationContext - ) - - var tweets: [Twitter.Entity.V2.Tweet] = [] - var users: [Twitter.Entity.V2.User] = [] - var media: [Twitter.Entity.V2.Media] = [] - var places: [Twitter.Entity.V2.Place] = [] - var polls: [Twitter.Entity.V2.Tweet.Poll] = [] - - for response in responses { - if let value = response.value.data { - tweets.append(contentsOf: value) - } - if let value = response.value.includes?.tweets { - tweets.append(contentsOf: value) - } - if let value = response.value.includes?.users { - users.append(contentsOf: value) - } - if let value = response.value.includes?.media { - media.append(contentsOf: value) - } - if let value = response.value.includes?.places { - places.append(contentsOf: value) - } - if let value = response.value.includes?.polls { - polls.append(contentsOf: value) - } - } - - let dictionary = Twitter.Response.V2.DictContent( - tweets: tweets, - users: users, - media: media, - places: places, - polls: polls - ) - - return .init(dictionary: dictionary) - } - -} +//// Fetch v1 API again to update v2 missing properies +//extension APIService { +// +// public struct TwitterBatchLookupResponse { +// let logger = Logger(subsystem: "APIService", category: "TwitterBatchLookupResponse") +// +// public var lookupDict: [Twitter.Entity.Tweet.ID: Twitter.Entity.Tweet] = [:] +// +// public func update(status: TwitterStatus, me: TwitterUser) { +// guard let lookupStatus = lookupDict[status.id] else { return } +// +// // like state +// lookupStatus.favorited.flatMap { +// status.update(isLike: $0, by: me) +// } +// // repost state +// lookupStatus.retweeted.flatMap { +// status.update(isRepost: $0, by: me) +// } +// // media +// if let twitterAttachments = lookupStatus.twitterAttachments { +// // gif +// let isGIF = twitterAttachments.contains(where: { $0.kind == .animatedGIF }) +// if isGIF { +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fix GIF missing") +// status.update(attachments: twitterAttachments) +// return +// } +// // media missing bug +// if status.attachments.isEmpty { +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fix media missing") +// status.update(attachments: twitterAttachments) +// return +// } +// } +// } +// +// public func update(statuses: [TwitterStatus], me: TwitterUser) { +// for status in statuses { +// update(status: status, me: me) +// } +// } +// } +// +// public func twitterBatchLookupResponses( +// statusIDs: [Twitter.Entity.Tweet.ID], +// authenticationContext: TwitterAuthenticationContext +// ) async -> [Twitter.Response.Content<[Twitter.Entity.Tweet]>] { +// let chunks = stride(from: 0, to: statusIDs.count, by: 100).map { +// statusIDs[$0.. Twitter.Response.Content<[Twitter.Entity.Tweet]>? in +// let query = Twitter.API.Lookup.LookupQuery(ids: Array(chunk)) +// let response = try? await Twitter.API.Lookup.tweets( +// session: self.session, +// query: query, +// authorization: authenticationContext.authorization +// ) +// return response +// } +// +// return _responses.compactMap { $0 } +// } +// +// public func twitterBatchLookupResponses( +// content: Twitter.API.V2.User.Timeline.HomeContent, +// authenticationContext: TwitterAuthenticationContext +// ) async -> [Twitter.Response.Content<[Twitter.Entity.Tweet]>] { +// let statusIDs: [Twitter.Entity.Tweet.ID] = { +// var ids: [Twitter.Entity.Tweet.ID] = [] +// ids.append(contentsOf: content.data?.map { $0.id } ?? []) +// ids.append(contentsOf: content.includes?.tweets?.map { $0.id } ?? []) +// return ids +// }() +// +// let responses = await twitterBatchLookupResponses( +// statusIDs: statusIDs, +// authenticationContext: authenticationContext +// ) +// +// return responses +// } +// +// public func twitterBatchLookup( +// statusIDs: [Twitter.Entity.Tweet.ID], +// authenticationContext: TwitterAuthenticationContext +// ) async throws -> TwitterBatchLookupResponse { +// let responses = await twitterBatchLookupResponses( +// statusIDs: statusIDs, +// authenticationContext: authenticationContext +// ) +// +// var lookupDict: [Twitter.Entity.Tweet.ID: Twitter.Entity.Tweet] = [:] +// for response in responses { +// for status in response.value { +// lookupDict[status.idStr] = status +// } +// } +// +// return .init(lookupDict: lookupDict) +// } +// +//} +// +//// Fetch v2 API again to update v2 only properies +//extension APIService { +// +// public struct TwitterBatchLookupResponseV2 { +// let logger = Logger(subsystem: "APIService", category: "TwitterBatchLookupResponseV2") +// +// let dictionary: Twitter.Response.V2.DictContent +// +// public func update(status: TwitterStatus, me: TwitterUser) { +// guard let lookupStatus = dictionary.tweetDict[status.id] else { return } +// guard let managedObjectContext = status.managedObjectContext else { return } +// +// let now = Date() +// +// // poll +// if let poll = dictionary.poll(for: lookupStatus) { +// let result = Persistence.TwitterPoll.createOrMerge( +// in: managedObjectContext, +// context: .init( +// entity: poll, +// me: me, +// networkDate: now +// ) +// ) +// status.attach(poll: result.poll) +// } +// +// // reply settings +// if let value = lookupStatus.replySettings { +// let replySettings = TwitterReplySettings(value: value.rawValue) +// status.update(replySettings: replySettings) +// } +// } +// +// public func update(statuses: [TwitterStatus], me: TwitterUser) { +// for status in statuses { +// update(status: status, me: me) +// } +// } +// } +// +// public func twitterBatchLookupResponsesV2( +// statusIDs: [Twitter.Entity.V2.Tweet.ID], +// authenticationContext: TwitterAuthenticationContext +// ) async -> [Twitter.Response.Content] { +// let chunks = stride(from: 0, to: statusIDs.count, by: 100).map { +// statusIDs[$0.. Twitter.Response.Content? in +// let response = try? await Twitter.API.V2.Lookup.statuses( +// session: self.session, +// query: .init(statusIDs: Array(chunk)), +// authorization: authenticationContext.authorization +// ) +// return response +// } +// +// return _responses.compactMap { $0 } +// } +// +// public func twitterBatchLookupV2( +// statusIDs: [Twitter.Entity.V2.Tweet.ID], +// authenticationContext: TwitterAuthenticationContext +// ) async throws -> TwitterBatchLookupResponseV2 { +// let responses = await twitterBatchLookupResponsesV2( +// statusIDs: statusIDs, +// authenticationContext: authenticationContext +// ) +// +// var tweets: [Twitter.Entity.V2.Tweet] = [] +// var users: [Twitter.Entity.V2.User] = [] +// var media: [Twitter.Entity.V2.Media] = [] +// var places: [Twitter.Entity.V2.Place] = [] +// var polls: [Twitter.Entity.V2.Tweet.Poll] = [] +// +// for response in responses { +// if let value = response.value.data { +// tweets.append(contentsOf: value) +// } +// if let value = response.value.includes?.tweets { +// tweets.append(contentsOf: value) +// } +// if let value = response.value.includes?.users { +// users.append(contentsOf: value) +// } +// if let value = response.value.includes?.media { +// media.append(contentsOf: value) +// } +// if let value = response.value.includes?.places { +// places.append(contentsOf: value) +// } +// if let value = response.value.includes?.polls { +// polls.append(contentsOf: value) +// } +// } +// +// let dictionary = Twitter.Response.V2.DictContent( +// tweets: tweets, +// users: users, +// media: media, +// places: places, +// polls: polls +// ) +// +// return .init(dictionary: dictionary) +// } +// +//} diff --git a/TwidereSDK/Sources/TwidereCore/State/AuthContext.swift b/TwidereSDK/Sources/TwidereCore/State/AuthContext.swift index b5c199e4..8faf7df7 100644 --- a/TwidereSDK/Sources/TwidereCore/State/AuthContext.swift +++ b/TwidereSDK/Sources/TwidereCore/State/AuthContext.swift @@ -22,29 +22,9 @@ public class AuthContext { // authentication public let authenticationContext: AuthenticationContext - // Twitter Guest - public private(set) var twitterGuestAuthorization: Twitter.API.Guest.GuestAuthorization? - private var lastTwitterGuestAuthorizationTimestamp = Date() - public init(authenticationContext: AuthenticationContext) { self.authenticationContext = authenticationContext // end init - - Timer.publish(every: 10, on: .main, in: .common) - .autoconnect() - .sink { [weak self] _ in - guard let self = self else { - return - } - Task { - if self.twitterGuestAuthorization == nil { - try await self.refreshTwitterGuestAuthorization() - } else if abs(self.lastTwitterGuestAuthorizationTimestamp.timeIntervalSinceNow) > 1 * 60 { - try await self.refreshTwitterGuestAuthorization() - } - } // end Task - } - .store(in: &disposeBag) } public convenience init?(authenticationIndex: AuthenticationIndex) { @@ -60,27 +40,6 @@ public class AuthContext { } -extension AuthContext { - public func twitterGuestAuthorization() async throws -> Twitter.API.Guest.GuestAuthorization { - if let twitterGuestAuthorization = twitterGuestAuthorization { - return twitterGuestAuthorization - } else { - return try await refreshTwitterGuestAuthorization() - } - } - - @discardableResult - public func refreshTwitterGuestAuthorization() async throws -> Twitter.API.Guest.GuestAuthorization { - let response = try await Twitter.API.Guest.active( - session: URLSession(configuration: .ephemeral) - ) - let guestAuthorization = Twitter.API.Guest.GuestAuthorization(token: response.value.guestToken) - twitterGuestAuthorization = guestAuthorization - lastTwitterGuestAuthorizationTimestamp = Date() - return guestAuthorization - } -} - #if DEBUG extension AuthContext { public static func mock(context: AppContext) -> AuthContext? { diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Home.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Home.swift index a2b0e3ad..8361c286 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Home.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Home.swift @@ -94,12 +94,15 @@ extension StatusFetchViewModel.Timeline.Home { public static func fetch(api: APIService, input: Input) async throws -> StatusFetchViewModel.Timeline.Output { switch input { case .twitter(let fetchContext): + throw AppError.implicit(.badRequest) + let responses = try await api.twitterHomeTimeline( query: .init( sinceID: fetchContext.sinceID, untilID: fetchContext.untilID, paginationToken: nil, - maxResults: fetchContext.maxResults ?? 100 + maxResults: fetchContext.maxResults ?? 100, + onlyMedia: fetchContext.filter.rule.contains(.onlyMedia) ), authenticationContext: fetchContext.authenticationContext ) diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift index 23981b60..97f9eb2e 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift @@ -165,7 +165,8 @@ extension StatusFetchViewModel.Timeline.User { sinceID: nil, untilID: nil, paginationToken: fetchContext.paginationToken, - maxResults: fetchContext.maxResults ?? 20 + maxResults: fetchContext.maxResults ?? 20, + onlyMedia: fetchContext.filter.rule.contains(.onlyMedia) ), authenticationContext: fetchContext.authenticationContext ) @@ -199,7 +200,8 @@ extension StatusFetchViewModel.Timeline.User { sinceID: nil, untilID: nil, paginationToken: fetchContext.paginationToken, - maxResults: fetchContext.maxResults ?? 20 + maxResults: fetchContext.maxResults ?? 20, + onlyMedia: fetchContext.filter.rule.contains(.onlyMedia) ), authenticationContext: fetchContext.authenticationContext ) diff --git a/TwidereSDK/Sources/TwitterSDK/API/Error/Twitter+API+Error+InternalError.swift b/TwidereSDK/Sources/TwitterSDK/API/Error/Twitter+API+Error+InternalError.swift deleted file mode 100644 index c1d0fa2e..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/Error/Twitter+API+Error+InternalError.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// Twitter+API+Error+InternalError.swift -// -// -// Created by Cirno MainasuK on 2020-12-25. -// - -import Foundation - -extension Twitter.API.Error { - public struct InternalError: Error, LocalizedError { - let message: String - - public init(message: String) { - self.message = message - } - - public var errorDescription: String? { - return "Internal Error" - } - - public var failureReason: String? { - return message - } - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/Error/Twitter+API+Error+ResponseError.swift b/TwidereSDK/Sources/TwitterSDK/API/Error/Twitter+API+Error+ResponseError.swift deleted file mode 100644 index e9815b16..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/Error/Twitter+API+Error+ResponseError.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Twitter+API+ResponseError.swift -// -// -// Created by Cirno MainasuK on 2020-12-25. -// - -import Foundation -import NIOHTTP1 - -extension Twitter.API.Error { - public struct ResponseError: Error { - public var httpResponseStatus: HTTPResponseStatus - public var twitterAPIError: TwitterAPIError? - - public init(httpResponseStatus: HTTPResponseStatus, twitterAPIError: Twitter.API.Error.TwitterAPIError?) { - self.httpResponseStatus = httpResponseStatus - self.twitterAPIError = twitterAPIError - } - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/Error/Twitter+API+Error+TwitterAPIError.swift b/TwidereSDK/Sources/TwitterSDK/API/Error/Twitter+API+Error+TwitterAPIError.swift deleted file mode 100644 index d8ac4ec1..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/Error/Twitter+API+Error+TwitterAPIError.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// File.swift -// -// -// Created by Cirno MainasuK on 2020-12-25. -// - -import Foundation - -// Ref: https://developer.twitter.com/en/support/twitter-api/error-troubleshooting -// Ref: https://developer.twitter.com/ja/docs/basics/response-codes (prefer) -extension Twitter.API.Error { - public enum TwitterAPIError: Error, Hashable { - - case custom(code: Int, message: String) - - // 63 - User has been suspended. Corresponds with HTTP 403 The user account has been suspended and information cannot be retrieved. - case userHasBeenSuspended - - // 88 - Corresponds with HTTP 429. The request limit for this resource has been reached for the current rate limit window. - case rateLimitExceeded - - // 136 - - case blockedFromViewingThisUserProfile - - // 162 - - case blockedFromRequestFollowingThisUser - - // 179 - Sorry, you are not authorized to see this status - case notAuthorizedToSeeThisStatus - - // 326 - Corresponds with HTTP 403. The user should log in to https://twitter.com to unlock their account before the user token can be used. - case accountIsTemporarilyLocked(message: String) - - init(code: Int, message: String = "") { - switch code { - case 63: self = .userHasBeenSuspended - case 88: self = .rateLimitExceeded - case 136: self = .blockedFromViewingThisUserProfile - case 162: self = .blockedFromRequestFollowingThisUser - case 179: self = .notAuthorizedToSeeThisStatus - case 326: self = .accountIsTemporarilyLocked(message: message) - default: self = .custom(code: code, message: message) - } - } - - init?(errorResponse: Twitter.API.ErrorResponse) { - guard let error = errorResponse.errors.first else { - return nil - } - - self.init(code: error.code, message: error.message) - } - - init?(errorResponseV2: Twitter.API.ErrorResponseV2) { - guard let error = errorResponseV2.errors.first else { - return nil - } - - if let title = error.title, title == "Authorization Error" { - self = .notAuthorizedToSeeThisStatus - return - } - - return nil - } - - init?(errorRequestResponse: Twitter.API.ErrorRequestResponse) { - switch (errorRequestResponse.request, errorRequestResponse.error) { - case (_, "Not authorized."): - self = .notAuthorizedToSeeThisStatus - default: - return nil - } - } - - public init?(responseContentError error: Twitter.Response.V2.ContentError) { - switch (error.title, error.detail) { - case ("Forbidden", let detail) where detail.hasPrefix("User has been suspended"): - self = .userHasBeenSuspended - default: - return nil - } - } - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Guest.swift b/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Guest.swift deleted file mode 100644 index 87fa485b..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Guest.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// Twitter+API+Guest.swift -// -// -// Created by MainasuK on 2022-8-22. -// - -import Foundation - -extension Twitter.API { - public enum Guest { } -} - -extension Twitter.API.Guest { - public struct GuestAuthorization: Hashable { - public static var userAgent: String { - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0 Safari/605.1.15" - } - - public static var authorization: String { - "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw" - } - - - public let userAgent: String - public let authorization: String - public let token: String - - public init( - userAgent: String = GuestAuthorization.userAgent, - authorization: String = GuestAuthorization.authorization, - token: String - ) { - self.userAgent = userAgent - self.authorization = authorization - self.token = token - } - } -} - -extension Twitter.API.Guest { - - private static var activeEndpoint: URL { - Twitter.API.endpointURL - .appendingPathComponent("guest") - .appendingPathComponent("activate") - .appendingPathExtension("json") - } - - public static func active( - session: URLSession - ) async throws -> Twitter.Response.Content { - var request = URLRequest( - url: activeEndpoint, - cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, - timeoutInterval: Twitter.API.timeoutInterval - ) - request.httpMethod = "POST" - request.setValue(GuestAuthorization.authorization, forHTTPHeaderField: "Authorization") - request.setValue(GuestAuthorization.userAgent, forHTTPHeaderField: "User-Agent") - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: ActiveContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct ActiveContent: Codable { - public let guestToken: String - - enum CodingKeys: String, CodingKey { - case guestToken = "guest_token" - } - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+OAuth+AccessToken.swift b/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+OAuth+AccessToken.swift deleted file mode 100644 index cf00d892..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+OAuth+AccessToken.swift +++ /dev/null @@ -1,128 +0,0 @@ -// -// Twitter+API+OAuth+CustomRequestToken.swift -// -// -// Created by MainasuK on 2022-4-21. -// - -import Foundation - -extension Twitter.API.OAuth { - public enum AccessToken { } -} - -extension Twitter.API.OAuth.AccessToken { - - static let accessTokenURL = URL(string: "https://api.twitter.com/oauth/access_token")! - - public static func accessToken( - session: URLSession, - query: AccessTokenQuery - ) async throws -> AccessTokenResponse { - let request = accessTokenURLRequest( - consumerKey: query.consumerKey, - consumerSecret: query.consumerSecret, - requestToken: query.requestToken, - pinCode: query.pinCode - ) - - let (data, _) = try await session.data(for: request, delegate: nil) - guard let body = String(data: data, encoding: .utf8), - let accessTokenResponse = AccessTokenResponse(urlEncodedForm: body) - else { - throw Twitter.API.Error.InternalError(message: "process requestToken response fail") - } - - return accessTokenResponse - } - - static func accessTokenURLRequest( - consumerKey: String, - consumerSecret: String, - requestToken: String, - pinCode: String - ) -> URLRequest { - var components = URLComponents(string: accessTokenURL.absoluteString)! - let queryItems = [ - URLQueryItem(name: "oauth_token", value: requestToken), - URLQueryItem(name: "oauth_verifier", value: pinCode) - ] - components.queryItems = queryItems - let requestURL = components.url! - var request = URLRequest( - url: requestURL, - cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, - timeoutInterval: Twitter.API.timeoutInterval - ) - request.httpMethod = "POST" - let authorizationHeader = Twitter.API.OAuth.authorizationHeader( - requestURL: requestURL, - requestFormQueryItems: queryItems, - httpMethod: "POST", - callbackURL: nil, - consumerKey: consumerKey, - consumerSecret: consumerSecret, - oauthToken: requestToken, - oauthTokenSecret: nil - ) - request.setValue(authorizationHeader, forHTTPHeaderField: Twitter.API.OAuth.authorizationField) - return request - } - - public struct AccessTokenQuery { - public let consumerKey: String - public let consumerSecret: String - public let requestToken: String - public let pinCode: String - - public init( - consumerKey: String, - consumerSecret: String, - requestToken: String, - pinCode: String - ) { - self.consumerKey = consumerKey - self.consumerSecret = consumerSecret - self.requestToken = requestToken - self.pinCode = pinCode - } - } - - public struct AccessTokenResponse: Codable { - public let oauthToken: String - public let oauthTokenSecret: String - public let userID: String - public let screenName: String - - enum CodingKeys: String, CodingKey, CaseIterable { - case oauthToken = "oauth_token" - case oauthTokenSecret = "oauth_token_secret" - case userID = "user_id" - case screenName = "screen_name" - } - - init?(urlEncodedForm form: String) { - var dict: [String: String] = [:] - for component in form.components(separatedBy: "&") { - let tuple = component.components(separatedBy: "=") - for key in CodingKeys.allCases { - if tuple[0] == key.rawValue { dict[key.rawValue] = tuple[1] } - } - } - - guard let oauthToken = dict[CodingKeys.oauthToken.rawValue], - let oauthTokenSecret = dict[CodingKeys.oauthTokenSecret.rawValue], - let userID = dict[CodingKeys.userID.rawValue], - let screenName = dict[CodingKeys.screenName.rawValue] else - { - return nil - } - - self.oauthToken = oauthToken - self.oauthTokenSecret = oauthTokenSecret - self.userID = userID - self.screenName = screenName - } - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+OAuth+RequestToken.swift b/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+OAuth+RequestToken.swift deleted file mode 100644 index c2a2dd18..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+OAuth+RequestToken.swift +++ /dev/null @@ -1,301 +0,0 @@ -// -// Twitter+API+OAuth+RequestToken.swift -// -// -// Created by MainasuK on 2022-4-21. -// - -import os.log -import Foundation -import CryptoKit - -extension Twitter.API.OAuth { - public enum RequestToken { - public enum Standard { } - public enum Relay { } - } -} - -extension Twitter.API.OAuth.RequestToken.Standard { - - public static func requestToken( - session: URLSession, - query: Query - ) async throws -> Response { - let request = requestTokenURLRequest( - consumerKey: query.consumerKey, - consumerKeySecret: query.consumerKeySecret - ) - let (data, _) = try await session.data(for: request, delegate: nil) - let templateURLString = Twitter.API.OAuth.requestTokenEndpointURL.absoluteString - guard let body = String(data: data, encoding: .utf8), - let components = URLComponents(string: templateURLString + "?" + body), - let requestTokenResponse = Response(queryItems: components.queryItems ?? []) - else { - throw Twitter.API.Error.InternalError(message: "process requestToken response fail") - } - return requestTokenResponse - } - - public struct Query { - public let consumerKey: String - public let consumerKeySecret: String - - public init(consumerKey: String, consumerKeySecret: String) { - self.consumerKey = consumerKey - self.consumerKeySecret = consumerKeySecret - } - } - - public struct Response: Codable, CustomDebugStringConvertible { - public let oauthToken: String - public let oauthTokenSecret: String - public let oauthCallbackConfirmed: Bool - - public enum CodingKeys: String, CodingKey { - case oauthToken = "oauth_token" - case oauthTokenSecret = "oauth_token_secret" - case oauthCallbackConfirmed = "oauth_callback_confirmed" - } - - init?(queryItems: [URLQueryItem]) { - var _oauthToken: String? - var _oauthTokenSecret: String? - var _oauthCallbackConfirmed: Bool? - for item in queryItems { - switch item.name { - case "oauth_token": _oauthToken = item.value - case "oauth_token_secret": _oauthTokenSecret = item.value - case "oauth_callback_confirmed": _oauthCallbackConfirmed = item.value == "true" - default: continue - } - } - - guard let oauthToken = _oauthToken, - let oauthTokenSecret = _oauthTokenSecret, - let oauthCallbackConfirmed = _oauthCallbackConfirmed else { - return nil - } - - self.oauthToken = oauthToken - self.oauthTokenSecret = oauthTokenSecret - self.oauthCallbackConfirmed = oauthCallbackConfirmed - } - - public var debugDescription: String { - """ - - oauth_token: \(oauthToken) - - oauth_token_secret: \(oauthTokenSecret) - - oauth_callback_confirmed: \(oauthCallbackConfirmed) - """ - } - } - - static func requestTokenURLRequest( - consumerKey: String, - consumerKeySecret: String - ) -> URLRequest { - var components = URLComponents(string: Twitter.API.OAuth.requestTokenEndpointURL.absoluteString)! - let queryItems = [URLQueryItem(name: "oauth_callback", value: "oob")] - components.queryItems = queryItems - let requestURL = components.url! - var request = URLRequest( - url: requestURL, - cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, - timeoutInterval: Twitter.API.timeoutInterval - ) - request.httpMethod = "POST" - let authorizationHeader = Twitter.API.OAuth.authorizationHeader( - requestURL: requestURL, - requestFormQueryItems: queryItems, - httpMethod: "POST", - callbackURL: nil, - consumerKey: consumerKey, - consumerSecret: consumerKeySecret, - oauthToken: nil, - oauthTokenSecret: nil - ) - request.setValue(authorizationHeader, forHTTPHeaderField: Twitter.API.OAuth.authorizationField) - return request - } - -} - -extension Twitter.API.OAuth.RequestToken.Relay { - - public static func requestToken( - session: URLSession, - query: Query - ) async throws -> Response { - let consumerKey = query.consumerKey - let hostPublicKey = query.hostPublicKey - let oauthEndpoint = query.oauthEndpoint - os_log("%{public}s[%{public}ld], %{public}s: request token %s", ((#file as NSString).lastPathComponent), #line, #function, oauthEndpoint) - - let clientEphemeralPrivateKey = Curve25519.KeyAgreement.PrivateKey() - let clientEphemeralPublicKey = clientEphemeralPrivateKey.publicKey - do { - let sharedSecret = try clientEphemeralPrivateKey.sharedSecretFromKeyAgreement(with: hostPublicKey) - let salt = clientEphemeralPublicKey.rawRepresentation + sharedSecret.withUnsafeBytes { Data($0) } - let wrapKey = sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: salt, sharedInfo: Data("request token exchange".utf8), outputByteCount: 32) - let consumerKeyBox = try ChaChaPoly.seal(Data(consumerKey.utf8), using: wrapKey) - let customRequestTokenPayload = Payload(exchangePublicKey: clientEphemeralPublicKey, consumerKeyBox: consumerKeyBox) - - var request = URLRequest(url: URL(string: oauthEndpoint + "/oauth/request_token")!, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: Twitter.API.timeoutInterval) - request.httpMethod = "POST" - request.addValue("application/json; charset=UTF-8", forHTTPHeaderField: "Content-Type") - request.httpBody = try JSONEncoder().encode(customRequestTokenPayload) - - let (data, _) = try await session.data(for: request, delegate: nil) - os_log("%{public}s[%{public}ld], %{public}s: request token response data: %s", ((#file as NSString).lastPathComponent), #line, #function, String(data: data, encoding: .utf8) ?? "") - let response = try JSONDecoder().decode(Response.Content.self, from: data) - os_log("%{public}s[%{public}ld], %{public}s: request token response: %s", ((#file as NSString).lastPathComponent), #line, #function, String(describing: response)) - - guard let exchangePublicKeyData = Data(base64Encoded: response.exchangePublicKey), - let exchangePublicKey = try? Curve25519.KeyAgreement.PublicKey(rawRepresentation: exchangePublicKeyData), - let sharedSecret = try? clientEphemeralPrivateKey.sharedSecretFromKeyAgreement(with: exchangePublicKey), - let combinedData = Data(base64Encoded: response.requestTokenBox) else - { - throw Twitter.API.Error.InternalError(message: "invalid requestToken response") - } - do { - let salt = exchangePublicKey.rawRepresentation + sharedSecret.withUnsafeBytes { Data($0) } - let wrapKey = sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: salt, sharedInfo: Data("request token response exchange".utf8), outputByteCount: 32) - let sealedBox = try ChaChaPoly.SealedBox(combined: combinedData) - let requestTokenData = try ChaChaPoly.open(sealedBox, using: wrapKey) - guard let requestToken = String(data: requestTokenData, encoding: .utf8) else { - throw Twitter.API.Error.InternalError(message: "invalid requestToken response") - } - let append = Response.Append( - requestToken: requestToken, - clientExchangePrivateKey: clientEphemeralPrivateKey, - hostExchangePublicKey: exchangePublicKey - ) - return Response( - content: response, - append: append - ) - } catch { - assertionFailure(error.localizedDescription) - throw Twitter.API.Error.InternalError(message: "process requestToken response fail") - } - } catch { - assertionFailure(error.localizedDescription) - throw error - } - } - - public struct Query { - public let consumerKey: String - public let hostPublicKey: Curve25519.KeyAgreement.PublicKey - public let oauthEndpoint: String - - public init(consumerKey: String, hostPublicKey: Curve25519.KeyAgreement.PublicKey, oauthEndpoint: String) { - self.consumerKey = consumerKey - self.hostPublicKey = hostPublicKey - self.oauthEndpoint = oauthEndpoint - } - } - - public struct Response { - public let content: Content - public let append: Append - - public struct Content: Codable { - public let exchangePublicKey: String - public let requestTokenBox: String - - enum CodingKeys: String, CodingKey, CaseIterable { - case exchangePublicKey = "exchange_public_key" - case requestTokenBox = "request_token_box" - } - } - - public struct Append { - public let requestToken: String - public let clientExchangePrivateKey: Curve25519.KeyAgreement.PrivateKey - public let hostExchangePublicKey: Curve25519.KeyAgreement.PublicKey - } - } - - struct Payload: Codable { - public let exchangePublicKey: String - public let consumerKeyBox: String - - public enum CodingKeys: String, CodingKey { - case exchangePublicKey = "exchange_public_key" - case consumerKeyBox = "consumer_key_box" - } - - init(exchangePublicKey: Curve25519.KeyAgreement.PublicKey, consumerKeyBox: ChaChaPoly.SealedBox) { - self.exchangePublicKey = exchangePublicKey.rawRepresentation.base64EncodedString() - self.consumerKeyBox = consumerKeyBox.combined.base64EncodedString() - } - } - - public struct OAuthCallback: Codable { - let exchangePublicKey: String - let authenticationBox: String - - enum CodingKeys: String, CodingKey, CaseIterable { - case exchangePublicKey = "exchange_public_key" - case authenticationBox = "authentication_box" - } - - public init?(callbackURL url: URL) { - guard let urlComponents = URLComponents(string: url.absoluteString) else { return nil } - guard let queryItems = urlComponents.queryItems, - let exchangePublicKey = queryItems.first(where: { $0.name == CodingKeys.exchangePublicKey.rawValue })?.value, - let authenticationBox = queryItems.first(where: { $0.name == CodingKeys.authenticationBox.rawValue })?.value else - { - return nil - } - self.exchangePublicKey = exchangePublicKey - self.authenticationBox = authenticationBox - } - - public func authentication(privateKey: Curve25519.KeyAgreement.PrivateKey) throws -> Authentication { - do { - guard let exchangePublicKeyData = Data(base64Encoded: exchangePublicKey), - let sealedBoxData = Data(base64Encoded: authenticationBox) else { - throw Twitter.API.Error.InternalError(message: "invalid callback") - } - let exchangePublicKey = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: exchangePublicKeyData) - let sharedSecret = try privateKey.sharedSecretFromKeyAgreement(with: exchangePublicKey) - let salt = exchangePublicKey.rawRepresentation + sharedSecret.withUnsafeBytes { Data($0) } - let wrapKey = sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: salt, sharedInfo: Data("authentication exchange".utf8), outputByteCount: 32) - let sealedBox = try ChaChaPoly.SealedBox(combined: sealedBoxData) - - let authenticationData = try ChaChaPoly.open(sealedBox, using: wrapKey) - let authentication = try JSONDecoder().decode(Authentication.self, from: authenticationData) - return authentication - - } catch { - if let error = error as? Twitter.API.Error.ResponseError { - throw error - } else { - throw Twitter.API.Error.InternalError(message: error.localizedDescription) - } - } - } - } - - public struct Authentication: Codable { - public let accessToken: String - public let accessTokenSecret: String - public let userID: String - public let screenName: String - public let consumerKey: String - public let consumerSecret: String - - public enum CodingKeys: String, CodingKey { - case accessToken = "access_token" - case accessTokenSecret = "access_token_secret" - case userID = "uesr_id" // server typo and need keep it - case screenName = "screen_name" - case consumerKey = "consumer_key" - case consumerSecret = "consumer_secret" - } - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+OAuth.swift b/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+OAuth.swift deleted file mode 100644 index 7e738ba0..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+OAuth.swift +++ /dev/null @@ -1,195 +0,0 @@ -// -// Twitter+API+OAuth.swift -// TwitterAPI -// -// Created by Cirno MainasuK on 2020-9-1. -// - -import os.log -import Foundation -import Combine -import CryptoKit - -extension Twitter.API.OAuth { - - static let requestTokenEndpointURL = URL(string: "https://api.twitter.com/oauth/request_token")! - static let authorizeEndpointURL = URL(string: "https://api.twitter.com/oauth/authorize")! - -} - -extension Twitter.API.OAuth { - -// public static func requestToken( -// session: URLSession, -// query: Twitter.API.OAuth.RequestTokenQueryContext -// ) async throws -> Twitter.API.OAuth.RequestTokenResponseContext { -// switch query { -// case .standard(let query): -// let response = try await Twitter.API.OAuth.RequestToken.Standard.requestToken( -// session: session, -// query: query -// ) -// return .standard(response) -// case .relay(let query): -// let response = try await Twitter.API.OAuth.RequestToken.Relay.requestToken( -// session: session, -// query: query -// ) -// return .relay(response) -// } -// } -// -// public enum RequestTokenQueryContext { -// case standard(query: Twitter.API.OAuth.RequestToken.Standard.Query) -// case relay(query: Twitter.API.OAuth.RequestToken.Relay.Query) -// } -// -// public enum RequestTokenResponseContext { -// case standard(Twitter.API.OAuth.RequestToken.Standard.Response) -// case relay(Twitter.API.OAuth.RequestToken.Relay.Response) -// } - -} - -extension Twitter.API.OAuth { - - static var authorizationField = "Authorization" - - static func authorizationHeader( - requestURL url: URL, - requestFormQueryItems formQueryItems: [URLQueryItem]?, - httpMethod: String, - callbackURL: URL?, - consumerKey: String, - consumerSecret: String, - oauthToken: String?, - oauthTokenSecret: String? - ) -> String { - var authorizationParameters = Dictionary() - authorizationParameters["oauth_version"] = "1.0" - authorizationParameters["oauth_callback"] = callbackURL?.absoluteString - authorizationParameters["oauth_consumer_key"] = consumerKey - authorizationParameters["oauth_signature_method"] = "HMAC-SHA1" - authorizationParameters["oauth_timestamp"] = String(Int(Date().timeIntervalSince1970)) - authorizationParameters["oauth_nonce"] = UUID().uuidString - - authorizationParameters["oauth_token"] = oauthToken - - authorizationParameters["oauth_signature"] = oauthSignature( - requestURL: url, - requestFormQueryItems: formQueryItems, - httpMethod: httpMethod, - consumerSecret: consumerSecret, - parameters: authorizationParameters, - oauthTokenSecret: oauthTokenSecret - ) - - var parameterComponents = authorizationParameters.urlEncodedQuery.components(separatedBy: "&") as [String] - parameterComponents.sort { $0 < $1 } - - var headerComponents = [String]() - for component in parameterComponents { - let subcomponent = component.components(separatedBy: "=") as [String] - if subcomponent.count == 2 { - headerComponents.append("\(subcomponent[0])=\"\(subcomponent[1])\"") - } - } - - return "OAuth " + headerComponents.joined(separator: ", ") - } - - static func oauthSignature( - requestURL url: URL, - requestFormQueryItems - formQueryItems: [URLQueryItem]?, - httpMethod: String, - consumerSecret: String, - parameters: Dictionary, - oauthTokenSecret: String? - ) -> String { - let encodedConsumerSecret = consumerSecret.urlEncoded - let encodedTokenSecret = oauthTokenSecret?.urlEncoded ?? "" - let signingKey = "\(encodedConsumerSecret)&\(encodedTokenSecret)" - - var parameters = parameters - - var components = URLComponents(string: url.absoluteString)! - for item in components.queryItems ?? [] { - parameters[item.name] = item.value - } - for item in formQueryItems ?? [] { - parameters[item.name] = item.value - } - - components.queryItems = nil - let baseURL = components.url! - - var parameterComponents = parameters.urlEncodedQuery.components(separatedBy: "&") - parameterComponents.sort { - let p0 = $0.components(separatedBy: "=") - let p1 = $1.components(separatedBy: "=") - if p0.first == p1.first { return p0.last ?? "" < p1.last ?? "" } - return p0.first ?? "" < p1.first ?? "" - } - - let parameterString = parameterComponents.joined(separator: "&") - let encodedParameterString = parameterString.urlEncoded - - let encodedURL = baseURL.absoluteString.urlEncoded - - let signatureBaseString = "\(httpMethod)&\(encodedURL)&\(encodedParameterString)" - let message = Data(signatureBaseString.utf8) - - let key = SymmetricKey(data: Data(signingKey.utf8)) - var hmac: HMAC = HMAC(key: key) - hmac.update(data: message) - let mac = hmac.finalize() - - let base64EncodedMac = Data(mac).base64EncodedString() - return base64EncodedMac - } - -} - -extension Twitter.API.OAuth { - - public struct Authorization: Hashable { - public let consumerKey: String - public let consumerSecret: String - public let accessToken: String - public let accessTokenSecret: String - - public init(consumerKey: String, consumerSecret: String, accessToken: String, accessTokenSecret: String) { - self.consumerKey = consumerKey - self.consumerSecret = consumerSecret - self.accessToken = accessToken - self.accessTokenSecret = accessTokenSecret - } - - func authorizationHeader(requestURL url: URL, requestFormQueryItems: [URLQueryItem]? = nil, httpMethod: String) -> String { - return Twitter.API.OAuth.authorizationHeader( - requestURL: url, - requestFormQueryItems: requestFormQueryItems, - httpMethod: httpMethod, - callbackURL: nil, - consumerKey: consumerKey, - consumerSecret: consumerSecret, - oauthToken: accessToken, - oauthTokenSecret: accessTokenSecret - ) - } - } - -} - -extension Twitter.API.OAuth { - - public static func authorizeURL(requestToken: String) -> URL { - var urlComponents = URLComponents(string: authorizeEndpointURL.absoluteString)! - urlComponents.queryItems = [ - URLQueryItem(name: "oauth_token", value: requestToken), - ] - return urlComponents.url! - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API.swift b/TwidereSDK/Sources/TwitterSDK/API/Twitter+API.swift deleted file mode 100644 index 1fd8291b..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API.swift +++ /dev/null @@ -1,327 +0,0 @@ -// -// Twitter+API.swift -// TwitterAPI -// -// Created by Cirno MainasuK on 2020-9-3. -// - -import Foundation -import NIOHTTP1 - -extension Twitter.API { - - public static let endpointURL = URL(string: "https://api.twitter.com/1.1/")! - public static let endpointV2URL = URL(string: "https://api.twitter.com/2/")! - public static let uploadEndpointURL = URL(string: "https://upload.twitter.com/1.1/")! - - public static let timeoutInterval: TimeInterval = 10 - public static let decoder: JSONDecoder = { - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .twitterStrategy - return decoder - }() - public static let httpHeaderDateFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz" - return formatter - }() - -} - -extension Twitter.API { - - public enum Error { } - - public enum Account { } - public enum Application { } - public enum Block { } - public enum Favorites { } - public enum Friendships { } - public enum Geo { } - public enum List { } - public enum Lookup { } - public enum Media { } - public enum Mute { } - public enum OAuth { } - public enum SavedSearch { } - public enum Search { } - public enum Statuses { - public enum Timeline { } - } - public enum Trend { } - public enum Users { } - -} - -extension Twitter.API { - - enum Method: String { - case GET, POST, PATCH, PUT, DELETE - } - - static func request( - url: URL, - method: Method, - query: Query?, - authorization: Twitter.API.OAuth.Authorization - ) -> URLRequest { - var components = URLComponents(string: url.absoluteString)! - components.queryItems = query?.queryItems - if let encodedQueryItems = query?.encodedQueryItems { - let percentEncodedQueryItems = (components.percentEncodedQueryItems ?? []) + encodedQueryItems - components.percentEncodedQueryItems = percentEncodedQueryItems - } - - let requestURL = components.url! - var request = URLRequest( - url: requestURL, - cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, - timeoutInterval: Twitter.API.timeoutInterval - ) - request.httpMethod = method.rawValue - - let authorizationValue = authorization.authorizationHeader( - requestURL: requestURL, - requestFormQueryItems: query?.formQueryItems, - httpMethod: method.rawValue - ) - request.setValue( - authorizationValue, - forHTTPHeaderField: Twitter.API.OAuth.authorizationField - ) - if let body = query?.body { - request.httpBody = body - } - if let contentType = query?.contentType { - request.setValue(contentType, forHTTPHeaderField: "Content-Type") - } - - return request - } - - static func request( - url: URL, - method: Method, - query: Query?, - authorization: Twitter.API.V2.OAuth2.Authorization - ) -> URLRequest { - var components = URLComponents(string: url.absoluteString)! - components.queryItems = query?.queryItems - if let encodedQueryItems = query?.encodedQueryItems { - let percentEncodedQueryItems = (components.percentEncodedQueryItems ?? []) + encodedQueryItems - components.percentEncodedQueryItems = percentEncodedQueryItems - } - - let requestURL = components.url! - var request = URLRequest( - url: requestURL, - cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, - timeoutInterval: Twitter.API.timeoutInterval - ) - request.httpMethod = method.rawValue - - let authorizationValue = authorization.authorizationHeader - request.setValue( - authorizationValue, - forHTTPHeaderField: Twitter.API.OAuth.authorizationField - ) - if let body = query?.body { - request.httpBody = body - } - if let contentType = query?.contentType { - request.setValue(contentType, forHTTPHeaderField: "Content-Type") - } - - return request - } - - static func request( - url: URL, - method: Method, - query: Query?, - authorization: Twitter.API.Guest.GuestAuthorization - ) -> URLRequest { - var components = URLComponents(string: url.absoluteString)! - components.queryItems = query?.queryItems - if let encodedQueryItems = query?.encodedQueryItems { - let percentEncodedQueryItems = (components.percentEncodedQueryItems ?? []) + encodedQueryItems - components.percentEncodedQueryItems = percentEncodedQueryItems - } - - let requestURL = components.url! - var request = URLRequest( - url: requestURL, - cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, - timeoutInterval: Twitter.API.timeoutInterval - ) - request.httpMethod = method.rawValue - - request.setValue( - authorization.userAgent, - forHTTPHeaderField: "User-Agent" - ) - request.setValue( - authorization.authorization, - forHTTPHeaderField: Twitter.API.OAuth.authorizationField - ) - request.setValue( - authorization.token, - forHTTPHeaderField: "x-guest-token" - ) - if let body = query?.body { - request.httpBody = body - } - if let contentType = query?.contentType { - request.setValue(contentType, forHTTPHeaderField: "Content-Type") - } - - return request - } - -} - -extension Twitter.API { - // Error Response when request V1 endpoint - struct ErrorResponse: Codable { - let errors: [ErrorDescription] - - struct ErrorDescription: Codable { - public let code: Int - public let message: String - } - } - - // Alternative Error Response when request V1 endpoint - struct ErrorRequestResponse: Codable { - let request: String - let error: String - } - - public struct ErrorResponseV2: Codable { - public let errors: [ErrorDescription] - public let title: String? - public let detail: String? - public let type: String? - - public struct ErrorDescription: Codable { - public let parameter: String? - public let parameters: ErrorDescriptionParameters? - - public let value: String? - public let message: String? - - public let title: String? - public let detail: String? - public let type: String? - } - - public struct ErrorDescriptionParameters: Codable { - public let expansions: [String]? - public let mediaFields: [String]? - public let placeFields: [String]? - public let poolFields: [String]? - public let userFields: [String]? - public let tweetFields: [String]? - - public enum CodingKeys: String, CodingKey { - case expansions - case mediaFields = "media.fields" - case placeFields = "place.fields" - case poolFields = "pool.fields" - case userFields = "user.fields" - case tweetFields = "tweet.fields" - } - } - } -} - -extension Twitter.API { - static func decode(type: T.Type, from data: Data, response: URLResponse) throws -> T where T : Decodable { - // decode data then decode error if could - do { - return try Twitter.API.decoder.decode(type, from: data) - } catch let decodeError { - #if DEBUG - print(String(data: data, encoding: .utf8) ?? "") - debugPrint(decodeError) - #endif - - guard let httpURLResponse = response as? HTTPURLResponse else { - assertionFailure() - throw decodeError - } - let httpResponseStatus = HTTPResponseStatus(statusCode: httpURLResponse.statusCode) - - if let errorResponse = try? Twitter.API.decoder.decode(ErrorResponse.self, from: data), - let twitterAPIError = Error.TwitterAPIError(errorResponse: errorResponse) { - throw Error.ResponseError(httpResponseStatus: httpResponseStatus, twitterAPIError: twitterAPIError) - } - - if let errorRequestResponse = try? Twitter.API.decoder.decode(ErrorRequestResponse.self, from: data), - let twitterAPIError = Error.TwitterAPIError(errorRequestResponse: errorRequestResponse) { - throw Error.ResponseError(httpResponseStatus: httpResponseStatus, twitterAPIError: twitterAPIError) - } - - if let errorResponseV2 = try? Twitter.API.decoder.decode(ErrorResponseV2.self, from: data), - let twitterAPIError = Error.TwitterAPIError(errorResponseV2: errorResponseV2) { - throw Error.ResponseError(httpResponseStatus: httpResponseStatus, twitterAPIError: twitterAPIError) - } - - // Twitter not return error code described in the document. Convert manually - if httpURLResponse.statusCode == 429 { - throw Error.ResponseError(httpResponseStatus: httpResponseStatus, twitterAPIError: .rateLimitExceeded) - } - - throw Error.ResponseError(httpResponseStatus: httpResponseStatus, twitterAPIError: nil) - } - } - - @available(*, deprecated, message: "") - static func request(url: URL, httpMethod: String, authorization: Twitter.API.OAuth.Authorization, queryItems: [URLQueryItem]? = nil, encodedQueryItems: [URLQueryItem]? = nil, formQueryItems: [URLQueryItem]? = nil) -> URLRequest { - var components = URLComponents(string: url.absoluteString)! - components.queryItems = queryItems - if let encodedQueryItems = encodedQueryItems { - components.percentEncodedQueryItems = (components.percentEncodedQueryItems ?? []) + encodedQueryItems - } - let requestURL = components.url! - var request = URLRequest( - url: requestURL, - cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, - timeoutInterval: Twitter.API.timeoutInterval - ) - request.setValue( - authorization.authorizationHeader(requestURL: requestURL, requestFormQueryItems: formQueryItems, httpMethod: httpMethod), - forHTTPHeaderField: Twitter.API.OAuth.authorizationField - ) - request.httpMethod = httpMethod - return request - } - -} - -extension JSONDecoder.DateDecodingStrategy { - fileprivate static let twitterStrategy = custom { decoder throws -> Date in - let container = try decoder.singleValueContainer() - let string = try container.decode(String.self) - - let formatterV1 = DateFormatter() - formatterV1.locale = Locale(identifier: "en") - formatterV1.dateFormat = "EEE MMM dd HH:mm:ss ZZZZZ yyyy" - if let date = formatterV1.date(from: string) { - return date - } - - let formatterV2 = ISO8601DateFormatter() - formatterV2.formatOptions.insert(.withFractionalSeconds) - if let date = formatterV2.date(from: string) { - return date - } - - let formatterV3 = ISO8601DateFormatter() - if let date = formatterV3.date(from: string) { - return date - } - - throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(string)") - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Account.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Account.swift deleted file mode 100644 index d0f9bc16..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Account.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// Twitter+API+Account.swift -// -// -// Created by Cirno MainasuK on 2020-9-28. -// - -import Foundation -import Combine - -extension Twitter.API.Account { - - static let verifyCredentialsEndpointURL = Twitter.API.endpointURL.appendingPathComponent("account/verify_credentials.json") - - public static func verifyCredentials( - session: URLSession, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: verifyCredentialsEndpointURL, - httpMethod: "GET", - authorization: authorization, - queryItems: nil - ) - - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: Twitter.Entity.User.self, from: data, response: response) - - return Twitter.Response.Content(value: value, response: response) - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Application.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Application.swift deleted file mode 100644 index dae290ae..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Application.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// Twitter+API+Application.swift -// -// -// Created by Cirno MainasuK on 2020-12-7. -// - -import Foundation -import Combine - -extension Twitter.API.Application { - - static let rateLimitStatusEndpointURL = Twitter.API.endpointURL.appendingPathComponent("application/rate_limit_status.json") - - public static func rateLimitStatus( - session: URLSession, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: rateLimitStatusEndpointURL, - method: .GET, - query: nil, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: Twitter.Entity.RateLimitStatus.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Block.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Block.swift deleted file mode 100644 index 2c5dd26b..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Block.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// Twitter+API+Block.swift -// -// -// Created by Cirno MainasuK on 2021-1-13. -// - -import Foundation -import Combine - -extension Twitter.API.Block { - - static let createEndpointURL = Twitter.API.endpointURL.appendingPathComponent("blocks/create.json") - static let destroyEndpointURL = Twitter.API.endpointURL.appendingPathComponent("blocks/destroy.json") - - public static func block(session: URLSession, authorization: Twitter.API.OAuth.Authorization, query: BlockUpdateQuery) -> AnyPublisher, Error> { - let url: URL = { - switch query.queryKind { - case .create: return createEndpointURL - case .destroy: return destroyEndpointURL - } - }() - var request = Twitter.API.request(url: url, httpMethod: "POST", authorization: authorization, queryItems: query.queryItems) - request.httpMethod = "POST" - return session.dataTaskPublisher(for: request) - .tryMap { data, response in - let value = try Twitter.API.decode(type: Twitter.Entity.User.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - .eraseToAnyPublisher() - } - -} - -extension Twitter.API.Block { - - public struct BlockUpdateQuery { - public let userID: Twitter.Entity.User.ID - public let queryKind: QueryKind - - public enum QueryKind { - case create - case destroy - } - - public init(userID: Twitter.Entity.User.ID, queryKind: Twitter.API.Block.BlockUpdateQuery.QueryKind) { - self.userID = userID - self.queryKind = queryKind - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - items.append(URLQueryItem(name: "user_id", value: userID)) - guard !items.isEmpty else { return nil } - return items - } - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Favorites.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Favorites.swift deleted file mode 100644 index 476e1937..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Favorites.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// Twitter+API+Favorites.swift -// -// -// Created by Cirno MainasuK on 2020-10-13. -// - -import Foundation -import Combine - -// TODO: V2 - -extension Twitter.API.Favorites { - - static let favoritesCreateEndpointURL = Twitter.API.endpointURL.appendingPathComponent("favorites/create.json") - static let favoritesDestroyEndpointURL = Twitter.API.endpointURL.appendingPathComponent("favorites/destroy.json") - - public static func favorites(session: URLSession, authorization: Twitter.API.OAuth.Authorization, favoriteKind: FavoriteKind, query: FavoriteQuery) -> AnyPublisher, Error> { - let url: URL = { - switch favoriteKind { - case .create: return favoritesCreateEndpointURL - case .destroy: return favoritesDestroyEndpointURL - } - }() - var request = Twitter.API.request(url: url, httpMethod: "POST", authorization: authorization, queryItems: query.queryItems) - request.httpMethod = "POST" - return session.dataTaskPublisher(for: request) - .tryMap { data, response in - let value = try Twitter.API.decode(type: Twitter.Entity.Tweet.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - .eraseToAnyPublisher() - } -} - -extension Twitter.API.Favorites { - - public enum FavoriteKind { - case create - case destroy - } - - public struct FavoriteQuery { - public let id: Twitter.Entity.Tweet.ID - - public init(id: Twitter.Entity.Tweet.ID) { - self.id = id - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - items.append(URLQueryItem(name: "id", value: id)) - guard !items.isEmpty else { return nil } - return items - } - } - - public struct ListQuery { - public let count: Int? - public let userID: String? - public let maxID: String? - - public init(count: Int? = nil, userID: Twitter.Entity.User.ID? = nil, maxID: String? = nil) { - self.count = count - self.userID = userID - self.maxID = maxID - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - if let count = self.count { - items.append(URLQueryItem(name: "count", value: String(count))) - } - if let userID = self.userID { - items.append(URLQueryItem(name: "user_id", value: userID)) - } - if let maxID = self.maxID { - items.append(URLQueryItem(name: "max_id", value: maxID)) - } - guard !items.isEmpty else { return nil } - return items - } - } -} - -extension Twitter.API.Favorites { - - static let listEndpointURL = Twitter.API.endpointURL.appendingPathComponent("favorites/list.json") - - // V1 - public static func list( - session: URLSession, - query: Twitter.API.Statuses.Timeline.TimelineQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content<[Twitter.Entity.Tweet]> { - assert(query.userID != nil && query.userID != "") - let request = Twitter.API.request( - url: listEndpointURL, - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: [Twitter.Entity.Tweet].self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Friendships.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Friendships.swift deleted file mode 100644 index 1c08ee8a..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Friendships.swift +++ /dev/null @@ -1,106 +0,0 @@ -// -// File.swift -// -// -// Created by Cirno MainasuK on 2020-11-2. -// - -import Foundation -import Combine - -extension Twitter.API.Friendships { - - static let showEndpointURL = Twitter.API.endpointURL.appendingPathComponent("friendships/show.json") // 180 in 15m - - public static func friendship( - session: URLSession, - query: FriendshipQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: showEndpointURL, - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: Twitter.Entity.Relationship.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct FriendshipQuery: Query { - - public let sourceID: Twitter.Entity.User.ID - public let targetID: Twitter.Entity.User.ID - - public init(sourceID: Twitter.Entity.User.ID, targetID: Twitter.Entity.User.ID) { - self.sourceID = sourceID - self.targetID = targetID - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - items.append(URLQueryItem(name: "source_id", value: sourceID)) - items.append(URLQueryItem(name: "target_id", value: targetID)) - guard !items.isEmpty else { return nil } - return items - } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { nil } - var body: Data? { nil } - } - -} - -extension Twitter.API.Friendships { - - static let createEndpointURL = Twitter.API.endpointURL.appendingPathComponent("friendships/create.json") // 400 in 1 day - static let destroyEndpointURL = Twitter.API.endpointURL.appendingPathComponent("friendships/destroy.json") - static let updateEndpointURL = Twitter.API.endpointURL.appendingPathComponent("friendships/update.json") - - @available(*, deprecated, message: "use V2") - public static func friendships(session: URLSession, authorization: Twitter.API.OAuth.Authorization, queryKind: UpdateQueryType, query: FriendshipUpdateQuery) -> AnyPublisher, Error> { - let url: URL = { - switch queryKind { - case .create: return createEndpointURL - case .destroy: return destroyEndpointURL - case .update: return updateEndpointURL - } - }() - var request = Twitter.API.request(url: url, httpMethod: "POST", authorization: authorization, queryItems: query.queryItems) - request.httpMethod = "POST" - return session.dataTaskPublisher(for: request) - .tryMap { data, response in - let value = try Twitter.API.decode(type: Twitter.Entity.User.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - .eraseToAnyPublisher() - } - -} - -extension Twitter.API.Friendships { - - public enum UpdateQueryType { - case create - case destroy - case update - } - - public struct FriendshipUpdateQuery { - public let userID: Twitter.Entity.User.ID - - public init(userID: Twitter.Entity.User.ID) { - self.userID = userID - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - items.append(URLQueryItem(name: "user_id", value: userID)) - guard !items.isEmpty else { return nil } - return items - } - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Geo.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Geo.swift deleted file mode 100644 index cf1b7f81..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Geo.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// Twitter+API+Geo.swift -// -// -// Created by Cirno MainasuK on 2020-10-27. -// - -import Foundation -import Combine - -extension Twitter.API.Geo { - - static let searchEndpointURL = Twitter.API.endpointURL.appendingPathComponent("geo/search.json") - - public static func search( - session: URLSession, - query: SearchQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: searchEndpointURL, - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: SearchResponse.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct SearchQuery: Query { - public let latitude: Double - public let longitude: Double - public let granularity: String - - public init( - latitude: Double, - longitude: Double, - granularity: String - ) { - self.latitude = latitude - self.longitude = longitude - self.granularity = granularity - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - items.append(URLQueryItem(name: "lat", value: "\(latitude)")) - items.append(URLQueryItem(name: "long", value: "\(longitude)")) - items.append(URLQueryItem(name: "granularity", value: granularity)) - guard !items.isEmpty else { return nil } - return items - } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { nil } - var body: Data? { nil } - } - - public struct SearchResponse: Codable { - public let result: Result - - public struct Result: Codable { - public let places: [Twitter.Entity.Place] - } - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+List.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+List.swift deleted file mode 100644 index 299bf6f7..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+List.swift +++ /dev/null @@ -1,107 +0,0 @@ -// -// Twitter+API+List.swift -// -// -// Created by MainasuK on 2022-3-10. -// - -import Foundation -import Combine - -// https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/create-manage-lists/api-reference/get-lists-show -extension Twitter.API.List { - - static let showEndpointURL = Twitter.API.endpointURL - .appendingPathComponent("lists") - .appendingPathComponent("show.json") - - public static func show( - session: URLSession, - query: ShowQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: showEndpointURL, - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: Twitter.Entity.List.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct ShowQuery: Query { - public let id: Twitter.Entity.List.ID - - public init( - id: Twitter.Entity.List.ID - ) { - self.id = id - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - items.append(URLQueryItem(name: "list_id", value: id)) - guard !items.isEmpty else { return nil } - return items - } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { nil } - var body: Data? { nil } - } - -} - -// https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/create-manage-lists/api-reference/get-lists-statuses -extension Twitter.API.List { - - static let statusesEndpointURL = Twitter.API.endpointURL - .appendingPathComponent("lists") - .appendingPathComponent("statuses.json") - - public static func statuses( - session: URLSession, - query: StatusesQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content<[Twitter.Entity.Tweet]> { - let request = Twitter.API.request( - url: statusesEndpointURL, - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: [Twitter.Entity.Tweet].self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct StatusesQuery: Query { - public let id: Twitter.Entity.List.ID - public let maxID: Twitter.Entity.Tweet.ID? - - public init( - id: Twitter.Entity.List.ID, - maxID: Twitter.Entity.Tweet.ID? - ) { - self.id = id - self.maxID = maxID - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - items.append(URLQueryItem(name: "list_id", value: id)) - maxID.flatMap { - items.append(URLQueryItem(name: "max_id", value: $0)) - } - guard !items.isEmpty else { return nil } - return items - } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { nil } - var body: Data? { nil } - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Lookup.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Lookup.swift deleted file mode 100644 index 3af59961..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Lookup.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// Twitter+API+Lookup.swift -// -// -// Created by Cirno MainasuK on 2020-12-15. -// - -import Foundation -import Combine - -extension Twitter.API.Lookup { - - static let statusesLookupEndpointURL = Twitter.API.endpointURL.appendingPathComponent("statuses/lookup.json") - - public static func tweets( - session: URLSession, - query: LookupQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content<[Twitter.Entity.Tweet]> { - let request = Twitter.API.request( - url: statusesLookupEndpointURL, - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: [Twitter.Entity.Tweet].self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct LookupQuery: Query { - public let ids: [String] - - public init(ids: [String]) { - self.ids = ids - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - - let ids = self.ids.joined(separator: ",") - // "id" not typo - items.append(URLQueryItem(name: "id", value: ids)) - items.append(URLQueryItem(name: "include_entities", value: "true")) - items.append(URLQueryItem(name: "include_ext_alt_text", value: "true")) - items.append(URLQueryItem(name: "tweet_mode", value: "extended")) - - guard !items.isEmpty else { return nil } - return items - } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { nil } - var body: Data? { nil } - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Media+Metadata.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Media+Metadata.swift deleted file mode 100644 index 25fe83fe..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Media+Metadata.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// Twitter+API+Media+Metadata.swift -// -// -// Created by MainasuK on 2022-6-1. -// - -import Foundation -import Combine - -extension Twitter.API.Media { - public enum Metadata { } -} - -extension Twitter.API.Media.Metadata { - - private static var createEndpointURL: URL { - Twitter.API.uploadEndpointURL - .appendingPathComponent("media") - .appendingPathComponent("metadata") - .appendingPathComponent("create.json") - } - - public static func create( - session: URLSession, - query: CreateQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: createEndpointURL, - method: .POST, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - guard data.isEmpty else { - let value = try Twitter.API.decode(type: CreateResponse.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - return Twitter.Response.Content(value: .init(), response: response) - } - - public struct CreateQuery: JSONEncodeQuery { - public let mediaID: String - public let altText: AltText - - public enum CodingKeys: String, CodingKey { - case mediaID = "media_id" - case altText = "alt_text" - } - - public init( - mediaID: String, - altText: String - ) { - self.mediaID = mediaID - self.altText = .init(text: altText) - } - - public struct AltText: Codable { - public let text: String - } - - var queryItems: [URLQueryItem]? { nil } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - } - - public struct CreateResponse: Codable { } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Media.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Media.swift deleted file mode 100644 index 7d338721..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Media.swift +++ /dev/null @@ -1,252 +0,0 @@ -// -// Twitter+API+Media.swift -// -// -// Created by Cirno MainasuK on 2020-10-26. -// - -import Foundation -import Combine - -extension Twitter.API.Media { - static let uploadEndpointURL = Twitter.API.uploadEndpointURL.appendingPathComponent("media/upload.json") -} - -extension Twitter.API.Media { - - public static func `init`( - session: URLSession, - query: InitQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: uploadEndpointURL, - method: .POST, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: InitResponse.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct InitQuery: Query { - public let command = "INIT" - public let totalBytes: Int - public let mediaType: String - public let mediaCategory: String - - public init( - totalBytes: Int, - mediaType: String, - mediaCategory: String - ) { - self.totalBytes = totalBytes - self.mediaType = mediaType.urlEncoded - self.mediaCategory = mediaCategory - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - items.append(URLQueryItem(name: "command", value: command)) - items.append(URLQueryItem(name: "total_bytes", value: "\(totalBytes)")) - items.append(URLQueryItem(name: "media_type", value: mediaType)) - items.append(URLQueryItem(name: "media_category", value: mediaCategory)) - guard !items.isEmpty else { return nil } - return items - } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { "application/x-www-form-urlencoded" } - var body: Data? { nil } - } - - public struct InitResponse: Codable { - public let mediaIDString: String - public let expiresAfterSecs: Int - - public enum CodingKeys: String, CodingKey { - case mediaIDString = "media_id_string" - case expiresAfterSecs = "expires_after_secs" - } - } - -} - -extension Twitter.API.Media { - - public static func append( - session: URLSession, - query: AppendQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - var request = Twitter.API.request( - url: uploadEndpointURL, - method: .POST, - query: query, - authorization: authorization - ) - request.timeoutInterval = 60 // should > 17 Kb/s for 1 MiB chunk - - let (data, response) = try await session.data(for: request, delegate: nil) - guard data.isEmpty else { - // try parse and throw error - do { - _ = try Twitter.API.decode(type: AppendResponse.self, from: data, response: response) - } catch { - throw error - } - // error should parsed. return empty response here for edge case - assertionFailure() - return Twitter.Response.Content(value: AppendResponse(), response: response) - } - - return Twitter.Response.Content(value: AppendResponse(), response: response) - } - - public struct AppendQuery: Query { - public let command = "APPEND" - public let mediaID: String - public let mediaData: String - public let segmentIndex: Int - - public init( - mediaID: String, - mediaData: String, - segmentIndex: Int - ) { - self.mediaID = mediaID - self.mediaData = mediaData - self.segmentIndex = segmentIndex - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - items.append(URLQueryItem(name: "command", value: command)) - items.append(URLQueryItem(name: "media_id", value: mediaID)) - items.append(URLQueryItem(name: "segment_index", value: "\(segmentIndex)")) - guard !items.isEmpty else { return nil } - return items - } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { - [URLQueryItem(name: "media_data", value: mediaData)] - } - var contentType: String? { "application/x-www-form-urlencoded" } - var body: Data? { - let content = "media_data=" + mediaData.urlEncoded - return content.data(using: .utf8) - } - } - - public struct AppendResponse: Codable { - // Void - } - -} - -extension Twitter.API.Media { - - public static func finalize( - session: URLSession, - query: FinalizeQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: uploadEndpointURL, - method: .POST, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: FinalizeResponse.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct FinalizeQuery: Query { - public let command = "FINALIZE" - public let mediaID: String - - public init(mediaID: String) { - self.mediaID = mediaID - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - items.append(URLQueryItem(name: "command", value: command)) - items.append(URLQueryItem(name: "media_id", value: mediaID)) - guard !items.isEmpty else { return nil } - return items - } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { "application/x-www-form-urlencoded" } - var body: Data? { nil } - } - - public struct FinalizeResponse: Codable { - public let mediaIDString: String - public let size: Int? - public let expiresAfterSecs: Int - public let processingInfo: ProcessingInfo? // server return it when media needs processing - - public enum CodingKeys: String, CodingKey { - case mediaIDString = "media_id_string" - case size - case expiresAfterSecs = "expires_after_secs" - case processingInfo = "processing_info" - } - - public struct ProcessingInfo: Codable { - public let state: String - public let checkAfterSecs: Int? - - public enum CodingKeys: String, CodingKey { - case state - case checkAfterSecs = "check_after_secs" - } - } - } - -} - -extension Twitter.API.Media { - - public static func status( - session: URLSession, - query: StatusQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: uploadEndpointURL, - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: FinalizeResponse.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct StatusQuery: Query { - public let command = "STATUS" - public let mediaID: String - - public init(mediaID: String) { - self.mediaID = mediaID - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - items.append(URLQueryItem(name: "command", value: command)) - items.append(URLQueryItem(name: "media_id", value: mediaID)) - guard !items.isEmpty else { return nil } - return items - } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { nil } - var body: Data? { nil } - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Mute.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Mute.swift deleted file mode 100644 index 305fb24c..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Mute.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// Twitter+API+Mute.swift -// -// -// Created by Cirno MainasuK on 2021-1-13. -// - -import Foundation -import Combine - -extension Twitter.API.Mute { - - static let createEndpointURL = Twitter.API.endpointURL.appendingPathComponent("mutes/users/create.json") - static let destroyEndpointURL = Twitter.API.endpointURL.appendingPathComponent("mutes/users/destroy.json") - - public static func mute(session: URLSession, authorization: Twitter.API.OAuth.Authorization, query: MuteUpdateQuery) -> AnyPublisher, Error> { - let url: URL = { - switch query.queryKind { - case .create: return createEndpointURL - case .destroy: return destroyEndpointURL - } - }() - var request = Twitter.API.request(url: url, httpMethod: "POST", authorization: authorization, queryItems: query.queryItems) - request.httpMethod = "POST" - return session.dataTaskPublisher(for: request) - .tryMap { data, response in - let value = try Twitter.API.decode(type: Twitter.Entity.User.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - .eraseToAnyPublisher() - } - -} - -extension Twitter.API.Mute { - - public struct MuteUpdateQuery { - public let userID: Twitter.Entity.User.ID - public let queryKind: QueryKind - - public enum QueryKind { - case create - case destroy - } - - public init(userID: Twitter.Entity.User.ID, queryKind: Twitter.API.Mute.MuteUpdateQuery.QueryKind) { - self.userID = userID - self.queryKind = queryKind - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - items.append(URLQueryItem(name: "user_id", value: userID)) - guard !items.isEmpty else { return nil } - return items - } - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+SavedSearch.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+SavedSearch.swift deleted file mode 100644 index 0b09e76d..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+SavedSearch.swift +++ /dev/null @@ -1,104 +0,0 @@ -// -// Twitter+API+SavedSearch.swift -// -// -// Created by MainasuK on 2021-12-22. -// - -import Foundation - -extension Twitter.API.SavedSearch { - - static let listEndpointURL = Twitter.API.endpointURL - .appendingPathComponent("saved_searches") - .appendingPathComponent("list.json") - - // Returns the authenticated user's saved search queries. - // doc: https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/get-saved_searches-list - public static func list( - session: URLSession, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content<[Twitter.Entity.SavedSearch]> { - let request = Twitter.API.request( - url: listEndpointURL, - method: .GET, - query: nil, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: [Twitter.Entity.SavedSearch].self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - -} - -extension Twitter.API.SavedSearch { - - static let createEndpointURL = Twitter.API.endpointURL - .appendingPathComponent("saved_searches") - .appendingPathComponent("create.json") - - // Create a new saved search for the authenticated user. A user may only have 25 saved searches. - // doc: https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/post-saved_searches-create - public static func create( - session: URLSession, - query: CreateQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: createEndpointURL, - method: .POST, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: Twitter.Entity.SavedSearch.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct CreateQuery: Query { - public let query: String - - public init(query: String) { - self.query = query - } - - var queryItems: [URLQueryItem]? { - [URLQueryItem(name: "query", value: query)] - } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { nil } - var body: Data? { nil } - } - -} - -extension Twitter.API.SavedSearch { - - static func destroyEndpointURL(id: Twitter.Entity.SavedSearch.ID) -> URL { - Twitter.API.endpointURL - .appendingPathComponent("saved_searches") - .appendingPathComponent("destroy") - .appendingPathComponent("\(id).json") - } - - // Destroys a saved search for the authenticating user. The authenticating user must be the owner of saved search id being destroyed. - // doc: https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/post-saved_searches-destroy-id - public static func destroy( - session: URLSession, - id: Twitter.Entity.SavedSearch.ID, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: destroyEndpointURL(id: id), - method: .POST, - query: nil, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: Twitter.Entity.SavedSearch.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Search.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Search.swift deleted file mode 100644 index e9626969..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Search.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// Twitter+API+Search.swift -// -// -// Created by Cirno MainasuK on 2021-1-20. -// - -import Foundation -import Combine - -extension Twitter.API.Search { - - static var tweetsEndpointURL = Twitter.API.endpointURL.appendingPathComponent("search/tweets.json") - - public static func tweets( - session: URLSession, - query: Twitter.API.Statuses.Timeline.TimelineQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: tweetsEndpointURL, - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: Twitter.API.Search.Content.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - -} - -extension Twitter.API.Search { - public class Content: Codable { - - public let statuses: [Twitter.Entity.Tweet]? - public let searchMetadata: SearchMetadata - - public enum CodingKeys: String, CodingKey { - case statuses - case searchMetadata = "search_metadata" - } - - public struct SearchMetadata: Codable { - public let nextResults: String? - public let query: String - public let count: Int - - public enum CodingKeys: String, CodingKey { - case nextResults = "next_results" - case query - case count - } - } - - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Statuses+Timeline.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Statuses+Timeline.swift deleted file mode 100644 index dc56aa04..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Statuses+Timeline.swift +++ /dev/null @@ -1,165 +0,0 @@ -// -// Twitter+API+Statuses+Timeline.swift -// -// -// Created by MainasuK on 2021/11/11. -// - -import Foundation - -public protocol TimelineQueryType { - var maxID: Twitter.Entity.Tweet.ID? { get } - var sinceID: Twitter.Entity.Tweet.ID? { get } -} - -extension Twitter.API.Statuses.Timeline { - public struct TimelineQuery: TimelineQueryType, Query { - - // share - public let count: Int? - public let maxID: Twitter.Entity.Tweet.ID? - public let sinceID: Twitter.Entity.Tweet.ID? - public let excludeReplies: Bool? - - // user timeline - public let userID: Twitter.Entity.User.ID? - - // search - public let query: String? - - public init( - count: Int? = nil, - userID: Twitter.Entity.User.ID? = nil, - maxID: Twitter.Entity.Tweet.ID? = nil, - sinceID: Twitter.Entity.Tweet.ID? = nil, - excludeReplies: Bool? = nil, - query: String? = nil - ) { - self.count = count - self.userID = userID - self.maxID = maxID - self.sinceID = sinceID - self.excludeReplies = excludeReplies - self.query = query - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - if let count = self.count { - items.append(URLQueryItem(name: "count", value: String(count))) - } - if let userID = self.userID { - items.append(URLQueryItem(name: "user_id", value: userID)) - } - if let maxID = self.maxID { - items.append(URLQueryItem(name: "max_id", value: maxID)) - } - if let sinceID = self.sinceID { - items.append(URLQueryItem(name: "since_id", value: sinceID)) - } - if let excludeReplies = self.excludeReplies { - items.append(URLQueryItem(name: "exclude_replies", value: excludeReplies ? "true" : "false")) - } - items.append(URLQueryItem(name: "include_ext_alt_text", value: "true")) - items.append(URLQueryItem(name: "tweet_mode", value: "extended")) - - guard !items.isEmpty else { return nil } - return items - } - var encodedQueryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - if let query = query { - items.append(URLQueryItem(name: "q", value: query.urlEncoded)) - } - guard !items.isEmpty else { return nil } - return items - } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { nil } - var body: Data? { nil } - - } -} - -extension Twitter.API.Statuses.Timeline { - - static let homeTimelineEndpointURL = Twitter.API.endpointURL.appendingPathComponent("statuses/home_timeline.json") - - public static func home( - session: URLSession, - query: TimelineQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content<[Twitter.Entity.Tweet]> { - let request = Twitter.API.request( - url: homeTimelineEndpointURL, - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - do { - let value = try Twitter.API.decode(type: [Twitter.Entity.Tweet].self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } catch { - debugPrint(error) - throw error - } - } - -} - -extension Twitter.API.Statuses.Timeline { - - static let userTimelineEndpointURL = Twitter.API.endpointURL.appendingPathComponent("statuses/user_timeline.json") - - public static func user( - session: URLSession, - query: TimelineQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content<[Twitter.Entity.Tweet]> { - let request = Twitter.API.request( - url: userTimelineEndpointURL, - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - do { - let value = try Twitter.API.decode(type: [Twitter.Entity.Tweet].self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } catch { - debugPrint(error) - throw error - } - } - -} - -extension Twitter.API.Statuses.Timeline { - - static let mentionTimelineEndpointURL = Twitter.API.endpointURL.appendingPathComponent("statuses/mentions_timeline.json") - - public static func mentions( - session: URLSession, - query: TimelineQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content<[Twitter.Entity.Tweet]> { - let request = Twitter.API.request( - url: mentionTimelineEndpointURL, - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - do { - let value = try Twitter.API.decode(type: [Twitter.Entity.Tweet].self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } catch { - debugPrint(error) - throw error - } - } - -} - - diff --git a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Statuses.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Statuses.swift deleted file mode 100644 index 42c68226..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Statuses.swift +++ /dev/null @@ -1,162 +0,0 @@ -// -// Twitter+API+Statuses.swift -// -// -// Created by Cirno MainasuK on 2020-10-15. -// - -import Foundation -import Combine - -extension Twitter.API.Statuses { - - static let updateEndpointURL = Twitter.API.endpointURL - .appendingPathComponent("statuses") - .appendingPathComponent("update.json") - - public static func update( - session: URLSession, - query: UpdateQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: updateEndpointURL, - method: .POST, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: Twitter.Entity.Tweet.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct UpdateQuery: Query { - public let status: String - public let inReplyToStatusID: Twitter.Entity.Tweet.ID? - public let autoPopulateReplyMetadata: Bool? - public let excludeReplyUserIDs: String? - public let mediaIDs: String? - public let latitude: Double? - public let longitude: Double? - public let placeID: String? - - public init( - status: String, - inReplyToStatusID: Twitter.Entity.Tweet.ID?, - autoPopulateReplyMetadata: Bool?, - excludeReplyUserIDs: String?, - mediaIDs: String?, - latitude: Double?, - longitude: Double?, - placeID: String? - ) { - self.status = status - self.inReplyToStatusID = inReplyToStatusID - self.autoPopulateReplyMetadata = autoPopulateReplyMetadata - self.excludeReplyUserIDs = excludeReplyUserIDs - self.mediaIDs = mediaIDs - self.latitude = latitude - self.longitude = longitude - self.placeID = placeID - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - inReplyToStatusID.flatMap { items.append(URLQueryItem(name: "in_reply_to_status_id", value: $0)) } - autoPopulateReplyMetadata.flatMap { items.append(URLQueryItem(name: "auto_populate_reply_metadata", value: $0 ? "true" : "false")) } - excludeReplyUserIDs.flatMap { items.append(URLQueryItem(name: "exclude_reply_user_ids", value: $0)) } - mediaIDs.flatMap { items.append(URLQueryItem(name: "media_ids", value: $0)) } - latitude.flatMap { items.append(URLQueryItem(name: "lat", value: String($0))) } - longitude.flatMap { items.append(URLQueryItem(name: "long", value: String($0))) } - placeID.flatMap { items.append(URLQueryItem(name: "place_id", value: $0)) } - guard !items.isEmpty else { return nil } - return items - } - var encodedQueryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - items.append(URLQueryItem(name: "status", value: status.urlEncoded)) - guard !items.isEmpty else { return nil } - return items - } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { nil } - var body: Data? { nil } - } - -} - -extension Twitter.API.Statuses { - - static func retweetEndpointURL(tweetID: Twitter.Entity.Tweet.ID) -> URL { return Twitter.API.endpointURL.appendingPathComponent("statuses/retweet/\(tweetID).json") } - static func unretweetEndpointURL(tweetID: Twitter.Entity.Tweet.ID) -> URL { return Twitter.API.endpointURL.appendingPathComponent("statuses/unretweet/\(tweetID).json") } - static func destroyEndpointURL(tweetID: Twitter.Entity.Tweet.ID) -> URL { return Twitter.API.endpointURL.appendingPathComponent("statuses/destroy/\(tweetID).json") } - - public static func retweet(session: URLSession, authorization: Twitter.API.OAuth.Authorization, retweetKind: RetweetKind, query: RetweetQuery) -> AnyPublisher, Error> { - let url: URL = { - switch retweetKind { - case .retweet: return retweetEndpointURL(tweetID: query.id) - case .unretweet: return unretweetEndpointURL(tweetID: query.id) - } - }() - var request = Twitter.API.request(url: url, httpMethod: "POST", authorization: authorization, queryItems: query.queryItems) - request.httpMethod = "POST" - return session.dataTaskPublisher(for: request) - .tryMap { data, response in - let value = try Twitter.API.decode(type: Twitter.Entity.Tweet.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - .eraseToAnyPublisher() - } - - public static func destroy(session: URLSession, authorization: Twitter.API.OAuth.Authorization, query: DestroyQuery) -> AnyPublisher, Error> { - let url = destroyEndpointURL(tweetID: query.id) - var request = Twitter.API.request(url: url, httpMethod: "POST", authorization: authorization, queryItems: query.queryItems) - request.httpMethod = "POST" - return session.dataTaskPublisher(for: request) - .tryMap { data, response in - let value = try Twitter.API.decode(type: Twitter.Entity.Tweet.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - .eraseToAnyPublisher() - } - -} - -extension Twitter.API.Statuses { - - public enum RetweetKind { - case retweet - case unretweet - } - - public struct RetweetQuery { - public let id: Twitter.Entity.Tweet.ID - - public init(id: Twitter.Entity.Tweet.ID) { - self.id = id - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - items.append(URLQueryItem(name: "id", value: id)) - guard !items.isEmpty else { return nil } - return items - } - } - - public struct DestroyQuery { - public let id: Twitter.Entity.Tweet.ID - - public init(id: Twitter.Entity.Tweet.ID) { - self.id = id - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - items.append(URLQueryItem(name: "id", value: id)) - guard !items.isEmpty else { return nil } - return items - } - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Trend.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Trend.swift deleted file mode 100644 index c4084d78..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Trend.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// Twitter+API+Trend.swift -// -// -// Created by MainasuK on 2021-12-28. -// - -import Foundation - -extension Twitter.API.Trend { - - // https://developer.twitter.com/en/docs/twitter-api/v1/trends/trends-for-location/api-reference/get-trends-place - static let trendsTopicEndpointURL = Twitter.API.endpointURL - .appendingPathComponent("trends") - .appendingPathComponent("place.json") - - public static func topics( - session: URLSession, - query: TopicQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content<[TopicResponse]> { - let request = Twitter.API.request( - url: trendsTopicEndpointURL, - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: [TopicResponse].self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct TopicQuery: Query { - public let id: Int - - public init(id: Int) { - self.id = id - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - items.append(URLQueryItem(name: "id", value: "\(id)")) - guard !items.isEmpty else { return nil } - return items - } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { nil } - var body: Data? { nil } - } - - public struct TopicResponse: Codable, Hashable { - public let trends: [Twitter.Entity.Trend] - public let asOf: Date - public let createdAt: Date - public let locations: [Location] - - enum CodingKeys: String, CodingKey { - case trends - case asOf = "as_of" - case createdAt = "created_at" - case locations - } - - public struct Location: Codable, Hashable { - public let name: String - public let woeid: Int - } - } - -} - -extension Twitter.API.Trend { - - // https://developer.twitter.com/en/docs/twitter-api/v1/trends/locations-with-trending-topics/api-reference/get-trends-available - static let trendsPlaceEndpointURL = Twitter.API.endpointURL - .appendingPathComponent("trends") - .appendingPathComponent("available.json") - - public static func places( - session: URLSession, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content<[Twitter.Entity.Trend.Place]> { - let request = Twitter.API.request( - url: trendsPlaceEndpointURL, - method: .GET, - query: nil, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: [Twitter.Entity.Trend.Place].self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Users+Search.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Users+Search.swift deleted file mode 100644 index 806d15aa..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Users+Search.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// Twitter+API+Users+Search.swift -// -// -// Created by Cirno MainasuK on 2021-10-25. -// - -import Foundation - -extension Twitter.API.Users { - - static let searchEndpointURL = Twitter.API.endpointURL.appendingPathComponent("users/search.json") - - public static func search( - session: URLSession, - query: SearchQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content<[Twitter.Entity.User]> { - let request = Twitter.API.request( - url: searchEndpointURL, - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: [Twitter.Entity.User].self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct SearchQuery: Query { - public let q: String - public let page: Int - public let count: Int - - public init( - q: String, - page: Int, - count: Int - ) { - self.q = q - self.page = page - self.count = min(count, 20) - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - items.append(URLQueryItem(name: "q", value: q)) - items.append(URLQueryItem(name: "page", value: "\(page)")) - items.append(URLQueryItem(name: "count", value: "\(count)")) - guard !items.isEmpty else { return nil } - return items - } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { nil } - var body: Data? { nil } - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Users.swift b/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Users.swift deleted file mode 100644 index 89962914..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V1/Twitter+API+Users.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// Twitter+API+Users.swift -// -// -// Created by Cirno MainasuK on 2020-10-30. -// - -import Foundation -import Combine - -extension Twitter.API.Users { - - static let reportSpamEndpointURL = Twitter.API.endpointURL.appendingPathComponent("users/report_spam.json") - - public static func reportSpam( - session: URLSession, - query: ReportSpamQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: reportSpamEndpointURL, - method: .POST, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: Twitter.Entity.User.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct ReportSpamQuery: Query { - public let userID: Twitter.Entity.User.ID - public let performBlock: Bool - - public init(userID: Twitter.Entity.User.ID, performBlock: Bool) { - self.userID = userID - self.performBlock = performBlock - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - items.append(URLQueryItem(name: "user_id", value: userID)) - items.append(URLQueryItem(name: "perform_block", value: performBlock ? "true" : "false")) - guard !items.isEmpty else { return nil } - return items - } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { nil } - var body: Data? { nil } - } - -} - diff --git a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+List+Member.swift b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+List+Member.swift deleted file mode 100644 index be18c9e4..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+List+Member.swift +++ /dev/null @@ -1,105 +0,0 @@ -// -// Twitter+API+V2+List+Member.swift -// -// -// Created by MainasuK on 2022-3-24. -// - -import Foundation - -extension Twitter.API.V2.List { - public enum Member { } -} - -// add: https://developer.twitter.com/en/docs/twitter-api/lists/list-members/api-reference/post-lists-id-members -extension Twitter.API.V2.List.Member { - - private static func addMemberEndpointURL(listID: Twitter.Entity.V2.List.ID) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("lists") - .appendingPathComponent(listID) - .appendingPathComponent("members") - } - - public static func add( - session: URLSession, - listID: Twitter.Entity.V2.List.ID, - query: AddMemberQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: addMemberEndpointURL(listID: listID), - method: .POST, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: Twitter.API.V2.List.Member.AddMemberContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct AddMemberQuery: JSONEncodeQuery { - public let userID: Twitter.Entity.V2.User.ID - - enum CodingKeys: String, CodingKey { - case userID = "user_id" - } - - public init(userID: Twitter.Entity.V2.User.ID) { - self.userID = userID - } - - var queryItems: [URLQueryItem]? { nil } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - } - - public struct AddMemberContent: Codable { - public let data: ContentData - - public struct ContentData: Codable { - public let isMember: Bool - - enum CodingKeys: String, CodingKey { - case isMember = "is_member" - } - } - } - -} - -// remove: https://developer.twitter.com/en/docs/twitter-api/lists/list-members/api-reference/delete-lists-id-members-user_id -extension Twitter.API.V2.List.Member { - - private static func removeMemberEndpointURL( - listID: Twitter.Entity.V2.List.ID, - userID: Twitter.Entity.V2.User.ID - ) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("lists") - .appendingPathComponent(listID) - .appendingPathComponent("members") - .appendingPathComponent(userID) - } - - public static func remove( - session: URLSession, - listID: Twitter.Entity.V2.List.ID, - userID: Twitter.Entity.V2.User.ID, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: removeMemberEndpointURL(listID: listID, userID: userID), - method: .DELETE, - query: nil, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: Twitter.API.V2.List.Member.RemoveMemberContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public typealias RemoveMemberContent = AddMemberContent - -} - diff --git a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+List.swift b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+List.swift deleted file mode 100644 index 0aa8401b..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+List.swift +++ /dev/null @@ -1,318 +0,0 @@ -// -// Twitter+API+V2+List.swift -// -// -// Created by MainasuK on 2022-3-11. -// - -import Foundation - -extension Twitter.API.V2 { - public enum List { } -} - -// lookup: https://developer.twitter.com/en/docs/twitter-api/lists/list-lookup/api-reference/get-lists-id -extension Twitter.API.V2.List { - - private static func lookupEndpointURL(listID: Twitter.Entity.V2.List.ID) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("lists") - .appendingPathComponent(listID) - } - - public static func lookup( - session: URLSession, - listID: Twitter.Entity.V2.List.ID, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: lookupEndpointURL(listID: listID), - method: .GET, - query: LookupQuery(), - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: Twitter.API.V2.List.LookupContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct LookupQuery: Query { - - var queryItems: [URLQueryItem]? { - let items: [URLQueryItem] = [ - [Twitter.Request.Expansions.ownerID].queryItem, - Twitter.Request.userFields.queryItem, - Twitter.Request.listFields.queryItem, - ] - return items - } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { nil } - var body: Data? { nil } - } - - public struct LookupContent: Codable { - public let data: Twitter.Entity.V2.List - public let includes: Includes? - - public struct Includes: Codable { - public let users: [Twitter.Entity.V2.User] - } - } - -} - -// https://developer.twitter.com/en/docs/twitter-api/lists/list-follows/api-reference/get-lists-id-followers -extension Twitter.API.V2.List { - - private static func followerEndpointURL(listID: Twitter.Entity.V2.List.ID) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("lists") - .appendingPathComponent(listID) - .appendingPathComponent("followers") - } - - public static func follower( - session: URLSession, - listID: Twitter.Entity.V2.List.ID, - query: FollowerQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: followerEndpointURL(listID: listID), - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: Twitter.API.V2.List.FollowerContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct FollowerQuery: Query { - public let maxResults: Int - public let nextToken: String? - - public init( - maxResults: Int = 20, - nextToken: String? - ) { - self.maxResults = min(100, max(10, maxResults)) - self.nextToken = nextToken - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [ - Twitter.Request.userFields.queryItem, - URLQueryItem(name: "max_results", value: String(maxResults)), - ] - if let nextToken = nextToken { - let item = URLQueryItem(name: "pagination_token", value: nextToken) - items.append(item) - } - return items - } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { nil } - var body: Data? { nil } - } - - public struct FollowerContent: Codable { - public let data: [Twitter.Entity.V2.User]? - public let meta: Meta - - public struct Meta: Codable { - public let resultCount: Int - public let previousToken: String? - public let nextToken: String? - - enum CodingKeys: String, CodingKey { - case resultCount = "result_count" - case previousToken = "previous_token" - case nextToken = "next_token" - } - } - } - -} - -// https://developer.twitter.com/en/docs/twitter-api/lists/list-members/api-reference/get-lists-id-members -extension Twitter.API.V2.List { - - private static func memberEndpointURL(listID: Twitter.Entity.V2.List.ID) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("lists") - .appendingPathComponent(listID) - .appendingPathComponent("members") - } - - public static func member( - session: URLSession, - listID: Twitter.Entity.V2.List.ID, - query: MemberQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: memberEndpointURL(listID: listID), - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: Twitter.API.V2.List.FollowerContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public typealias MemberQuery = FollowerQuery - public typealias MemberContent = FollowerContent - -} - -// create: https://developer.twitter.com/en/docs/twitter-api/lists/manage-lists/api-reference/post-lists -extension Twitter.API.V2.List { - - private static func listsEndpointURL() -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("lists") - } - - public static func create( - session: URLSession, - query: CreateQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: listsEndpointURL(), - method: .POST, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: Twitter.API.V2.List.CreateContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct CreateQuery: JSONEncodeQuery { - public let name: String - public let description: String? - public let `private`: Bool? - - public init( - name: String, - description: String?, - private: Bool? - ) { - self.name = name - self.description = description - self.private = `private` - } - - var queryItems: [URLQueryItem]? { nil } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - } - - public struct CreateContent: Codable { - public let data: ContentData - - public struct ContentData: Codable { - public let id: Twitter.Entity.V2.List.ID - public let name: String - } - } - -} - -// update: https://developer.twitter.com/en/docs/twitter-api/lists/manage-lists/api-reference/put-lists-id -extension Twitter.API.V2.List { - - private static func updateListEndpointURL(listID: Twitter.Entity.V2.List.ID) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("lists") - .appendingPathComponent(listID) - } - - public static func update( - session: URLSession, - listID: Twitter.Entity.V2.List.ID, - query: UpdateQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: updateListEndpointURL(listID: listID), - method: .PUT, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: Twitter.API.V2.List.UpdateContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct UpdateQuery: JSONEncodeQuery { - public let name: String? - public let description: String? - public let `private`: Bool? - - public init( - name: String?, - description: String?, - private: Bool? - ) { - self.name = name - self.description = description - self.private = `private` - } - - var queryItems: [URLQueryItem]? { nil } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - } - - public struct UpdateContent: Codable { - public let data: ContentData - - public struct ContentData: Codable { - public let updated: Bool - } - } - -} - - -// delete: https://developer.twitter.com/en/docs/twitter-api/lists/manage-lists/api-reference/delete-lists-id -extension Twitter.API.V2.List { - - private static func deleteListEndpointURL(listID: Twitter.Entity.V2.List.ID) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("lists") - .appendingPathComponent(listID) - } - - public static func delete( - session: URLSession, - listID: Twitter.Entity.V2.List.ID, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: deleteListEndpointURL(listID: listID), - method: .DELETE, - query: nil, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: Twitter.API.V2.List.DeleteContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct DeleteContent: Codable { - public let data: ContentData - - public struct ContentData: Codable { - public let deleted: Bool - } - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Lookup.swift b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Lookup.swift deleted file mode 100644 index 725c4889..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Lookup.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// Twitter+API+V2+Lookup.swift -// TwitterAPI -// -// Created by Cirno MainasuK on 2020-9-15. -// - -import Foundation -import Combine - -extension Twitter.API.V2 { - public enum Lookup { } -} - -// https://developer.twitter.com/en/docs/twitter-api/tweets/lookup/api-reference/get-tweets -extension Twitter.API.V2.Lookup { - - static let tweetsEndpointURL = Twitter.API.endpointV2URL.appendingPathComponent("tweets") - - public static func statuses( - session: URLSession, - query: StatusLookupQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: tweetsEndpointURL, - method: .GET, - query: query, - authorization: authorization - ) - - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: Twitter.API.V2.Lookup.Content.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - static var expansions: [Twitter.Request.Expansions] { - return [ - .attachmentsPollIDs, - .attachmentsMediaKeys, - .authorID, - .entitiesMentionsUsername, - .geoPlaceID, - .inReplyToUserID, - .referencedTweetsID, - .referencedTweetsIDAuthorID - ] - } - - public struct StatusLookupQuery: Query { - public let statusIDs: [Twitter.Entity.Tweet.ID] - - public init(statusIDs: [Twitter.Entity.Tweet.ID]) { - self.statusIDs = statusIDs - } - - var queryItems: [URLQueryItem]? { - let ids = statusIDs.joined(separator: ",") - return [ - Twitter.API.V2.Lookup.expansions.queryItem, - Twitter.Request.tweetsFields.queryItem, - Twitter.Request.userFields.queryItem, - Twitter.Request.mediaFields.queryItem, - Twitter.Request.placeFields.queryItem, - Twitter.Request.pollFields.queryItem, - URLQueryItem(name: "ids", value: ids), - ] - } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { nil } - var body: Data? { nil } - } - - public struct Content: Codable { - public let data: [Twitter.Entity.V2.Tweet]? - public let includes: Include? - - public struct Include: Codable { - public let users: [Twitter.Entity.V2.User]? - public let tweets: [Twitter.Entity.V2.Tweet]? - public let media: [Twitter.Entity.V2.Media]? - public let places: [Twitter.Entity.V2.Place]? - public let polls: [Twitter.Entity.V2.Tweet.Poll]? - } - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+OAuth2+AccessToken.swift b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+OAuth2+AccessToken.swift deleted file mode 100644 index 151eefea..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+OAuth2+AccessToken.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// Twitter+API+V2+OAuth2+AccessToken.swift -// -// -// Created by MainasuK on 2022-4-24. -// - -import Foundation - -extension Twitter.API.V2.OAuth2 { - public enum AccessToken { } -} - -extension Twitter.API.V2.OAuth2.AccessToken { - - static let accessTokenURL = URL(string: "https://api.twitter.com/2/oauth2/token")! - - public static func accessToken( - session: URLSession, - query: AccessTokenQuery - ) async throws -> AccessTokenResponse { - var request = URLRequest( - url: accessTokenURL, - cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, - timeoutInterval: Twitter.API.timeoutInterval - ) - request.httpMethod = "POST" - if let contentType = query.contentType { - request.setValue(contentType, forHTTPHeaderField: "Content-Type") - } - if let body = query.body { - request.httpBody = body - } - - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: AccessTokenResponse.self, from: data, response: response) - return value - } - - public struct AccessTokenQuery: Query { - public let code: String - public let grantType: String - public let clientID: String - public let redirectURI: String - public let codeVerifier: String - - enum CodingKeys: String, CodingKey { - case code - case grantType = "grant_type" - case clientID = "client_id" - case redirectURI = "redirect_uri" - case codeVerifier = "code_verifier" - } - - public init( - code: String, - grantType: String = "authorization_code", - clientID: String, - redirectURI: String, - codeVerifier: String - ) { - self.code = code - self.grantType = grantType - self.clientID = clientID - self.redirectURI = redirectURI - self.codeVerifier = codeVerifier - } - - var queryItems: [URLQueryItem]? { nil } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { "application/x-www-form-urlencoded" } - var body: Data? { - let content = [ - CodingKeys.code.rawValue: code, - CodingKeys.grantType.rawValue: grantType, - CodingKeys.clientID.rawValue: clientID, - CodingKeys.redirectURI.rawValue: redirectURI, - CodingKeys.codeVerifier.rawValue: codeVerifier, - ].urlEncodedQuery - return content.data(using: .utf8) - } - } - - public struct AccessTokenResponse: Codable { - public let tokenType: String - public let expiresIn: Int - public let scope: String - public let accessToken: String - public let refreshToken: String - - enum CodingKeys: String, CodingKey { - case tokenType = "token_type" - case expiresIn = "expires_in" - case scope - case accessToken = "access_token" - case refreshToken = "refresh_token" - } - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+OAuth2+Authorize.swift b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+OAuth2+Authorize.swift deleted file mode 100644 index 51cc9d6c..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+OAuth2+Authorize.swift +++ /dev/null @@ -1,257 +0,0 @@ -// -// Twitter+API+V2+OAuth2+Authorize.swift -// -// -// Created by MainasuK on 2022-4-24. -// - -import os.log -import Foundation -import CryptoKit - -extension Twitter.API.V2.OAuth2 { - public enum Authorize { - public enum Standard { } - public enum Relay { } - } -} - -extension Twitter.API.V2.OAuth2.Authorize.Standard { - - public struct OAuthCallback: Codable { - public let state: String - public let code: String - - enum CodingKeys: String, CodingKey, CaseIterable { - case state - case code - } - - public init?(callbackURL url: URL) { - guard let urlComponents = URLComponents(string: url.absoluteString) else { return nil } - guard let queryItems = urlComponents.queryItems, - let state = queryItems.first(where: { $0.name == CodingKeys.state.rawValue })?.value, - let code = queryItems.first(where: { $0.name == CodingKeys.code.rawValue })?.value else - { - return nil - } - self.state = state - self.code = code - } - } - -} - -extension Twitter.API.V2.OAuth2.Authorize.Relay { - - static let logger = Logger(subsystem: "Twitter.API.V2.OAuth2.Authorize.Relay", category: "API") - - static let callbackURL = URL(string: "twidere://authentication/oauth2/callback")! - - public static func authorize( - session: URLSession, - query: Query - ) async throws -> Response { - - let clientEphemeralPrivateKey = Curve25519.KeyAgreement.PrivateKey() - let clientEphemeralPublicKey = clientEphemeralPrivateKey.publicKey - do { - let sharedSecret = try clientEphemeralPrivateKey.sharedSecretFromKeyAgreement(with: query.hostPublicKey) - let salt = clientEphemeralPublicKey.rawRepresentation + sharedSecret.withUnsafeBytes { Data($0) } - let wrapKey = sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: salt, sharedInfo: Data("oauth2".utf8), outputByteCount: 32) - let box = Box( - clientID: query.clientID, - consumerKey: query.consumerKey, - consumerKeySecret: query.consumerKeySecret - ) - let boxData = try JSONEncoder().encode(box) - let sealedBox = try ChaChaPoly.seal(boxData, using: wrapKey) - let payload = Payload( - exchangePublicKey: clientEphemeralPublicKey.rawRepresentation.base64EncodedString(), - box: sealedBox.combined.base64EncodedString() - ) - var request = URLRequest( - url: query.endpoint.appendingPathComponent("/oauth2"), - cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, - timeoutInterval: Twitter.API.timeoutInterval - ) - request.httpMethod = "POST" - request.addValue("application/json; charset=UTF-8", forHTTPHeaderField: "Content-Type") - request.httpBody = try JSONEncoder().encode(payload) - - let (data, _) = try await session.data(for: request, delegate: nil) - let content = try JSONDecoder().decode(Response.Content.self, from: data) - os_log("%{public}s[%{public}ld], %{public}s: request token response: %s", ((#file as NSString).lastPathComponent), #line, #function, String(describing: content)) - let response = Response( - content: content, - append: .init(clientExchangePrivateKey: clientEphemeralPrivateKey) - ) - return response - } catch { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): error: \(error.localizedDescription)") - throw error - } - } // end func - - public struct Query { - public let clientID: String - public let consumerKey: String - public let consumerKeySecret: String? - public let endpoint: URL - public let hostPublicKey: Curve25519.KeyAgreement.PublicKey - - public init( - clientID: String, - consumerKey: String, - consumerKeySecret: String?, - endpoint: URL, - hostPublicKey: Curve25519.KeyAgreement.PublicKey - ) { - self.clientID = clientID - self.consumerKey = consumerKey - self.consumerKeySecret = consumerKeySecret - self.endpoint = endpoint - self.hostPublicKey = hostPublicKey - } - } - - public struct Response { - public let content: Content - public let append: Append - - public struct Content: Codable { - public let challenge: String - public let state: String - - public init( - challenge: String, - state: String - ) { - self.challenge = challenge - self.state = state - } - } - - public struct Append { - public let clientExchangePrivateKey: Curve25519.KeyAgreement.PrivateKey - } - } - - - public struct Payload: Codable { - /// client ephemeral public key` - public let exchangePublicKey: String - - /// sealed Box - public let box: String - - enum CodingKeys: String, CodingKey, CaseIterable { - case exchangePublicKey = "exchange_public_key" - case box - } - } - - public struct Box: Codable { - public let clientID: String - public let consumerKey: String - public let consumerKeySecret: String? - - enum CodingKeys: String, CodingKey, CaseIterable { - case clientID = "client_id" - case consumerKey = "consumer_key" - case consumerKeySecret = "consumer_key_secret" - } - } - - public struct OAuthCallback: Codable { - let exchangePublicKey: String - let authenticationBox: String - - enum CodingKeys: String, CodingKey, CaseIterable { - case exchangePublicKey = "exchange_public_key" - case authenticationBox = "authentication_box" - } - - public init?(callbackURL url: URL) { - guard let urlComponents = URLComponents(string: url.absoluteString) else { return nil } - guard let queryItems = urlComponents.queryItems, - let exchangePublicKey = queryItems.first(where: { $0.name == CodingKeys.exchangePublicKey.rawValue })?.value, - let authenticationBox = queryItems.first(where: { $0.name == CodingKeys.authenticationBox.rawValue })?.value else - { - return nil - } - self.exchangePublicKey = exchangePublicKey - self.authenticationBox = authenticationBox - } - - public func authentication(privateKey: Curve25519.KeyAgreement.PrivateKey) throws -> Authentication { - do { - guard let exchangePublicKeyData = Data(base64Encoded: exchangePublicKey), - let sealedBoxData = Data(base64Encoded: authenticationBox) else { - throw Twitter.API.Error.InternalError(message: "invalid callback") - } - let exchangePublicKey = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: exchangePublicKeyData) - let sharedSecret = try privateKey.sharedSecretFromKeyAgreement(with: exchangePublicKey) - let salt = exchangePublicKey.rawRepresentation + sharedSecret.withUnsafeBytes { Data($0) } - let wrapKey = sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: salt, sharedInfo: Data("authentication exchange".utf8), outputByteCount: 32) - let sealedBox = try ChaChaPoly.SealedBox(combined: sealedBoxData) - - let authenticationData = try ChaChaPoly.open(sealedBox, using: wrapKey) - let authentication = try JSONDecoder().decode(Authentication.self, from: authenticationData) - return authentication - - } catch { - if let error = error as? Twitter.API.Error.ResponseError { - throw error - } else { - throw Twitter.API.Error.InternalError(message: error.localizedDescription) - } - } - } - } - - public struct Authentication: Codable { - // oauth1.0a - public let oauthConsumerKey: String - public let oauthConsumerSecret: String - public let oauthAccessToken: String - public let oauthAccessTokenSecret: String - public let userID: String - public let screenName: String - // oauth2.0 - public let oauth2AccessToken: String - public let oauth2RefreshToken: String - - enum CodingKeys: String, CodingKey { - case oauthConsumerKey = "oauth_consumer_key" - case oauthConsumerSecret = "oauth_consumer_secret" - case oauthAccessToken = "oauth_access_token" - case oauthAccessTokenSecret = "oauth_access_token_secret" - case userID = "user_id" - case screenName = "screen_name" - case oauth2AccessToken = "oauth2_access_token" - case oauth2RefreshToken = "oauth2_refresh_token" - } - - public init( - oauthConsumerKey: String, - oauthConsumerSecret: String, - oauthAccessToken: String, - oauthAccessTokenSecret: String, - userID: String, - screenName: String, - oauth2AccessToken: String, - oauth2RefreshToken: String - ) { - self.oauthConsumerKey = oauthConsumerKey - self.oauthConsumerSecret = oauthConsumerSecret - self.oauthAccessToken = oauthAccessToken - self.oauthAccessTokenSecret = oauthAccessTokenSecret - self.userID = userID - self.screenName = screenName - self.oauth2AccessToken = oauth2AccessToken - self.oauth2RefreshToken = oauth2RefreshToken - } - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+OAuth2.swift b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+OAuth2.swift deleted file mode 100644 index e4c62d62..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+OAuth2.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// Twitter+API+V2+OAuth2.swift -// -// -// Created by MainasuK on 2022-4-21. -// - -import os.log -import Foundation -import CryptoKit - -extension Twitter.API.V2 { - public enum OAuth2 { } -} - -extension Twitter.API.V2.OAuth2 { - - static let logger = Logger(subsystem: "Twitter.API.V2.OAuth2", category: "API") - static let authorizeEndpointURL = URL(string: "https://twitter.com/i/oauth2/authorize")! -} - -extension Twitter.API.V2.OAuth2 { - - public static func authorizeURL( - endpoint: URL, - clientID: String, - challenge: String, - state: String - ) -> URL { - let redirectURI = endpoint - .appendingPathComponent("oauth2") - .appendingPathComponent("callback") - var components = URLComponents(string: authorizeEndpointURL.absoluteString)! - components.percentEncodedQueryItems = [ - URLQueryItem(name: "response_type", value: "code"), - URLQueryItem(name: "client_id", value: clientID.urlEncoded), - URLQueryItem(name: "scope", value: "tweet.read users.read follows.read follows.write offline.access bookmark.read".urlEncoded), - URLQueryItem(name: "state", value: state), - URLQueryItem(name: "code_challenge", value: challenge), - URLQueryItem(name: "code_challenge_method", value: "S256"), - URLQueryItem(name: "redirect_uri", value: redirectURI.absoluteString.urlEncoded), - ] - let authorizeURL = components.url! - return authorizeURL - } - -} - -extension Twitter.API.V2.OAuth2 { - - public struct Authorization: Hashable { - public let accessToken: String - public let refreshToken: String - - public init( - accessToken: String, - refreshToken: String - ) { - self.accessToken = accessToken - self.refreshToken = refreshToken - } - - var authorizationHeader: String { - return "Bearer \(accessToken)" - } - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Search.swift b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Search.swift deleted file mode 100644 index 675838ef..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Search.swift +++ /dev/null @@ -1,130 +0,0 @@ -// -// Twitter+API+V2+Search.swift -// -// -// Created by Cirno MainasuK on 2020-10-16. -// - -import os.log -import Foundation -import Combine - -extension Twitter.API.V2 { - public enum Search { } -} - -/// https://developer.twitter.com/en/docs/twitter-api/tweets/search/api-reference -extension Twitter.API.V2.Search { - - static let tweetsSearchRecentEndpointURL = Twitter.API.endpointV2URL.appendingPathComponent("tweets/search/recent") - - public static func recentTweet( - session: URLSession, - query: Twitter.API.V2.Search.RecentTweetQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: tweetsSearchRecentEndpointURL, - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: Twitter.API.V2.Search.Content.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - static var expansions: [Twitter.Request.Expansions] { - return [ - .attachmentsPollIDs, - .attachmentsMediaKeys, - .authorID, - .entitiesMentionsUsername, - .geoPlaceID, - .inReplyToUserID, - .referencedTweetsID, - .referencedTweetsIDAuthorID - ] - } - - public struct RecentTweetQuery: Query { - public let query: String - public let maxResults: Int - public let sinceID: Twitter.Entity.V2.Tweet.ID? - public let startTime: Date? - public let nextToken: String? - - public init( - query: String, - maxResults: Int, - sinceID: Twitter.Entity.V2.Tweet.ID?, - startTime: Date?, - nextToken: String? - ) { - self.query = query - self.maxResults = min(100, max(10, maxResults)) - self.sinceID = sinceID - self.startTime = startTime - self.nextToken = nextToken - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [ - Twitter.API.V2.Search.expansions.queryItem, - Twitter.Request.tweetsFields.queryItem, - Twitter.Request.userFields.queryItem, - Twitter.Request.mediaFields.queryItem, - Twitter.Request.placeFields.queryItem, - Twitter.Request.pollFields.queryItem, - URLQueryItem(name: "max_results", value: String(maxResults)), - ] - sinceID.flatMap { items.append(URLQueryItem(name: "since_id", value: $0)) } - nextToken.flatMap { items.append(URLQueryItem(name: "next_token", value: $0)) } - return items - } - var encodedQueryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [ - URLQueryItem(name: "query", value: query.urlEncoded) - ] - if let startTime = startTime { - let formatter = ISO8601DateFormatter() - let time = formatter.string(from: startTime) - let item = URLQueryItem(name: "start_time", value: time.urlEncoded) - items.append(item) - } - return items - } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { nil } - var body: Data? { nil } - } - - public struct Content: Codable { - public let data: [Twitter.Entity.V2.Tweet]? - public let includes: Include? - public let meta: Meta - - public struct Include: Codable { - public let users: [Twitter.Entity.V2.User]? - public let tweets: [Twitter.Entity.V2.Tweet]? - public let media: [Twitter.Entity.V2.Media]? - public let places: [Twitter.Entity.V2.Place]? - public let polls: [Twitter.Entity.V2.Tweet.Poll]? - } - - public struct Meta: Codable { - public let newestID: String? - public let oldestID: String? - public let resultCount: Int - public let nextToken: String? - - public enum CodingKeys: String, CodingKey { - case newestID = "newest_id" - case oldestID = "oldest_id" - case resultCount = "result_count" - case nextToken = "next_token" - } - } - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Status+Delete.swift b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Status+Delete.swift deleted file mode 100644 index 8f1e52a6..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Status+Delete.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// Twitter+API+V2+Status+Delete.swift -// -// -// Created by MainasuK on 2021-12-16. -// - -import Foundation - -extension Twitter.API.V2.Status { - public enum Delete { } -} - -// doc: https://developer.twitter.com/en/docs/twitter-api/tweets/manage-tweets/api-reference/delete-tweets-id -extension Twitter.API.V2.Status.Delete { - - private static func deleteEndpointURL(statusID: Twitter.Entity.V2.Tweet.ID) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("tweets") - .appendingPathComponent(statusID) - } - - public static func delete( - session: URLSession, - statusID: Twitter.Entity.V2.Tweet.ID, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: deleteEndpointURL(statusID: statusID), - method: .DELETE, - query: nil, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: DeleteContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct DeleteContent: Codable { - public let data: ContentData - - public struct ContentData: Codable { - public let deleted: Bool - } - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Status+List.swift b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Status+List.swift deleted file mode 100644 index cd4db75b..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Status+List.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// Twitter+API+V2+Status+List.swift -// -// -// Created by MainasuK on 2022-3-2. -// - -import Foundation - -extension Twitter.API.V2.Status { - public enum List { } -} - -// List Tweets Lookup -// https://developer.twitter.com/en/docs/twitter-api/lists/list-tweets/api-reference/get-lists-id-tweets -extension Twitter.API.V2.Status.List { - - static func tweetsEndpointURL(listID: Twitter.Entity.V2.List.ID) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("lists") - .appendingPathComponent(listID) - .appendingPathComponent("tweets") - } - - public static func statuses( - session: URLSession, - listID: Twitter.Entity.V2.List.ID, - query: StatusesQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: tweetsEndpointURL(listID: listID), - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: StatusesContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct StatusesQuery: Query { - public let maxResults: Int - public let nextToken: String? - - public init( - maxResults: Int = 20, - nextToken: String? - ) { - self.maxResults = min(100, max(10, maxResults)) - self.nextToken = nextToken - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [ - [Twitter.Request.Expansions.authorID].queryItem, - Twitter.Request.userFields.queryItem, - Twitter.Request.tweetsFields.queryItem, - URLQueryItem(name: "max_results", value: String(maxResults)), - ] - if let nextToken = nextToken { - let item = URLQueryItem(name: "pagination_token", value: nextToken) - items.append(item) - } - return items - } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { nil } - var body: Data? { nil } - } - - public struct StatusesContent: Codable { - public let data: [Twitter.Entity.V2.Tweet]? - public let includes: Includes? - public let meta: Meta - - public struct Includes: Codable { - public let users: [Twitter.Entity.V2.User] - } - - public struct Meta: Codable { - public let resultCount: Int - public let previousToken: String? - public let nextToken: String? - - enum CodingKeys: String, CodingKey { - case resultCount = "result_count" - case previousToken = "previous_token" - case nextToken = "next_token" - } - } - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Status+Timeline.swift b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Status+Timeline.swift deleted file mode 100644 index 61bc15f8..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Status+Timeline.swift +++ /dev/null @@ -1,244 +0,0 @@ -// -// Twitter+API+V2+Status+Timeline.swift -// -// -// Created by MainasuK on 2023/3/27. -// - -import Foundation - -extension Twitter.API.V2.Status { - public enum Timeline { } -} - -extension Twitter.API.V2.Status.Timeline { - private static func conversationEndpointURL(statusID: Twitter.Entity.V2.Tweet.ID) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("timeline") - .appendingPathComponent("conversation") - .appendingPathComponent(statusID) - .appendingPathExtension("json") - } - - public static func conversation( - session: URLSession, - statusID: Twitter.Entity.V2.Tweet.ID, - query: TimelineQuery, - authorization: Twitter.API.Guest.GuestAuthorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: conversationEndpointURL(statusID: statusID), - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: Twitter.API.V2.Status.Timeline.ConversationContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct TimelineQuery: Query { - public let cursor: String? - - public init(cursor: String?) { - self.cursor = cursor - } - - public var queryItems: [URLQueryItem]? { nil } - public var encodedQueryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - cursor.flatMap { items.append(URLQueryItem(name: "cursor", value: $0.urlEncoded)) } - guard !items.isEmpty else { return nil } - return items - } - public var formQueryItems: [URLQueryItem]? { nil } - public var contentType: String? { nil } - public var body: Data? { nil } - } - - public struct ConversationContent: Decodable { - public let globalObjects: GlobalObjects - public let timeline: Timeline - - public struct GlobalObjects: Codable { - public let tweets: [Twitter.Entity.Internal.Tweet] - public let users: [Twitter.Entity.User] - - public init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - tweets = try { - let dict = try values.decode([String: Twitter.Entity.Internal.Tweet].self, forKey: .tweets) - return Array(dict.values) - }() - users = try { - let dict = try values.decode([String: Twitter.Entity.User].self, forKey: .users) - return Array(dict.values) - }() - } - } - - public struct Timeline: Decodable { - public let id: String - public let entries: [Entry] - public let topCursor: String? - public let bottomCursor: String? - - public enum Entry { - case tweet(statusID: Twitter.Entity.Internal.Tweet.ID) - case conversationThread(components: [Twitter.Entity.Internal.Tweet.ID]) - } - - enum CodingKeys: String, CodingKey { - case id - case instructions - } - - enum InstructionKeys: String, CodingKey { - case clearCache - case addEntries - } - - enum EntryContainerKeys: String, CodingKey { - case entries - } - - enum EntryKeys: String, CodingKey { - case content - } - - enum EntryItemKeys: String, CodingKey { - case item - } - - enum EntryOprationKeys: String, CodingKey { - case item - } - - enum TweetKeys: String, CodingKey { - case id - } - - enum CursorKeys: String, CodingKey { - case value - case cursorType - } - - public init(from decoder: Decoder) throws { - // -> timeline - let values = try decoder.container(keyedBy: CodingKeys.self) - // -> timeline.id - self.id = try values.decode(String.self, forKey: .id) - - var entriesResult: [Entry] = [] - var topCursor: String? - var bottomCursor: String? - - // -> timeline.instructions[] - var container = try values.nestedUnkeyedContainer(forKey: .instructions) - while !container.isAtEnd { - // -> timeline.instructions[index] - let instructionContainer = try container.nestedContainer(keyedBy: InstructionKeys.self) - // -> timeline.instructions[index].addEntries - guard let entryContainer = try? instructionContainer.nestedContainer(keyedBy: EntryContainerKeys.self, forKey: .addEntries) else { - continue - } - - // -> timeline.instructions[index] { .addEntries } - var entries = try entryContainer.nestedUnkeyedContainer(forKey: .entries) - while !entries.isAtEnd { - // -> timeline.instructions[index] { .addEntries.entries } - let entry = try entries.nestedContainer(keyedBy: EntryKeys.self) - - if let content = try? entry.decode(ItemTweetEntry.self, forKey: .content) { - entriesResult.append(.tweet(statusID: content.item.content.tweet.id)) - } else if let content = try? entry.decode(ItemConversationThreadEntry.self, forKey: .content) { - var components: [Twitter.Entity.V2.Tweet.ID] = [] - for component in content.item.content.conversationThread.conversationComponents { - let id = component.conversationTweetComponent.tweet.id - components.append(id) - } - entriesResult.append(.conversationThread(components: components)) - } else if let content = try? entry.decode(OperationEntry.self, forKey: .content) { - switch content.operation.cursor.cursorType.lowercased() { - case "top": - topCursor = content.operation.cursor.value - case "bottom": - bottomCursor = content.operation.cursor.value - case "ShowMoreThreads".lowercased(): - bottomCursor = content.operation.cursor.value - case "ShowMoreThreadsPrompt".lowercased(): - bottomCursor = content.operation.cursor.value - default: - assertionFailure() - continue - } - } else { - continue - } - } - } - - self.entries = entriesResult - self.topCursor = topCursor - self.bottomCursor = bottomCursor - } // end init - } - - struct ItemTweetEntry: Codable { - let item: Item - - struct Item: Codable { - let content: Content - - struct Content: Codable { - let tweet: Tweet - - struct Tweet: Codable { - let id: String - } - } - } - } // end ItemTweetEntry - - struct ItemConversationThreadEntry: Codable { - let item: Item - - struct Item: Codable { - let content: Content - - struct Content: Codable { - let conversationThread: ConversationThread - - struct ConversationThread: Codable { - let conversationComponents: [ConversationComponent] - - struct ConversationComponent: Codable { - let conversationTweetComponent: ConversationTweetComponent - } - - struct ConversationTweetComponent: Codable { - let tweet: Tweet - - struct Tweet: Codable { - let id: String - } - } - } - } - } - } // end ItemConversationThreadEntry - - struct OperationEntry: Codable { - let operation: Operation - - struct Operation: Codable { - let cursor: Cursor - - struct Cursor: Codable { - let value: String - let cursorType: String - } - } - } // end OperationEntry - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Status.swift b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Status.swift deleted file mode 100644 index 72803391..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+Status.swift +++ /dev/null @@ -1,147 +0,0 @@ -// -// Twitter+API+V2+Status.swift -// -// -// Created by MainasuK on 2022-4-21. -// - -import Foundation - -extension Twitter.API.V2 { - public enum Status { } -} - -// doc: https://developer.twitter.com/en/docs/twitter-api/tweets/manage-tweets/api-reference/post-tweets -extension Twitter.API.V2.Status { - - private static var tweetEndpointURL: URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("tweets") - } - - public static func publish( - session: URLSession, - query: PublishQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: tweetEndpointURL, - method: .POST, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: PublishContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct PublishQuery: JSONEncodeQuery { - public let forSuperFollowersOnly: Bool? - public let geo: Twitter.Entity.V2.Tweet.Geo? - public let media: Media? - public let poll: Poll? - public let quoteTweetID: Twitter.Entity.V2.Tweet.ID? - public let reply: Reply? - public let replySettings: Twitter.Entity.V2.Tweet.ReplySettings? - public let text: String? - - enum CodingKeys: String, CodingKey { - case forSuperFollowersOnly = "for_super_followers_only" - case geo - case media - case poll - case reply - case quoteTweetID = "quote_tweet_id" - case replySettings = "reply_settings" - case text - } - - public init( - forSuperFollowersOnly: Bool?, - geo: Twitter.Entity.V2.Tweet.Geo?, - media: Media?, - poll: Poll?, - reply: Reply?, - quoteTweetID: Twitter.Entity.V2.Tweet.ID?, - replySettings: Twitter.Entity.V2.Tweet.ReplySettings?, - text: String? - ) { - self.forSuperFollowersOnly = forSuperFollowersOnly - self.geo = geo - self.media = media - self.poll = poll - self.reply = reply - self.quoteTweetID = quoteTweetID - self.text = text - self.replySettings = { - switch replySettings { - case .everyone: - return nil - default: - return replySettings - } - }() - } - - var queryItems: [URLQueryItem]? { nil } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - } - - public struct PublishContent: Codable { - public let data: ContentData - - public struct ContentData: Codable { - public let id: String - public let text: String - } - } - -} - -extension Twitter.API.V2.Status { - public struct Media: Codable { - public let mediaIDs: [Twitter.Entity.V2.Media.ID]? - - enum CodingKeys: String, CodingKey { - case mediaIDs = "media_ids" - } - - public init(mediaIDs: [Twitter.Entity.V2.Media.ID]?) { - self.mediaIDs = mediaIDs - } - } - - public struct Poll: Codable { - public let options: [String] - public let durationMinutes: Int - - enum CodingKeys: String, CodingKey { - case options - case durationMinutes = "duration_minutes" - } - - public init(options: [String], durationMinutes: Int) { - self.options = options - self.durationMinutes = durationMinutes - } - } - - public struct Reply: Codable { - public let excludeReplyUserIDs: [Twitter.Entity.V2.User.ID]? - public let inReplyToTweetID: Twitter.Entity.V2.Tweet.ID? - - enum CodingKeys: String, CodingKey { - case excludeReplyUserIDs = "exclude_reply_user_ids" - case inReplyToTweetID = "in_reply_to_tweet_id" - } - - public init( - excludeReplyUserIDs: [Twitter.Entity.V2.User.ID]?, - inReplyToTweetID: Twitter.Entity.V2.Tweet.ID? - ) { - self.excludeReplyUserIDs = excludeReplyUserIDs - self.inReplyToTweetID = inReplyToTweetID - } - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Block.swift b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Block.swift deleted file mode 100644 index 4c9b97b3..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Block.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// Twitter+API+V2+User+Block.swift -// -// -// Created by Cirno MainasuK on 2021-10-21. -// - -import Foundation - -extension Twitter.API.V2.User { - public enum Block { } -} - -// doc: https://developer.twitter.com/en/docs/twitter-api/users/blocks/introduction -extension Twitter.API.V2.User.Block { - - // https://developer.twitter.com/en/docs/twitter-api/users/blocks/api-reference/post-users-user_id-blocking - static func blockEndpointURL(sourceUserID: Twitter.Entity.User.ID) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("users") - .appendingPathComponent(sourceUserID) - .appendingPathComponent("blocking") - } - - public static func block( - session: URLSession, - sourceUserID: Twitter.Entity.User.ID, - query: BlockQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: blockEndpointURL(sourceUserID: sourceUserID), - method: .POST, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: BlockContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct BlockQuery: JSONEncodeQuery { - - public let targetUserID: Twitter.Entity.User.ID - - public init(targetUserID: Twitter.Entity.User.ID) { - self.targetUserID = targetUserID - } - - enum CodingKeys: String, CodingKey { - case targetUserID = "target_user_id" - } - - var queryItems: [URLQueryItem]? { nil } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - } - - public struct BlockContent: Codable { - public let data: ContentData - - public struct ContentData: Codable { - public let blocking: Bool - } - } - -} - -extension Twitter.API.V2.User.Block { - - // https://developer.twitter.com/en/docs/twitter-api/users/blocks/api-reference/delete-users-user_id-blocking - static func unblockEndpointURL( - sourceUserID: Twitter.Entity.User.ID, - targetUserID: Twitter.Entity.User.ID - ) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("users") - .appendingPathComponent(sourceUserID) - .appendingPathComponent("blocking") - .appendingPathComponent(targetUserID) - } - - public static func unblock( - session: URLSession, - sourceUserID: Twitter.Entity.User.ID, - targetUserID: Twitter.Entity.User.ID, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let url = unblockEndpointURL(sourceUserID: sourceUserID, targetUserID: targetUserID) - let request = Twitter.API.request( - url: url, - method: .DELETE, - query: nil, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: BlockContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Follow.swift b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Follow.swift deleted file mode 100644 index 0d8c1680..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Follow.swift +++ /dev/null @@ -1,218 +0,0 @@ -// -// Twitter+API+V2+User+Follow.swift -// -// -// Created by Cirno MainasuK on 2021-10-19. -// - -import Foundation - -extension Twitter.API.V2.User { - public enum Follow { } -} - -// Request follow user -// https://developer.twitter.com/en/docs/twitter-api/users/follows/introduction -extension Twitter.API.V2.User.Follow { - - static func followEndpointURL( - sourceUserID: Twitter.Entity.V2.User.ID - ) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("users") - .appendingPathComponent(sourceUserID) - .appendingPathComponent("following") - } - - public static func follow( - session: URLSession, - sourceUserID: Twitter.Entity.V2.User.ID, - targetUserID: Twitter.Entity.V2.User.ID, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let query = FollowQuery(targetUserID: targetUserID) - let request = Twitter.API.request( - url: followEndpointURL(sourceUserID: sourceUserID), - method: .POST, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: FollowContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct FollowQuery: JSONEncodeQuery { - public let targetUserID: Twitter.Entity.V2.User.ID - - enum CodingKeys: String, CodingKey { - case targetUserID = "target_user_id" - } - - public init(targetUserID: Twitter.Entity.V2.User.ID) { - self.targetUserID = targetUserID - } - - var queryItems: [URLQueryItem]? { nil } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - } - - public struct FollowContent: Codable { - public let data: ContentData - - public struct ContentData: Codable { - public let following: Bool - public let pendingFollow: Bool? - - enum CodingKeys: String, CodingKey { - case following - case pendingFollow = "pending_follow" - } - - } - } - -} - -// Cancel follow user -extension Twitter.API.V2.User.Follow { - - static func undoFollowEndpointURL( - sourceUserID: Twitter.Entity.V2.User.ID, - targetUserID: Twitter.Entity.V2.User.ID - ) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("users") - .appendingPathComponent(sourceUserID) - .appendingPathComponent("following") - .appendingPathComponent(targetUserID) - } - - public static func undoFollow( - session: URLSession, - sourceUserID: Twitter.Entity.V2.User.ID, - targetUserID: Twitter.Entity.V2.User.ID, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let url = undoFollowEndpointURL(sourceUserID: sourceUserID, targetUserID: targetUserID) - let request = Twitter.API.request( - url: url, - method: .DELETE, - query: nil, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: FollowContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - -} - -extension Twitter.API.V2.User.Follow { - public struct FriendshipListQuery: Query { - public let userID: Twitter.Entity.V2.User.ID - public let maxResults: Int - public let paginationToken: String? - - public init(userID: Twitter.Entity.V2.User.ID, maxResults: Int, paginationToken: String?) { - self.userID = userID - self.maxResults = min(1000, max(10, maxResults)) - self.paginationToken = paginationToken - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [] - items.append(Twitter.Request.tweetsFields.queryItem) - items.append(Twitter.Request.userFields.queryItem) - items.append(URLQueryItem(name: "max_results", value: String(maxResults))) - paginationToken.flatMap { - items.append(URLQueryItem(name: "pagination_token", value: $0)) - } - guard !items.isEmpty else { return nil } - return items - } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { nil } - var body: Data? { nil } - } - - public struct FriendshipListContent: Codable { - public let data: [Twitter.Entity.V2.User]? - public let includes: Include? - public let errors: [Twitter.Response.V2.ContentError]? - public let meta: Meta - - public struct Include: Codable { - public let tweets: [Twitter.Entity.V2.Tweet]? - } - - public struct Meta: Codable { - public let resultCount: Int - public let nextToken: String? - - public enum CodingKeys: String, CodingKey { - case resultCount = "result_count" - case nextToken = "next_token" - } - } - } -} - -// Returns a list of users the specified userID following -// https://developer.twitter.com/en/docs/twitter-api/users/follows/api-reference/get-users-id-following -extension Twitter.API.V2.User.Follow { - - static func followingEndpointURL(userID: Twitter.Entity.V2.User.ID) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("users") - .appendingPathComponent(userID) - .appendingPathComponent("following") - } - - public static func followingList( - session: URLSession, - query: Twitter.API.V2.User.Follow.FriendshipListQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: followingEndpointURL(userID: query.userID), - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: Twitter.API.V2.User.Follow.FriendshipListContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - -} - -// Returns a list of users who are followers of the specified user ID. -// https://developer.twitter.com/en/docs/twitter-api/users/follows/api-reference/get-users-id-followers -extension Twitter.API.V2.User.Follow { - - static func followerListEndpointURL(userID: Twitter.Entity.V2.User.ID) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("users") - .appendingPathComponent(userID) - .appendingPathComponent("followers") - } - - public static func followers( - session: URLSession, - query: Twitter.API.V2.User.Follow.FriendshipListQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: followerListEndpointURL(userID: query.userID), - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: Twitter.API.V2.User.Follow.FriendshipListContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Like.swift b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Like.swift deleted file mode 100644 index 23c3ce2a..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Like.swift +++ /dev/null @@ -1,99 +0,0 @@ -// -// Twitter+API+V2+User+Like.swift -// Twitter+API+V2+User+Like -// -// Created by Cirno MainasuK on 2021-9-8. -// - -import Foundation - -extension Twitter.API.V2.User { - public enum Like { } -} - -// https://developer.twitter.com/en/docs/twitter-api/tweets/likes/api-reference/post-users-id-likes -extension Twitter.API.V2.User.Like { - - static func likeEndpointURL( - userID: Twitter.Entity.V2.User.ID - ) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("users") - .appendingPathComponent(userID) - .appendingPathComponent("likes") - } - - public static func like( - session: URLSession, - query: LikeQuery, - userID: Twitter.Entity.V2.User.ID, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: likeEndpointURL(userID: userID), - method: .POST, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: LikeContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct LikeQuery: JSONEncodeQuery { - public let tweetID: Twitter.Entity.V2.Tweet.ID - - enum CodingKeys: String, CodingKey { - case tweetID = "tweet_id" - } - - public init(tweetID: Twitter.Entity.V2.Tweet.ID) { - self.tweetID = tweetID - } - - var queryItems: [URLQueryItem]? { nil } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - } - - public struct LikeContent: Codable { - public let data: ContentData - - public struct ContentData: Codable { - public let liked: Bool - } - } - -} - -extension Twitter.API.V2.User.Like { - - static func undoLikeEndpointURL( - userID: Twitter.Entity.V2.User.ID, - statusID: Twitter.Entity.V2.Tweet.ID - ) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("users") - .appendingPathComponent(userID) - .appendingPathComponent("likes") - .appendingPathComponent(statusID) - } - - public static func undoLike( - session: URLSession, - userID: Twitter.Entity.V2.User.ID, - statusID: Twitter.Entity.V2.Tweet.ID, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: undoLikeEndpointURL(userID: userID, statusID: statusID), - method: .DELETE, - query: nil, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: LikeContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+List.swift b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+List.swift deleted file mode 100644 index 7007a6c0..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+List.swift +++ /dev/null @@ -1,243 +0,0 @@ -// -// Twitter+API+Users+List.swift -// -// -// Created by MainasuK on 2022-2-28. -// - -import Foundation - -extension Twitter.API.V2.User { - public enum List { } -} - -// https://developer.twitter.com/en/docs/twitter-api/lists/list-lookup/api-reference/get-users-id-owned_lists -extension Twitter.API.V2.User.List { - - private static func ownedListEndpointURL(userID: Twitter.Entity.V2.User.ID) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("users") - .appendingPathComponent(userID) - .appendingPathComponent("owned_lists") - } - - public static func onwedLists( - session: URLSession, - userID: Twitter.Entity.V2.User.ID, - query: OwnedListsQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: ownedListEndpointURL(userID: userID), - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: OwnedListsContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct OwnedListsQuery: Query { - public let maxResults: Int - public let nextToken: String? - - public init( - maxResults: Int = 20, - nextToken: String? - ) { - self.maxResults = min(100, max(10, maxResults)) - self.nextToken = nextToken - } - - var queryItems: [URLQueryItem]? { - var items: [URLQueryItem] = [ - [Twitter.Request.Expansions.ownerID].queryItem, - Twitter.Request.userFields.queryItem, - Twitter.Request.listFields.queryItem, - URLQueryItem(name: "max_results", value: String(maxResults)), - ] - if let nextToken = nextToken { - let item = URLQueryItem(name: "pagination_token", value: nextToken) - items.append(item) - } - return items - } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { nil } - var body: Data? { nil } - } - - public struct OwnedListsContent: Codable { - public let data: [Twitter.Entity.V2.List]? - public let includes: Includes? - public let meta: Meta - - public struct Includes: Codable { - public let users: [Twitter.Entity.V2.User] - } - - public struct Meta: Codable { - public let resultCount: Int - public let previousToken: String? - public let nextToken: String? - - enum CodingKeys: String, CodingKey { - case resultCount = "result_count" - case previousToken = "previous_token" - case nextToken = "next_token" - } - } - } - -} - -// https://developer.twitter.com/en/docs/twitter-api/lists/list-follows/api-reference/get-users-id-followed_lists -extension Twitter.API.V2.User.List { - - private static func followedListEndpointURL(userID: Twitter.Entity.V2.User.ID) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("users") - .appendingPathComponent(userID) - .appendingPathComponent("followed_lists") - } - - public static func followedLists( - session: URLSession, - userID: Twitter.Entity.V2.User.ID, - query: FollowedListsQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: followedListEndpointURL(userID: userID), - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: FollowedListsContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public typealias FollowedListsQuery = OwnedListsQuery - public typealias FollowedListsContent = OwnedListsContent - -} - -// https://developer.twitter.com/en/docs/twitter-api/lists/list-members/api-reference/get-users-id-list_memberships -extension Twitter.API.V2.User.List { - - private static func membershipsEndpointURL(userID: Twitter.Entity.V2.User.ID) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("users") - .appendingPathComponent(userID) - .appendingPathComponent("list_memberships") - } - - public static func listMemberships( - session: URLSession, - userID: Twitter.Entity.V2.User.ID, - query: ListMembershipsQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: membershipsEndpointURL(userID: userID), - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: ListMembershipsContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public typealias ListMembershipsQuery = OwnedListsQuery - public typealias ListMembershipsContent = OwnedListsContent - -} - -// https://developer.twitter.com/en/docs/twitter-api/lists/list-follows/api-reference/post-users-id-followed-lists -// https://developer.twitter.com/en/docs/twitter-api/lists/list-follows/api-reference/delete-users-id-followed-lists-list_id -extension Twitter.API.V2.User.List { - - private static func followEndpointURL(userID: Twitter.Entity.V2.User.ID) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("users") - .appendingPathComponent(userID) - .appendingPathComponent("followed_lists") - } - - private static func unfollowEndpointURL( - userID: Twitter.Entity.V2.User.ID, - listID: Twitter.Entity.V2.List.ID - ) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("users") - .appendingPathComponent(userID) - .appendingPathComponent("followed_lists") - .appendingPathComponent(listID) - } - - public static func follow( - session: URLSession, - userID: Twitter.Entity.V2.User.ID, - query: FollowQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: followEndpointURL(userID: userID), - method: .POST, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: FollowContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public static func unfollow( - session: URLSession, - userID: Twitter.Entity.V2.User.ID, - listID: Twitter.Entity.V2.List.ID, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: unfollowEndpointURL(userID: userID, listID: listID), - method: .DELETE, - query: nil, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: FollowContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct FollowQuery: JSONEncodeQuery { - - public let id: Twitter.Entity.V2.List.ID - - enum CodingKeys: String, CodingKey { - case id = "list_id" - } - - public init( - id: Twitter.Entity.V2.List.ID - ) { - self.id = id - } - - var queryItems: [URLQueryItem]? { nil } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - } - - public struct FollowContent: Codable { - public let data: ContentData - - public struct ContentData: Codable { - public let following: Bool - } - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Lookup.swift b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Lookup.swift deleted file mode 100644 index 63d734e2..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Lookup.swift +++ /dev/null @@ -1,257 +0,0 @@ -// -// Twitter+API+V2+UserLookup.swift -// -// -// Created by Cirno MainasuK on 2020-11-27. -// - -import Foundation -import Combine - -extension Twitter.API.V2.User { - public enum Lookup { } -} - -extension Twitter.API.V2.User.Lookup { - - private static let usersEndpointURL = Twitter.API.endpointV2URL.appendingPathComponent("users") - - public static func users( - session: URLSession, - userIDs: [Twitter.Entity.User.ID], - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let query = UserIDLookupQuery(userIDs: userIDs) - let request = Twitter.API.request( - url: usersEndpointURL, - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: Twitter.API.V2.User.Lookup.Content.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct UserIDLookupQuery: Query { - public let userIDs: [Twitter.Entity.V2.User.ID] - - public init(userIDs: [Twitter.Entity.V2.User.ID]) { - self.userIDs = userIDs - } - - var queryItems: [URLQueryItem]? { - let userIDs = userIDs.joined(separator: ",") - let expansions: [Twitter.Request.Expansions] = [.pinnedTweetID] - let tweetsFields: [Twitter.Request.TwitterFields] = [ - .attachments, - .authorID, - .contextAnnotations, - .conversationID, - .created_at, - .entities, - .geo, - .id, - .inReplyToUserID, - .lang, - .publicMetrics, - .possiblySensitive, - .referencedTweets, - .source, - .text, - .withheld, - ] - let userFields: [Twitter.Request.UserFields] = [ - .createdAt, - .description, - .entities, - .id, - .location, - .name, - .pinnedTweetID, - .profileImageURL, - .protected, - .publicMetrics, - .url, - .username, - .verified, - .withheld - ] - return [ - expansions.queryItem, - tweetsFields.queryItem, - userFields.queryItem, - URLQueryItem(name: "ids", value: userIDs), - ] - } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { nil } - var body: Data? { nil } - } - -} - -extension Twitter.API.V2.User.Lookup { - - private static let usersByEndpointURL = Twitter.API.endpointV2URL.appendingPathComponent("users/by") - - public static func users( - session: URLSession, - usernames: [String], - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let query = UsernameLookupQuery(usernames: usernames) - let request = Twitter.API.request( - url: usersByEndpointURL, - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: Twitter.API.V2.User.Lookup.Content.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct UsernameLookupQuery: Query { - public let usernames: [String] - - public init(usernames: [String]) { - self.usernames = usernames - } - - var queryItems: [URLQueryItem]? { - let usernames = usernames.joined(separator: ",") - let expansions: [Twitter.Request.Expansions] = [.pinnedTweetID] - let tweetsFields: [Twitter.Request.TwitterFields] = [ - .attachments, - .authorID, - .contextAnnotations, - .conversationID, - .created_at, - .entities, - .geo, - .id, - .inReplyToUserID, - .lang, - .publicMetrics, - .possiblySensitive, - .referencedTweets, - .source, - .text, - .withheld, - ] - let userFields: [Twitter.Request.UserFields] = [ - .createdAt, - .description, - .entities, - .id, - .location, - .name, - .pinnedTweetID, - .profileImageURL, - .protected, - .publicMetrics, - .url, - .username, - .verified, - .withheld - ] - return [ - expansions.queryItem, - tweetsFields.queryItem, - userFields.queryItem, - URLQueryItem(name: "usernames", value: usernames), - ] - } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { nil } - var body: Data? { nil } - } - -} - -// https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me -extension Twitter.API.V2.User.Lookup { - - private static let meEndpointURL = Twitter.API.endpointV2URL.appendingPathComponent("users/me") - - public static func me( - session: URLSession, - authorization: Twitter.API.V2.OAuth2.Authorization - ) async throws -> Twitter.Response.Content { - let query = MeLookupQuery() - let request = Twitter.API.request( - url: meEndpointURL, - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: MeLookupContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct MeLookupQuery: Query { - - var queryItems: [URLQueryItem]? { - let expansions: [Twitter.Request.Expansions] = [.pinnedTweetID] - let tweetsFields: [Twitter.Request.TwitterFields] = [ - .attachments, - .authorID, - .contextAnnotations, - .conversationID, - .created_at, - .entities, - .geo, - .id, - .inReplyToUserID, - .lang, - .publicMetrics, - .possiblySensitive, - .referencedTweets, - .source, - .text, - .withheld, - ] - let userFields: [Twitter.Request.UserFields] = [ - .createdAt, - .description, - .entities, - .id, - .location, - .name, - .pinnedTweetID, - .profileImageURL, - .protected, - .publicMetrics, - .url, - .username, - .verified, - .withheld - ] - return [ - expansions.queryItem, - tweetsFields.queryItem, - userFields.queryItem, - ] - } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { nil } - var body: Data? { nil } - } - - public struct MeLookupContent: Codable { - public let data: Twitter.Entity.V2.User - } - -} - -extension Twitter.API.V2.User.Lookup { - public struct Content: Codable { - public let data: [Twitter.Entity.V2.User]? - public let errors: [Twitter.Response.V2.ContentError]? - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Mute.swift b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Mute.swift deleted file mode 100644 index 98e0d370..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Mute.swift +++ /dev/null @@ -1,99 +0,0 @@ -// -// Twitter+API+V2+User+Mute.swift -// -// -// Created by MainasuK on 2021-12-6. -// - -import Foundation - -extension Twitter.API.V2.User { - public enum Mute { } -} - -// doc: https://developer.twitter.com/en/docs/twitter-api/users/mutes/introduction -extension Twitter.API.V2.User.Mute { - - static func muteEndpointURL(sourceUserID: Twitter.Entity.User.ID) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("users") - .appendingPathComponent(sourceUserID) - .appendingPathComponent("muting") - } - - public static func mute( - session: URLSession, - sourceUserID: Twitter.Entity.User.ID, - query: MuteQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: muteEndpointURL(sourceUserID: sourceUserID), - method: .POST, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: MuteContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct MuteQuery: JSONEncodeQuery { - - public let targetUserID: Twitter.Entity.User.ID - - public init(targetUserID: Twitter.Entity.User.ID) { - self.targetUserID = targetUserID - } - - enum CodingKeys: String, CodingKey { - case targetUserID = "target_user_id" - } - - var queryItems: [URLQueryItem]? { nil } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - } - - public struct MuteContent: Codable { - public let data: ContentData - - public struct ContentData: Codable { - public let muting: Bool - } - } - -} - -extension Twitter.API.V2.User.Mute { - - static func unmuteEndpointURL( - sourceUserID: Twitter.Entity.User.ID, - targetUserID: Twitter.Entity.User.ID - ) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("users") - .appendingPathComponent(sourceUserID) - .appendingPathComponent("muting") - .appendingPathComponent(targetUserID) - } - - public static func unmute( - session: URLSession, - sourceUserID: Twitter.Entity.User.ID, - targetUserID: Twitter.Entity.User.ID, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let url = unmuteEndpointURL(sourceUserID: sourceUserID, targetUserID: targetUserID) - let request = Twitter.API.request( - url: url, - method: .DELETE, - query: nil, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: MuteContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Retweet.swift b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Retweet.swift deleted file mode 100644 index 88e50dc4..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Retweet.swift +++ /dev/null @@ -1,98 +0,0 @@ -// -// Twitter+API+V2+User+Retweet.swift -// Twitter+API+V2+User+Retweet -// -// Created by Cirno MainasuK on 2021-9-8. -// - -import Foundation - -extension Twitter.API.V2.User { - public enum Retweet { } -} - -extension Twitter.API.V2.User.Retweet { - - static func retweetEndpointURL( - userID: Twitter.Entity.V2.User.ID - ) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("users") - .appendingPathComponent(userID) - .appendingPathComponent("retweets") - } - - public static func retweet( - session: URLSession, - query: RetweetQuery, - userID: Twitter.Entity.V2.User.ID, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: retweetEndpointURL(userID: userID), - method: .POST, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: RetweetContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - public struct RetweetQuery: JSONEncodeQuery { - public let tweetID: Twitter.Entity.V2.Tweet.ID - - enum CodingKeys: String, CodingKey { - case tweetID = "tweet_id" - } - - public init(tweetID: Twitter.Entity.V2.Tweet.ID) { - self.tweetID = tweetID - } - - var queryItems: [URLQueryItem]? { nil } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - } - - public struct RetweetContent: Codable { - public let data: ContentData - - public struct ContentData: Codable { - public let retweeted: Bool - } - } - -} - -extension Twitter.API.V2.User.Retweet { - - static func undoRetweetEndpointURL( - userID: Twitter.Entity.V2.User.ID, - statusID: Twitter.Entity.V2.Tweet.ID - ) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("users") - .appendingPathComponent(userID) - .appendingPathComponent("retweets") - .appendingPathComponent(statusID) - } - - public static func undoRetweet( - session: URLSession, - userID: Twitter.Entity.V2.User.ID, - statusID: Twitter.Entity.V2.Tweet.ID, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: undoRetweetEndpointURL(userID: userID, statusID: statusID), - method: .DELETE, - query: nil, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: RetweetContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Timeline.swift b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Timeline.swift deleted file mode 100644 index d76993d6..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User+Timeline.swift +++ /dev/null @@ -1,211 +0,0 @@ -// -// Twitter+API+V2+User+Timeline.swift -// -// -// Created by MainasuK on 2022-6-6. -// - -import Foundation - -extension Twitter.API.V2.User { - public enum Timeline { } -} - -// Home -// https://developer.twitter.com/en/docs/twitter-api/tweets/timelines/api-reference/get-users-id-reverse-chronological -extension Twitter.API.V2.User.Timeline { - - private static func homeTimelineEndpointURL(userID: Twitter.Entity.V2.User.ID) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("users") - .appendingPathComponent(userID) - .appendingPathComponent("timelines") - .appendingPathComponent("reverse_chronological") - } - - public static func home( - session: URLSession, - userID: Twitter.Entity.V2.User.ID, - query: HomeQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: homeTimelineEndpointURL(userID: userID), - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: HomeContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - static var homeQueryExpansions: [Twitter.Request.Expansions] { - return [ - .attachmentsPollIDs, - .attachmentsMediaKeys, - .authorID, - .entitiesMentionsUsername, - .geoPlaceID, - .inReplyToUserID, - .referencedTweetsID, - .referencedTweetsIDAuthorID - ] - } - - public struct HomeQuery: Query { - - public let sinceID: Twitter.Entity.V2.Tweet.ID? - public let untilID: Twitter.Entity.V2.Tweet.ID? - public let paginationToken: String? - public let maxResults: Int? - - public init( - sinceID: Twitter.Entity.V2.Tweet.ID?, - untilID: Twitter.Entity.V2.Tweet.ID?, - paginationToken: String?, - maxResults: Int? - ) { - self.sinceID = sinceID - self.untilID = untilID - self.paginationToken = paginationToken - self.maxResults = maxResults - } - - var queryItems: [URLQueryItem]? { - var queryItems: [URLQueryItem] = [ - Twitter.API.V2.User.Timeline.homeQueryExpansions.queryItem, - Twitter.Request.tweetsFields.queryItem, - Twitter.Request.userFields.queryItem, - Twitter.Request.mediaFields.queryItem, - Twitter.Request.placeFields.queryItem, - Twitter.Request.pollFields.queryItem, - ] - sinceID.flatMap { - queryItems.append(URLQueryItem(name: "since_id", value: $0)) - } - untilID.flatMap { - queryItems.append(URLQueryItem(name: "until_id", value: $0)) - } - paginationToken.flatMap { - queryItems.append(URLQueryItem(name: "pagination_token", value: $0)) - } - maxResults.flatMap { - queryItems.append(URLQueryItem(name: "max_results", value: String($0))) - } - guard !queryItems.isEmpty else { return nil } - return queryItems - } - var encodedQueryItems: [URLQueryItem]? { nil } - var formQueryItems: [URLQueryItem]? { nil } - var contentType: String? { nil } - var body: Data? { nil } - } - - public struct HomeContent: Codable { - public let data: [Twitter.Entity.V2.Tweet]? - public let includes: Includes? - public let meta: Meta - - public struct Includes: Codable { - public let tweets: [Twitter.Entity.V2.Tweet]? - public let users: [Twitter.Entity.V2.User]? - public let media: [Twitter.Entity.V2.Media]? - public let places: [Twitter.Entity.V2.Place]? - public let polls: [Twitter.Entity.V2.Tweet.Poll]? - } - - public struct Meta: Codable { - public let resultCount: Int - public let newestID: Twitter.Entity.V2.Tweet.ID? - public let oldestID: Twitter.Entity.V2.Tweet.ID? - public let previousToken: String? - public let nextToken: String? - - enum CodingKeys: String, CodingKey { - case resultCount = "result_count" - case newestID = "newest_id" - case oldestID = "oldest_id" - case previousToken = "previous_token" - case nextToken = "next_token" - } - } - } - -} - -// Tweets -// https://developer.twitter.com/en/docs/twitter-api/tweets/timelines/api-reference/get-users-id-tweets -extension Twitter.API.V2.User.Timeline { - - private static func tweetsTimelineEndpointURL(userID: Twitter.Entity.V2.User.ID) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("users") - .appendingPathComponent(userID) - .appendingPathComponent("tweets") - } - - public static func tweets( - session: URLSession, - userID: Twitter.Entity.V2.User.ID, - query: TweetsQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: tweetsTimelineEndpointURL(userID: userID), - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: HomeContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - static var tweetsQueryExpansions: [Twitter.Request.Expansions] { - return homeQueryExpansions - } - - public typealias TweetsQuery = HomeQuery - - public typealias TweetsContent = HomeContent - -} - -// Likes -// https://developer.twitter.com/en/docs/twitter-api/tweets/likes/api-reference/get-users-id-liked_tweets -extension Twitter.API.V2.User.Timeline { - - private static func likedTweetsTimelineEndpointURL(userID: Twitter.Entity.V2.User.ID) -> URL { - return Twitter.API.endpointV2URL - .appendingPathComponent("users") - .appendingPathComponent(userID) - .appendingPathComponent("liked_tweets") - } - - public static func likes( - session: URLSession, - userID: Twitter.Entity.V2.User.ID, - query: TweetsQuery, - authorization: Twitter.API.OAuth.Authorization - ) async throws -> Twitter.Response.Content { - let request = Twitter.API.request( - url: likedTweetsTimelineEndpointURL(userID: userID), - method: .GET, - query: query, - authorization: authorization - ) - let (data, response) = try await session.data(for: request, delegate: nil) - let value = try Twitter.API.decode(type: HomeContent.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - - static var likesQueryExpansions: [Twitter.Request.Expansions] { - return homeQueryExpansions - } - - public typealias LikesQuery = HomeQuery - - public typealias LikesContent = HomeContent - -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User.swift b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User.swift deleted file mode 100644 index fcf9b7b6..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2+User.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// Twitter+API+V2+User.swift -// -// -// Created by MainasuK on 2022-4-21. -// - -import Foundation - -extension Twitter.API.V2 { - public enum User { } -} diff --git a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2.swift b/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2.swift deleted file mode 100644 index 48d824d1..00000000 --- a/TwidereSDK/Sources/TwitterSDK/API/V2/Twitter+API+V2.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// Twitter+API+V2.swift -// -// -// Created by MainasuK on 2022-4-21. -// - -import Foundation - -extension Twitter.API { - public enum V2 { } -} diff --git a/TwidereSDK/Sources/TwitterSDK/AuthorizationContext/Twitter+AuthorizationContext+OAuth.swift b/TwidereSDK/Sources/TwitterSDK/AuthorizationContext/Twitter+AuthorizationContext+OAuth.swift deleted file mode 100644 index c7c56c4c..00000000 --- a/TwidereSDK/Sources/TwitterSDK/AuthorizationContext/Twitter+AuthorizationContext+OAuth.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// Twitter+AuthorizationContext+OAuth.swift -// -// -// Created by MainasuK on 2022-4-24. -// - -import Foundation -import CryptoKit - -extension Twitter.AuthorizationContext { - public enum OAuth { - public enum Standard { } - public enum Relay { } - } -} - -extension Twitter.AuthorizationContext.OAuth { - public enum Context { - case standard(Standard.Context) - case relay(Relay.Context) - - public enum RequestTokenResponse { - case standard(Twitter.API.OAuth.RequestToken.Standard.Response) - case relay(Twitter.API.OAuth.RequestToken.Relay.Response) - } - - public func requestToken(session: URLSession) async throws -> RequestTokenResponse { - switch self { - case .standard(let context): - let response = try await Twitter.API.OAuth.RequestToken.Standard.requestToken( - session: session, - query: .init( - consumerKey: context.consumerKey, - consumerKeySecret: context.consumerKeySecret - ) - ) - return .standard(response) - case .relay(let context): - let response = try await Twitter.API.OAuth.RequestToken.Relay.requestToken( - session: session, - query: .init( - consumerKey: context.consumerKey, - hostPublicKey: context.hostPublicKey, - oauthEndpoint: context.oauthEndpoint - ) - ) - return .relay(response) - } - } - } -} - -extension Twitter.AuthorizationContext.OAuth.Standard { - public struct Context { - public let consumerKey: String - public let consumerKeySecret: String - - public init(consumerKey: String, consumerKeySecret: String) { - self.consumerKey = consumerKey - self.consumerKeySecret = consumerKeySecret - } - } -} - -extension Twitter.AuthorizationContext.OAuth.Relay { - public struct Context { - public let consumerKey: String - public let hostPublicKey: Curve25519.KeyAgreement.PublicKey - public let oauthEndpoint: String - - public init(consumerKey: String, hostPublicKey: Curve25519.KeyAgreement.PublicKey, oauthEndpoint: String) { - self.consumerKey = consumerKey - self.hostPublicKey = hostPublicKey - self.oauthEndpoint = oauthEndpoint - } - } -} - diff --git a/TwidereSDK/Sources/TwitterSDK/AuthorizationContext/Twitter+AuthorizationContext+OAuth2.swift b/TwidereSDK/Sources/TwitterSDK/AuthorizationContext/Twitter+AuthorizationContext+OAuth2.swift deleted file mode 100644 index f788f9a2..00000000 --- a/TwidereSDK/Sources/TwitterSDK/AuthorizationContext/Twitter+AuthorizationContext+OAuth2.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// Twitter+AuthorizationContext+OAuth2.swift -// -// -// Created by MainasuK on 2022-4-24. -// - -import Foundation -import CryptoKit - -extension Twitter.AuthorizationContext { - public enum OAuth2 { - public enum Relay { } - } -} - -extension Twitter.AuthorizationContext.OAuth2 { - public enum Context { - case relay(Relay.Context) - } -} - -extension Twitter.AuthorizationContext.OAuth2.Relay { - public typealias Context = Twitter.API.V2.OAuth2.Authorize.Relay.Query - public typealias Response = Twitter.API.V2.OAuth2.Authorize.Relay.Response -} - -extension Twitter.AuthorizationContext.OAuth2.Relay.Context { - public func authorize(session: URLSession) async throws -> Twitter.AuthorizationContext.OAuth2.Relay.Response { - return try await Twitter.API.V2.OAuth2.Authorize.Relay.authorize( - session: session, - query: self - ) - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/AuthorizationContext/Twitter+AuthorizationContext.swift b/TwidereSDK/Sources/TwitterSDK/AuthorizationContext/Twitter+AuthorizationContext.swift deleted file mode 100644 index b788dd11..00000000 --- a/TwidereSDK/Sources/TwitterSDK/AuthorizationContext/Twitter+AuthorizationContext.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Twitter+AuthorizationContext.swift -// -// -// Created by MainasuK on 2022-4-24. -// - -import Foundation - -extension Twitter { - public enum AuthorizationContext { - case oauth(OAuth.Context) - case oauth2(OAuth2.Context) - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity.swift b/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity.swift deleted file mode 100644 index 5a6370b5..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/Twitter+Entity.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// Twitter+Entity.swift -// TwitterAPI -// -// Created by Cirno MainasuK on 2020-9-3. -// - -import Foundation - -extension Twitter.Entity { } diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Coordinates.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Coordinates.swift deleted file mode 100644 index 271359ab..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Coordinates.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// Twitter+Entity+Coordinates.swift -// TwitterSDK -// -// Created by Cirno MainasuK on 2020-9-16. -// - -import Foundation - -extension Twitter.Entity { - public struct Coordinates: Codable { - public var type: String - public var coordinates: [Double] - } -} - -extension Twitter.Entity.Coordinates: Equatable { } diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+ExtendedEntities.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+ExtendedEntities.swift deleted file mode 100644 index a4de3f5e..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+ExtendedEntities.swift +++ /dev/null @@ -1,173 +0,0 @@ -// -// Twitter+ExtendedEntities.swift -// TwitterAPI -// -// Created by Cirno MainasuK on 2020-9-3. -// - -import Foundation -import CoreGraphics - -extension Twitter.Entity { - public struct ExtendedEntities: Codable { - public let media: [Media]? - - } -} - -extension Twitter.Entity.ExtendedEntities: Equatable { } - -extension Twitter.Entity.ExtendedEntities { - public struct Media: Codable { - public let id: Double? - public let idStr: String? - public let indices: [Int]? - public let mediaURL: String? - public let mediaURLHTTPS: String? - public let url: String? - public let displayURL: String? - public let expandedURL: String? - public let type: String? - public let sizes: Sizes? - public let videoInfo: VideoInfo? - public let sourceStatusID: Double? - public let sourceStatusIDStr: String? - public let sourceUserID: Int? - public let sourceUserIDStr: String? - public let extAltText: String? - - enum CodingKeys: String, CodingKey { - case id = "id" - case idStr = "id_str" - case indices = "indices" - case mediaURL = "media_url" - case mediaURLHTTPS = "media_url_https" - case url = "url" - case displayURL = "display_url" - case expandedURL = "expanded_url" - case type = "type" - case sizes = "sizes" - case videoInfo = "video_info" - case sourceStatusID = "source_status_id" - case sourceStatusIDStr = "source_status_id_str" - case sourceUserID = "source_user_id" - case sourceUserIDStr = "source_user_id_str" - case extAltText = "ext_alt_text" - } - } -} - -extension Twitter.Entity.ExtendedEntities.Media: Equatable { } - - -extension Twitter.Entity.ExtendedEntities.Media { - - public struct Sizes: Codable { - public let thumbnail: Size? - public let small: Size? - public let medium: Size? - public let large: Size? - - enum CodingKeys: String, CodingKey { - case thumbnail = "thumb" - case small = "small" - case medium = "medium" - case large = "large" - } - - public enum SizeKind: String { - case thumbnail = "thumb" - case small - case medium - case large - } - - public func size(kind: SizeKind) -> Size? { - switch kind { - case .thumbnail: return thumbnail - case .small: return small - case .medium: return medium - case .large: return large - } - } - } - - public struct Size: Codable { - public let w: Int? - public let h: Int? - public let resize: String? - - enum CodingKeys: String, CodingKey { - case w = "w" - case h = "h" - case resize = "resize" - } - - public init(w: Int?, h: Int?, resize: String?) { - self.w = w - self.h = h - self.resize = resize - } - } - - public enum Resize: String { - case fit - case crop - } - - public struct VideoInfo: Codable { - public let durationMillis: Int? - public let variants: [Variant]? - - enum CodingKeys: String, CodingKey { - case durationMillis = "duration_millis" - case variants - } - - public struct Variant: Codable { - public let bitrate: Int? - public let contentType: String - public let url: String - - enum CodingKeys: String, CodingKey { - case bitrate - case contentType = "content_type" - case url - } - } - } - -} - -extension Twitter.Entity.ExtendedEntities.Media.Sizes: Equatable { } -extension Twitter.Entity.ExtendedEntities.Media.Size: Equatable { } -extension Twitter.Entity.ExtendedEntities.Media.VideoInfo: Equatable { } -extension Twitter.Entity.ExtendedEntities.Media.VideoInfo.Variant: Equatable { } - -extension Twitter.Entity.ExtendedEntities.Media { - - public var assetURL: String? { - switch type { - case "animated_gif": return videoInfo?.variants?.max(by: { ($0.bitrate ?? 0) < ($1.bitrate ?? 0) })?.url - case "video": return videoInfo?.variants?.max(by: { ($0.bitrate ?? 0) < ($1.bitrate ?? 0) })?.url - case "photo": return mediaURLHTTPS - default: return nil - } - } - - public var previewImageURL: String? { - switch type { - case "animated_gif": return mediaURLHTTPS - case "video": return mediaURLHTTPS - default: return nil - } - } - - public var durationMS: Int? { - switch type { - case "video": return videoInfo?.durationMillis - default: return nil - } - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Internal+Tweet.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Internal+Tweet.swift deleted file mode 100644 index c2a94138..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Internal+Tweet.swift +++ /dev/null @@ -1,99 +0,0 @@ -// -// Twitter+Tweet.swift -// TwitterAPI -// -// Created by Cirno MainasuK on 2020-9-3. -// - -import Foundation - -extension Twitter.Entity.Internal { - public class Tweet: Codable { - - public typealias ID = String - - // Fundamental - public let createdAt: Date - public let idStr: ID - - public let text: String? - - public let userIDStr: Twitter.Entity.User.ID - - public let entities: Twitter.Entity.Tweet.Entities? - - // public let coordinates: Coordinates? - // public let place: Place? - - //let contributors: JSONNull? - public let favoriteCount: Int? - - public let retweeted: Bool - public let retweetCount: Int? - public let retweetedStatusIDStr: ID? - - public let inReplyToScreenName: String? - public let inReplyToStatusIDStr: ID? - public let inReplyToUserIDStr: Twitter.Entity.User.ID? - - public let isQuoteStatus: Bool - public let quotedStatusIDStr: String? - - public let lang: String? - - //let possiblySensitive: Bool? - //let possiblySensitiveAppealable: Bool? - - public let source: String? - public let truncated: Bool? - - public enum CodingKeys: String, CodingKey { - // Fundamental - case createdAt = "created_at" - case idStr = "id_str" - - case text - - case userIDStr = "user_id_str" - - case entities - - // case coordinates = "coordinates" - // case place = "place" - - //case contributors = "contributors" - case favoriteCount = "favorite_count" - - case retweeted = "retweeted" - case retweetCount = "retweet_count" - case retweetedStatusIDStr = "retweeted_status_id_str" - - case inReplyToScreenName = "in_reply_to_screen_name" - case inReplyToStatusIDStr = "in_reply_to_status_id_str" - case inReplyToUserIDStr = "in_reply_to_user_id_str" - - case isQuoteStatus = "is_quote_status" - case quotedStatusIDStr = "quoted_status_id_str" - - case lang - - //case possiblySensitive = "possibly_sensitive" - //case possiblySensitiveAppealable = "possibly_sensitive_appealable" - - case source - case truncated - } - } -} - -extension Twitter.Entity.Internal.Tweet: Hashable { - - public static func == (lhs: Twitter.Entity.Internal.Tweet, rhs: Twitter.Entity.Internal.Tweet) -> Bool { - lhs.idStr == rhs.idStr - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(idStr) - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Internal.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Internal.swift deleted file mode 100644 index 6e4efcdb..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Internal.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// Twitter+Entity+Internal.swift -// -// -// Created by MainasuK on 2022-9-5. -// - -import Foundation - -extension Twitter.Entity { - public enum Internal { } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+List.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+List.swift deleted file mode 100644 index 3f8b3741..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+List.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// Twitter+Entity+List.swift -// -// -// Created by MainasuK on 2022-3-10. -// - -import Foundation - -// Note: -// use the v2 as the persist model -// this model is for query following relationship only -extension Twitter.Entity { - public struct List: Codable { - public typealias ID = String - - public let id: ID - - public let name: String - public let uri: String - public let following: Bool - - - enum CodingKeys: String, CodingKey { - case id = "id_str" - - case name - case uri - case following - } - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Place.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Place.swift deleted file mode 100644 index 55207202..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Place.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// Twitter+Entity+Place.swift -// TwitterAPI -// -// Created by Cirno MainasuK on 2020-9-3. -// - -import Foundation - -extension Twitter.Entity { - public struct Place: Codable { - public typealias ID = String - public let id: ID - - public let country: String? - public let countryCode: String? - public let fullName: String? - public let name: String? - public let placeType: String? - public let url: String? - - enum CodingKeys: String, CodingKey { - case id = "id" - - case country = "country" - case countryCode = "country_code" - case fullName = "full_name" - case name = "name" - case placeType = "place_type" - case url = "url" - } - } -} - -extension Twitter.Entity.Place: Equatable { } diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+QuotedStatus.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+QuotedStatus.swift deleted file mode 100644 index 7a7fbc70..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+QuotedStatus.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// Twitter+QuotedStatus.swift -// TwitterAPI -// -// Created by Cirno MainasuK on 2020-9-11. -// - -import Foundation - -extension Twitter.Entity { - /// https://developer.twitter.com/en/docs/twitter-api/v1/data-dictionary/overview/intro-to-tweet-json#quotetweet - public class QuotedStatus: Twitter.Entity.Tweet { } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+RateLimitStatus.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+RateLimitStatus.swift deleted file mode 100644 index e87d4bc6..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+RateLimitStatus.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// Twitter+Entity+RateLimitStatus.swift -// -// -// Created by Cirno MainasuK on 2020-12-7. -// - -import Foundation -import SwiftyJSON - -extension Twitter.Entity { - /// https://developer.twitter.com/en/docs/twitter-api/v1/developer-utilities/rate-limit-status/overview - public struct RateLimitStatus: Codable { - - public let rateLimitContext: RateLimitContext - public let resources: JSON - - enum CodingKeys: String, CodingKey { - case rateLimitContext = "rate_limit_context" - case resources - } - - public struct RateLimitContext: Codable { - public let accessToken: String - enum CodingKeys: String, CodingKey { - case accessToken = "access_token" - } - } - } -} - -extension Twitter.Entity.RateLimitStatus { - public struct Status: Codable { - public let limit: Int - public let remaining: Int - public let reset: Int - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Relationship.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Relationship.swift deleted file mode 100644 index 63f73617..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Relationship.swift +++ /dev/null @@ -1,99 +0,0 @@ -// -// Twitter+Entity+Relationship.swift -// -// -// Created by Cirno MainasuK on 2020-12-23. -// - -import Foundation - -extension Twitter.Entity { - - // doc: https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/follow-search-get-users/api-reference/get-friendships-show - public struct Relationship: Codable { - public let source: RelationshipSource - public let target: RelationshipTarget - - enum CodingKeys: String, CodingKey { - case relationship - } - - enum RelationshipKeys: String, CodingKey { - case source - case target - } - - public init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - let relationshipValues = try values.nestedContainer(keyedBy: RelationshipKeys.self, forKey: .relationship) - source = try relationshipValues.decode(RelationshipSource.self, forKey: .source) - target = try relationshipValues.decode(RelationshipTarget.self, forKey: .target) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - var relationshipContainer = container.nestedContainer(keyedBy: RelationshipKeys.self, forKey: .relationship) - try relationshipContainer.encode(source, forKey: .source) - try relationshipContainer.encode(target, forKey: .target) - } - } - - public struct RelationshipSource: Codable { - public typealias ID = String - - public let idStr: ID - public let screenName: String - public let following: Bool - public let followedBy: Bool - public let liveFollowing: Bool? - public let followingReceived: Bool? - public let followingRequested: Bool? - public let notificationsEnabled: Bool? - public let canDM: Bool? - public let blocking: Bool? - public let blockedBy: Bool? - public let muting: Bool? - public let wantRetweets: Bool? - public let allReplies: Bool? - public let markedSpam: Bool? - - enum CodingKeys: String, CodingKey { - case idStr = "id_str" - case screenName = "screen_name" - case following = "following" - case followedBy = "followed_by" - case liveFollowing = "live_following" - case followingReceived = "following_received" - case followingRequested = "following_requested" - case notificationsEnabled = "notifications_enabled" - case canDM = "can_dm" - case blocking = "blocking" - case blockedBy = "blocked_by" - case muting = "muting" - case wantRetweets = "want_retweets" - case allReplies = "all_replies" - case markedSpam = "marked_spam" - } - } - - public struct RelationshipTarget: Codable { - public typealias ID = String - - public let idStr: ID - public let screenName: String - public let following: Bool - public let followedBy: Bool - public let followingReceived: Bool? - public let followingRequested: Bool? - - enum CodingKeys: String, CodingKey { - case idStr = "id_str" - case screenName = "screen_name" - case following = "following" - case followedBy = "followed_by" - case followingReceived = "following_received" - case followingRequested = "following_requested" - } - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+RetweetedStatus.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+RetweetedStatus.swift deleted file mode 100644 index b1fefb22..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+RetweetedStatus.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// Twitter+RetweetedStatus.swift -// TwitterAPI -// -// Created by Cirno MainasuK on 2020-9-3. -// - -import Foundation - -extension Twitter.Entity { - /// https://developer.twitter.com/en/docs/twitter-api/v1/data-dictionary/overview/intro-to-tweet-json#retweet - public class RetweetedStatus: Twitter.Entity.Tweet { } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+SavedSearch.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+SavedSearch.swift deleted file mode 100644 index 95cedd2a..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+SavedSearch.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// Twitter+Entity+SavedSearch.swift -// -// -// Created by MainasuK on 2021-12-22. -// - -import Foundation - -extension Twitter.Entity { - public struct SavedSearch: Codable { - public typealias ID = String - - public let idStr: ID - public let name: String - public let query: String - public let createdAt: Date - - enum CodingKeys: String, CodingKey { - case idStr = "id_str" - case name - case query - case createdAt = "created_at" - } - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Trend+Place.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Trend+Place.swift deleted file mode 100644 index 622b5ddf..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Trend+Place.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// Twitter+Entity+Trend+Place.swift -// -// -// Created by MainasuK on 2022-4-15. -// - -import Foundation - -extension Twitter.Entity.Trend { - public struct Place: Codable, Identifiable { - public var id: Int { - return woeid - } - - public let name: String - public let woeid: Int - public let parentID: Int - - public let placeType: PlaceType? - public let country: String? - public let countryCode: String? - public let fullName: String? - public let url: String? - - enum CodingKeys: String, CodingKey { - case name - case woeid - case parentID = "parentid" - - case placeType = "place_type" - case country - case countryCode = "country_code" - case fullName = "full_name" - case url = "url" - } - - public struct PlaceType: Codable { - public let code: Int - public let name: String - } - } -} - -extension Twitter.Entity.Trend.Place: Hashable { - public static func == (lhs: Twitter.Entity.Trend.Place, rhs: Twitter.Entity.Trend.Place) -> Bool { - return lhs.woeid == rhs.woeid - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(woeid) - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Trend.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Trend.swift deleted file mode 100644 index d6c90a92..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Trend.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// Twitter+Entity+Trend.swift -// -// -// Created by MainasuK on 2021-12-27. -// - -import Foundation - -extension Twitter.Entity { - public struct Trend: Codable, Hashable { - public let name: String - public let url: String - public let query: String - public let tweetVolume: Int? - - enum CodingKeys: String, CodingKey { - case name = "name" - case url = "url" - case query = "query" - case tweetVolume = "tweet_volume" - } - - public init( - name: String, - url: String, - query: String, - tweetVolume: Int? - ) { - self.name = name - self.url = url - self.query = query - self.tweetVolume = tweetVolume - } - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Tweet+Entities.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Tweet+Entities.swift deleted file mode 100644 index 72270098..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Tweet+Entities.swift +++ /dev/null @@ -1,123 +0,0 @@ -// -// Twitter+Entity+Tweet+Entities.swift -// TwitterAPI -// -// Created by Cirno MainasuK on 2020-9-3. -// - -import Foundation - -extension Twitter.Entity.Tweet { - public struct Entities: Codable { - public let symbols: [Symbol]? - public let userMentions: [UserMention]? - public let urls: [URL]? - public let hashtags: [Hashtag]? - public let polls: [Poll]? - - public enum CodingKeys: String, CodingKey { - case symbols = "symbols" - case userMentions = "user_mentions" - case urls = "urls" - case hashtags = "hashtags" - case polls = "polls" - } - } -} - -extension Twitter.Entity.Tweet.Entities: Equatable { } - -extension Twitter.Entity.Tweet.Entities { - - public struct Symbol: Codable { - public let text: String? - public let indices: [Int]? - - enum CodingKeys: String, CodingKey { - case text = "text" - case indices = "indices" - } - } - - public struct UserMention: Codable { - public let screenName: String? /// username - public let name: String? /// nickname - public let id: Int? - public let idStr: String? - public let indices: [Int]? - - enum CodingKeys: String, CodingKey { - case screenName = "screen_name" - case name = "name" - case id = "id" - case idStr = "id_str" - case indices = "indices" - } - } - - public struct URL: Codable { - public let url: String? - public let expandedURL: String? - public let displayURL: String? - public let indices: [Int]? - - enum CodingKeys: String, CodingKey { - case url = "url" - case expandedURL = "expanded_url" - case displayURL = "display_url" - case indices = "indices" - } - } - - public struct Hashtag: Codable { - public let text: String? - public let indices: [Int]? - - enum CodingKeys: String, CodingKey { - case text = "text" - case indices = "indices" - } - } - - public struct Poll: Codable { - public let options: [Option]? - public let endDatetime: String? - public let durationMinutes: Int? - - enum CodingKeys: String, CodingKey { - case options = "options" - case endDatetime = "end_datetime" - case durationMinutes = "duration_minutes" - } - - public init(options: [Option]?, endDatetime: String?, durationMinutes: Int?) { - self.options = options - self.endDatetime = endDatetime - self.durationMinutes = durationMinutes - } - - // MARK: - Option - public struct Option: Codable { - public let position: Int? - public let text: String? - - enum CodingKeys: String, CodingKey { - case position = "position" - case text = "text" - } - - public init(position: Int?, text: String?) { - self.position = position - self.text = text - } - } - } - -} - -extension Twitter.Entity.Tweet.Entities.Symbol: Equatable { } -extension Twitter.Entity.Tweet.Entities.UserMention: Equatable { } -extension Twitter.Entity.Tweet.Entities.URL: Equatable { } -extension Twitter.Entity.Tweet.Entities.Hashtag: Equatable { } -extension Twitter.Entity.Tweet.Entities.Poll: Equatable { } -extension Twitter.Entity.Tweet.Entities.Poll.Option: Equatable { } diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Tweet.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Tweet.swift deleted file mode 100644 index 761ae662..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+Tweet.swift +++ /dev/null @@ -1,105 +0,0 @@ -// -// Twitter+Tweet.swift -// TwitterAPI -// -// Created by Cirno MainasuK on 2020-9-3. -// - -import Foundation - -extension Twitter.Entity { - public class Tweet: Codable { - - public typealias ID = String - - // Fundamental - public let createdAt: Date - public let idStr: ID - - public let text: String? - public let fullText: String? - - public let user: User - public let entities: Entities - public let extendedEntities: ExtendedEntities? - - public let coordinates: Coordinates? - public let place: Place? - - //let contributors: JSONNull? - public let favorited: Bool? - public let favoriteCount: Int? - - public let retweeted: Bool? - public let retweetCount: Int? - public let retweetedStatus: RetweetedStatus? - - public let inReplyToScreenName: String? - //let inReplyToStatusID: JSONNull? - public let inReplyToStatusIDStr: ID? - //let inReplyToUserID: JSONNull? - public let inReplyToUserIDStr: User.ID? - //let isQuoteStatus: Bool - public let lang: String? - //let possiblySensitive: Bool? - //let possiblySensitiveAppealable: Bool? - - public let quotedStatusIDStr: String? - public let quotedStatus: QuotedStatus? - - public let source: String? - //let truncated: Bool - - public enum CodingKeys: String, CodingKey { - // Fundamental - case createdAt = "created_at" - case idStr = "id_str" - - case text = "text" - case fullText = "full_text" - - case user = "user" - case entities = "entities" - case extendedEntities = "extended_entities" - - case coordinates = "coordinates" - case place = "place" - - //case contributors = "contributors" - case favorited = "favorited" - case favoriteCount = "favorite_count" - - case retweeted = "retweeted" - case retweetCount = "retweet_count" - case retweetedStatus = "retweeted_status" - - case quotedStatusIDStr = "quoted_status_id_str" - case quotedStatus = "quoted_status" - - case inReplyToScreenName = "in_reply_to_screen_name" - //case inReplyToStatusID = "in_reply_to_status_id" - case inReplyToStatusIDStr = "in_reply_to_status_id_str" - //case inReplyToUserID = "in_reply_to_user_id" - case inReplyToUserIDStr = "in_reply_to_user_id_str" - //case isQuoteStatus = "is_quote_status" - case lang = "lang" - //case possiblySensitive = "possibly_sensitive" - //case possiblySensitiveAppealable = "possibly_sensitive_appealable" - - case source = "source" - //case truncated = "truncated" - } - } -} - -extension Twitter.Entity.Tweet: Hashable { - - public static func == (lhs: Twitter.Entity.Tweet, rhs: Twitter.Entity.Tweet) -> Bool { - lhs.idStr == rhs.idStr - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(idStr) - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+User+Entities.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+User+Entities.swift deleted file mode 100644 index 49e195f6..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+User+Entities.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// Twitter+Entity+User+Entities.swift -// -// -// Created by Cirno MainasuK on 2020-11-26. -// - -import Foundation - -extension Twitter.Entity.User { - public struct Entities: Codable { - // FIXME: - public let url: URL? - public let description: Description? - } -} - -extension Twitter.Entity.User.Entities: Equatable { } - -extension Twitter.Entity.User.Entities { - - public struct URL: Codable { - public let urls: [URLNode]? - } - - public struct Description: Codable { - public let urls: [URLNode]? - } - - public struct URLNode: Codable { - public let url: String? - public let expandedURL: String? - public let displayURL: String? - public let indices: [Int]? - - enum CodingKeys: String, CodingKey { - case url = "url" - case expandedURL = "expanded_url" - case displayURL = "display_url" - case indices = "indices" - } - } - -} - -extension Twitter.Entity.User.Entities.URL: Equatable { } -extension Twitter.Entity.User.Entities.Description: Equatable { } -extension Twitter.Entity.User.Entities.URLNode: Equatable { } diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+User.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+User.swift deleted file mode 100644 index f99f91d2..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V1/Twitter+Entity+User.swift +++ /dev/null @@ -1,144 +0,0 @@ -// -// Twitter+Entity+User.swift -// TwitterAPI -// -// Created by Cirno MainasuK on 2020-9-3. -// - -import Foundation - -extension Twitter.Entity { - public struct User: Codable { - - public typealias ID = String - - // Fundamental - public let idStr: ID - // nickname - public let name: String - /// @username without "@" - public let screenName: String - - public let userDescription: String? - public let entities: Entities? - - public let location: String? - public let url: String? - public let protected: Bool? - - public let followersCount: Int? - public let friendsCount: Int? - public let listedCount: Int? - public let favouritesCount: Int? - public let statusesCount: Int? - - public let createdAt: Date? - - public let geoEnabled: Bool? - public let verified: Bool? - public let contributorsEnabled: Bool? - - public let profileImageURLHTTPS: String? - public let profileBannerURL: String? - - public let profileLinkColor: String? - public let profileSidebarBorderColor: String? - public let profileSidebarFillColor: String? - public let profileTextColor: String? - public let hasExtendedProfile: Bool? - public let defaultProfile: Bool? - public let defaultProfileImage: Bool? - public let following: Bool? - public let followRequestSent: Bool? - public let notifications: Bool? - - enum CodingKeys: String, CodingKey { - case idStr = "id_str" - - case name = "name" - case screenName = "screen_name" - - case userDescription = "description" - case entities = "entities" - - case url = "url" - case location = "location" - case protected = "protected" - - case followersCount = "followers_count" - case friendsCount = "friends_count" - case listedCount = "listed_count" - case createdAt = "created_at" - case favouritesCount = "favourites_count" - - case geoEnabled = "geo_enabled" - case verified = "verified" - case statusesCount = "statuses_count" - case contributorsEnabled = "contributors_enabled" - - case profileImageURLHTTPS = "profile_image_url_https" - case profileBannerURL = "profile_banner_url" - - case profileLinkColor = "profile_link_color" - case profileSidebarBorderColor = "profile_sidebar_border_color" - case profileSidebarFillColor = "profile_sidebar_fill_color" - case profileTextColor = "profile_text_color" - case hasExtendedProfile = "has_extended_profile" - case defaultProfile = "default_profile" - case defaultProfileImage = "default_profile_image" - case following = "following" - case followRequestSent = "follow_request_sent" - case notifications = "notifications" - } - - } -} - -extension Twitter.Entity.User: Equatable { } - - -extension Twitter.Entity.User { - public enum ProfileImageSize: String { - case original - case reasonablySmall = "reasonably_small" // 128 * 128 - case bigger // 73 * 73 - case normal // 48 * 48 - case mini // 24 * 24 - - static var suffixedSizes: [ProfileImageSize] { - return [.reasonablySmall, .bigger, .normal, .mini] - } - } - - /// https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/user-profile-images-and-banners - public func avatarImageURL(size: ProfileImageSize = .reasonablySmall) -> URL? { - guard let imageURLString = profileImageURLHTTPS, var imageURL = URL(string: imageURLString) else { return nil } - - let pathExtension = imageURL.pathExtension - imageURL.deletePathExtension() - - var imageIdentifier = imageURL.lastPathComponent - imageURL.deleteLastPathComponent() - for suffixedSize in Twitter.Entity.User.ProfileImageSize.suffixedSizes { - imageIdentifier.deleteSuffix("_\(suffixedSize.rawValue)") - } - - switch size { - case .original: - imageURL.appendPathComponent(imageIdentifier) - default: - imageURL.appendPathComponent(imageIdentifier + "_" + size.rawValue) - } - - imageURL.appendPathExtension(pathExtension) - - return imageURL - } -} - -extension String { - mutating func deleteSuffix(_ suffix: String) { - guard hasSuffix(suffix) else { return } - removeLast(suffix.count) - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Entities.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Entities.swift deleted file mode 100644 index 93245a2f..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Entities.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// Twitter+Entity+V2+Tweet+Entities.swift -// -// -// Created by Cirno MainasuK on 2020-10-19. -// - -import Foundation - -extension Twitter.Entity.V2 { - public class Entities: Codable { - // tweet - public let urls: [URL]? - public let hashtags: [Hashtag]? - public let mentions: [Mention]? - - // user - public let url: Entities? - public let description: Entities? - } -} - -extension Twitter.Entity.V2.Entities { - - public struct URL: Codable { - public let start: Int - public let end: Int - public let url: String - - // optional - public let expandedURL: String? - public let displayURL: String? - public let status: Int? - public let title: String? - public let description: String? - public let unwoundURL: String? - - public enum CodingKeys: String, CodingKey { - case start = "start" - case end = "end" - case url = "url" - case expandedURL = "expanded_url" - case displayURL = "display_url" - case status - case title - case description - case unwoundURL = "unwound_url" - } - } - - public struct Hashtag: Codable { - public let start: Int - public let end: Int - public let tag: String - - public enum CodingKeys: String, CodingKey { - case start = "start" - case end = "end" - case tag = "tag" - } - } - - public struct Mention: Codable { - public let start: Int - public let end: Int - public let username: String - public let id: Twitter.Entity.V2.User.ID? - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+List.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+List.swift deleted file mode 100644 index 935d3123..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+List.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// Twitter+Entity+V2+List.swift -// -// -// Created by MainasuK on 2022-2-28. -// - -import Foundation - -extension Twitter.Entity.V2 { - public struct List: Codable { - public typealias ID = String - - public let id: ID - public let name: String - - public let `private`: Bool? - public let memberCount: Int? - public let followerCount: Int? - public let description: String? - public let ownerID: Twitter.Entity.V2.User.ID? - public let createdAt: Date? - - public enum CodingKeys: String, CodingKey { - case id - case name - case `private` - case memberCount = "member_count" - case followerCount = "follower_count" - case description - case ownerID = "owner_id" - case createdAt = "created_at" - } - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Media+PublicMetrics.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Media+PublicMetrics.swift deleted file mode 100644 index f4a43f3b..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Media+PublicMetrics.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Twitter+Entity+V2+Media+PublicMetrics.swift -// -// -// Created by Cirno MainasuK on 2020/10/21. -// - -import Foundation - -extension Twitter.Entity.V2.Media { - public struct PublicMetrics: Codable { - public let viewCount: Int? - - public enum CodingKeys: String, CodingKey { - case viewCount = "view_count" - } - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Media.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Media.swift deleted file mode 100644 index e0ed442c..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Media.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// Twitter+Entity+V2+Media.swift -// -// -// Created by Cirno MainasuK on 2020/10/21. -// - -import Foundation - -extension Twitter.Entity.V2 { - public struct Media: Codable, Identifiable { - public typealias ID = String - - public var id: ID { mediaKey } - - public let mediaKey: ID - public let type: String - - public let durationMS: Int? - public let width: Int? - public let height: Int? - public let url: String? - public let previewImageURL: String? - public let publicMetrics: PublicMetrics? - public let altText: String? - - enum CodingKeys: String, CodingKey { - case mediaKey = "media_key" - case type - - case durationMS = "duration_ms" - case width - case height - case url - case previewImageURL = "preview_image_url" - case publicMetrics = "public_metrics" - case altText = "alt_text" - } - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Place.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Place.swift deleted file mode 100644 index c49f81f4..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Place.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// Twitter+Entity+V2+Place.swift -// -// -// Created by Cirno MainasuK on 2020-10-15. -// - -import Foundation - -extension Twitter.Entity.V2 { - public struct Place: Codable, Identifiable { - public typealias ID = String - - public let id: ID - public let fullName: String - - public let country: String? - public let countryCode: String? - public let name: String? - public let placeType: String? - - enum CodingKeys: String, CodingKey { - case id = "id" - case fullName = "full_name" - - case country = "country" - case countryCode = "country_code" - case name = "name" - case placeType = "place_type" - } - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+ReferencedTweet.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+ReferencedTweet.swift deleted file mode 100644 index 7d00d032..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+ReferencedTweet.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// Twitter+Entity+V2+ReferencedTweet.swift -// -// -// Created by Cirno MainasuK on 2020-10-19. -// - -import Foundation - -extension Twitter.Entity.V2.Tweet { - public struct ReferencedTweet: Codable { - public let `type`: ReferencedType? - public let id: Twitter.Entity.V2.Tweet.ID? - - public enum CodingKeys: String, CodingKey { - case `type` = "type" - case id - } - } - -} - -extension Twitter.Entity.V2.Tweet.ReferencedTweet { - public enum ReferencedType: String, Codable { - case repliedTo = "replied_to" - case quoted - case retweeted - - public enum CodingKeys: String, CodingKey { - case repliedTo = "replied_to" - case quoted - case retweeted - } - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Tweet+Attachments.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Tweet+Attachments.swift deleted file mode 100644 index b39e8cdb..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Tweet+Attachments.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// File.swift -// -// -// Created by Cirno MainasuK on 2020/10/21. -// - -import Foundation - -extension Twitter.Entity.V2.Tweet { - public struct Attachments: Codable { - public let mediaKeys: [Twitter.Entity.V2.Media.ID]? - public let pollIDs: [Twitter.Entity.V2.Tweet.Poll.ID]? - - public enum CodingKeys: String, CodingKey { - case mediaKeys = "media_keys" - case pollIDs = "poll_ids" - } - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Tweet+Geo.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Tweet+Geo.swift deleted file mode 100644 index d28a3635..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Tweet+Geo.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// Twitter+Entity+V2+Tweet+Geo.swift -// -// -// Created by Cirno MainasuK on 2020-10-19. -// - -import Foundation - -extension Twitter.Entity.V2.Tweet { - public struct Geo: Codable { - public let placeID: Twitter.Entity.V2.Place.ID? - public let coordinates: Coordinate? - - public init( - placeID: Twitter.Entity.V2.Place.ID?, - coordinates: Twitter.Entity.V2.Tweet.Geo.Coordinate? = nil - ) { - self.placeID = placeID - self.coordinates = coordinates - } - - public enum CodingKeys: String, CodingKey { - case placeID = "place_id" - case coordinates - } - } -} - -extension Twitter.Entity.V2.Tweet.Geo { - public struct Coordinate: Codable { - public let type: String - public let coordinates: [Double] - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Tweet+Poll.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Tweet+Poll.swift deleted file mode 100644 index 0890b874..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Tweet+Poll.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// Twitter+Entity+V2+Tweet+Poll.swift -// -// -// Created by MainasuK on 2022-6-6. -// - -import Foundation - -extension Twitter.Entity.V2.Tweet { - public struct Poll: Codable, Identifiable { - public typealias ID = String - - public let id: ID - public let options: [Option] - - public let votingStatus: VotingStatus - public let durationMinutes: Int? - public let endDatetime: Date? - - public enum CodingKeys: String, CodingKey { - case id - case options - case votingStatus = "voting_status" - case durationMinutes = "duration_minutes" - case endDatetime = "end_datetime" - } - } -} - -extension Twitter.Entity.V2.Tweet.Poll { - public enum VotingStatus: String, Codable, CaseIterable { - case open - case closed - } - - public struct Option: Codable { - public let position: Int - public let label: String - public let votes: Int - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Tweet+PublicMetrics.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Tweet+PublicMetrics.swift deleted file mode 100644 index 7d280e0c..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Tweet+PublicMetrics.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// Twitter+Entity+V2+Tweet+PublicMetrics.swift -// -// -// Created by Cirno MainasuK on 2020-10-19. -// - -import Foundation - -extension Twitter.Entity.V2.Tweet { - public struct PublicMetrics: Codable { - public let retweetCount: Int - public let replyCount: Int - public let likeCount: Int - public let quoteCount: Int - - public enum CodingKeys: String, CodingKey { - case retweetCount = "retweet_count" - case replyCount = "reply_count" - case likeCount = "like_count" - case quoteCount = "quote_count" - } - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Tweet+ReplySettings.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Tweet+ReplySettings.swift deleted file mode 100644 index 09059252..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Tweet+ReplySettings.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// Twitter+Entity+V2+Tweet+ReplySettings.swift -// -// -// Created by MainasuK on 2022-5-24. -// - -import Foundation - -extension Twitter.Entity.V2.Tweet { - public enum ReplySettings: String, Codable, Hashable, CaseIterable { - case everyone - case following - case mentionedUsers = "mentionedUsers" // value not has underscore?! - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Tweet.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Tweet.swift deleted file mode 100644 index c2b2819e..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Tweet.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// Twitter+Entity+V2+Tweet.swift -// TwitterAPI -// -// Created by Cirno MainasuK on 2020-9-15. -// - -import Foundation - -extension Twitter.Entity.V2 { - public struct Tweet: Codable, Identifiable { - - public typealias ID = String - public typealias ConversationID = String - - // Fundamental - public let id: ID - public let text: String - - // Extra - public let attachments: Attachments? - public let authorID: String? - // public let contextAnnotations - public let conversationID: ConversationID? - public let createdAt: Date // client required - public let entities: Entities? - public let geo: Geo? - public let inReplyToUserID: User.ID? - public let lang: String? - public let publicMetrics: PublicMetrics? - public let possiblySensitive: Bool? - public let referencedTweets: [ReferencedTweet]? - public let replySettings: ReplySettings? - public let source: String? - public let withheld: Withheld? - - public enum CodingKeys: String, CodingKey { - case id - case text - - case attachments - case authorID = "author_id" - //case context_annotations - case conversationID = "conversation_id" - case createdAt = "created_at" - case entities - case inReplyToUserID = "in_reply_to_user_id" - case geo - case lang - case publicMetrics = "public_metrics" - case possiblySensitive = "possibly_sensitive" - case referencedTweets = "referenced_tweets" - case replySettings = "reply_settings" - case source - case withheld - } - - } -} - - diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+User+PublicMetrics.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+User+PublicMetrics.swift deleted file mode 100644 index 0d651417..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+User+PublicMetrics.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// Twitter+Entity+V2+User+PublicMetrics.swift -// -// -// Created by Cirno MainasuK on 2020-10-20. -// - -import Foundation - -extension Twitter.Entity.V2.User { - public struct PublicMetrics: Codable { - public let followersCount: Int - public let followingCount: Int - public let tweetCount: Int - public let listedCount: Int - - public enum CodingKeys: String, CodingKey { - case followersCount = "followers_count" - case followingCount = "following_count" - case tweetCount = "tweet_count" - case listedCount = "listed_count" - } - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+User.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+User.swift deleted file mode 100644 index 7d423843..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+User.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// Twitter+Entity+V2+User.swift -// -// -// Created by Cirno MainasuK on 2020-10-16. -// - -import Foundation - -extension Twitter.Entity.V2 { - public struct User: Codable, Identifiable { - public typealias ID = String - - // Fundamental - public let id: ID - public let name: String - public let username: String - - // Extra - public let createdAt: Date? - public let description: String? - public let entities: Entities? - public let location: String? - public let pinnedTweetID: Tweet.ID? - public let profileImageURL: String? - public let protected: Bool? - public let publicMetrics: PublicMetrics? - public let url: String? - public let verified: Bool? - public let withheld: Withheld? - - public enum CodingKeys: String, CodingKey { - case id - case name - case username - - case createdAt = "created_at" - case description = "description" - case entities = "entities" - case location = "location" - case pinnedTweetID = "pinned_tweet_id" - case profileImageURL = "profile_image_url" - case protected = "protected" - case publicMetrics = "public_metrics" - case url = "url" - case verified = "verified" - case withheld = "withheld" - } - } -} - -extension Twitter.Entity.V2.User { - public enum ProfileImageSize: String { - case original - case reasonablySmall = "reasonably_small" // 128 * 128 - case bigger // 73 * 73 - case normal // 48 * 48 - case mini // 24 * 24 - - static var suffixedSizes: [ProfileImageSize] { - return [.reasonablySmall, .bigger, .normal, .mini] - } - } - - /// https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/user-profile-images-and-banners - public func avatarImageURL(size: ProfileImageSize = .reasonablySmall) -> URL? { - guard let imageURLString = profileImageURL, var imageURL = URL(string: imageURLString) else { return nil } - - let pathExtension = imageURL.pathExtension - imageURL.deletePathExtension() - - var imageIdentifier = imageURL.lastPathComponent - imageURL.deleteLastPathComponent() - for suffixedSize in Twitter.Entity.User.ProfileImageSize.suffixedSizes { - imageIdentifier.deleteSuffix("_\(suffixedSize.rawValue)") - } - - switch size { - case .original: - imageURL.appendPathComponent(imageIdentifier) - default: - imageURL.appendPathComponent(imageIdentifier + "_" + size.rawValue) - } - - imageURL.appendPathExtension(pathExtension) - - return imageURL - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Withheld.swift b/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Withheld.swift deleted file mode 100644 index ffd90790..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Entity/V2/Twitter+Entity+V2+Withheld.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Twitter+Entity+V2+Tweet+Withheld.swift -// -// -// Created by Cirno MainasuK on 2020-10-19. -// - -import Foundation - -extension Twitter.Entity.V2 { - public struct Withheld: Codable, Hashable { - public let copyright: Bool? - public let countryCodes: [String]? - - public enum CodingKeys: String, CodingKey { - case copyright - case countryCodes = "country_codes" - } - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Helper.swift b/TwidereSDK/Sources/TwitterSDK/Helper.swift deleted file mode 100644 index 0bf9991f..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Helper.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// Helper.swift -// -// -// Created by Cirno MainasuK on 2020-10-16. -// - -import Foundation - -// MARK: - Helper - -extension String { - - var urlEncoded: String { - let customAllowedSet = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~") - return self.addingPercentEncoding(withAllowedCharacters: customAllowedSet)! - } - -} - -extension Dictionary { - - var queryString: String { - var parts = [String]() - - for (key, value) in self { - let query: String = "\(key)=\(value)" - parts.append(query) - } - - return parts.joined(separator: "&") - } - - var urlEncodedQuery: String { - var parts = [String]() - - for (key, value) in self { - let keyString = "\(key)".urlEncoded - let valueString = "\(value)".urlEncoded - let query = "\(keyString)=\(valueString)" - parts.append(query) - } - - return parts.joined(separator: "&") - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/Request/Query.swift b/TwidereSDK/Sources/TwitterSDK/Request/Query.swift deleted file mode 100644 index d59b19a3..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Request/Query.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// Query.swift -// Query -// -// Created by Cirno MainasuK on 2021-8-19. -// - -import Foundation - -protocol Query { - var queryItems: [URLQueryItem]? { get } - var encodedQueryItems: [URLQueryItem]? { get } - var formQueryItems: [URLQueryItem]? { get } - var contentType: String? { get } - var body: Data? { get } -} - -protocol JSONEncodeQuery: Query, Encodable { } - -extension Query where Self: JSONEncodeQuery { - var contentType: String? { - return "application/json; charset=utf-8" - } - - var body: Data? { - return try? JSONEncoder().encode(self) - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+Expansions.swift b/TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+Expansions.swift deleted file mode 100644 index 91abc8e7..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+Expansions.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// Twitter+Request+Expansions.swift -// TwitterAPI -// -// Created by Cirno MainasuK on 2020-9-16. -// - -import Foundation - -extension Twitter.Request { - public enum Expansions: String { - case attachmentsPollIDs = "attachments.poll_ids" - case attachmentsMediaKeys = "attachments.media_keys" - case authorID = "author_id" - case entitiesMentionsUsername = "entities.mentions.username" - case geoPlaceID = "geo.place_id" - case inReplyToUserID = "in_reply_to_user_id" - case referencedTweetsID = "referenced_tweets.id" - case referencedTweetsIDAuthorID = "referenced_tweets.id.author_id" - case pinnedTweetID = "pinned_tweet_id" - case ownerID = "owner_id" - } -} - -extension Collection where Element == Twitter.Request.Expansions { - public var queryItem: URLQueryItem { - let value = self.map { $0.rawValue }.joined(separator: ",") - return URLQueryItem(name: "expansions", value: value) - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+ListFields.swift b/TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+ListFields.swift deleted file mode 100644 index 13c94fe6..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+ListFields.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// Twitter+Request+ListFields.swift -// -// -// Created by MainasuK on 2022-2-28. -// - -import Foundation - -extension Twitter.Request { - public enum ListFields: String, CaseIterable { - case createdAt = "created_at" - case followerCount = "follower_count" - case memberCount = "member_count" - case `private` = "private" - case description = "description" - case ownerID = "owner_id" - } -} - -extension Collection where Element == Twitter.Request.ListFields { - public var queryItem: URLQueryItem { - let value = self.map { $0.rawValue }.joined(separator: ",") - return URLQueryItem(name: "list.fields", value: value) - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+MediaFields.swift b/TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+MediaFields.swift deleted file mode 100644 index 92c66854..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+MediaFields.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// Twitter+Request+MediaFields.swift -// TwitterAPI -// -// Created by Cirno MainasuK on 2020-9-16. -// - -import Foundation - -extension Twitter.Request { - public enum MediaFields: String, CaseIterable { - case durationMS = "duration_ms" - case height = "height" - case mediaKey = "media_key" - case previewImageURL = "preview_image_url" - case type = "type" - case url = "url" - case width = "width" - case publicMetrics = "public_metrics" - case nonPublicMetrics = "non_public_metrics" - case organicMetrics = "organic_metrics" - case promotedMetrics = "promoted_metrics" - case altText = "alt_text" - } -} - -extension Collection where Element == Twitter.Request.MediaFields { - public var queryItem: URLQueryItem { - let value = self.map { $0.rawValue }.joined(separator: ",") - return URLQueryItem(name: "media.fields", value: value) - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+PlaceFields.swift b/TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+PlaceFields.swift deleted file mode 100644 index 61f547a2..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+PlaceFields.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// Twitter+Request+PlaceFields.swift -// TwitterAPI -// -// Created by Cirno MainasuK on 2020-9-16. -// - -import Foundation - -extension Twitter.Request { - public enum PlaceFields: String, CaseIterable { - case containedWithin = "contained_within" - case country = "country" - case countryCode = "country_code" - case fullName = "full_name" - case geo = "geo" - case id = "id" - case name = "name" - case placeType = "place_type" - } -} - -extension Collection where Element == Twitter.Request.PlaceFields { - public var queryItem: URLQueryItem { - let value = self.map { $0.rawValue }.joined(separator: ",") - return URLQueryItem(name: "place.fields", value: value) - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+PollFields.swift b/TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+PollFields.swift deleted file mode 100644 index db38f062..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+PollFields.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// Twitter+Request+PollFields.swift -// TwitterAPI -// -// Created by Cirno MainasuK on 2020-9-16. -// - -import Foundation - -extension Twitter.Request { - public enum PollFields: String, CaseIterable { - case durationMinutes = "duration_minutes" - case endDatetime = "end_datetime" - case id = "id" - case options = "options" - case votingStatus = "voting_status" - } -} - -extension Collection where Element == Twitter.Request.PollFields { - public var queryItem: URLQueryItem { - let value = self.map { $0.rawValue }.joined(separator: ",") - return URLQueryItem(name: "poll.fields", value: value) - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+TweetFields.swift b/TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+TweetFields.swift deleted file mode 100644 index 526c17d0..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+TweetFields.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// Twitter+Request+TweetFields.swift -// TwitterAPI -// -// Created by Cirno MainasuK on 2020-9-15. -// - -import Foundation - -extension Twitter.Request { - public enum TwitterFields: String, CaseIterable { - case attachments = "attachments" - case authorID = "author_id" - case contextAnnotations = "context_annotations" - case conversationID = "conversation_id" - case created_at = "created_at" - case entities = "entities" - case geo = "geo" - case id = "id" - case inReplyToUserID = "in_reply_to_user_id" - case lang = "lang" - case nonPublicMetrics = "non_public_metrics" - case publicMetrics = "public_metrics" - case organicMetrics = "organic_metrics" - case promotedMetrics = "promoted_metrics" - case possiblySensitive = "possibly_sensitive" - case referencedTweets = "referenced_tweets" - case replySettings = "reply_settings" - case source = "source" - case text = "text" - case withheld = "withheld" - } -} - -extension Collection where Element == Twitter.Request.TwitterFields { - public var queryItem: URLQueryItem { - let value = self.map { $0.rawValue }.joined(separator: ",") - return URLQueryItem(name: "tweet.fields", value: value) - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+UserFields.swift b/TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+UserFields.swift deleted file mode 100644 index 2bb1adc8..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request+UserFields.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// Twitter+Request+UserFields.swift -// TwitterAPI -// -// Created by Cirno MainasuK on 2020-9-16. -// - -import Foundation - -extension Twitter.Request { - public enum UserFields: String, CaseIterable { - case createdAt = "created_at" - case description = "description" - case entities = "entities" - case id = "id" - case location = "location" - case name = "name" - case pinnedTweetID = "pinned_tweet_id" - case profileImageURL = "profile_image_url" - case protected = "protected" - case publicMetrics = "public_metrics" - case url = "url" - case username = "username" - case verified = "verified" - case withheld = "withheld" - } -} - -extension Collection where Element == Twitter.Request.UserFields { - public var queryItem: URLQueryItem { - let value = self.map { $0.rawValue }.joined(separator: ",") - return URLQueryItem(name: "user.fields", value: value) - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request.swift b/TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request.swift deleted file mode 100644 index 7075ac53..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Request/Twitter+Request.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -// Twitter+Request.swift -// TwitterAPI -// -// Created by Cirno MainasuK on 2020-9-15. -// - -import Foundation - -extension Twitter.Request { - -} - -// TODO: unit tests -extension Twitter.Request { - static let expansions: [Twitter.Request.Expansions] = [ - .attachmentsPollIDs, - .attachmentsMediaKeys, - .authorID, - .entitiesMentionsUsername, - .geoPlaceID, - .inReplyToUserID, - .referencedTweetsID, - .referencedTweetsIDAuthorID - ] - static let tweetsFields: [Twitter.Request.TwitterFields] = [ - .attachments, - .authorID, - .contextAnnotations, - .conversationID, - .created_at, - .entities, - .geo, - .id, - .inReplyToUserID, - .lang, - .publicMetrics, - .possiblySensitive, - .referencedTweets, - .replySettings, - .source, - .text, - .withheld, - ] - static let userFields: [Twitter.Request.UserFields] = [ - .createdAt, - .description, - .entities, - .id, - .location, - .name, - .pinnedTweetID, - .profileImageURL, - .protected, - .publicMetrics, - .url, - .username, - .verified, - .withheld - ] - static let mediaFields: [Twitter.Request.MediaFields] = [ - .durationMS, - .height, - .mediaKey, - .previewImageURL, - .type, - .url, - .width, - .publicMetrics, - .altText - ] - static let placeFields: [Twitter.Request.PlaceFields] = [ - .containedWithin, - .country, - .countryCode, - .fullName, - .geo, - .id, - .name, - .placeType, - ] - static let pollFields: [Twitter.Request.PollFields] = [ - .durationMinutes, - .endDatetime, - .id, - .options, - .votingStatus, - ] - static let listFields: [Twitter.Request.ListFields] = [ - .createdAt, - .followerCount, - .memberCount, - .private, - .description, - .ownerID, - ] -} diff --git a/TwidereSDK/Sources/TwitterSDK/Response/Twitter+Response.swift b/TwidereSDK/Sources/TwitterSDK/Response/Twitter+Response.swift deleted file mode 100644 index e041a902..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Response/Twitter+Response.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// Twitter+Response.swift -// -// -// Created by Cirno MainasuK on 2020-10-19. -// - -import Foundation - -extension Twitter.Response { - public struct Content { - // entity - public let value: T - - // standard fields - public let date: Date? - - // application fields - public let rateLimit: Twitter.Response.RateLimit? - public let responseTime: Int? - - public var networkDate: Date { - return date ?? Date() - } - - public init(value: T, response: URLResponse) { - self.value = value - - self.date = { - guard let string = (response as? HTTPURLResponse)?.value(forHTTPHeaderField: "date") else { return nil } - return Twitter.API.httpHeaderDateFormatter.date(from: string) - }() - - self.rateLimit = RateLimit(response: response) - self.responseTime = { - guard let string = (response as? HTTPURLResponse)?.value(forHTTPHeaderField: "x-response-time") else { return nil } - return Int(string) - }() - } - - init(value: T, old: Twitter.Response.Content) { - self.value = value - self.date = old.date - self.rateLimit = old.rateLimit - self.responseTime = old.responseTime - } - } -} - -extension Twitter.Response.Content { - public func map(_ transform: (T) -> R) -> Twitter.Response.Content { - return Twitter.Response.Content(value: transform(value), old: self) - } -} - -extension Twitter.Response { - public struct RateLimit { - public let limit: Int - public let remaining: Int - public let reset: Date - - public init(limit: Int, remaining: Int, reset: Date) { - self.limit = limit - self.remaining = remaining - self.reset = reset - } - - public init?(response: URLResponse) { - guard let response = response as? HTTPURLResponse else { - return nil - } - - guard let limitString = response.value(forHTTPHeaderField: "x-rate-limit-limit"), - let limit = Int(limitString), - let remainingString = response.value(forHTTPHeaderField: "x-rate-limit-remaining"), - let remaining = Int(remainingString) else { - return nil - } - - guard let resetTimestampString = response.value(forHTTPHeaderField: "x-rate-limit-reset"), - let resetTimestamp = Int(resetTimestampString) else { - return nil - } - let reset = Date(timeIntervalSince1970: Double(resetTimestamp)) - - self.init(limit: limit, remaining: remaining, reset: reset) - } - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Response/V2/Twitter+Response+V2+ContentError.swift b/TwidereSDK/Sources/TwitterSDK/Response/V2/Twitter+Response+V2+ContentError.swift deleted file mode 100644 index 3ace1ce5..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Response/V2/Twitter+Response+V2+ContentError.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// Twitter+Response+V2+ContentError.swift -// -// -// Created by Cirno MainasuK on 2020-12-22. -// - -import Foundation - -extension Twitter.Response.V2 { - public struct ContentError: Codable { - public let detail: String - public let title: String - public let resourceType: String - public let parameter: String - public let value: String - public let type: String - - public enum CodingKeys: String, CodingKey { - case detail - case title - case resourceType = "resource_type" - case parameter - case value - case type - } - } -} diff --git a/TwidereSDK/Sources/TwitterSDK/Response/V2/Twitter+Response+V2+DictContent.swift b/TwidereSDK/Sources/TwitterSDK/Response/V2/Twitter+Response+V2+DictContent.swift deleted file mode 100644 index e59de593..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Response/V2/Twitter+Response+V2+DictContent.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// Twitter+Response+V2+DictContent.swift -// -// -// Created by Cirno MainasuK on 2020-10-19. -// - -import Foundation -import OrderedCollections - -extension Twitter.Response.V2 { - - public class DictContent { - public let tweetDict: OrderedDictionary - public let userDict: OrderedDictionary - public let mediaDict: OrderedDictionary - public let placeDict: OrderedDictionary - public let pollDict: OrderedDictionary - - public init( - tweetDict: OrderedDictionary, - userDict: OrderedDictionary, - mediaDict: OrderedDictionary, - placeDict: OrderedDictionary, - pollDict: OrderedDictionary - ) { - self.tweetDict = tweetDict - self.userDict = userDict - self.mediaDict = mediaDict - self.placeDict = placeDict - self.pollDict = pollDict - } - - public convenience init( - tweets: [Twitter.Entity.V2.Tweet], - users: [Twitter.Entity.V2.User], - media: [Twitter.Entity.V2.Media], - places: [Twitter.Entity.V2.Place], - polls: [Twitter.Entity.V2.Tweet.Poll] - ) { - self.init( - tweetDict: Twitter.Response.V2.DictContent.collect(array: tweets), - userDict: Twitter.Response.V2.DictContent.collect(array: users), - mediaDict: Twitter.Response.V2.DictContent.collect(array: media), - placeDict: Twitter.Response.V2.DictContent.collect(array: places), - pollDict: Twitter.Response.V2.DictContent.collect(array: polls) - ) - } - } - -} - -extension Twitter.Response.V2.DictContent { - - private static func collect(array: [T]) -> OrderedDictionary { - var dict: OrderedDictionary = [:] - for element in array { - guard dict[element.id] == nil else { - continue - } - dict[element.id] = element - } - return dict - } - -} - -extension Twitter.Response.V2.DictContent { - - public func media(for tweet: Twitter.Entity.V2.Tweet) -> [Twitter.Entity.V2.Media]? { - guard let mediaKeys = tweet.attachments?.mediaKeys else { return nil } - var array: [Twitter.Entity.V2.Media] = [] - for mediaKey in mediaKeys { - guard let media = mediaDict[mediaKey] else { continue } - array.append(media) - } - guard !array.isEmpty else { return nil } - return array - } - - public func place(for tweet: Twitter.Entity.V2.Tweet) -> Twitter.Entity.V2.Place? { - guard let placeID = tweet.geo?.placeID else { return nil } - return placeDict[placeID] - } - - public func poll(for tweet: Twitter.Entity.V2.Tweet) -> Twitter.Entity.V2.Tweet.Poll? { - guard let pollID = tweet.attachments?.pollIDs?.first else { return nil } - return pollDict[pollID] - } - -} diff --git a/TwidereSDK/Sources/TwitterSDK/Twitter.swift b/TwidereSDK/Sources/TwitterSDK/Twitter.swift deleted file mode 100644 index 8a2811af..00000000 --- a/TwidereSDK/Sources/TwitterSDK/Twitter.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// Twitter.swift -// TwitterAPI -// -// Created by Cirno MainasuK on 2020-9-1. -// - -import Foundation - -public enum Twitter { - public enum Request { } - public enum Response { - public enum V2 { } - } - public enum API { } - public enum Entity { - public enum V2 { } - } -} diff --git a/TwidereSDK/Sources/TwitterSDKTests/TwitterSDKTests.swift b/TwidereSDK/Sources/TwitterSDKTests/TwitterSDKTests.swift deleted file mode 100644 index 71bc10da..00000000 --- a/TwidereSDK/Sources/TwitterSDKTests/TwitterSDKTests.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// TwitterSDKTests.swift -// TwitterSDKTests -// -// Created by Cirno MainasuK on 2021-8-12. -// - -import os.log -import XCTest -@testable import TwitterSDK -import CommonOSLog - -final class TwitterSDKTests: XCTestCase { - - let logger = Logger() - - func testSmoke() throws { } -} - -// Note: -// only for unit test! -// https://gist.github.com/shobotch/5160017 -extension TwitterSDKTests { - - var consumerKey: String { "3rJOl1ODzm9yZy63FACdg" } - var consumerSecret: String { "5jPoQ5kQvMJFDYRNE8bQ4rHuds4xJqhvgNJM4awaE8" } - - func testOAuthRequestToken() async throws { -// let query = Twitter.API.OAuth.RequestTokenQuery(consumerKey: consumerKey, consumerSecret: consumerSecret) -// let response = try await Twitter.API.OAuth.requestToken(session: URLSession.shared, query: query) -// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): response: \n\(response.debugDescription)") - } -} diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index f2e3bbfa..85db4f63 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -580,6 +580,7 @@ DB47AB1F27CCC18500CD73C7 /* ListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListItem.swift; sourceTree = ""; }; DB47AB2027CCC18500CD73C7 /* ListSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListSection.swift; sourceTree = ""; }; DB47AB2C27CE085800CD73C7 /* ListViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ListViewModel+Diffable.swift"; sourceTree = ""; }; + DB49977E29F25E83001C0843 /* TwitterSDK */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TwitterSDK; path = ../TwitterSDK; sourceTree = ""; }; DB51DC192715581E00A0D8FB /* ProfileDashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDashboardView.swift; sourceTree = ""; }; DB51DC1B2715588E00A0D8FB /* ProfileDashboardMeterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDashboardMeterView.swift; sourceTree = ""; }; DB51DC372716AEF000A0D8FB /* TabBarPager */ = {isa = PBXFileReference; lastKnownFileType = folder; path = TabBarPager; sourceTree = ""; }; @@ -2037,6 +2038,7 @@ DBDA8E1524FCF8A3006750DC = { isa = PBXGroup; children = ( + DB49977E29F25E83001C0843 /* TwitterSDK */, DBE399D829EFE3A7008FA278 /* CoverFlowStackLayout */, DB51DC372716AEF000A0D8FB /* TabBarPager */, DB43239D251491EB004FAAEC /* TwidereSDK */, diff --git a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift index fe2229ab..ce82cb70 100644 --- a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift @@ -128,6 +128,8 @@ extension NotificationTimelineViewModel { do { switch (scope, authContext.authenticationContext) { case (.twitter, .twitter(let authenticationContext)): + throw AppError.implicit(.badRequest) + _ = try await context.apiService.twitterMentionTimeline( query: Twitter.API.Statuses.Timeline.TimelineQuery( maxID: nil diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift index cbf83323..9e715f24 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift @@ -375,32 +375,29 @@ extension StatusThreadViewModel { return } logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch conversation for \(conversationRootStatusID), cursor: \(cursor.value ?? "")") - let guestAuthorization = try await authContext.twitterGuestAuthorization() let response = try await context.apiService.twitterStatusConversation( conversationRootStatusID: conversationRootStatusID, query: .init(cursor: cursor.value), - guestAuthentication: guestAuthorization, authenticationContext: authenticationContext ) // update cursor - if let cursor = response.value.timeline.topCursor { + if let cursor = response.value.topCursor { self.topCursor = .value(cursor) } else { self.topCursor = .noMore } - if let cursor = response.value.timeline.bottomCursor { + if let cursor = response.value.bottomCursor { self.bottomCursor = .value(cursor) } else { self.bottomCursor = .noMore } - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch conversation: \(response.value.globalObjects.tweets.count) tweets, \(response.value.globalObjects.users.count) users, top cursor: \(response.value.timeline.topCursor ?? ""), bottom cursor: \(response.value.timeline.bottomCursor ?? ""), timeline entries \(response.value.timeline.entries.count)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch conversation success, top cursor: \(response.value.topCursor ?? ""), bottom cursor: \(response.value.bottomCursor ?? "")") - let timeline = response.value.timeline let statusDict: [Twitter.Entity.V2.Tweet.ID: ManagedObjectRecord] = { var dict: [TwitterStatus.ID: ManagedObjectRecord] = [:] let request = TwitterStatus.sortedFetchRequest - let statusIDs = response.value.globalObjects.tweets.map { $0.idStr } + let statusIDs = response.value.statusIDs request.predicate = TwitterStatus.predicate(ids: statusIDs) let result = try? context.managedObjectContext.fetch(request) for status in result ?? [] { @@ -411,35 +408,25 @@ extension StatusThreadViewModel { }() let topThreads: [Thread] = { var threads: [Thread] = [] - for entry in timeline.entries { - switch entry { - case .tweet(let statusID): - guard let status = statusDict[statusID] else { - continue - } - threads.append(.selfThread(status: .twitter(record: status))) - default: + for statusID in response.value.data.thread { + guard let status = statusDict[statusID] else { continue } + threads.append(.selfThread(status: .twitter(record: status))) } return threads }() let bottomThreads: [Thread] = { var threads: [Thread] = [] - for entry in timeline.entries { - switch entry { - case .conversationThread(let componentIDs): - let components = componentIDs - .compactMap { statusDict[$0] } - .map { StatusRecord.twitter(record: $0) } - guard !components.isEmpty else { - assertionFailure() - continue - } - threads.append(.conversationThread(components: components)) - default: + for array in response.value.data.consersation { + let components = array + .compactMap { statusDict[$0] } + .map { StatusRecord.twitter(record: $0) } + guard !components.isEmpty else { + assertionFailure() continue } + threads.append(.conversationThread(components: components)) } return threads }() From cd1162e45351ac6742f6cbd7d2c98a841db78f00 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 25 Apr 2023 20:10:17 +0800 Subject: [PATCH 062/128] feat: hide the home and notification tabs. --- TwidereSDK/Package.swift | 2 +- .../Twitter/Extension/TwitterEntity.swift | 2 +- .../APIService/APIService+Status+Search.swift | 30 +------ ...StatusFetchViewModel+Timeline+Search.swift | 4 + .../StatusFetchViewModel+Timeline+User.swift | 1 + .../StatusFetchViewModel+Timeline.swift | 1 + .../Container/MediaGridContainerView.swift | 2 - .../Container/MediaStackContainerView.swift | 2 - .../GIFVideoPlayerRepresentable.swift | 15 ++++ TwidereX.xcodeproj/project.pbxproj | 2 - .../xcshareddata/swiftpm/Package.resolved | 9 ++ .../Status/StatusMediaGallerySection.swift | 1 - .../Scene/Profile/ProfileViewController.swift | 45 +--------- .../Paging/ProfilePagingViewModel.swift | 82 ++++++++++++++++--- .../Root/MainTab/MainTabBarController.swift | 27 ++++-- .../SearchResult/SearchResultViewModel.swift | 2 +- 16 files changed, 127 insertions(+), 100 deletions(-) diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index 5c50c10b..4ce7e9fa 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -51,7 +51,7 @@ let package = Package( .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.6.0"), .package(url: "https://github.com/tid-kijyun/Kanna.git", from: "5.2.7"), .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"), - .package(url: "https://github.com/TwidereProject/TwitterSDK.git", from: "0.1.0"), + .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.2.0"), .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"), .package(name: "CoverFlowStackLayout", path: "../CoverFlowStackLayout"), ], diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Extension/TwitterEntity.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Extension/TwitterEntity.swift index e217490f..a622a664 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Extension/TwitterEntity.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Extension/TwitterEntity.swift @@ -102,7 +102,7 @@ extension TwitterEntity.URLEntity { ) } - public init(entity: Twitter.Entity.V2.Entities.URL) { + public init(entity: Twitter.Entity.V2.Entities.URLNode) { self.init( start: entity.start, end: entity.end, diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Search.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Search.swift index 7dffce2e..60defdce 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Search.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Search.swift @@ -180,35 +180,7 @@ extension APIService { ) ) } // end .performChanges { … } - - // query and update entity video/GIF attribute from V1 API - do { - let statusIDs: [Twitter.Entity.Tweet.ID] = { - var statusIDs: Set = Set() - for status in response.value.data ?? [] { - guard let mediaKeys = status.attachments?.mediaKeys else { continue } - for mediaKey in mediaKeys { - guard let media = dictionary.mediaDict[mediaKey], - media.attachmentKind == .video || media.attachmentKind == .animatedGIF - else { continue } - - statusIDs.insert(status.id) - } - } - return Array(statusIDs) - }() - if !statusIDs.isEmpty { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch \(statusIDs.count) missing assetURL from V1 API…") - _ = try await twitterStatusV1( - statusIDs: statusIDs, - authenticationContext: authenticationContext - ) - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch missing assetURL from V1 API success") - } - } catch { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch missing assetURL from V1 API fail: \(error.localizedDescription)") - } - + return response } diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Search.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Search.swift index 3354d29d..5bd76964 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Search.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Search.swift @@ -134,6 +134,9 @@ extension StatusFetchViewModel.Timeline.Search { switch self { case .v2(let response): guard let nextToken = response.value.meta.nextToken else { return nil } + guard fetchContext.nextToken != nextToken else { return nil } + guard let data = response.value.data, data.count > 1 else { return nil } + let fetchContext = fetchContext.map(untilID: response.value.meta.oldestID, nextToken: nextToken) return .twitter(fetchContext) case .v1(let response): @@ -177,6 +180,7 @@ extension StatusFetchViewModel.Timeline.Search { startTime: nil, nextToken: fetchContext.nextToken ) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Search] Searching…") let response = try await api.searchTwitterStatus( query: query, authenticationContext: fetchContext.authenticationContext diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift index 97f9eb2e..c847bfeb 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift @@ -137,6 +137,7 @@ extension StatusFetchViewModel.Timeline.User { switch self { case .v2(let response): guard let nextToken = response.value.meta.nextToken else { return nil } + guard nextToken != fetchContext.paginationToken else { return nil } let fetchContext = fetchContext.map(paginationToken: nextToken) return .twitter(fetchContext) case .v1(let response): diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift index bd8fef76..df62f8a1 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift @@ -7,6 +7,7 @@ import os.log import CoreData +import Combine import Foundation import TwitterSDK import MastodonSDK diff --git a/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift b/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift index f085a4f6..a7b71b96 100644 --- a/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift +++ b/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift @@ -193,8 +193,6 @@ extension MediaGridContainerView { }) .overlay(alignment: .bottom) { MediaMetaIndicatorView(viewModel: viewModels[index]) - .padding(EdgeInsets(top: 0, leading: 11, bottom: 8, trailing: 11)) - .allowsHitTesting(false) } .overlay( RoundedRectangle(cornerRadius: MediaGridContainerView.cornerRadius) diff --git a/TwidereSDK/Sources/TwidereUI/Container/MediaStackContainerView.swift b/TwidereSDK/Sources/TwidereUI/Container/MediaStackContainerView.swift index e66d9715..2fc08603 100644 --- a/TwidereSDK/Sources/TwidereUI/Container/MediaStackContainerView.swift +++ b/TwidereSDK/Sources/TwidereUI/Container/MediaStackContainerView.swift @@ -34,8 +34,6 @@ public struct MediaStackContainerView: View { .clipShape(RoundedRectangle(cornerRadius: 12)) .overlay(alignment: .bottom) { MediaMetaIndicatorView(viewModel: item) - .padding(EdgeInsets(top: 0, leading: 11, bottom: 8, trailing: 11)) - .allowsHitTesting(false) } .overlay( RoundedRectangle(cornerRadius: 12) diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/GIFVideoPlayerRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/GIFVideoPlayerRepresentable.swift index a923716a..c59eb607 100644 --- a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/GIFVideoPlayerRepresentable.swift +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/GIFVideoPlayerRepresentable.swift @@ -5,10 +5,12 @@ // Created by MainasuK on 2023/2/28. // +import os.log import UIKit import SwiftUI import Combine import AVKit +import AVFoundation public struct GIFVideoPlayerRepresentable: UIViewRepresentable { @@ -78,6 +80,19 @@ public struct GIFVideoPlayerRepresentable: UIViewRepresentable { } .store(in: &disposeBag) } // end func + + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + disposeBag.removeAll() + + player?.pause() + player = nil + playerLooper?.disableLooping() + playerLooper = nil + representable.controller.player = nil + representable.controller.removeFromParent() + representable.controller.view.removeFromSuperview() + } } // end Coordinator } diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 85db4f63..f2e3bbfa 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -580,7 +580,6 @@ DB47AB1F27CCC18500CD73C7 /* ListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListItem.swift; sourceTree = ""; }; DB47AB2027CCC18500CD73C7 /* ListSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListSection.swift; sourceTree = ""; }; DB47AB2C27CE085800CD73C7 /* ListViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ListViewModel+Diffable.swift"; sourceTree = ""; }; - DB49977E29F25E83001C0843 /* TwitterSDK */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TwitterSDK; path = ../TwitterSDK; sourceTree = ""; }; DB51DC192715581E00A0D8FB /* ProfileDashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDashboardView.swift; sourceTree = ""; }; DB51DC1B2715588E00A0D8FB /* ProfileDashboardMeterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDashboardMeterView.swift; sourceTree = ""; }; DB51DC372716AEF000A0D8FB /* TabBarPager */ = {isa = PBXFileReference; lastKnownFileType = folder; path = TabBarPager; sourceTree = ""; }; @@ -2038,7 +2037,6 @@ DBDA8E1524FCF8A3006750DC = { isa = PBXGroup; children = ( - DB49977E29F25E83001C0843 /* TwitterSDK */, DBE399D829EFE3A7008FA278 /* CoverFlowStackLayout */, DB51DC372716AEF000A0D8FB /* TabBarPager */, DB43239D251491EB004FAAEC /* TwidereSDK */, diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index 26d5ab46..7347332a 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -253,6 +253,15 @@ "version": "0.0.3" } }, + { + "package": "TwitterSDK", + "repositoryURL": "https://github.com/TwidereProject/TwitterSDK.git", + "state": { + "branch": null, + "revision": "da05cf601b1dccdac4c83421fca58f336dd48d3e", + "version": "0.2.0" + } + }, { "package": "UITextView+Placeholder", "repositoryURL": "https://github.com/MainasuK/UITextView-Placeholder.git", diff --git a/TwidereX/Diffable/Status/StatusMediaGallerySection.swift b/TwidereX/Diffable/Status/StatusMediaGallerySection.swift index 66ee487c..89a9e713 100644 --- a/TwidereX/Diffable/Status/StatusMediaGallerySection.swift +++ b/TwidereX/Diffable/Status/StatusMediaGallerySection.swift @@ -40,7 +40,6 @@ extension StatusMediaGallerySection { MediaStackContainerView(viewModel: viewModel) } .margins(.vertical, 0) // remove vertical margins - } } diff --git a/TwidereX/Scene/Profile/ProfileViewController.swift b/TwidereX/Scene/Profile/ProfileViewController.swift index 48bcbe7a..3adade2f 100644 --- a/TwidereX/Scene/Profile/ProfileViewController.swift +++ b/TwidereX/Scene/Profile/ProfileViewController.swift @@ -52,51 +52,12 @@ final class ProfileViewController: UIViewController, NeedsDependency, DrawerSide }() private(set) lazy var profilePagingViewController: ProfilePagingViewController = { let profilePagingViewController = ProfilePagingViewController() - - let userTimelineViewModel = UserTimelineViewModel( + profilePagingViewController.viewModel = ProfilePagingViewModel( context: context, authContext: authContext, - timelineContext: .init( - timelineKind: .status, - userIdentifier: viewModel.$userIdentifier - ) + coordinator: coordinator, + userIdentifier: viewModel.$userIdentifier ) - userTimelineViewModel.isFloatyButtonDisplay = false - - let userMediaTimelineViewModel = UserMediaTimelineViewModel( - context: context, - authContext: authContext, - timelineContext: .init( - timelineKind: .media, - userIdentifier: viewModel.$userIdentifier - ) - ) - userMediaTimelineViewModel.isFloatyButtonDisplay = false - - let userLikeTimelineViewModel = UserTimelineViewModel( - context: context, - authContext: authContext, - timelineContext: .init( - timelineKind: .like, - userIdentifier: viewModel.$userIdentifier - ) - ) - userLikeTimelineViewModel.isFloatyButtonDisplay = false - - profilePagingViewController.viewModel = { - let profilePagingViewModel = ProfilePagingViewModel( - userTimelineViewModel: userTimelineViewModel, - userMediaTimelineViewModel: userMediaTimelineViewModel, - userLikeTimelineViewModel: userLikeTimelineViewModel - ) - profilePagingViewModel.viewControllers.forEach { viewController in - if let viewController = viewController as? NeedsDependency { - viewController.context = context - viewController.coordinator = coordinator - } - } - return profilePagingViewModel - }() return profilePagingViewController }() diff --git a/TwidereX/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift b/TwidereX/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift index fb3ddd27..3ef4358a 100644 --- a/TwidereX/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift +++ b/TwidereX/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift @@ -13,30 +13,88 @@ import TabBarPager final class ProfilePagingViewModel: NSObject { // input + let context: AppContext + let authContext: AuthContext @Published var displayLikeTimeline: Bool = true // output - let homeTimelineViewController = UserTimelineViewController() - let mediaTimelineViewController = UserMediaTimelineViewController() - let likeTimelineViewController = UserLikeTimelineViewController() + let userTimelineViewController: UserTimelineViewController + let mediaTimelineViewController: UserMediaTimelineViewController + let likeTimelineViewController: UserLikeTimelineViewController? init( - userTimelineViewModel: UserTimelineViewModel, - userMediaTimelineViewModel: UserMediaTimelineViewModel, - userLikeTimelineViewModel: UserTimelineViewModel + context: AppContext, + authContext: AuthContext, + coordinator: SceneCoordinator, + userIdentifier: Published.Publisher? ) { - homeTimelineViewController.viewModel = userTimelineViewModel - mediaTimelineViewController.viewModel = userMediaTimelineViewModel - likeTimelineViewController.viewModel = userLikeTimelineViewModel + self.context = context + self.authContext = authContext + self.userTimelineViewController = { + let viewController = UserTimelineViewController() + let viewModel = UserTimelineViewModel( + context: context, + authContext: authContext, + timelineContext: .init( + timelineKind: .status, + userIdentifier: userIdentifier + ) + ) + viewModel.isFloatyButtonDisplay = false + viewController.context = context + viewController.coordinator = coordinator + viewController.viewModel = viewModel + return viewController + }() + self.mediaTimelineViewController = { + let viewController = UserMediaTimelineViewController() + let viewModel = UserMediaTimelineViewModel( + context: context, + authContext: authContext, + timelineContext: .init( + timelineKind: .media, + userIdentifier: userIdentifier + ) + ) + viewModel.isFloatyButtonDisplay = false + viewController.context = context + viewController.coordinator = coordinator + viewController.viewModel = viewModel + return viewController + }() + self.likeTimelineViewController = { + switch authContext.authenticationContext { + case .twitter: return nil + default: break + } + let viewController = UserTimelineViewController() + let viewModel = UserTimelineViewModel( + context: context, + authContext: authContext, + timelineContext: .init( + timelineKind: .like, + userIdentifier: userIdentifier + ) + ) + viewModel.isFloatyButtonDisplay = false + viewController.context = context + viewController.coordinator = coordinator + viewController.viewModel = viewModel + return viewController + }() super.init() + // end init } var viewControllers: [UIViewController & TabBarPage] { - return [ - homeTimelineViewController, + var viewControllers: [UIViewController & TabBarPage] = [ + userTimelineViewController, mediaTimelineViewController, - likeTimelineViewController, ] + if let likeTimelineViewController = likeTimelineViewController { + viewControllers.append(likeTimelineViewController) + } + return viewControllers } deinit { diff --git a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift index f5b8c011..0559eff2 100644 --- a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift +++ b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift @@ -27,13 +27,8 @@ final class MainTabBarController: UITabBarController, NeedsDependency { private let doubleTapGestureRecognizer = UITapGestureRecognizer.doubleTapGestureRecognizer - @Published var tabs: [TabBarItem] = [ - .home, - .notification, - .search, - .me, - ] - @Published var currentTab: TabBarItem = .home + @Published var tabs: [TabBarItem] + @Published var currentTab: TabBarItem static var popToRootAfterActionTolerance: TimeInterval { 0.5 } var lastPopToRootTime = CACurrentMediaTime() @@ -47,6 +42,24 @@ final class MainTabBarController: UITabBarController, NeedsDependency { self.context = context self.coordinator = coordinator self.authContext = authContext + let tabs: [TabBarItem] = { + switch authContext.authenticationContext { + case .twitter: + return [ + .search, + .me, + ] + case .mastodon: + return [ + .home, + .notification, + .search, + .me, + ] + } // end switch + }() + self.tabs = tabs + self.currentTab = tabs.first ?? .me super.init(nibName: nil, bundle: nil) UserDefaults.shared.publisher(for: \.tabBarTapScrollPreference) diff --git a/TwidereX/Scene/Search/SearchResult/SearchResultViewModel.swift b/TwidereX/Scene/Search/SearchResult/SearchResultViewModel.swift index e61a5549..3ffee2dc 100644 --- a/TwidereX/Scene/Search/SearchResult/SearchResultViewModel.swift +++ b/TwidereX/Scene/Search/SearchResult/SearchResultViewModel.swift @@ -72,7 +72,7 @@ extension SearchResultViewModel { case .twitter(let authenticationContext): scopes = [ .status(title: L10n.Scene.Search.Tabs.tweets), - .media(title: L10n.Scene.Search.Tabs.media), + // .media(title: L10n.Scene.Search.Tabs.media), .user(title: L10n.Scene.Search.Tabs.users), ] userIdentifier = UserIdentifier.twitter(.init(id: authenticationContext.userID)) From 4dce71b10c299f3dc10d85254179f82bc9e7b829 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 27 Apr 2023 18:36:43 +0800 Subject: [PATCH 063/128] feat: migrate the home tab to list tab --- TwidereSDK/Package.swift | 2 +- .../CoreDataStack 7.xcdatamodel/contents | 4 +- .../Entity/Mastodon/MastodonList.swift | 8 + .../Entity/Twitter/TwitterList.swift | 8 + .../APIService/APIService+Status+List.swift | 40 +++- .../APIService/APIService+Status+Search.swift | 6 +- .../StatusFetchViewModel+Timeline+List.swift | 12 +- ...StatusFetchViewModel+Timeline+Search.swift | 4 +- .../StatusFetchViewModel+Timeline.swift | 1 + .../Container/MediaStackContainerView.swift | 103 ++++++--- .../ComposeContentViewController.swift | 57 ++--- TwidereX.xcodeproj/project.pbxproj | 16 ++ .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Diffable/Misc/TabBar/TabBarItem.swift | 33 +-- .../Status/StatusMediaGallerySection.swift | 9 +- ...HomeListStatusTimelineViewController.swift | 210 ++++++++++++++++++ .../HostListStatusTimelineViewModel.swift | 205 +++++++++++++++++ .../List/List/ListViewModel+Diffable.swift | 12 +- .../MediaPreviewViewController.swift | 10 +- .../Root/MainTab/MainTabBarController.swift | 1 + .../StatusMediaGalleryCollectionCell.swift | 138 +----------- .../TimelineViewModel+LoadOldestState.swift | 20 +- .../Grid/GridTimelineViewController.swift | 66 +----- .../List/ListStatusTimelineViewModel.swift | 5 + 24 files changed, 676 insertions(+), 298 deletions(-) create mode 100644 TwidereX/Scene/List/HomeList/HomeListStatusTimelineViewController.swift create mode 100644 TwidereX/Scene/List/HomeList/HostListStatusTimelineViewModel.swift diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index 4ce7e9fa..e1132126 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -51,7 +51,7 @@ let package = Package( .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.6.0"), .package(url: "https://github.com/tid-kijyun/Kanna.git", from: "5.2.7"), .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"), - .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.2.0"), + .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.3.0"), .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"), .package(name: "CoverFlowStackLayout", path: "../CoverFlowStackLayout"), ], diff --git a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 7.xcdatamodel/contents b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 7.xcdatamodel/contents index 5e397ba5..a55159d7 100644 --- a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 7.xcdatamodel/contents +++ b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 7.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -49,6 +49,7 @@ + @@ -208,6 +209,7 @@ + diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonList.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonList.swift index da871665..46978fcb 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonList.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonList.swift @@ -23,6 +23,9 @@ final public class MastodonList: NSManagedObject { // sourcery: autoUpdatableObject, autoGenerateProperty @NSManaged public private(set) var updatedAt: Date + // sourcery: autoUpdatableObject + @NSManaged public private(set) var activeAt: Date? + // many-to-one relationship // sourcery: autoGenerateRelationship @NSManaged public private(set) var owner: MastodonUser @@ -160,6 +163,11 @@ extension MastodonList: AutoUpdatableObject { self.updatedAt = updatedAt } } + public func update(activeAt: Date?) { + if self.activeAt != activeAt { + self.activeAt = activeAt + } + } // sourcery:end // public func update(`private`: Bool) { diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterList.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterList.swift index 733e895e..601ec544 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterList.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterList.swift @@ -32,6 +32,9 @@ final public class TwitterList: NSManagedObject { // sourcery: autoUpdatableObject, autoGenerateProperty @NSManaged public private(set) var updatedAt: Date + // sourcery: autoUpdatableObject + @NSManaged public private(set) var activeAt: Date? + // many-to-one relationship // sourcery: autoGenerateRelationship @NSManaged public private(set) var owner: TwitterUser @@ -167,6 +170,11 @@ extension TwitterList: AutoUpdatableObject { self.updatedAt = updatedAt } } + public func update(activeAt: Date?) { + if self.activeAt != activeAt { + self.activeAt = activeAt + } + } // sourcery:end public func update(`private`: Bool) { diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+List.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+List.swift index d1598b2a..a6e793d6 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+List.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+List.swift @@ -18,7 +18,7 @@ extension APIService { list: ManagedObjectRecord, query: Twitter.API.V2.Status.List.StatusesQuery, authenticationContext: TwitterAuthenticationContext - ) async throws -> Twitter.Response.Content { + ) async throws -> Twitter.Response.Content { let managedObjectContext = backgroundManagedObjectContext let _listID: TwitterList.ID? = await managedObjectContext.perform { @@ -36,14 +36,40 @@ extension APIService { authorization: authenticationContext.authorization ) - if let statusIDs = response.value.data?.compactMap({ $0.id }), !statusIDs.isEmpty { - assert(statusIDs.count <= 100) - _ = try await twitterStatus( - statusIDs: statusIDs, - authenticationContext: authenticationContext - ) + #if DEBUG + // log time cost + let start = CACurrentMediaTime() + defer { + // log rate limit + response.logRateLimit() + + let end = CACurrentMediaTime() + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: persist cost %.2fs", ((#file as NSString).lastPathComponent), #line, #function, end - start) } + #endif + let content = response.value + let dictionary = Twitter.Response.V2.DictContent( + tweets: [content.data, content.includes?.tweets].compactMap { $0 }.flatMap { $0 }, + users: content.includes?.users ?? [], + media: content.includes?.media ?? [], + places: content.includes?.places ?? [], + polls: content.includes?.polls ?? [] + ) + + try await managedObjectContext.performChanges { + let me = authenticationContext.authenticationRecord.object(in: managedObjectContext)?.user + + _ = Persistence.Twitter.persist( + in: managedObjectContext, + context: Persistence.Twitter.PersistContextV2( + dictionary: dictionary, + me: me, + networkDate: response.networkDate + ) + ) + } // end .performChanges { … } + return response } diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Search.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Search.swift index 60defdce..7ce1dcfe 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Search.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Search.swift @@ -100,7 +100,7 @@ extension APIService { searchText: String, nextToken: String?, authenticationContext: TwitterAuthenticationContext - ) async throws -> Twitter.Response.Content { + ) async throws -> Twitter.Response.Content { let query = Twitter.API.V2.Search.RecentTweetQuery( query: searchText, maxResults: APIService.defaultSearchCount, @@ -122,7 +122,7 @@ extension APIService { startTime: Date?, nextToken: String?, authenticationContext: TwitterAuthenticationContext - ) async throws -> Twitter.Response.Content { + ) async throws -> Twitter.Response.Content { let query = Twitter.API.V2.Search.RecentTweetQuery( query: "conversation_id:\(conversationID) (to:\(authorID) OR from:\(authorID))", maxResults: APIService.conversationSearchCount, @@ -139,7 +139,7 @@ extension APIService { public func searchTwitterStatus( query: Twitter.API.V2.Search.RecentTweetQuery, authenticationContext: TwitterAuthenticationContext - ) async throws -> Twitter.Response.Content { + ) async throws -> Twitter.Response.Content { let response = try await Twitter.API.V2.Search.recentTweet( session: session, query: query, diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+List.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+List.swift index f4fd0517..063051b5 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+List.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+List.swift @@ -7,6 +7,7 @@ import os.log import Foundation +import CoreData import CoreDataStack import TwitterSDK import MastodonSDK @@ -25,6 +26,7 @@ extension StatusFetchViewModel.Timeline.List { } public struct TwitterFetchContext: Hashable { + public let managedObjectContext: NSManagedObjectContext public let authenticationContext: TwitterAuthenticationContext public let list: ManagedObjectRecord public let paginationToken: String? @@ -35,6 +37,7 @@ extension StatusFetchViewModel.Timeline.List { public var needsAPIFallback = false public init( + managedObjectContext: NSManagedObjectContext, authenticationContext: TwitterAuthenticationContext, list: ManagedObjectRecord, paginationToken: String?, @@ -42,6 +45,7 @@ extension StatusFetchViewModel.Timeline.List { maxResults: Int?, filter: StatusFetchViewModel.Timeline.Filter ) { + self.managedObjectContext = managedObjectContext self.authenticationContext = authenticationContext self.list = list self.paginationToken = paginationToken @@ -52,6 +56,7 @@ extension StatusFetchViewModel.Timeline.List { func map(paginationToken: String) -> TwitterFetchContext { return TwitterFetchContext( + managedObjectContext: managedObjectContext, authenticationContext: authenticationContext, list: list, paginationToken: paginationToken, @@ -63,6 +68,7 @@ extension StatusFetchViewModel.Timeline.List { func map(maxID: Twitter.Entity.Tweet.ID) -> TwitterFetchContext { return TwitterFetchContext( + managedObjectContext: managedObjectContext, authenticationContext: authenticationContext, list: list, paginationToken: paginationToken, @@ -125,7 +131,7 @@ extension StatusFetchViewModel.Timeline.List { extension StatusFetchViewModel.Timeline.List { enum TwitterResponse { - case v2(Twitter.Response.Content) + case v2(Twitter.Response.Content) case v1(Twitter.Response.Content<[Twitter.Entity.Tweet]>) func filter(fetchContext: TwitterFetchContext) -> StatusFetchViewModel.Result { @@ -143,8 +149,8 @@ extension StatusFetchViewModel.Timeline.List { func backInput(fetchContext: TwitterFetchContext) -> Input? { switch self { case .v2(let response): - guard let nextToken = response.value.meta.previousToken else { return nil } - let fetchContext = fetchContext.map(paginationToken: nextToken) + guard let previousToken = response.value.meta.previousToken else { return nil } + let fetchContext = fetchContext.map(paginationToken: previousToken) return .twitter(fetchContext) case .v1: return nil diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Search.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Search.swift index 5bd76964..9160ef80 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Search.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Search.swift @@ -115,7 +115,7 @@ extension StatusFetchViewModel.Timeline.Search { extension StatusFetchViewModel.Timeline.Search { enum TwitterResponse { - case v2(Twitter.Response.Content) + case v2(Twitter.Response.Content) case v1(Twitter.Response.Content) func filter(fetchContext: TwitterFetchContext) -> StatusFetchViewModel.Result { @@ -186,7 +186,7 @@ extension StatusFetchViewModel.Timeline.Search { authenticationContext: fetchContext.authenticationContext ) return .v2(response) - } catch let error as Twitter.API.Error.ResponseError where error.twitterAPIError == .rateLimitExceeded { + } catch let error as Twitter.API.Error.ResponseError where error.twitterAPIError == .rateLimitExceeded || error.httpResponseStatus == .notFound { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Rate Limit] fallback to v1") let queryText: String = try { let searchText = fetchContext.searchText.trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift index df62f8a1..939a531e 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift @@ -381,6 +381,7 @@ extension StatusFetchViewModel.Timeline { throw AppError.implicit(.internal(reason: "Use invalid list record for Twitter list status lookup")) } return .list(.twitter(.init( + managedObjectContext: fetchContext.managedObjectContext, authenticationContext: authenticationContext, list: record, paginationToken: nil, diff --git a/TwidereSDK/Sources/TwidereUI/Container/MediaStackContainerView.swift b/TwidereSDK/Sources/TwidereUI/Container/MediaStackContainerView.swift index 2fc08603..1462e880 100644 --- a/TwidereSDK/Sources/TwidereUI/Container/MediaStackContainerView.swift +++ b/TwidereSDK/Sources/TwidereUI/Container/MediaStackContainerView.swift @@ -13,52 +13,85 @@ public struct MediaStackContainerView: View { @ObservedObject public private(set) var viewModel: ViewModel - public init(viewModel: MediaStackContainerView.ViewModel) { + public let handler: (MediaView.ViewModel, MediaView.ViewModel.Action) -> Void + + public init( + viewModel: MediaStackContainerView.ViewModel, + handler: @escaping (MediaView.ViewModel, MediaView.ViewModel.Action) -> Void + ) { self.viewModel = viewModel + self.handler = handler } public var body: some View { GeometryReader { root in let dimension = min(root.size.width, root.size.height) - CoverFlowStackScrollView { - HStack(spacing: .zero) { - ForEach(Array(viewModel.items.enumerated()), id: \.0) { index, item in - GeometryReader { geo in - let transformAttribute = viewModel.transformAttribute(at: index) - ZStack { - MediaView(viewModel: item) - .frame( - width: transformAttribute.transformFrame.width, - height: transformAttribute.transformFrame.height - ) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .overlay(alignment: .bottom) { - MediaMetaIndicatorView(viewModel: item) - } - .overlay( - RoundedRectangle(cornerRadius: 12) - .stroke(Color(uiColor: .placeholderText).opacity(0.5), lineWidth: 1) - ) - .offset( - x: transformAttribute.offsetX, - y: transformAttribute.offsetY - ) - } - .frame(width: dimension, height: dimension) - } - .frame(width: dimension, height: dimension) - .zIndex(Double(999 - index)) + switch viewModel.items.count { + case 1: + MediaView(viewModel: viewModel.items[0]) + .frame(width: dimension, height: dimension) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .overlay(alignment: .bottom) { + MediaMetaIndicatorView(viewModel: viewModel.items[0]) + } + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color(uiColor: .placeholderText).opacity(0.5), lineWidth: 1) + ) + .onTapGesture { + handler(viewModel.items[0], .preview) } - } // HStack - } contentOffsetDidUpdate: { contentOffset in - viewModel.contentOffset = contentOffset - } contentSizeDidUpdate: { contentSize in - viewModel.contentSize = contentSize - } // end ScrollView + default: + CoverFlowStackScrollView { + HStack(spacing: .zero) { + ForEach(Array(viewModel.items.enumerated()), id: \.0) { index, item in + mediaView(viewModel: item, at: index, dimension: dimension) + } // end ForEach + } // HStack + } contentOffsetDidUpdate: { contentOffset in + viewModel.contentOffset = contentOffset + } contentSizeDidUpdate: { contentSize in + viewModel.contentSize = contentSize + } // end CoverFlowStackScrollView + } // end switch } // end GeometryReader } } +extension MediaStackContainerView { + private func mediaView(viewModel: MediaView.ViewModel, at index: Int, dimension: CGFloat) -> some View { + GeometryReader { geo in + let transformAttribute = self.viewModel.transformAttribute(at: index) + ZStack { + MediaView(viewModel: viewModel) + .frame( + width: transformAttribute.transformFrame.width, + height: transformAttribute.transformFrame.height + ) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .overlay(alignment: .bottom) { + MediaMetaIndicatorView(viewModel: viewModel) + } + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color(uiColor: .placeholderText).opacity(0.5), lineWidth: 1) + ) + .offset( + x: transformAttribute.offsetX, + y: transformAttribute.offsetY + ) + } + .frame(width: dimension, height: dimension) + .onTapGesture { + handler(viewModel, .preview) + } + } + .frame(width: dimension, height: dimension) + .zIndex(Double(999 - index)) + } // GeometryReader + +} + extension MediaStackContainerView { public class ViewModel: ObservableObject { diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewController.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewController.swift index 9237acee..52de0679 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewController.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -81,34 +81,35 @@ extension ComposeContentViewController { hostingViewController.didMove(toParent: self) // mention - pick action - viewModel.mentionPickPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - guard let self = self else { return } - guard let authContext = self.viewModel.authContext else { return } - guard let primaryItem = self.viewModel.primaryMentionPickItem else { return } - - let mentionPickViewModel = MentionPickViewModel( - context: self.viewModel.context, - authContext: authContext, - primaryItem: primaryItem, - secondaryItems: self.viewModel.secondaryMentionPickItems - ) - let mentionPickViewController = MentionPickViewController() - mentionPickViewController.viewModel = mentionPickViewModel - mentionPickViewController.delegate = self - - let navigationController = AdaptiveStatusBarStyleNavigationController(rootViewController: mentionPickViewController) - navigationController.modalPresentationStyle = .pageSheet - if let sheetPresentationController = navigationController.sheetPresentationController { - sheetPresentationController.detents = [.medium(), .large()] - sheetPresentationController.selectedDetentIdentifier = .medium - sheetPresentationController.prefersScrollingExpandsWhenScrolledToEdge = false - sheetPresentationController.prefersGrabberVisible = true - } - self.present(navigationController, animated: true, completion: nil) - } - .store(in: &disposeBag) + // FIXME: TODO +// viewModel.mentionPickPublisher +// .receive(on: DispatchQueue.main) +// .sink { [weak self] _ in +// guard let self = self else { return } +// guard let authContext = self.viewModel.authContext else { return } +// guard let primaryItem = self.viewModel.primaryMentionPickItem else { return } +// +// let mentionPickViewModel = MentionPickViewModel( +// context: self.viewModel.context, +// authContext: authContext, +// primaryItem: primaryItem, +// secondaryItems: self.viewModel.secondaryMentionPickItems +// ) +// let mentionPickViewController = MentionPickViewController() +// mentionPickViewController.viewModel = mentionPickViewModel +// mentionPickViewController.delegate = self +// +// let navigationController = AdaptiveStatusBarStyleNavigationController(rootViewController: mentionPickViewController) +// navigationController.modalPresentationStyle = .pageSheet +// if let sheetPresentationController = navigationController.sheetPresentationController { +// sheetPresentationController.detents = [.medium(), .large()] +// sheetPresentationController.selectedDetentIdentifier = .medium +// sheetPresentationController.prefersScrollingExpandsWhenScrolledToEdge = false +// sheetPresentationController.prefersGrabberVisible = true +// } +// self.present(navigationController, animated: true, completion: nil) +// } +// .store(in: &disposeBag) // attachment - preview action viewModel.mediaPreviewPublisher diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index f2e3bbfa..fd5f2125 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -353,6 +353,8 @@ DBFA47212859C4F300C9FF7F /* UserLikeTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFA47202859C4F300C9FF7F /* UserLikeTimelineViewModel.swift */; }; DBFA4A2025A5924C00D51703 /* ListTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFA4A1F25A5924C00D51703 /* ListTimelineViewModel+Diffable.swift */; }; DBFADA8B2872BEDE00B512D6 /* TabBarPager in Frameworks */ = {isa = PBXBuildFile; productRef = DBFADA8A2872BEDE00B512D6 /* TabBarPager */; }; + DBFCA3F429F9185100B9DCA3 /* HomeListStatusTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCA3F329F9185100B9DCA3 /* HomeListStatusTimelineViewController.swift */; }; + DBFCA3F729F9191F00B9DCA3 /* HostListStatusTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCA3F629F9191F00B9DCA3 /* HostListStatusTimelineViewModel.swift */; }; DBFCC44725667C620016698E /* UILabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCC44625667C620016698E /* UILabel.swift */; }; DBFCEF1228939C9600EEBFB1 /* UserHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCEF1128939C9600EEBFB1 /* UserHistoryViewController.swift */; }; DBFCEF1528939CAC00EEBFB1 /* UserHistoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCEF1428939CAC00EEBFB1 /* UserHistoryViewModel.swift */; }; @@ -856,6 +858,8 @@ DBFA471E2859C4AE00C9FF7F /* UserLikeTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLikeTimelineViewController.swift; sourceTree = ""; }; DBFA47202859C4F300C9FF7F /* UserLikeTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLikeTimelineViewModel.swift; sourceTree = ""; }; DBFA4A1F25A5924C00D51703 /* ListTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ListTimelineViewModel+Diffable.swift"; sourceTree = ""; }; + DBFCA3F329F9185100B9DCA3 /* HomeListStatusTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeListStatusTimelineViewController.swift; sourceTree = ""; }; + DBFCA3F629F9191F00B9DCA3 /* HostListStatusTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostListStatusTimelineViewModel.swift; sourceTree = ""; }; DBFCC44625667C620016698E /* UILabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UILabel.swift; sourceTree = ""; }; DBFCC46225668B860016698E /* DrawerSidebarPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawerSidebarPresentationController.swift; sourceTree = ""; }; DBFCEF1128939C9600EEBFB1 /* UserHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserHistoryViewController.swift; sourceTree = ""; }; @@ -1347,6 +1351,7 @@ DB46D11B27DB2703003B8BA1 /* ListUser */, DB2C0BDB27DF14970033FC94 /* EditList */, DBA6B30F27E9CB7D004D052D /* AddListMember */, + DBFCA3F529F9185400B9DCA3 /* HomeList */, ); path = List; sourceTree = ""; @@ -2354,6 +2359,15 @@ path = Like; sourceTree = ""; }; + DBFCA3F529F9185400B9DCA3 /* HomeList */ = { + isa = PBXGroup; + children = ( + DBFCA3F329F9185100B9DCA3 /* HomeListStatusTimelineViewController.swift */, + DBFCA3F629F9191F00B9DCA3 /* HostListStatusTimelineViewModel.swift */, + ); + path = HomeList; + sourceTree = ""; + }; DBFCEF1328939C9900EEBFB1 /* User */ = { isa = PBXGroup; children = ( @@ -3026,6 +3040,7 @@ DB2FFF3E258B78B0003DBC19 /* AVPlayer.swift in Sources */, DBCB4047255B683B00DD8D8F /* AccountListTableViewCell.swift in Sources */, DBF639FC259B333A009E12C8 /* EmptyStateView.swift in Sources */, + DBFCA3F429F9185100B9DCA3 /* HomeListStatusTimelineViewController.swift in Sources */, DBA6B30C27E9B4CB004D052D /* DataSourceFacade+Banner.swift in Sources */, DB25C4C527798E1A00EC1435 /* DataSourceFacade+SavedSearch.swift in Sources */, DBA210332759D79C000B7CB2 /* FollowingListViewController.swift in Sources */, @@ -3040,6 +3055,7 @@ DB9EC4EA255A6BC7005403AA /* TwitterAccountUnlockViewController.swift in Sources */, DB1E48142772CE850074F6A0 /* SearchViewModel+Diffable.swift in Sources */, DB71C7D1271EB09A00BE3819 /* DataSourceFacade+Friendship.swift in Sources */, + DBFCA3F729F9191F00B9DCA3 /* HostListStatusTimelineViewModel.swift in Sources */, DB442473285B177B0095AECF /* SearchMediaTimelineViewModel.swift in Sources */, DB1D3DEA289388CD008F0BD0 /* HistoryViewController.swift in Sources */, DB92DB3F2898026B0011B564 /* UserHistoryViewController+DataSourceProvider.swift in Sources */, diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7347332a..18e90c65 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -258,8 +258,8 @@ "repositoryURL": "https://github.com/TwidereProject/TwitterSDK.git", "state": { "branch": null, - "revision": "da05cf601b1dccdac4c83421fca58f336dd48d3e", - "version": "0.2.0" + "revision": "972037346b474f928fe0b2a998c15242ba132f35", + "version": "0.3.0" } }, { diff --git a/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift b/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift index 08c6fbc9..fa9defd3 100644 --- a/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift +++ b/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift @@ -12,6 +12,7 @@ import TwidereLocalization enum TabBarItem: Int, Hashable { case home + case homeList case notification case search case me @@ -34,25 +35,27 @@ extension TabBarItem { var title: String { switch self { - case .home: return L10n.Scene.Timeline.title - case .notification: return L10n.Scene.Notification.title - case .search: return L10n.Scene.Search.title - case .me: return L10n.Scene.Profile.title - case .local: return L10n.Scene.Local.title - case .federated: return L10n.Scene.Federated.title - case .messages: return L10n.Scene.Messages.title - case .likes: return L10n.Scene.Likes.title - case .history: return L10n.Scene.History.title - case .lists: return L10n.Scene.Lists.title - case .trends: return L10n.Scene.Trends.title - case .drafts: return L10n.Scene.Drafts.title - case .settings: return L10n.Scene.Settings.title + case .home: return L10n.Scene.Timeline.title + case .homeList: return L10n.Scene.Timeline.title + case .notification: return L10n.Scene.Notification.title + case .search: return L10n.Scene.Search.title + case .me: return L10n.Scene.Profile.title + case .local: return L10n.Scene.Local.title + case .federated: return L10n.Scene.Federated.title + case .messages: return L10n.Scene.Messages.title + case .likes: return L10n.Scene.Likes.title + case .history: return L10n.Scene.History.title + case .lists: return L10n.Scene.Lists.title + case .trends: return L10n.Scene.Trends.title + case .drafts: return L10n.Scene.Drafts.title + case .settings: return L10n.Scene.Settings.title } } var image: UIImage { switch self { case .home: return Asset.ObjectTools.house.image.withRenderingMode(.alwaysTemplate) + case .homeList: return Asset.ObjectTools.house.image.withRenderingMode(.alwaysTemplate) case .notification: return Asset.ObjectTools.bell.image.withRenderingMode(.alwaysTemplate) case .search: return Asset.ObjectTools.magnifyingglass.image.withRenderingMode(.alwaysTemplate) case .me: return Asset.Human.person.image.withRenderingMode(.alwaysTemplate) @@ -89,6 +92,10 @@ extension TabBarItem { let _viewController = HomeTimelineViewController() _viewController.viewModel = HomeTimelineViewModel(context: context, authContext: authContext) viewController = _viewController + case .homeList: + let _viewController = HomeListStatusTimelineViewController() + _viewController.viewModel = HomeListStatusTimelineViewModel(context: context, authContext: authContext) + viewController = _viewController case .notification: let _viewController = NotificationViewController() _viewController.viewModel = NotificationViewModel(context: context, authContext: authContext, coordinator: coordinator) diff --git a/TwidereX/Diffable/Status/StatusMediaGallerySection.swift b/TwidereX/Diffable/Status/StatusMediaGallerySection.swift index 89a9e713..4e527a56 100644 --- a/TwidereX/Diffable/Status/StatusMediaGallerySection.swift +++ b/TwidereX/Diffable/Status/StatusMediaGallerySection.swift @@ -29,6 +29,7 @@ extension StatusMediaGallerySection { ) -> UICollectionViewDiffableDataSource { let statusRecordCell = UICollectionView.CellRegistration { cell, indexPath, record in + cell.delegate = configuration.statusMediaGalleryCollectionCellDelegate context.managedObjectContext.performAndWait { guard let status = record.object(in: context.managedObjectContext) else { assertionFailure() @@ -37,7 +38,13 @@ extension StatusMediaGallerySection { let items = MediaView.ViewModel.viewModels(from: status) let viewModel = MediaStackContainerView.ViewModel(items: items) cell.contentConfiguration = UIHostingConfiguration { - MediaStackContainerView(viewModel: viewModel) + MediaStackContainerView( + viewModel: viewModel, + handler: { [weak cell] mediaViewModel, _ in + guard let cell = cell else { return } + configuration.statusMediaGalleryCollectionCellDelegate?.statusMediaGalleryCollectionCell(cell, mediaStackContainerViewModel: viewModel, didSelectMediaView: mediaViewModel) + } + ) } .margins(.vertical, 0) // remove vertical margins } diff --git a/TwidereX/Scene/List/HomeList/HomeListStatusTimelineViewController.swift b/TwidereX/Scene/List/HomeList/HomeListStatusTimelineViewController.swift new file mode 100644 index 00000000..242b1be1 --- /dev/null +++ b/TwidereX/Scene/List/HomeList/HomeListStatusTimelineViewController.swift @@ -0,0 +1,210 @@ +// +// HomeListStatusTimelineViewController.swift +// TwidereX +// +// Created by MainasuK on 2023/4/26. +// Copyright © 2023 Twidere. All rights reserved. +// + +import os.log +import UIKit +import Combine +import TwidereLocalization + +final class HomeListStatusTimelineViewController: UIViewController, NeedsDependency, DrawerSidebarTransitionHostViewController, MediaPreviewableViewController { + + let logger = Logger(subsystem: "HomeListStatusTimelineViewController", category: "ViewController") + + // MARK: NeedsDependency + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + + // MARK: DrawerSidebarTransitionHostViewController + private(set) var drawerSidebarTransitionController: DrawerSidebarTransitionController! + let avatarBarButtonItem = AvatarBarButtonItem() + + // MARK: MediaPreviewTransitionHostViewController + let mediaPreviewTransitionController = MediaPreviewTransitionController() + + public var viewModel: HomeListStatusTimelineViewModel! + var disposeBag = Set() + + var listStatusTimelineViewController: ListStatusTimelineViewController? +} + +extension HomeListStatusTimelineViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + drawerSidebarTransitionController = DrawerSidebarTransitionController(hostViewController: self) + + view.backgroundColor = .systemBackground + + // setup avatarBarButtonItem + if navigationController?.viewControllers.first == self { + coordinator.$needsSetupAvatarBarButtonItem + .receive(on: DispatchQueue.main) + .sink { [weak self] needsSetupAvatarBarButtonItem in + guard let self = self else { return } + self.navigationItem.leftBarButtonItem = needsSetupAvatarBarButtonItem ? self.avatarBarButtonItem : nil + } + .store(in: &disposeBag) + } + avatarBarButtonItem.avatarButton.addTarget(self, action: #selector(HomeListStatusTimelineViewController.avatarButtonPressed(_:)), for: .touchUpInside) + avatarBarButtonItem.delegate = self + + viewModel.delegate = self + viewModel.viewDidAppear + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + let user = self.viewModel.authContext.authenticationContext.user(in: self.context.managedObjectContext) + self.avatarBarButtonItem.configure(user: user) + } + .store(in: &disposeBag) + + navigationItem.titleMenuProvider = { [weak self] _ -> UIMenu? in + guard let self = self else { return nil } + + let menuContext = self.viewModel.createHomeListMenuContext() + + var children: [UIMenuElement] = [ + menuContext.ownedListMenu, + menuContext.subscribedListMenu, + ] + + if menuContext.isEmpty { + let deferredMenuElement = UIDeferredMenuElement.uncached { handler in + Task { + let manageListAction = UIAction(title: "Manage List", image: UIImage(systemName: "list.bullet")) { [weak self] _ in + guard let self = self else { return } + guard let me = self.authContext.authenticationContext.user(in: self.context.managedObjectContext)?.asRecord else { return } + let compositeListViewModel = CompositeListViewModel( + context: self.context, + authContext: self.authContext, + kind: .lists(me) + ) + self.coordinator.present(scene: .compositeList(viewModel: compositeListViewModel), from: self, transition: .show) + } + handler([manageListAction]) + } // end Task + } + children.append(deferredMenuElement) + } + + // root menu + return UIMenu(children: children) + } + + viewModel.$homeListMenuContext + .sink { [weak self] menuContext in + guard let self = self else { return } + self.attachTimelineViewController(menuContext: menuContext) + } + .store(in: &disposeBag) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + viewModel.viewDidAppear.send() + } + +} + +extension HomeListStatusTimelineViewController { + + @objc private func avatarButtonPressed(_ sender: UIButton) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + let drawerSidebarViewModel = DrawerSidebarViewModel(context: context, authContext: viewModel.authContext) + coordinator.present(scene: .drawerSidebar(viewModel: drawerSidebarViewModel), from: self, transition: .custom(animated: true, transitioningDelegate: drawerSidebarTransitionController)) + } + + private func selectListMenuAction(_ viewModel: HomeListStatusTimelineViewModel.HomeListMenuActionViewModel) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + } + + private func attachTimelineViewController(menuContext: HomeListStatusTimelineViewModel.HomeListMenuContext?) { + guard let menuContext = menuContext, + let activeMenuActionViewModel = menuContext.activeMenuActionViewModel + else { + detachTimeline() + return + } + + let newList = activeMenuActionViewModel.list.asRecord + var isSameList: Bool { + guard let viewModel = listStatusTimelineViewController?.viewModel as? ListStatusTimelineViewModel else { return false } + return viewModel.list == newList + } + guard !isSameList else { return } + + // detach + detachTimeline() + + // attach + let viewController = ListStatusTimelineViewController() + viewController.context = context + viewController.coordinator = coordinator + viewController.viewModel = ListStatusTimelineViewModel( + context: context, + authContext: authContext, + list: activeMenuActionViewModel.list.asRecord + ) + self.listStatusTimelineViewController = viewController + self.title = activeMenuActionViewModel.title + + addChild(viewController) + viewController.willMove(toParent: self) + viewController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(viewController.view) + NSLayoutConstraint.activate([ + viewController.view.topAnchor.constraint(equalTo: view.topAnchor), + viewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + viewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + viewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + viewController.didMove(toParent: self) + } + + private func detachTimeline() { + listStatusTimelineViewController?.willMove(toParent: nil) + listStatusTimelineViewController?.view.removeFromSuperview() + listStatusTimelineViewController?.didMove(toParent: nil) + listStatusTimelineViewController?.removeFromParent() + title = L10n.Scene.Timeline.title + } + +} + +// MARK: - AuthContextProvider +extension HomeListStatusTimelineViewController: AuthContextProvider { + var authContext: AuthContext { viewModel.authContext } +} + +// MARK: - AvatarBarButtonItemDelegate +extension HomeListStatusTimelineViewController: AvatarBarButtonItemDelegate { } + +// MARK: - HomeListStatusTimelineViewModelDelegate +extension HomeListStatusTimelineViewController: HomeListStatusTimelineViewModelDelegate { + func homeListStatusTimelineViewModel( + _ viewModel: HomeListStatusTimelineViewModel, + menuActionDidSelect menuActionViewModel: HomeListStatusTimelineViewModel.HomeListMenuActionViewModel + ) { + let list = menuActionViewModel.list.asRecord + let managedObjectContext = context.backgroundManagedObjectContext + Task { + try await managedObjectContext.performChanges { + guard let object = list.object(in: managedObjectContext) else { return } + switch object { + case .twitter(let object): + object.update(activeAt: Date()) + case .mastodon(let object): + object.update(activeAt: Date()) + } // end switch + } + self.viewModel.createHomeListMenuContext() + } // end Task + } // end func +} diff --git a/TwidereX/Scene/List/HomeList/HostListStatusTimelineViewModel.swift b/TwidereX/Scene/List/HomeList/HostListStatusTimelineViewModel.swift new file mode 100644 index 00000000..a101401b --- /dev/null +++ b/TwidereX/Scene/List/HomeList/HostListStatusTimelineViewModel.swift @@ -0,0 +1,205 @@ +// +// HostListStatusTimelineViewModel.swift +// TwidereX +// +// Created by MainasuK on 2023/4/26. +// Copyright © 2023 Twidere. All rights reserved. +// + +import UIKit +import Combine +import CoreDataStack + +protocol HomeListStatusTimelineViewModelDelegate: AnyObject { + func homeListStatusTimelineViewModel(_ viewModel: HomeListStatusTimelineViewModel, menuActionDidSelect menuActionViewModel: HomeListStatusTimelineViewModel.HomeListMenuActionViewModel) +} + +final class HomeListStatusTimelineViewModel: ObservableObject { + + var disposeBag = Set() + + // input + let context: AppContext + let authContext: AuthContext + + let viewDidAppear = CurrentValueSubject(Void()) + + let ownedListViewModel: ListViewModel + let subscribedListViewModel: ListViewModel + + weak var delegate: HomeListStatusTimelineViewModelDelegate? + + // output + @Published var ownedListMenuActionViewModels: [HomeListMenuActionViewModel] = [] + @Published var subscribedListMenuActionViewModels: [HomeListMenuActionViewModel] = [] + @Published var homeListMenuContext: HomeListMenuContext? + + init( + context: AppContext, + authContext: AuthContext + ) { + self.context = context + self.authContext = authContext + if let me = authContext.authenticationContext.user(in: context.managedObjectContext)?.asRecord { + self.ownedListViewModel = ListViewModel(context: context, authContext: authContext, kind: .owned(user: me)) + self.subscribedListViewModel = ListViewModel(context: context, authContext: authContext, kind: .subscribed(user: me)) + } else { + self.ownedListViewModel = ListViewModel(context: context, authContext: authContext, kind: .none) + self.subscribedListViewModel = ListViewModel(context: context, authContext: authContext, kind: .none) + } + // end init + + Publishers.CombineLatest( + $ownedListMenuActionViewModels, + $subscribedListMenuActionViewModels + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] _, _ in + guard let self = self else { return } + _ = self.createHomeListMenuContext() + } + .store(in: &disposeBag) + + ownedListViewModel.fetchedResultController.$records + .receive(on: DispatchQueue.main) + .compactMap { [weak self] records -> [HomeListMenuActionViewModel]? in + guard let self = self else { return nil } + return records + .compactMap { record in record.object(in: self.context.managedObjectContext) } + .map { object in HomeListMenuActionViewModel(list: object) } + } + .assign(to: &$ownedListMenuActionViewModels) + subscribedListViewModel.fetchedResultController.$records + .receive(on: DispatchQueue.main) + .compactMap { [weak self] records -> [HomeListMenuActionViewModel]? in + guard let self = self else { return nil } + return records + .compactMap { record in record.object(in: self.context.managedObjectContext) } + .map { object in HomeListMenuActionViewModel(list: object) } + } + .assign(to: &$subscribedListMenuActionViewModels) + } + +} + +extension HomeListStatusTimelineViewModel { + class HomeListMenuActionViewModel: ObservableObject { + var disposeBag = Set() + + // input + let list: ListObject + + // output + @Published var title: String = "" + @Published var activeAt: Date? = nil + + init(list: ListObject) { + self.list = list + // end init + + setup(list: list) + } + } + + struct HomeListMenuContext { + let ownedListMenu: UIMenu + let subscribedListMenu: UIMenu + let isEmpty: Bool + let activeMenuActionViewModel: HomeListMenuActionViewModel? + } +} + +extension HomeListStatusTimelineViewModel { + @discardableResult + func createHomeListMenuContext() -> HomeListMenuContext { + let ownedListMenuActionViewModels = self.ownedListMenuActionViewModels + let subscribedListMenuActionViewModels = self.subscribedListMenuActionViewModels + + + let latestActiveViewModel: HomeListMenuActionViewModel? = { + var menuActionViewModels: [HomeListMenuActionViewModel] = [] + menuActionViewModels.append(contentsOf: ownedListMenuActionViewModels) + menuActionViewModels.append(contentsOf: subscribedListMenuActionViewModels) + + var latestActiveViewModel = menuActionViewModels.first + for menuActionViewModel in menuActionViewModels { + guard let activeAt = menuActionViewModel.activeAt else { continue } + if let latestActiveAt = latestActiveViewModel?.activeAt { + if activeAt > latestActiveAt { + latestActiveViewModel = menuActionViewModel + } else { + continue + } + } else { + latestActiveViewModel = menuActionViewModel + } + } + return latestActiveViewModel + }() + + // owned lists + let ownedListMenuActions: [UIMenuElement] = ownedListMenuActionViewModels.map { viewModel in + let state: UIMenuElement.State = viewModel === latestActiveViewModel ? .on : .off + return UIAction(title: viewModel.title, state: state) { [weak self] _ in + guard let self = self else { return } + self.delegate?.homeListStatusTimelineViewModel(self, menuActionDidSelect: viewModel) + } + } + let ownedListMenu = UIMenu(title: "Lists", options: .displayInline, children: ownedListMenuActions) + // subscribed lists + let subscribedListMenuActions: [UIMenuElement] = subscribedListMenuActionViewModels.map { viewModel in + let state: UIMenuElement.State = viewModel === latestActiveViewModel ? .on : .off + return UIAction(title: viewModel.title, state: state) { [weak self] _ in + guard let self = self else { return } + self.delegate?.homeListStatusTimelineViewModel(self, menuActionDidSelect: viewModel) + } + } + let subscribedListMenu = UIMenu(title: "Subscribed", options: .displayInline, children: subscribedListMenuActions) + + let isEmpty: Bool = { + guard ownedListMenuActions.isEmpty else { return false } + guard subscribedListMenuActions.isEmpty else { return false } + return true + }() + + + let homeListMenuContext = HomeListMenuContext( + ownedListMenu: ownedListMenu, + subscribedListMenu: subscribedListMenu, + isEmpty: isEmpty, + activeMenuActionViewModel: latestActiveViewModel + ) + self.homeListMenuContext = homeListMenuContext + + return homeListMenuContext + } // end func +} + +extension HomeListStatusTimelineViewModel.HomeListMenuActionViewModel { + func setup(list: ListObject) { + switch list { + case .twitter(let object): + setup(list: object) + case .mastodon(let object): + setup(list: object) + } + } + + func setup(list: TwitterList) { + list.publisher(for: \.name) + .assign(to: \.title, on: self) + .store(in: &disposeBag) + list.publisher(for: \.activeAt) + .assign(to: \.activeAt, on: self) + .store(in: &disposeBag) + } + + func setup(list: MastodonList) { + list.publisher(for: \.title) + .assign(to: \.title, on: self) + .store(in: &disposeBag) + list.publisher(for: \.activeAt) + .assign(to: \.activeAt, on: self) + .store(in: &disposeBag) + } +} diff --git a/TwidereX/Scene/List/List/ListViewModel+Diffable.swift b/TwidereX/Scene/List/List/ListViewModel+Diffable.swift index 1c56b243..a0945249 100644 --- a/TwidereX/Scene/List/List/ListViewModel+Diffable.swift +++ b/TwidereX/Scene/List/List/ListViewModel+Diffable.swift @@ -22,10 +22,18 @@ extension ListViewModel { fetchedResultController.$records .receive(on: DispatchQueue.main) - .asyncMap { records -> NSDiffableDataSourceSnapshot? in + .asyncMap { [weak self] records -> NSDiffableDataSourceSnapshot? in + guard let self = self else { return nil } var snapshot = NSDiffableDataSourceSnapshot() - let section = ListSection.twitter(kind: .owned) + let section: ListSection = { + switch self.kind { + case .none: return ListSection.twitter(kind: .owned) + case .owned: return ListSection.twitter(kind: .owned) + case .subscribed: return ListSection.twitter(kind: .subscribed) + case .listed: return ListSection.twitter(kind: .listed) + } + }() snapshot.appendSections([section]) let items = records.map { ListItem.list(record: $0, style: .plain) } diff --git a/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift b/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift index 330dfa1f..f36591a1 100644 --- a/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift +++ b/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift @@ -134,7 +134,9 @@ extension MediaPreviewViewController { pageControl.bottomAnchor.constraint(equalTo: pageControlBackgroundVisualEffectView.bottomAnchor), ]) - if let status = viewModel.status { + mediaInfoDescriptionView.isHidden = true + +// if let status = viewModel.status { // mediaInfoDescriptionView.configure( // statusObject: status, // configurationContext: .init( @@ -143,9 +145,9 @@ extension MediaPreviewViewController { // viewLayoutFramePublisher: viewModel.$viewLayoutFrame // ) // ) - } else { - mediaInfoDescriptionView.isHidden = true - } +// } else { +// mediaInfoDescriptionView.isHidden = true +// } pageControl.numberOfPages = viewModel.viewControllers.count pageControl.isHidden = viewModel.viewControllers.count == 1 diff --git a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift index 0559eff2..99d88c3e 100644 --- a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift +++ b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift @@ -46,6 +46,7 @@ final class MainTabBarController: UITabBarController, NeedsDependency { switch authContext.authenticationContext { case .twitter: return [ + .homeList, .search, .me, ] diff --git a/TwidereX/Scene/Share/View/CollectionViewCell/StatusMediaGalleryCollectionCell.swift b/TwidereX/Scene/Share/View/CollectionViewCell/StatusMediaGalleryCollectionCell.swift index 10b91bda..cee5be77 100644 --- a/TwidereX/Scene/Share/View/CollectionViewCell/StatusMediaGalleryCollectionCell.swift +++ b/TwidereX/Scene/Share/View/CollectionViewCell/StatusMediaGalleryCollectionCell.swift @@ -12,69 +12,21 @@ import Combine import CoverFlowStackCollectionViewLayout protocol StatusMediaGalleryCollectionCellDelegate: AnyObject { - func statusMediaGalleryCollectionCell(_ cell: StatusMediaGalleryCollectionCell, coverFlowCollectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) - func statusMediaGalleryCollectionCell(_ cell: StatusMediaGalleryCollectionCell, toggleContentWarningOverlayViewDisplay contentWarningOverlayView: ContentWarningOverlayView) + func statusMediaGalleryCollectionCell(_ cell: StatusMediaGalleryCollectionCell, mediaStackContainerViewModel: MediaStackContainerView.ViewModel, didSelectMediaView mediaViewModel: MediaView.ViewModel) + // func statusMediaGalleryCollectionCell(_ cell: StatusMediaGalleryCollectionCell, toggleContentWarningOverlayViewDisplay contentWarningOverlayView: ContentWarningOverlayView) } final class StatusMediaGalleryCollectionCell: UICollectionViewCell { let logger = Logger(subsystem: "StatusMediaGalleryCollectionCell", category: "Cell") -// weak var delegate: StatusMediaGalleryCollectionCellDelegate? -// -// var disposeBag = Set() -// private(set) lazy var viewModel: ViewModel = { -// let viewModel = ViewModel() -// viewModel.bind(cell: self) -// return viewModel -// }() - -// let sensitiveToggleButtonBlurVisualEffectView: UIVisualEffectView = { -// let visualEffectView = UIVisualEffectView(effect: ContentWarningOverlayView.blurVisualEffect) -// visualEffectView.layer.masksToBounds = true -// visualEffectView.layer.cornerRadius = 6 -// visualEffectView.layer.cornerCurve = .continuous -// return visualEffectView -// }() -// let sensitiveToggleButtonVibrancyVisualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: ContentWarningOverlayView.blurVisualEffect)) -// let sensitiveToggleButton: HitTestExpandedButton = { -// let button = HitTestExpandedButton(type: .system) -// button.setImage(Asset.Human.eyeSlashMini.image.withRenderingMode(.alwaysTemplate), for: .normal) -// return button -// }() - -// public let contentWarningOverlayView: ContentWarningOverlayView = { -// let overlay = ContentWarningOverlayView() -// overlay.layer.masksToBounds = true -// overlay.layer.cornerRadius = MediaView.cornerRadius -// overlay.layer.cornerCurve = .continuous -// return overlay -// }() - -// let mediaView = MediaView() - -// let collectionViewLayout: CoverFlowStackCollectionViewLayout = { -// let layout = CoverFlowStackCollectionViewLayout() -// layout.sizeScaleRatio = 0.9 -// return layout -// }() -// private(set) lazy var collectionView: UICollectionView = { -// let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) -// collectionView.backgroundColor = .clear -// collectionView.layer.masksToBounds = true -//// collectionView.layer.cornerRadius = MediaView.cornerRadius -// collectionView.layer.cornerCurve = .continuous -// return collectionView -// }() -// var diffableDataSource: UICollectionViewDiffableDataSource? + weak var delegate: StatusMediaGalleryCollectionCellDelegate? override func prepareForReuse() { super.prepareForReuse() contentConfiguration = nil -// disposeBag.removeAll() -// mediaView.prepareForReuse() -// diffableDataSource?.applySnapshotUsingReloadData(.init()) + delegate = nil } override init(frame: CGRect) { @@ -92,87 +44,7 @@ final class StatusMediaGalleryCollectionCell: UICollectionViewCell { extension StatusMediaGalleryCollectionCell { private func _init() { -// mediaView.translatesAutoresizingMaskIntoConstraints = false -// contentView.addSubview(mediaView) -// NSLayoutConstraint.activate([ -// mediaView.topAnchor.constraint(equalTo: contentView.topAnchor), -// mediaView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), -// mediaView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), -// mediaView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), -// ]) -// -// collectionView.translatesAutoresizingMaskIntoConstraints = false -// contentView.addSubview(collectionView) -// NSLayoutConstraint.activate([ -// collectionView.topAnchor.constraint(equalTo: contentView.topAnchor), -// collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), -// collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), -// collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), -// ]) -// -// // sensitiveToggleButton -// sensitiveToggleButtonBlurVisualEffectView.translatesAutoresizingMaskIntoConstraints = false -// contentView.addSubview(sensitiveToggleButtonBlurVisualEffectView) -// NSLayoutConstraint.activate([ -// sensitiveToggleButtonBlurVisualEffectView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), -// sensitiveToggleButtonBlurVisualEffectView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8), -// ]) -// -// sensitiveToggleButtonVibrancyVisualEffectView.translatesAutoresizingMaskIntoConstraints = false -// sensitiveToggleButtonBlurVisualEffectView.contentView.addSubview(sensitiveToggleButtonVibrancyVisualEffectView) -// NSLayoutConstraint.activate([ -// sensitiveToggleButtonVibrancyVisualEffectView.topAnchor.constraint(equalTo: sensitiveToggleButtonBlurVisualEffectView.contentView.topAnchor), -// sensitiveToggleButtonVibrancyVisualEffectView.leadingAnchor.constraint(equalTo: sensitiveToggleButtonBlurVisualEffectView.contentView.leadingAnchor), -// sensitiveToggleButtonVibrancyVisualEffectView.trailingAnchor.constraint(equalTo: sensitiveToggleButtonBlurVisualEffectView.contentView.trailingAnchor), -// sensitiveToggleButtonVibrancyVisualEffectView.bottomAnchor.constraint(equalTo: sensitiveToggleButtonBlurVisualEffectView.contentView.bottomAnchor), -// ]) -// -// sensitiveToggleButton.translatesAutoresizingMaskIntoConstraints = false -// sensitiveToggleButtonVibrancyVisualEffectView.contentView.addSubview(sensitiveToggleButton) -// NSLayoutConstraint.activate([ -// sensitiveToggleButton.topAnchor.constraint(equalTo: sensitiveToggleButtonVibrancyVisualEffectView.contentView.topAnchor, constant: 4), -// sensitiveToggleButton.leadingAnchor.constraint(equalTo: sensitiveToggleButtonVibrancyVisualEffectView.contentView.leadingAnchor, constant: 4), -// sensitiveToggleButtonVibrancyVisualEffectView.contentView.trailingAnchor.constraint(equalTo: sensitiveToggleButton.trailingAnchor, constant: 4), -// sensitiveToggleButtonVibrancyVisualEffectView.contentView.bottomAnchor.constraint(equalTo: sensitiveToggleButton.bottomAnchor, constant: 4), -// ]) -// -// // contentWarningOverlayView -// contentWarningOverlayView.translatesAutoresizingMaskIntoConstraints = false -// contentView.addSubview(contentWarningOverlayView) // should add to container -// NSLayoutConstraint.activate([ -// contentWarningOverlayView.topAnchor.constraint(equalTo: contentView.topAnchor), -// contentWarningOverlayView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), -// contentWarningOverlayView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), -// contentWarningOverlayView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), -// ]) -// -// // delegate interaction to collection view -// mediaView.isUserInteractionEnabled = false -// -// collectionView.delegate = self -// let configuration = CoverFlowStackSection.Configuration() -// diffableDataSource = CoverFlowStackSection.diffableDataSource( -// collectionView: collectionView, -// configuration: configuration -// ) -// -// sensitiveToggleButton.addTarget(self, action: #selector(StatusMediaGalleryCollectionCell.sensitiveToggleButtonDidPressed(_:)), for: .touchUpInside) -// contentWarningOverlayView.delegate = self + // nothing } } - -//extension StatusMediaGalleryCollectionCell { -// @objc private func sensitiveToggleButtonDidPressed(_ sender: UIButton) { -// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") -//// delegate?.statusMediaGalleryCollectionCell(self, toggleContentWarningOverlayViewDisplay: contentWarningOverlayView) -// } -//} -// -//// MARK: - UICollectionViewDelegate -//extension StatusMediaGalleryCollectionCell: UICollectionViewDelegate { -// func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { -// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): did select \(indexPath.debugDescription)") -// delegate?.statusMediaGalleryCollectionCell(self, coverFlowCollectionView: collectionView, didSelectItemAt: indexPath) -// } -//} diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift index 354a3d8e..3b5bb6c9 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift @@ -71,6 +71,8 @@ extension TimelineViewModel.LoadOldestState { class Loading: TimelineViewModel.LoadOldestState { var nextInput: StatusFetchViewModel.Timeline.Input? + var failCount = 0 + var nonce = UUID() override func isValidNextState(_ stateClass: AnyClass) -> Bool { switch stateClass { @@ -90,15 +92,24 @@ extension TimelineViewModel.LoadOldestState { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - // reset when reloading + // reset nextInput when reloading switch previousState { case is Reloading: nextInput = nil default: break } + + // reset fail count if needs + switch previousState { + case is Fail: + failCount += 1 + default: + failCount = 0 + } guard let viewModel = viewModel, let _ = stateMachine else { return } + let nonce = self.nonce Task { let managedObjectContext = viewModel.context.managedObjectContext @@ -114,7 +125,12 @@ extension TimelineViewModel.LoadOldestState { return status } } - + + let failCount = UInt64(min(failCount, 60)) + if failCount > 0 { + try? await Task.sleep(nanoseconds: failCount * .second) + } + guard nonce == self.nonce else { return } await fetch(anchor: _anchorRecord) } // end Task } // end func diff --git a/TwidereX/Scene/Timeline/Base/Grid/GridTimelineViewController.swift b/TwidereX/Scene/Timeline/Base/Grid/GridTimelineViewController.swift index 110924b9..ac78564c 100644 --- a/TwidereX/Scene/Timeline/Base/Grid/GridTimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/Grid/GridTimelineViewController.swift @@ -128,66 +128,12 @@ extension GridTimelineViewController { // MARK: - UICollectionViewDelegate extension GridTimelineViewController: UICollectionViewDelegate { - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): did select \(indexPath.debugDescription)") - guard let cell = collectionView.cellForItem(at: indexPath) as? StatusMediaGalleryCollectionCell else { return } -// Task { -// let source = DataSourceItem.Source(collectionViewCell: nil, indexPath: indexPath) -// guard let item = await item(from: source) else { -// assertionFailure() -// return -// } -// guard let status = await item.status(in: self.context.managedObjectContext) else { -// assertionFailure("only works for status data provider") -// return -// } -// await DataSourceFacade.coordinateToMediaPreviewScene( -// provider: self, -// target: .status, -// status: status, -// mediaPreviewContext: DataSourceFacade.MediaPreviewContext( -// containerView: .mediaView(cell.mediaView), -// mediaView: cell.mediaView, -// index: 0 // <-- only one attachment -// ) -// ) -// } - } + } // MARK: - StatusMediaGalleryCollectionCellDelegate extension GridTimelineViewController: StatusMediaGalleryCollectionCellDelegate { - func statusMediaGalleryCollectionCell(_ cell: StatusMediaGalleryCollectionCell, coverFlowCollectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { -// Task { -// let source = DataSourceItem.Source(collectionViewCell: cell, indexPath: nil) -// guard let item = await item(from: source) else { -// assertionFailure() -// return -// } -// guard let status = await item.status(in: self.context.managedObjectContext) else { -// assertionFailure("only works for status data provider") -// return -// } -// -// guard let cell = coverFlowCollectionView.cellForItem(at: indexPath) as? CoverFlowStackMediaCollectionCell else { -// assertionFailure() -// return -// } -// -// await DataSourceFacade.coordinateToMediaPreviewScene( -// provider: self, -// target: .status, -// status: status, -// mediaPreviewContext: DataSourceFacade.MediaPreviewContext( -// containerView: .mediaView(cell.mediaView), -// mediaView: cell.mediaView, -// index: indexPath.row -// ) -// ) -// } - } - - func statusMediaGalleryCollectionCell(_ cell: StatusMediaGalleryCollectionCell, toggleContentWarningOverlayViewDisplay contentWarningOverlayView: ContentWarningOverlayView) { + func statusMediaGalleryCollectionCell(_ cell: StatusMediaGalleryCollectionCell, mediaStackContainerViewModel: TwidereUI.MediaStackContainerView.ViewModel, didSelectMediaView mediaViewModel: TwidereUI.MediaView.ViewModel) { Task { let source = DataSourceItem.Source(collectionViewCell: cell, indexPath: nil) guard let item = await item(from: source) else { @@ -198,13 +144,11 @@ extension GridTimelineViewController: StatusMediaGalleryCollectionCellDelegate { assertionFailure("only works for status data provider") return } - - try await DataSourceFacade.responseToToggleMediaSensitiveAction( + await DataSourceFacade.coordinateToStatusThreadScene( provider: self, - target: .status, - status: status + kind: .status(status) ) - } + } // end Task } } diff --git a/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel.swift b/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel.swift index 4a4aa562..0ca81231 100644 --- a/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel.swift @@ -13,6 +13,9 @@ import CoreDataStack import TwidereCore final class ListStatusTimelineViewModel: ListTimelineViewModel { + + // input + let list: ListRecord // output @Published var title: String? @@ -23,11 +26,13 @@ final class ListStatusTimelineViewModel: ListTimelineViewModel { authContext: AuthContext, list: ListRecord ) { + self.list = list super.init( context: context, authContext: authContext, kind: .list(list: list) ) + // end init isFloatyButtonDisplay = false statusRecordFetchedResultController.userIdentifier = authContext.authenticationContext.userIdentifier From 9be07fc31b0d10ee44ac22ac4df3699b13c0b177 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 27 Apr 2023 18:43:40 +0800 Subject: [PATCH 064/128] chore: update the version to 2.0.0 (123) --- NotificationService/Info.plist | 4 +- ShareExtension/Info.plist | 4 +- TwidereX.xcodeproj/project.pbxproj | 63 ++++++++++-------------------- TwidereX/Info.plist | 4 +- TwidereXIntent/Info.plist | 4 +- TwidereXTests/Info.plist | 4 +- TwidereXUITests/Info.plist | 4 +- 7 files changed, 33 insertions(+), 54 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 8c410563..690966d2 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -3,7 +3,7 @@ CFBundleShortVersionString - 1.4.3 + 2.0.0 CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 118 + 123 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 301b7e36..8361434e 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.3 + 2.0.0 CFBundleVersion - 118 + 123 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index fd5f2125..3ea47a0e 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3431,7 +3431,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 118; + CURRENT_PROJECT_VERSION = 123; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3440,7 +3440,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.5; + MARKETING_VERSION = 2.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3458,7 +3458,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 118; + CURRENT_PROJECT_VERSION = 123; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3467,7 +3467,6 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.2.5; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereXTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -3482,7 +3481,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 118; + CURRENT_PROJECT_VERSION = 123; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3491,7 +3490,6 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereXUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -3507,7 +3505,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 118; + CURRENT_PROJECT_VERSION = 123; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3518,7 +3516,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -3535,9 +3532,8 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 118; + CURRENT_PROJECT_VERSION = 123; DEVELOPMENT_TEAM = 7LFDZ96332; - GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3547,7 +3543,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX.TwidereXIntent; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -3566,10 +3561,9 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 118; + CURRENT_PROJECT_VERSION = 123; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; - GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3578,7 +3572,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.2.5; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -3597,9 +3590,8 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 118; + CURRENT_PROJECT_VERSION = 123; DEVELOPMENT_TEAM = 7LFDZ96332; - GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3608,7 +3600,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.2.5; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -3627,9 +3618,8 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 118; + CURRENT_PROJECT_VERSION = 123; DEVELOPMENT_TEAM = 7LFDZ96332; - GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3638,7 +3628,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.2.5; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -3655,7 +3644,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 118; + CURRENT_PROJECT_VERSION = 123; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3666,7 +3655,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -3683,7 +3671,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 118; + CURRENT_PROJECT_VERSION = 123; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3694,7 +3682,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -3838,7 +3825,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 118; + CURRENT_PROJECT_VERSION = 123; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3847,7 +3834,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.5; + MARKETING_VERSION = 2.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3870,7 +3857,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 118; + CURRENT_PROJECT_VERSION = 123; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3879,7 +3866,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.5; + MARKETING_VERSION = 2.0.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3897,7 +3884,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 118; + CURRENT_PROJECT_VERSION = 123; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3906,7 +3893,6 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.2.5; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereXTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -3923,7 +3909,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 118; + CURRENT_PROJECT_VERSION = 123; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3932,7 +3918,6 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.2.5; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereXTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -3947,7 +3932,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 118; + CURRENT_PROJECT_VERSION = 123; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3956,7 +3941,6 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereXUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -3971,7 +3955,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 118; + CURRENT_PROJECT_VERSION = 123; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3980,7 +3964,6 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereXUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -3996,9 +3979,8 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 118; + CURRENT_PROJECT_VERSION = 123; DEVELOPMENT_TEAM = 7LFDZ96332; - GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -4008,7 +3990,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX.TwidereXIntent; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -4025,9 +4006,8 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 118; + CURRENT_PROJECT_VERSION = 123; DEVELOPMENT_TEAM = 7LFDZ96332; - GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -4037,7 +4017,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX.TwidereXIntent; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index ea111c39..ab6bbb60 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -38,9 +38,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.3 + 2.0.0 CFBundleVersion - 118 + 123 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index f55d373c..fb3ac11f 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.3 + 2.0.0 CFBundleVersion - 118 + 123 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index bbc26090..c814a6d4 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.3 + 2.0.0 CFBundleVersion - 118 + 123 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index bbc26090..c814a6d4 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.3 + 2.0.0 CFBundleVersion - 118 + 123 From 08084cedb4392908f6b2b4a30cc232b9b25cd414 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 28 Apr 2023 19:17:20 +0800 Subject: [PATCH 065/128] fix: hide the Likes tab and fix History tab --- Podfile.lock | 2 +- .../Content/UserView+ViewModel.swift | 5 ++ .../Sources/TwidereUI/Content/UserView.swift | 7 ++ .../Misc/History/HistorySection.swift | 73 ++++++++++--------- TwidereX/Diffable/User/UserSection.swift | 1 - .../Status/StatusHistoryViewController.swift | 23 ++++++ .../StatusHistoryViewModel+Diffable.swift | 1 + .../User/UserHistoryViewController.swift | 23 ++++++ .../User/UserHistoryViewModel+Diffable.swift | 1 + .../DrawerSidebarViewModel+Diffable.swift | 2 +- .../Scene/Root/Sidebar/SidebarViewModel.swift | 2 +- 11 files changed, 103 insertions(+), 37 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index b1d2a85d..f7ddadb7 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -159,4 +159,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: e2c773b0e4d6bfd3166d7794b7f8babe8f9b9b92 -COCOAPODS: 1.11.3 +COCOAPODS: 1.12.1 diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift index c1a1ac32..8481e2ba 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift @@ -167,6 +167,11 @@ extension UserView.ViewModel { // accessory: none case friend + // headline: name | lock + // subheadline: username + // accessory: none + case history + // header: notification // headline: name | lock | username // subheadline: follower count diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift index b6981a88..92ccd0f6 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift @@ -8,6 +8,7 @@ import os.log import UIKit +import SwiftUI import Combine import MetaTextKit import MetaLabel @@ -186,6 +187,8 @@ extension UserView { nameLabel case .friend: nameLabel + case .history: + nameLabel case .notification: nameLabel case .mentionPick: @@ -211,6 +214,8 @@ extension UserView { usernameLabel case .friend: usernameLabel + case .history: + usernameLabel case .notification: usernameLabel case .mentionPick: @@ -237,6 +242,8 @@ extension UserView { EmptyView() case .friend: EmptyView() + case .history: + EmptyView() case .notification: if viewModel.isFollowRequestActionDisplay { followRequestActionView diff --git a/TwidereX/Diffable/Misc/History/HistorySection.swift b/TwidereX/Diffable/Misc/History/HistorySection.swift index 7f1af493..a7fd4dfe 100644 --- a/TwidereX/Diffable/Misc/History/HistorySection.swift +++ b/TwidereX/Diffable/Misc/History/HistorySection.swift @@ -8,6 +8,7 @@ import os.log import UIKit +import SwiftUI import Combine import CoreData import CoreDataStack @@ -31,10 +32,11 @@ extension HistorySection { static func diffableDataSource( tableView: UITableView, context: AppContext, + authContext: AuthContext, configuration: Configuration ) -> UITableViewDiffableDataSource { tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self)) -// tableView.register(UserRelationshipStyleTableViewCell.self, forCellReuseIdentifier: String(describing: UserRelationshipStyleTableViewCell.self)) + tableView.register(UserTableViewCell.self, forCellReuseIdentifier: String(describing: UserTableViewCell.self)) let diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in // data source should dispatch in main thread @@ -48,39 +50,44 @@ extension HistorySection { return UITableViewCell() } // status -// if let status = history.statusObject { -// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell -// StatusSection.setupStatusPollDataSource( -// context: context, -// statusView: cell.statusView, -// configurationContext: configuration.statusViewConfigurationContext -// ) -// configure( -// tableView: tableView, -// cell: cell, -// viewModel: StatusTableViewCell.ViewModel(value: .statusObject(status)), -// configuration: configuration -// ) -// return cell -// } -// // user -// if let user = history.userObject { -// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserRelationshipStyleTableViewCell.self), for: indexPath) as! UserRelationshipStyleTableViewCell -// let authenticationContext = configuration.userViewConfigurationContext.authContext.authenticationContext -// let me = authenticationContext.user(in: context.managedObjectContext) -// let viewModel = UserTableViewCell.ViewModel( -// user: user, -// me: me, -// notification: nil -// ) -// configure( -// cell: cell, -// viewModel: viewModel, -// configuration: configuration -// ) -// return cell -// } + if let status = history.statusObject { + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell + cell.statusViewTableViewCellDelegate = configuration.statusViewTableViewCellDelegate + + let viewModel = StatusView.ViewModel( + status: status, + authContext: authContext, + delegate: cell, + viewLayoutFramePublisher: configuration.viewLayoutFramePublisher + ) + cell.contentConfiguration = UIHostingConfiguration { + StatusView(viewModel: viewModel) + } + .margins(.vertical, 0) // remove vertical margins + return cell + } + // user + if let user = history.userObject { + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell + cell.userViewTableViewCellDelegate = configuration.userViewTableViewCellDelegate + + let _viewModel = UserView.ViewModel( + user: user, + authContext: authContext, + kind: .history, + delegate: cell + ) + guard let viewModel = _viewModel else { + return UITableViewCell() + } + cell.contentConfiguration = UIHostingConfiguration { + UserView(viewModel: viewModel) + } + .margins(.vertical, 0) // remove vertical margins + return cell + } + assertionFailure() return UITableViewCell() } return cell diff --git a/TwidereX/Diffable/User/UserSection.swift b/TwidereX/Diffable/User/UserSection.swift index 38823309..a5da70fc 100644 --- a/TwidereX/Diffable/User/UserSection.swift +++ b/TwidereX/Diffable/User/UserSection.swift @@ -19,7 +19,6 @@ extension UserSection { struct Configuration { weak var userViewTableViewCellDelegate: UserViewTableViewCellDelegate? - // let userViewConfigurationContext: UserView.ConfigurationContext } static func diffableDataSource( diff --git a/TwidereX/Scene/History/Status/StatusHistoryViewController.swift b/TwidereX/Scene/History/Status/StatusHistoryViewController.swift index 3509fcb0..650f4f81 100644 --- a/TwidereX/Scene/History/Status/StatusHistoryViewController.swift +++ b/TwidereX/Scene/History/Status/StatusHistoryViewController.swift @@ -63,6 +63,29 @@ extension StatusHistoryViewController { tableView.deselectRow(with: transitionCoordinator, animated: animated) } + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + viewModel.viewLayoutFrame.update(view: view) + } + + override func viewSafeAreaInsetsDidChange() { + super.viewSafeAreaInsetsDidChange() + + viewModel.viewLayoutFrame.update(view: view) + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + coordinator.animate {[weak self] _ in + guard let self = self else { return } + self.viewModel.viewLayoutFrame.update(view: self.view) + } completion: { _ in + // do nothing + } + } + } // MARK: - UITableViewDelegate diff --git a/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift b/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift index 9fe3a4f1..777c2d03 100644 --- a/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift +++ b/TwidereX/Scene/History/Status/StatusHistoryViewModel+Diffable.swift @@ -18,6 +18,7 @@ extension StatusHistoryViewModel { diffableDataSource = HistorySection.diffableDataSource( tableView: tableView, context: context, + authContext: authContext, configuration: .init( statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, userViewTableViewCellDelegate: nil, diff --git a/TwidereX/Scene/History/User/UserHistoryViewController.swift b/TwidereX/Scene/History/User/UserHistoryViewController.swift index 519baef5..0e929391 100644 --- a/TwidereX/Scene/History/User/UserHistoryViewController.swift +++ b/TwidereX/Scene/History/User/UserHistoryViewController.swift @@ -63,6 +63,29 @@ extension UserHistoryViewController { tableView.deselectRow(with: transitionCoordinator, animated: animated) } + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + viewModel.viewLayoutFrame.update(view: view) + } + + override func viewSafeAreaInsetsDidChange() { + super.viewSafeAreaInsetsDidChange() + + viewModel.viewLayoutFrame.update(view: view) + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + coordinator.animate {[weak self] _ in + guard let self = self else { return } + self.viewModel.viewLayoutFrame.update(view: self.view) + } completion: { _ in + // do nothing + } + } + } // MARK: - AuthContextProvider diff --git a/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift b/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift index 3e5fbf65..0f44b70c 100644 --- a/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift +++ b/TwidereX/Scene/History/User/UserHistoryViewModel+Diffable.swift @@ -18,6 +18,7 @@ extension UserHistoryViewModel { diffableDataSource = HistorySection.diffableDataSource( tableView: tableView, context: context, + authContext: authContext, configuration: .init( statusViewTableViewCellDelegate: nil, userViewTableViewCellDelegate: userViewTableViewCellDelegate, diff --git a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewModel+Diffable.swift b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewModel+Diffable.swift index 2735ec1a..24fcac7e 100644 --- a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewModel+Diffable.swift +++ b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewModel+Diffable.swift @@ -32,7 +32,7 @@ extension DrawerSidebarViewModel { let authenticationContext = self.authContext.authenticationContext switch authenticationContext { case .twitter: - snapshot.appendItems([.likes], toSection: .main) + // snapshot.appendItems([.likes], toSection: .main) if preferredEnableHistory { snapshot.appendItems([.history], toSection: .main) } diff --git a/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift b/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift index e00cd753..f81e4c11 100644 --- a/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift @@ -54,7 +54,7 @@ final class SidebarViewModel: ObservableObject { var items: [TabBarItem] = [] switch self.authContext.authenticationContext { case .twitter: - items.append(contentsOf: [.likes]) + // items.append(contentsOf: [.likes]) if preferredEnableHistory { items.append(contentsOf: [.history]) } From ea7972a0fc8096f76f1706c8d537a35b8a8b2d3b Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 28 Apr 2023 19:17:53 +0800 Subject: [PATCH 066/128] chore: update version to 2.0.0 (124) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 36 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 690966d2..9b7d7bd6 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 123 + 124 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 8361434e..9dd52091 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 123 + 124 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 3ea47a0e..dda5ca6b 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3431,7 +3431,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 123; + CURRENT_PROJECT_VERSION = 124; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3458,7 +3458,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 123; + CURRENT_PROJECT_VERSION = 124; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3481,7 +3481,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 123; + CURRENT_PROJECT_VERSION = 124; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3505,7 +3505,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 123; + CURRENT_PROJECT_VERSION = 124; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3532,7 +3532,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 123; + CURRENT_PROJECT_VERSION = 124; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3561,7 +3561,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 123; + CURRENT_PROJECT_VERSION = 124; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3590,7 +3590,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 123; + CURRENT_PROJECT_VERSION = 124; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3618,7 +3618,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 123; + CURRENT_PROJECT_VERSION = 124; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3644,7 +3644,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 123; + CURRENT_PROJECT_VERSION = 124; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3671,7 +3671,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 123; + CURRENT_PROJECT_VERSION = 124; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3825,7 +3825,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 123; + CURRENT_PROJECT_VERSION = 124; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3857,7 +3857,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 123; + CURRENT_PROJECT_VERSION = 124; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3884,7 +3884,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 123; + CURRENT_PROJECT_VERSION = 124; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3909,7 +3909,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 123; + CURRENT_PROJECT_VERSION = 124; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3932,7 +3932,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 123; + CURRENT_PROJECT_VERSION = 124; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3955,7 +3955,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 123; + CURRENT_PROJECT_VERSION = 124; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3979,7 +3979,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 123; + CURRENT_PROJECT_VERSION = 124; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -4006,7 +4006,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 123; + CURRENT_PROJECT_VERSION = 124; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index ab6bbb60..9dc98138 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 123 + 124 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index fb3ac11f..9d5965a5 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 123 + 124 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index c814a6d4..f7ed50bd 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 123 + 124 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index c814a6d4..f7ed50bd 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 123 + 124 From 4b8a7f7825aab834bc0eea73dadcec482231e03c Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 5 May 2023 19:46:05 +0800 Subject: [PATCH 067/128] chore: restore "deprecated" (but not) Twitter APIs. Fix: Mastodon open the profile may crash issue --- TwidereSDK/Package.swift | 2 +- .../CoreDataStack 7.xcdatamodel/contents | 1 + .../Authentication/AuthenticationIndex.swift | 9 ++ .../AuthenticationContext.swift | 9 ++ .../StatusFetchViewModel+Timeline.swift | 33 +++--- .../TwidereUI/Content/StatusHeaderView.swift | 3 - .../Content/StatusView+ViewModel.swift | 24 ++-- .../TwidereUI/Content/StatusView.swift | 52 +++++---- .../xcshareddata/swiftpm/Package.resolved | 4 +- ...HomeListStatusTimelineViewController.swift | 93 ++++++++++----- .../HostListStatusTimelineViewModel.swift | 110 ++++++++++++------ ...tificationTimelineViewModel+Diffable.swift | 4 +- .../Scene/Profile/LocalProfileViewModel.swift | 32 ++++- .../Scene/Profile/MeProfileViewModel.swift | 8 +- .../Scene/Profile/ProfileViewController.swift | 19 +-- TwidereX/Scene/Profile/ProfileViewModel.swift | 5 +- .../Profile/RemoteProfileViewModel.swift | 44 ++++++- .../Paging/ProfilePagingViewController.swift | 9 -- .../Paging/ProfilePagingViewModel.swift | 12 +- .../DrawerSidebarViewModel+Diffable.swift | 2 +- .../Root/MainTab/MainTabBarController.swift | 1 + .../Scene/Root/Sidebar/SidebarViewModel.swift | 2 +- 22 files changed, 316 insertions(+), 162 deletions(-) diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index e1132126..f36b14e6 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -51,7 +51,7 @@ let package = Package( .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.6.0"), .package(url: "https://github.com/tid-kijyun/Kanna.git", from: "5.2.7"), .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"), - .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.3.0"), + .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.4.0"), .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"), .package(name: "CoverFlowStackLayout", path: "../CoverFlowStackLayout"), ], diff --git a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 7.xcdatamodel/contents b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 7.xcdatamodel/contents index a55159d7..426ef9ee 100644 --- a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 7.xcdatamodel/contents +++ b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 7.xcdatamodel/contents @@ -3,6 +3,7 @@ + diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Authentication/AuthenticationIndex.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Authentication/AuthenticationIndex.swift index 45e4bf8f..a89420ca 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Authentication/AuthenticationIndex.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Authentication/AuthenticationIndex.swift @@ -26,6 +26,9 @@ final public class AuthenticationIndex: NSManagedObject { @NSManaged public private(set) var createdAt: Date @NSManaged public private(set) var activeAt: Date + // Record the home timeline latest active date + @NSManaged public private(set) var homeTimelineActiveAt: Date? + // one-to-one relationship @NSManaged public private(set) var twitterAuthentication: TwitterAuthentication? @NSManaged public private(set) var mastodonAuthentication: MastodonAuthentication? @@ -60,6 +63,12 @@ extension AuthenticationIndex { } } + public func update(homeTimelineActiveAt: Date) { + if self.homeTimelineActiveAt != homeTimelineActiveAt { + self.homeTimelineActiveAt = homeTimelineActiveAt + } + } + } extension AuthenticationIndex { diff --git a/TwidereSDK/Sources/TwidereCore/Model/Authentication/AuthenticationContext.swift b/TwidereSDK/Sources/TwidereCore/Model/Authentication/AuthenticationContext.swift index 9cef62dc..f9869868 100644 --- a/TwidereSDK/Sources/TwidereCore/Model/Authentication/AuthenticationContext.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/Authentication/AuthenticationContext.swift @@ -57,6 +57,15 @@ extension AuthenticationContext { .flatMap { UserObject.mastodon(object: $0.user) } } } + + public func authenticationIndex(in managedObjectContext: NSManagedObjectContext) -> AuthenticationIndex? { + switch self { + case .twitter(let authenticationContext): + return authenticationContext.authenticationRecord.object(in: managedObjectContext)?.authenticationIndex + case .mastodon(let authenticationContext): + return authenticationContext.authenticationRecord.object(in: managedObjectContext)?.authenticationIndex + } + } } extension AuthenticationContext { diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift index 939a531e..c871bd12 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift @@ -18,6 +18,8 @@ extension StatusFetchViewModel { } extension StatusFetchViewModel.Timeline { + public static let logger = Logger(subsystem: "StatusFetchViewModel", category: "Timeline") + public enum Kind { case home case `public`(isLocal: Bool) @@ -501,19 +503,24 @@ extension StatusFetchViewModel.Timeline { api: APIService, input: Input ) async throws -> Output { - switch input { - case .home(let input): - return try await Home.fetch(api: api, input: input) - case .public(let input): - return try await Public.fetch(api: api, input: input) - case .hashtag(let input): - return try await Hashtag.fetch(api: api, input: input) - case .list(let input): - return try await List.fetch(api: api, input: input) - case .search(let input): - return try await Search.fetch(api: api, input: input) - case .user(let input): - return try await User.fetch(api: api, input: input) + do { + switch input { + case .home(let input): + return try await Home.fetch(api: api, input: input) + case .public(let input): + return try await Public.fetch(api: api, input: input) + case .hashtag(let input): + return try await Hashtag.fetch(api: api, input: input) + case .list(let input): + return try await List.fetch(api: api, input: input) + case .search(let input): + return try await Search.fetch(api: api, input: input) + case .user(let input): + return try await User.fetch(api: api, input: input) + } + } catch { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch \(String(describing: input)) failure…") + throw error } } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift index 463d345a..88e4318b 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusHeaderView.swift @@ -59,9 +59,6 @@ extension StatusHeaderView { @Published public var hasHangingAvatar: Bool = false @Published public var avatarDimension: CGFloat = StatusView.hangingAvatarButtonDimension - // output - public var viewSize: CGSize = .zero - public init( image: UIImage, label: MetaContent diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index 6db6f0b3..44d8e9e7 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -955,19 +955,23 @@ extension StatusView.ViewModel { } var cellTopMargin: CGFloat { - return parentViewModel == nil ? 12 : 0 - } - - var topConversationLinkViewHeight: CGFloat { - var height: CGFloat = cellTopMargin - if let statusHeaderViewModel = statusHeaderViewModel { - height += statusHeaderViewModel.viewSize.height - height += StatusView.statusHeaderBottomSpacing - height += parentViewModel?.cellTopMargin ?? 0 + switch kind { + case .quote: return .zero + case _ where parentViewModel == nil: return 12 + default: return .zero } - return height } +// var topConversationLinkViewHeight: CGFloat { +// var height: CGFloat = cellTopMargin +// if let statusHeaderViewModel = statusHeaderViewModel { +// height += statusHeaderViewModel.viewSize.height +// height += StatusView.statusHeaderBottomSpacing +// height += parentViewModel?.cellTopMargin ?? 0 +// } +// return height +// } + var hasToolbar: Bool { switch kind { case .timeline, .conversationRoot, .conversationThread: diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index 02219116..bf1ac3c4 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -29,7 +29,7 @@ public protocol StatusViewDelegate: AnyObject { func statusView(_ viewModel: StatusView.ViewModel, textViewDidSelectMeta meta: Meta) // media - func statusView(_ viewModel: StatusView.ViewModel, mediaViewModel mediaViewModel: MediaView.ViewModel, action: MediaView.ViewModel.Action) + func statusView(_ viewModel: StatusView.ViewModel, mediaViewModel: MediaView.ViewModel, action: MediaView.ViewModel.Action) func statusView(_ viewModel: StatusView.ViewModel, toggleContentWarningOverlayDisplay isReveal: Bool) // poll @@ -73,23 +73,41 @@ public struct StatusView: View { public var body: some View { VStack(spacing: .zero) { if let repostViewModel = viewModel.repostViewModel { + // cell top margin + Color.clear.frame(height: viewModel.cellTopMargin) // header if let statusHeaderViewModel = repostViewModel.statusHeaderViewModel { StatusHeaderView(viewModel: statusHeaderViewModel) - .background(GeometryReader { proxy in - Color.clear.preference( - key: ViewFrameKey.self, - value: proxy.frame(in: .local) - ) - .onPreferenceChange(ViewFrameKey.self) { frame in - statusHeaderViewModel.viewSize = frame.size - } - }) Color.clear.frame(height: StatusView.statusHeaderBottomSpacing) } // post StatusView(viewModel: repostViewModel) } else { + // cell top margin + Color.clear.frame(height: viewModel.cellTopMargin) + .overlay { + Group { + // top conversation link + switch viewModel.kind { + case .conversationThread, .conversationRoot: + HStack(spacing: .zero) { + VStack(alignment: .center, spacing: .zero) { + Rectangle() + .foregroundColor(Color(uiColor: .separator)) + .background(.clear) + .frame(width: 1) + .frame(maxHeight: .infinity) + .opacity(viewModel.isTopConversationLinkLineViewDisplay ? 1 : 0) + } + .frame(width: Self.hangingAvatarButtonDimension) // avatar button width + .frame(maxHeight: .infinity) + Spacer() + } + default: + EmptyView() + } + } + } HStack(alignment: .top, spacing: .zero) { if viewModel.hasHangingAvatar { avatarButton @@ -231,34 +249,22 @@ public struct StatusView: View { } } // end HStack .overlay { + // bottom conversation link HStack(alignment: .top, spacing: .zero) { VStack(alignment: .center, spacing: 0) { Color.clear .frame(width: StatusView.hangingAvatarButtonDimension, height: StatusView.hangingAvatarButtonDimension) - // bottom conversation link Rectangle() .foregroundColor(Color(uiColor: .separator)) .background(.clear) .frame(width: 1) .opacity(viewModel.isBottomConversationLinkLineViewDisplay ? 1 : 0) } - .overlay(alignment: .top) { - Group { - // top conversation link - Rectangle() - .foregroundColor(Color(uiColor: .separator)) - .background(.clear) - .frame(width: 1, height: viewModel.topConversationLinkViewHeight) - .offset(y: -viewModel.topConversationLinkViewHeight) - .opacity(viewModel.isTopConversationLinkLineViewDisplay ? 1 : 0) - } // end Group - } Spacer() } // end HStack } // end overlay } // end if … else … } // end VStack - .padding(.top, viewModel.cellTopMargin) .onReceive(viewModel.$isContentSensitiveToggled) { _ in // trigger tableView reload to update the cell height viewModel.delegate?.statusView(viewModel, viewHeightDidChange: Void()) diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index 18e90c65..f9f16352 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -258,8 +258,8 @@ "repositoryURL": "https://github.com/TwidereProject/TwitterSDK.git", "state": { "branch": null, - "revision": "972037346b474f928fe0b2a998c15242ba132f35", - "version": "0.3.0" + "revision": "94732843985776a7b59d8e24cb4e026f20ae8942", + "version": "0.4.0" } }, { diff --git a/TwidereX/Scene/List/HomeList/HomeListStatusTimelineViewController.swift b/TwidereX/Scene/List/HomeList/HomeListStatusTimelineViewController.swift index 242b1be1..122d8d9c 100644 --- a/TwidereX/Scene/List/HomeList/HomeListStatusTimelineViewController.swift +++ b/TwidereX/Scene/List/HomeList/HomeListStatusTimelineViewController.swift @@ -29,7 +29,7 @@ final class HomeListStatusTimelineViewController: UIViewController, NeedsDepende public var viewModel: HomeListStatusTimelineViewModel! var disposeBag = Set() - var listStatusTimelineViewController: ListStatusTimelineViewController? + var listStatusTimelineViewController: ListTimelineViewController? } extension HomeListStatusTimelineViewController { @@ -70,6 +70,7 @@ extension HomeListStatusTimelineViewController { let menuContext = self.viewModel.createHomeListMenuContext() var children: [UIMenuElement] = [ + menuContext.homeTimelineMenu, menuContext.ownedListMenu, menuContext.subscribedListMenu, ] @@ -133,25 +134,46 @@ extension HomeListStatusTimelineViewController { return } - let newList = activeMenuActionViewModel.list.asRecord - var isSameList: Bool { - guard let viewModel = listStatusTimelineViewController?.viewModel as? ListStatusTimelineViewModel else { return false } - return viewModel.list == newList + var isSameTimeline: Bool { + switch activeMenuActionViewModel.timeline { + case .home: + guard let _ = listStatusTimelineViewController?.viewModel as? HomeTimelineViewModel else { return false } + return true + case .list(let list): + guard let viewModel = listStatusTimelineViewController?.viewModel as? ListStatusTimelineViewModel else { return false } + return viewModel.list == list.asRecord + } } - guard !isSameList else { return } + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): isSameTimeline: \(isSameTimeline)") + guard !isSameTimeline else { return } // detach detachTimeline() // attach - let viewController = ListStatusTimelineViewController() - viewController.context = context - viewController.coordinator = coordinator - viewController.viewModel = ListStatusTimelineViewModel( - context: context, - authContext: authContext, - list: activeMenuActionViewModel.list.asRecord - ) + let viewController: ListTimelineViewController = { + switch activeMenuActionViewModel.timeline { + case .home: + let viewController = HomeTimelineViewController() + viewController.context = context + viewController.coordinator = coordinator + viewController.viewModel = HomeTimelineViewModel( + context: context, + authContext: authContext + ) + return viewController + case .list(let list): + let viewController = ListStatusTimelineViewController() + viewController.context = context + viewController.coordinator = coordinator + viewController.viewModel = ListStatusTimelineViewModel( + context: context, + authContext: authContext, + list: list.asRecord + ) + return viewController + } + }() self.listStatusTimelineViewController = viewController self.title = activeMenuActionViewModel.title @@ -192,19 +214,34 @@ extension HomeListStatusTimelineViewController: HomeListStatusTimelineViewModelD _ viewModel: HomeListStatusTimelineViewModel, menuActionDidSelect menuActionViewModel: HomeListStatusTimelineViewModel.HomeListMenuActionViewModel ) { - let list = menuActionViewModel.list.asRecord - let managedObjectContext = context.backgroundManagedObjectContext - Task { - try await managedObjectContext.performChanges { - guard let object = list.object(in: managedObjectContext) else { return } - switch object { - case .twitter(let object): - object.update(activeAt: Date()) - case .mastodon(let object): - object.update(activeAt: Date()) - } // end switch - } - self.viewModel.createHomeListMenuContext() - } // end Task + switch menuActionViewModel.timeline { + case .home(let authenticationIndex): + let authenticationIndex = authenticationIndex.asRecrod + let managedObjectContext = context.backgroundManagedObjectContext + Task { + let now = Date() + try await managedObjectContext.performChanges { + guard let object = authenticationIndex.object(in: managedObjectContext) else { return } + object.update(homeTimelineActiveAt: now) + } + self.viewModel.homeTimelineMenuActionViewModels.first?.activeAt = now + self.viewModel.createHomeListMenuContext() + } // end Task + case .list(let list): + let list = list.asRecord + let managedObjectContext = context.backgroundManagedObjectContext + Task { + try await managedObjectContext.performChanges { + guard let object = list.object(in: managedObjectContext) else { return } + switch object { + case .twitter(let object): + object.update(activeAt: Date()) + case .mastodon(let object): + object.update(activeAt: Date()) + } // end switch + } + self.viewModel.createHomeListMenuContext() + } // end Task + } } // end func } diff --git a/TwidereX/Scene/List/HomeList/HostListStatusTimelineViewModel.swift b/TwidereX/Scene/List/HomeList/HostListStatusTimelineViewModel.swift index a101401b..93d5961b 100644 --- a/TwidereX/Scene/List/HomeList/HostListStatusTimelineViewModel.swift +++ b/TwidereX/Scene/List/HomeList/HostListStatusTimelineViewModel.swift @@ -30,6 +30,7 @@ final class HomeListStatusTimelineViewModel: ObservableObject { weak var delegate: HomeListStatusTimelineViewModelDelegate? // output + @Published var homeTimelineMenuActionViewModels: [HomeListMenuActionViewModel] @Published var ownedListMenuActionViewModels: [HomeListMenuActionViewModel] = [] @Published var subscribedListMenuActionViewModels: [HomeListMenuActionViewModel] = [] @Published var homeListMenuContext: HomeListMenuContext? @@ -47,6 +48,10 @@ final class HomeListStatusTimelineViewModel: ObservableObject { self.ownedListViewModel = ListViewModel(context: context, authContext: authContext, kind: .none) self.subscribedListViewModel = ListViewModel(context: context, authContext: authContext, kind: .none) } + self.homeTimelineMenuActionViewModels = { + guard let authenticationIndex = authContext.authenticationContext.authenticationIndex(in: context.managedObjectContext) else { return [] } + return [HomeListMenuActionViewModel(timeline: .home(authenticationIndex))] + }() // end init Publishers.CombineLatest( @@ -56,7 +61,9 @@ final class HomeListStatusTimelineViewModel: ObservableObject { .receive(on: DispatchQueue.main) .sink { [weak self] _, _ in guard let self = self else { return } - _ = self.createHomeListMenuContext() + Task { @MainActor in + _ = self.createHomeListMenuContext() + } // end Task } .store(in: &disposeBag) @@ -66,7 +73,7 @@ final class HomeListStatusTimelineViewModel: ObservableObject { guard let self = self else { return nil } return records .compactMap { record in record.object(in: self.context.managedObjectContext) } - .map { object in HomeListMenuActionViewModel(list: object) } + .map { object in HomeListMenuActionViewModel(timeline: .list(object)) } } .assign(to: &$ownedListMenuActionViewModels) subscribedListViewModel.fetchedResultController.$records @@ -75,7 +82,7 @@ final class HomeListStatusTimelineViewModel: ObservableObject { guard let self = self else { return nil } return records .compactMap { record in record.object(in: self.context.managedObjectContext) } - .map { object in HomeListMenuActionViewModel(list: object) } + .map { object in HomeListMenuActionViewModel(timeline: .list(object)) } } .assign(to: &$subscribedListMenuActionViewModels) } @@ -87,21 +94,66 @@ extension HomeListStatusTimelineViewModel { var disposeBag = Set() // input - let list: ListObject + let timeline: Timeline // output @Published var title: String = "" @Published var activeAt: Date? = nil - init(list: ListObject) { - self.list = list + init(timeline: Timeline) { + self.timeline = timeline // end init - setup(list: list) + setup(timeline: timeline) + } + + enum Timeline { + case home(AuthenticationIndex) + case list(ListObject) } } +} + +extension HomeListStatusTimelineViewModel.HomeListMenuActionViewModel { + func setup(timeline: Timeline) { + switch timeline { + case .home(let authenticationIndex): + title = L10n.Scene.Timeline.title + authenticationIndex.publisher(for: \.homeTimelineActiveAt) + .assign(to: \.activeAt, on: self) + .store(in: &disposeBag) + case .list(let list): + switch list { + case .twitter(let object): + setup(list: object) + case .mastodon(let object): + setup(list: object) + } + } + } + + func setup(list: TwitterList) { + list.publisher(for: \.name) + .assign(to: \.title, on: self) + .store(in: &disposeBag) + list.publisher(for: \.activeAt) + .assign(to: \.activeAt, on: self) + .store(in: &disposeBag) + } + func setup(list: MastodonList) { + list.publisher(for: \.title) + .assign(to: \.title, on: self) + .store(in: &disposeBag) + list.publisher(for: \.activeAt) + .assign(to: \.activeAt, on: self) + .store(in: &disposeBag) + } +} + +extension HomeListStatusTimelineViewModel { struct HomeListMenuContext { + let homeTimelineMenu: UIMenu let ownedListMenu: UIMenu let subscribedListMenu: UIMenu let isEmpty: Bool @@ -110,14 +162,16 @@ extension HomeListStatusTimelineViewModel { } extension HomeListStatusTimelineViewModel { + @MainActor @discardableResult func createHomeListMenuContext() -> HomeListMenuContext { + let homeTimelineMenuActionViewModels = self.homeTimelineMenuActionViewModels let ownedListMenuActionViewModels = self.ownedListMenuActionViewModels let subscribedListMenuActionViewModels = self.subscribedListMenuActionViewModels - let latestActiveViewModel: HomeListMenuActionViewModel? = { var menuActionViewModels: [HomeListMenuActionViewModel] = [] + menuActionViewModels.append(contentsOf: homeTimelineMenuActionViewModels) menuActionViewModels.append(contentsOf: ownedListMenuActionViewModels) menuActionViewModels.append(contentsOf: subscribedListMenuActionViewModels) @@ -137,6 +191,16 @@ extension HomeListStatusTimelineViewModel { return latestActiveViewModel }() + // home timeline + let homeTimelineMenuActions: [UIMenuElement] = homeTimelineMenuActionViewModels.map { viewModel in + let state: UIMenuElement.State = viewModel === latestActiveViewModel ? .on : .off + return UIAction(title: viewModel.title, state: state) { [weak self] _ in + guard let self = self else { return } + self.delegate?.homeListStatusTimelineViewModel(self, menuActionDidSelect: viewModel) + } + } + let homeTimelineMenu = UIMenu(title: "", options: .displayInline, children: homeTimelineMenuActions) + // owned lists let ownedListMenuActions: [UIMenuElement] = ownedListMenuActionViewModels.map { viewModel in let state: UIMenuElement.State = viewModel === latestActiveViewModel ? .on : .off @@ -162,8 +226,8 @@ extension HomeListStatusTimelineViewModel { return true }() - let homeListMenuContext = HomeListMenuContext( + homeTimelineMenu: homeTimelineMenu, ownedListMenu: ownedListMenu, subscribedListMenu: subscribedListMenu, isEmpty: isEmpty, @@ -175,31 +239,3 @@ extension HomeListStatusTimelineViewModel { } // end func } -extension HomeListStatusTimelineViewModel.HomeListMenuActionViewModel { - func setup(list: ListObject) { - switch list { - case .twitter(let object): - setup(list: object) - case .mastodon(let object): - setup(list: object) - } - } - - func setup(list: TwitterList) { - list.publisher(for: \.name) - .assign(to: \.title, on: self) - .store(in: &disposeBag) - list.publisher(for: \.activeAt) - .assign(to: \.activeAt, on: self) - .store(in: &disposeBag) - } - - func setup(list: MastodonList) { - list.publisher(for: \.title) - .assign(to: \.title, on: self) - .store(in: &disposeBag) - list.publisher(for: \.activeAt) - .assign(to: \.activeAt, on: self) - .store(in: &disposeBag) - } -} diff --git a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift index ce82cb70..fbdb7158 100644 --- a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift @@ -127,9 +127,7 @@ extension NotificationTimelineViewModel { func loadLatest() async { do { switch (scope, authContext.authenticationContext) { - case (.twitter, .twitter(let authenticationContext)): - throw AppError.implicit(.badRequest) - + case (.twitter, .twitter(let authenticationContext)): _ = try await context.apiService.twitterMentionTimeline( query: Twitter.API.Statuses.Timeline.TimelineQuery( maxID: nil diff --git a/TwidereX/Scene/Profile/LocalProfileViewModel.swift b/TwidereX/Scene/Profile/LocalProfileViewModel.swift index 5d4bdd09..f2ff5892 100644 --- a/TwidereX/Scene/Profile/LocalProfileViewModel.swift +++ b/TwidereX/Scene/Profile/LocalProfileViewModel.swift @@ -10,14 +10,19 @@ import Foundation final class LocalProfileViewModel: ProfileViewModel { - init( + convenience init( context: AppContext, authContext: AuthContext, userRecord: UserRecord ) { - super.init( + self.init( context: context, - authContext: authContext + authContext: authContext, + displayLikeTimeline: Self.displayLikeTimeline( + context: context, + authContext: authContext, + userRecord: userRecord + ) ) setup(user: userRecord) @@ -41,3 +46,24 @@ final class LocalProfileViewModel: ProfileViewModel { } // end func setup(user:) } + +extension LocalProfileViewModel { + static func displayLikeTimeline( + context: AppContext, + authContext: AuthContext, + userRecord: UserRecord + ) -> Bool { + let managedObjectContext = context.managedObjectContext + let result: Bool = managedObjectContext.performAndWait { + guard let object = userRecord.object(in: managedObjectContext) else { return false } + switch object { + case .twitter: + return true + case .mastodon(let user): + guard case let .mastodon(authenticationContext) = authContext.authenticationContext else { return false } + return user.id == authenticationContext.userID + } + } + return result + } +} diff --git a/TwidereX/Scene/Profile/MeProfileViewModel.swift b/TwidereX/Scene/Profile/MeProfileViewModel.swift index 5e2ceb9f..78f28d05 100644 --- a/TwidereX/Scene/Profile/MeProfileViewModel.swift +++ b/TwidereX/Scene/Profile/MeProfileViewModel.swift @@ -14,11 +14,15 @@ import TwitterSDK final class MeProfileViewModel: ProfileViewModel { - override init( + convenience init( context: AppContext, authContext: AuthContext ) { - super.init(context: context,authContext: authContext) + self.init( + context: context, + authContext: authContext, + displayLikeTimeline: true + ) // end init self.user = authContext.authenticationContext.user(in: context.managedObjectContext) diff --git a/TwidereX/Scene/Profile/ProfileViewController.swift b/TwidereX/Scene/Profile/ProfileViewController.swift index 3adade2f..9e6487dd 100644 --- a/TwidereX/Scene/Profile/ProfileViewController.swift +++ b/TwidereX/Scene/Profile/ProfileViewController.swift @@ -56,6 +56,7 @@ final class ProfileViewController: UIViewController, NeedsDependency, DrawerSide context: context, authContext: authContext, coordinator: coordinator, + displayLikeTimeline: viewModel.displayLikeTimeline, userIdentifier: viewModel.$userIdentifier ) return profilePagingViewController @@ -142,24 +143,6 @@ extension ProfileViewController { tabBarPagerController.delegate = self tabBarPagerController.dataSource = self - Publishers.CombineLatest( - viewModel.$user, - viewModel.$me - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] user, me in - guard let self = self else { return } - guard let user = user, let me = me else { return } - - // set like timeline display - switch (user, me) { - case (.mastodon(let userObject), .mastodon(let meObject)): - self.profilePagingViewController.viewModel.displayLikeTimeline = userObject.objectID == meObject.objectID - default: - self.profilePagingViewController.viewModel.displayLikeTimeline = true - } - } - .store(in: &disposeBag) Publishers.CombineLatest( viewModel.relationshipViewModel.$optionSet, // update trigger diff --git a/TwidereX/Scene/Profile/ProfileViewModel.swift b/TwidereX/Scene/Profile/ProfileViewModel.swift index ba300f30..6dfb433c 100644 --- a/TwidereX/Scene/Profile/ProfileViewModel.swift +++ b/TwidereX/Scene/Profile/ProfileViewModel.swift @@ -27,6 +27,7 @@ class ProfileViewModel: ObservableObject { let viewDidAppear = CurrentValueSubject(Void()) // output + let displayLikeTimeline: Bool @Published var userRecord: UserRecord? @Published var userIdentifier: UserIdentifier? = nil let relationshipViewModel = RelationshipViewModel() @@ -35,10 +36,12 @@ class ProfileViewModel: ObservableObject { init( context: AppContext, - authContext: AuthContext + authContext: AuthContext, + displayLikeTimeline: Bool ) { self.context = context self.authContext = authContext + self.displayLikeTimeline = displayLikeTimeline // end init // bind data after publisher setup diff --git a/TwidereX/Scene/Profile/RemoteProfileViewModel.swift b/TwidereX/Scene/Profile/RemoteProfileViewModel.swift index 02e46fc2..b957a07f 100644 --- a/TwidereX/Scene/Profile/RemoteProfileViewModel.swift +++ b/TwidereX/Scene/Profile/RemoteProfileViewModel.swift @@ -14,12 +14,20 @@ import MastodonSDK final class RemoteProfileViewModel: ProfileViewModel { - init( + convenience init( context: AppContext, authContext: AuthContext, profileContext: ProfileContext ) { - super.init(context: context, authContext: authContext) + self.init( + context: context, + authContext: authContext, + displayLikeTimeline: RemoteProfileViewModel.displayLikeTimeline( + context: context, + authContext: authContext, + profileContext: profileContext + ) + ) configure(profileContext: profileContext) } @@ -166,3 +174,35 @@ extension RemoteProfileViewModel { } } + +extension RemoteProfileViewModel { + static func displayLikeTimeline( + context: AppContext, + authContext: AuthContext, + profileContext: ProfileContext + ) -> Bool { + switch profileContext { + case .record(let record): + let managedObjectContext = context.managedObjectContext + let result: Bool = managedObjectContext.performAndWait { + switch record { + case .twitter: + return true + case .mastodon(let record): + guard case let .mastodon(authenticationContext) = authContext.authenticationContext else { return false } + guard let object = record.object(in: managedObjectContext) else { return false } + return object.id == authenticationContext.userID + } + } + return result + case .twitter: + return true + case .mastodon(let mastodonContext): + guard case let .mastodon(authenticationContext) = authContext.authenticationContext else { return false } + switch mastodonContext { + case .userID(let userID): + return userID == authenticationContext.userID + } + } + } +} diff --git a/TwidereX/Scene/Profile/Segmented/Paging/ProfilePagingViewController.swift b/TwidereX/Scene/Profile/Segmented/Paging/ProfilePagingViewController.swift index d9dda21b..dcf12fd9 100644 --- a/TwidereX/Scene/Profile/Segmented/Paging/ProfilePagingViewController.swift +++ b/TwidereX/Scene/Profile/Segmented/Paging/ProfilePagingViewController.swift @@ -78,15 +78,6 @@ extension ProfilePagingViewController { } super.viewDidLoad() - - viewModel.$displayLikeTimeline - .receive(on: DispatchQueue.main) - .removeDuplicates() - .sink { [weak self] _ in - guard let self = self else { return } - self.reloadPagerTabStripView() - } - .store(in: &disposeBag) } } diff --git a/TwidereX/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift b/TwidereX/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift index 3ef4358a..87b5d25a 100644 --- a/TwidereX/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift +++ b/TwidereX/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift @@ -15,7 +15,7 @@ final class ProfilePagingViewModel: NSObject { // input let context: AppContext let authContext: AuthContext - @Published var displayLikeTimeline: Bool = true + let displayLikeTimeline: Bool // output let userTimelineViewController: UserTimelineViewController @@ -26,10 +26,12 @@ final class ProfilePagingViewModel: NSObject { context: AppContext, authContext: AuthContext, coordinator: SceneCoordinator, + displayLikeTimeline: Bool, userIdentifier: Published.Publisher? ) { self.context = context self.authContext = authContext + self.displayLikeTimeline = displayLikeTimeline self.userTimelineViewController = { let viewController = UserTimelineViewController() let viewModel = UserTimelineViewModel( @@ -63,10 +65,10 @@ final class ProfilePagingViewModel: NSObject { return viewController }() self.likeTimelineViewController = { - switch authContext.authenticationContext { - case .twitter: return nil - default: break - } + // switch authContext.authenticationContext { + // case .twitter: return nil + // default: break + // } let viewController = UserTimelineViewController() let viewModel = UserTimelineViewModel( context: context, diff --git a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewModel+Diffable.swift b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewModel+Diffable.swift index 24fcac7e..2735ec1a 100644 --- a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewModel+Diffable.swift +++ b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewModel+Diffable.swift @@ -32,7 +32,7 @@ extension DrawerSidebarViewModel { let authenticationContext = self.authContext.authenticationContext switch authenticationContext { case .twitter: - // snapshot.appendItems([.likes], toSection: .main) + snapshot.appendItems([.likes], toSection: .main) if preferredEnableHistory { snapshot.appendItems([.history], toSection: .main) } diff --git a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift index 99d88c3e..a45cafae 100644 --- a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift +++ b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift @@ -47,6 +47,7 @@ final class MainTabBarController: UITabBarController, NeedsDependency { case .twitter: return [ .homeList, + .notification, .search, .me, ] diff --git a/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift b/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift index f81e4c11..e00cd753 100644 --- a/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift @@ -54,7 +54,7 @@ final class SidebarViewModel: ObservableObject { var items: [TabBarItem] = [] switch self.authContext.authenticationContext { case .twitter: - // items.append(contentsOf: [.likes]) + items.append(contentsOf: [.likes]) if preferredEnableHistory { items.append(contentsOf: [.history]) } From 73c6e74f934fe89487db76a99c86d40718483bbb Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 5 May 2023 19:46:44 +0800 Subject: [PATCH 068/128] chore: update version to 2.0.0 (125) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 36 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 9b7d7bd6..5350483f 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 124 + 125 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 9dd52091..4b75f20f 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 124 + 125 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index dda5ca6b..c4b7e5a2 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3431,7 +3431,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 124; + CURRENT_PROJECT_VERSION = 125; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3458,7 +3458,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 124; + CURRENT_PROJECT_VERSION = 125; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3481,7 +3481,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 124; + CURRENT_PROJECT_VERSION = 125; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3505,7 +3505,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 124; + CURRENT_PROJECT_VERSION = 125; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3532,7 +3532,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 124; + CURRENT_PROJECT_VERSION = 125; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3561,7 +3561,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 124; + CURRENT_PROJECT_VERSION = 125; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3590,7 +3590,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 124; + CURRENT_PROJECT_VERSION = 125; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3618,7 +3618,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 124; + CURRENT_PROJECT_VERSION = 125; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3644,7 +3644,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 124; + CURRENT_PROJECT_VERSION = 125; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3671,7 +3671,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 124; + CURRENT_PROJECT_VERSION = 125; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3825,7 +3825,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 124; + CURRENT_PROJECT_VERSION = 125; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3857,7 +3857,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 124; + CURRENT_PROJECT_VERSION = 125; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3884,7 +3884,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 124; + CURRENT_PROJECT_VERSION = 125; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3909,7 +3909,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 124; + CURRENT_PROJECT_VERSION = 125; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3932,7 +3932,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 124; + CURRENT_PROJECT_VERSION = 125; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3955,7 +3955,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 124; + CURRENT_PROJECT_VERSION = 125; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3979,7 +3979,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 124; + CURRENT_PROJECT_VERSION = 125; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -4006,7 +4006,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 124; + CURRENT_PROJECT_VERSION = 125; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index 9dc98138..697cf1d3 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 124 + 125 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index 9d5965a5..051466d7 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 124 + 125 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index f7ed50bd..2cc5125d 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 124 + 125 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index f7ed50bd..2cc5125d 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 124 + 125 From 357e7fe90cc139e3cfe7b6c6159922dc201cc271 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 8 May 2023 14:13:40 +0800 Subject: [PATCH 069/128] fix: Twitter home timeline fetch issue --- TwidereSDK/Package.swift | 2 +- .../ViewModel/Status/StatusFetchViewModel+Timeline+Home.swift | 2 -- TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index f36b14e6..97172674 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -51,7 +51,7 @@ let package = Package( .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.6.0"), .package(url: "https://github.com/tid-kijyun/Kanna.git", from: "5.2.7"), .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"), - .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.4.0"), + .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.5.0"), .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"), .package(name: "CoverFlowStackLayout", path: "../CoverFlowStackLayout"), ], diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Home.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Home.swift index 8361c286..5ea4cd75 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Home.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Home.swift @@ -94,8 +94,6 @@ extension StatusFetchViewModel.Timeline.Home { public static func fetch(api: APIService, input: Input) async throws -> StatusFetchViewModel.Timeline.Output { switch input { case .twitter(let fetchContext): - throw AppError.implicit(.badRequest) - let responses = try await api.twitterHomeTimeline( query: .init( sinceID: fetchContext.sinceID, diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index f9f16352..2490a8d5 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -258,8 +258,8 @@ "repositoryURL": "https://github.com/TwidereProject/TwitterSDK.git", "state": { "branch": null, - "revision": "94732843985776a7b59d8e24cb4e026f20ae8942", - "version": "0.4.0" + "revision": "3682d6e00f7721bbc31b7e490bf2f63faae684fc", + "version": "0.5.0" } }, { From 4f7cd611ed2c68a26f790c36085710d9e5dbfafb Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 8 May 2023 14:22:45 +0800 Subject: [PATCH 070/128] fix: media transition use wrong target issue. Update the media view placeholder color and page indicator --- .../Container/MediaGridContainerView.swift | 18 ++++---- .../Facade/DataSourceFacade+Media.swift | 2 +- .../MediaPreviewViewController.swift | 44 +++++++++---------- ...wViewControllerAnimatedTransitioning.swift | 2 +- .../MediaPreviewTransitionItem.swift | 12 +++-- .../MediaPreviewableViewController.swift | 10 +++-- 6 files changed, 48 insertions(+), 40 deletions(-) diff --git a/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift b/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift index a7b71b96..979989f2 100644 --- a/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift +++ b/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift @@ -172,7 +172,7 @@ extension MediaGridContainerView { ) default: Rectangle() - .fill(Color(uiColor: .placeholderText)) + .fill(.clear) .frame(width: width, height: height) .overlay( MediaView(viewModel: viewModel) @@ -180,16 +180,18 @@ extension MediaGridContainerView { ) } } + .background(Color(uiColor: .placeholderText).opacity(0.3)) .cornerRadius(MediaGridContainerView.cornerRadius) .clipped() .background(GeometryReader { proxy in - Color.clear.preference( - key: ViewFrameKey.self, - value: proxy.frame(in: .global) - ) - .onPreferenceChange(ViewFrameKey.self) { frame in - viewModels[index].frameInWindow = frame - } + Color.clear + .preference( + key: ViewFrameKey.self, + value: proxy.frame(in: .global) + ) + .onPreferenceChange(ViewFrameKey.self) { frame in + viewModels[index].frameInWindow = frame + } }) .overlay(alignment: .bottom) { MediaMetaIndicatorView(viewModel: viewModels[index]) diff --git a/TwidereX/Protocol/Facade/DataSourceFacade+Media.swift b/TwidereX/Protocol/Facade/DataSourceFacade+Media.swift index 570e9f8a..fa3ada5d 100644 --- a/TwidereX/Protocol/Facade/DataSourceFacade+Media.swift +++ b/TwidereX/Protocol/Facade/DataSourceFacade+Media.swift @@ -45,7 +45,7 @@ extension DataSourceFacade { preloadThumbnails: thumbnails )), mediaPreviewTransitionItem: { - let source = MediaPreviewTransitionItem.Source.mediaView(mediaViewModel) + let source = MediaPreviewTransitionItem.Source.mediaView(mediaViewModel, viewModels: statusViewModel.mediaViewModels) let item = MediaPreviewTransitionItem( source: source, previewableViewController: provider diff --git a/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift b/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift index f36591a1..8936ae3f 100644 --- a/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift +++ b/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift @@ -122,7 +122,8 @@ extension MediaPreviewViewController { visualEffectView.contentView.addSubview(pageControlBackgroundVisualEffectView) NSLayoutConstraint.activate([ pageControlBackgroundVisualEffectView.centerXAnchor.constraint(equalTo: mediaInfoDescriptionView.centerXAnchor), - mediaInfoDescriptionView.topAnchor.constraint(equalTo: pageControlBackgroundVisualEffectView.bottomAnchor, constant: 8), + view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: pageControlBackgroundVisualEffectView.bottomAnchor, constant: 16), + // mediaInfoDescriptionView.topAnchor.constraint(equalTo: pageControlBackgroundVisualEffectView.bottomAnchor, constant: 8), ]) pageControl.translatesAutoresizingMaskIntoConstraints = false @@ -165,29 +166,24 @@ extension MediaPreviewViewController { closeButton.addTarget(self, action: #selector(MediaPreviewViewController.closeButtonPressed(_:)), for: .touchUpInside) // bind view model -// viewModel.$currentPage -// .receive(on: DispatchQueue.main) -// .sink { [weak self] index in -// guard let self = self else { return } -// // update page control -// self.pageControl.currentPage = index -// -// // update mediaGridContainerView -// switch self.viewModel.transitionItem.source { -// case .none: -// break -// case .attachment: -// break -// case .attachments(let mediaGridContainerView): -// UIView.animate(withDuration: 0.3) { -// mediaGridContainerView.setAlpha(1) -// mediaGridContainerView.setAlpha(0, index: index) -// } -// case .profileAvatar, .profileBanner: -// break -// } -// } -// .store(in: &disposeBag) + viewModel.$currentPage + .receive(on: DispatchQueue.main) + .sink { [weak self] index in + guard let self = self else { return } + // update page control + self.pageControl.currentPage = index + + // update mediaGridContainerView + switch self.viewModel.transitionItem.source { + case .none: + break + case .mediaView: + self.viewModel.transitionItem.source.updateAppearance(position: .current, index: index) + case .profileAvatar, .profileBanner: + break + } + } + .store(in: &disposeBag) viewModel.$currentPage .receive(on: DispatchQueue.main) diff --git a/TwidereX/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift b/TwidereX/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift index b12bb5df..5dc9632b 100644 --- a/TwidereX/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift +++ b/TwidereX/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift @@ -187,7 +187,7 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { animator.addCompletion { position in if position == .end { // reset appearance - self.transitionItem.source.updateAppearance(position: position, index: nil) + self.transitionItem.source.updateAppearance(position: position, index: fromVC.viewModel.currentPage) } } diff --git a/TwidereX/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift b/TwidereX/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift index 4fcdf2b8..3d4cfc18 100644 --- a/TwidereX/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift +++ b/TwidereX/Scene/Transition/MediaPreview/MediaPreviewTransitionItem.swift @@ -46,7 +46,7 @@ class MediaPreviewTransitionItem: Identifiable { extension MediaPreviewTransitionItem { enum Source { case none - case mediaView(MediaView.ViewModel) + case mediaView(MediaView.ViewModel, viewModels: [MediaView.ViewModel]) case profileAvatar(ProfileHeaderView) case profileBanner(ProfileHeaderView) @@ -57,8 +57,14 @@ extension MediaPreviewTransitionItem { switch self { case .none: break - case .mediaView(let viewModel): - viewModel.shouldHideForTransitioning = position != .end + case .mediaView(let viewModel, let viewModels): + let shouldHideForTransitioning = position != .end + viewModels.forEach { $0.shouldHideForTransitioning = false } + if let index = index, let viewModel = viewModels[safe: index] { + viewModel.shouldHideForTransitioning = shouldHideForTransitioning + } else { + viewModel.shouldHideForTransitioning = shouldHideForTransitioning + } case .profileAvatar(let view): // TODO: break diff --git a/TwidereX/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift b/TwidereX/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift index 8583f199..a3d0b914 100644 --- a/TwidereX/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift +++ b/TwidereX/Scene/Transition/MediaPreview/MediaPreviewableViewController.swift @@ -24,9 +24,13 @@ extension MediaPreviewableViewController { height: 44 ) return frame - case .mediaView(let mediaViewModel): - guard mediaViewModel.frameInWindow != .zero else { return nil } - return mediaViewModel.frameInWindow + case .mediaView(let mediaViewModel, let viewModels): + guard let _viewModel = viewModels[safe: index] else { + guard mediaViewModel.frameInWindow != .zero else { return nil } + return mediaViewModel.frameInWindow + } + guard _viewModel.frameInWindow != .zero else { return nil } + return _viewModel.frameInWindow case .profileAvatar: return nil // TODO: case .profileBanner: From e3a9589d31dbeb92337c0a8d233f3f5ccd5733c3 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 8 May 2023 17:05:47 +0800 Subject: [PATCH 071/128] feat: cut off navigation bar and tab bar during media transition --- ...wViewControllerAnimatedTransitioning.swift | 164 ++++++++---------- 1 file changed, 74 insertions(+), 90 deletions(-) diff --git a/TwidereX/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift b/TwidereX/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift index 5dc9632b..d69bc375 100644 --- a/TwidereX/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift +++ b/TwidereX/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift @@ -244,44 +244,9 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { } // calculate transition mask -// let maskLayerToRect: CGRect? = { -// guard case .attachments = transitionItem.source else { return nil } -// guard let navigationBar = toVC.navigationController?.navigationBar, let navigationBarSuperView = navigationBar.superview else { return nil } -// let navigationBarFrameInWindow = navigationBarSuperView.convert(navigationBar.frame, to: nil) -// -// // crop rect top edge -// var rect = transitionMaskView.frame -// let _toViewFrameInWindow = toVC.view.superview.flatMap { $0.convert(toVC.view.frame, to: nil) } -// if let toViewFrameInWindow = _toViewFrameInWindow, toViewFrameInWindow.minY > navigationBarFrameInWindow.maxY { -// rect.origin.y = toViewFrameInWindow.minY -// } else { -// rect.origin.y = navigationBarFrameInWindow.maxY + UIView.separatorLineHeight(of: toVC.view) // extra hairline -// } -// -// return rect -// }() -// let maskLayerToPath = maskLayerToRect.flatMap { UIBezierPath(rect: $0) }?.cgPath -// let maskLayerToFinalRect: CGRect? = { -// guard case .attachments = transitionItem.source else { return nil } -// var rect = maskLayerToRect ?? transitionMaskView.frame -// // clip tabBar when bar visible -// guard let tabBarController = toVC.tabBarController, -// !tabBarController.tabBar.isHidden, -// let tabBarSuperView = tabBarController.tabBar.superview -// else { return rect } -// let tabBarFrameInWindow = tabBarSuperView.convert(tabBarController.tabBar.frame, to: nil) -// let offset = rect.maxY - tabBarFrameInWindow.minY -// guard offset > 0 else { return rect } -// rect.size.height -= offset -// return rect -// }() -// -// // FIXME: -// let maskLayerToFinalPath = maskLayerToFinalRect.flatMap { UIBezierPath(rect: $0) }?.cgPath -// -// if let maskLayerToPath = maskLayerToPath { -// maskLayer.path = maskLayerToPath -// } + if let path = createTransitionItemMaskLayerPath(transitionContext: transitionContext) { + transitionItem.interactiveTransitionMaskLayer?.path = path + } } mediaPreviewTransitionContext.transitionView.isHidden = true @@ -413,60 +378,16 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { let velocity = convert(gestureVelocity, for: transitionItem) let itemAnimator = MediaHostToMediaPreviewViewControllerAnimatedTransitioning.animator(initialVelocity: velocity) - var maskLayerToFinalPath: CGPath? - if toPosition == .end, - let transitionMaskView = transitionItem.interactiveTransitionMaskView, - let snapshot = transitionItem.snapshotTransitioning { - let toVC = transitionItem.previewableViewController - - var needsMaskWithAnimation = true -// let maskLayerToRect: CGRect? = { -// guard case .attachments = transitionItem.source else { return nil } -// guard let navigationBar = toVC.navigationController?.navigationBar, let navigationBarSuperView = navigationBar.superview else { return nil } -// let navigationBarFrameInWindow = navigationBarSuperView.convert(navigationBar.frame, to: nil) -// -// // crop rect top edge -// var rect = transitionMaskView.frame -// let _toViewFrameInWindow = toVC.view.superview.flatMap { $0.convert(toVC.view.frame, to: nil) } -// if let toViewFrameInWindow = _toViewFrameInWindow, toViewFrameInWindow.minY > navigationBarFrameInWindow.maxY { -// rect.origin.y = toViewFrameInWindow.minY -// } else { -// rect.origin.y = navigationBarFrameInWindow.maxY + UIView.separatorLineHeight(of: toVC.view) // extra hairline -// } -// -// if rect.minY < snapshot.frame.minY { -// needsMaskWithAnimation = false -// } -// -// return rect -// }() -// let maskLayerToPath = maskLayerToRect.flatMap { UIBezierPath(rect: $0) }?.cgPath -// -// if let maskLayer = transitionItem.interactiveTransitionMaskLayer, !needsMaskWithAnimation { -// maskLayer.path = maskLayerToPath -// } -// -// let maskLayerToFinalRect: CGRect? = { -// guard case .attachments = transitionItem.source else { return nil } -// var rect = maskLayerToRect ?? transitionMaskView.frame -// // clip rect bottom when tabBar visible -// guard let tabBarController = toVC.tabBarController, -// !tabBarController.tabBar.isHidden, -// let tabBarSuperView = tabBarController.tabBar.superview -// else { return rect } -// let tabBarFrameInWindow = tabBarSuperView.convert(tabBarController.tabBar.frame, to: nil) -// let offset = rect.maxY - tabBarFrameInWindow.minY -// guard offset > 0 else { return rect } -// rect.size.height -= offset -// return rect -// }() -// maskLayerToFinalPath = maskLayerToFinalRect.flatMap { UIBezierPath(rect: $0) }?.cgPath - } + // create the mask path and apply it in the to .end animation + let maskLayerPath: CGPath? = { + guard toPosition == .end else { return nil } + let path = createTransitionItemMaskLayerPath(transitionContext: transitionContext) + return path + }() itemAnimator.addAnimations { - if let maskLayer = self.transitionItem.interactiveTransitionMaskLayer, - let maskLayerToFinalPath = maskLayerToFinalPath { - maskLayer.path = maskLayerToFinalPath + if let path = maskLayerPath { + self.transitionItem.interactiveTransitionMaskLayer?.path = path } if toPosition == .end { switch self.transitionItem.source { @@ -540,3 +461,66 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { } } + +extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { + private func createTransitionItemMaskLayerPath(transitionContext: UIViewControllerContextTransitioning) -> CGPath? { + guard let interactiveTransitionMaskView = transitionItem.interactiveTransitionMaskView else { return nil } + guard let snapshotTransitioning = transitionItem.snapshotTransitioning else { return nil } + + switch transitionItem.source { + case .mediaView: break + case .profileAvatar: return nil + case .profileBanner: return nil + case .none: return nil + } + + // cutoff top navigation bar + let navigationBarCutoffMaskRect: CGRect? = { + let toVC = transitionItem.previewableViewController + guard let navigationBar = toVC.navigationController?.navigationBar, + let navigationBarSuperView = navigationBar.superview + else { return nil } + let navigationBarFrameInWindow = navigationBarSuperView.convert(navigationBar.frame, to: nil) + + var rect = interactiveTransitionMaskView.frame + let _toViewFrameInWindow = toVC.view.superview.flatMap { $0.convert(toVC.view.frame, to: nil) } + if let toViewFrameInWindow = _toViewFrameInWindow, toViewFrameInWindow.minY > navigationBarFrameInWindow.maxY { + rect.origin.y = toViewFrameInWindow.minY + } else { + rect.origin.y = navigationBarFrameInWindow.maxY + UIView.separatorLineHeight(of: toVC.view) // extra hairline + } + + guard snapshotTransitioning.frame.minY > rect.minY else { + return nil + } + return rect + }() + + // cutoff tabBar when bar visible + let tabBarCutoffMaskRect: CGRect? = { + let toVC = transitionItem.previewableViewController + guard let tabBarController = toVC.tabBarController, + !tabBarController.tabBar.isHidden, + let tabBarSuperView = tabBarController.tabBar.superview + else { return nil } + let tabBarFrameInWindow = tabBarSuperView.convert(tabBarController.tabBar.frame, to: nil) + + var rect = interactiveTransitionMaskView.frame + let offset = rect.maxY - tabBarFrameInWindow.minY + guard offset > 0 else { return nil } + rect.size.height -= offset + return rect + }() + + var rect = interactiveTransitionMaskView.frame + let cutoffRects: [CGRect] = [ + navigationBarCutoffMaskRect ?? interactiveTransitionMaskView.frame, + tabBarCutoffMaskRect ?? interactiveTransitionMaskView.frame + ] + for cutoffRect in cutoffRects { + rect = rect.intersection(cutoffRect) + } + + return UIBezierPath(rect: rect).cgPath + } +} From 3c8579f8ddf85185692b449c1cd8bc1f8c7aaed0 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 9 May 2023 14:49:08 +0800 Subject: [PATCH 072/128] fix: make the List member menu/membership button works --- .../List/ListMembershipViewModel.swift | 16 +- .../Container/BadgeClipContainer.swift | 49 +++++ .../Content/UserContentView+ViewModel.swift | 132 ------------ .../TwidereUI/Content/UserContentView.swift | 53 ----- .../Content/UserView+Configuration.swift | 39 ---- .../Content/UserView+ViewModel.swift | 193 ++++++++---------- .../Sources/TwidereUI/Content/UserView.swift | 182 +++++++++++------ .../User/UserViewTableViewCellDelegate.swift | 5 + ...ovider+UserViewTableViewCellDelegate.swift | 116 ++++++----- .../ListUser/ListUserViewController.swift | 62 +----- .../ListUser/ListUserViewModel+Diffable.swift | 4 +- .../User/SearchUserViewModel+Diffable.swift | 2 +- 12 files changed, 341 insertions(+), 512 deletions(-) create mode 100644 TwidereSDK/Sources/TwidereUI/Container/BadgeClipContainer.swift delete mode 100644 TwidereSDK/Sources/TwidereUI/Content/UserContentView+ViewModel.swift delete mode 100644 TwidereSDK/Sources/TwidereUI/Content/UserContentView.swift diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/List/ListMembershipViewModel.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/List/ListMembershipViewModel.swift index 3e3896bc..b2723c44 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/List/ListMembershipViewModel.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/List/ListMembershipViewModel.swift @@ -14,9 +14,10 @@ public protocol ListMembershipViewModelDelegate: AnyObject { } public final class ListMembershipViewModel { - + let logger = Logger(subsystem: "ListMembershipViewModel", category: "ViewModel") + public var id = UUID() public weak var delegate: ListMembershipViewModelDelegate? // input @@ -55,6 +56,7 @@ public final class ListMembershipViewModel { extension ListMembershipViewModel { + @MainActor public func add( user: UserRecord, authenticationContext: AuthenticationContext @@ -77,6 +79,7 @@ extension ListMembershipViewModel { } } + @MainActor public func remove( user: UserRecord, authenticationContext: AuthenticationContext @@ -100,3 +103,14 @@ extension ListMembershipViewModel { } } + +// MARK: - ListMembershipViewModel +extension ListMembershipViewModel: Hashable { + public static func == (lhs: ListMembershipViewModel, rhs: ListMembershipViewModel) -> Bool { + return lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} diff --git a/TwidereSDK/Sources/TwidereUI/Container/BadgeClipContainer.swift b/TwidereSDK/Sources/TwidereUI/Container/BadgeClipContainer.swift new file mode 100644 index 00000000..158cac34 --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Container/BadgeClipContainer.swift @@ -0,0 +1,49 @@ +// +// BadgeClipContainer.swift +// +// +// Created by MainasuK on 2023/5/9. +// + +import SwiftUI + +public struct BadgeClipContainer: View { + + public let content: Content + public let badge: Badge + + public init( + @ViewBuilder content: () -> Content, + @ViewBuilder badge: () -> Badge + ) { + self.content = content() + self.badge = badge() + } + + public var body: some View { + ZStack(alignment: Alignment(horizontal: .trailing, vertical: .bottom)) { + content + badge + .scaleEffect(1.2) + .alignmentGuide(HorizontalAlignment.trailing, computeValue: { d in d.width - 4 }) + .alignmentGuide(VerticalAlignment.bottom, computeValue: { d in d.height - 4 }) + .blendMode(.destinationOut) + .overlay { + badge + } + + } + .compositingGroup() + } +} + +struct BadgeClipContainer_Previews: PreviewProvider { + static var previews: some View { + BadgeClipContainer(content: { + Color.blue + .frame(width: 44, height: 44) + }, badge: { + Image(uiImage: Asset.Badge.verified.image) + }) + } +} diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserContentView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/UserContentView+ViewModel.swift deleted file mode 100644 index cd5941c2..00000000 --- a/TwidereSDK/Sources/TwidereUI/Content/UserContentView+ViewModel.swift +++ /dev/null @@ -1,132 +0,0 @@ -// -// UserContentView+ViewModel.swift -// -// -// Created by MainasuK on 2022-7-12. -// - -import Foundation -import Combine -import CoreData -import CoreDataStack -import SwiftUI -import TwidereCore -import Meta - -extension UserContentView { - public class ViewModel: ObservableObject { - - // input - public let user: UserObject - public let accessoryType: AccessoryType - - // output - @Published public var platform: Platform = .none - - @Published public var name: MetaContent = PlaintextMetaContent(string: " ") - @Published public var username: MetaContent = PlaintextMetaContent(string: " ") - @Published public var acct: MetaContent = PlaintextMetaContent(string: " ") - @Published public var avatarImageURL: URL? - - @Published public var protected: Bool = false - - public init( - user: UserObject, - accessoryType: AccessoryType - ) { - self.user = user - self.accessoryType = accessoryType - // end init - - // configure() - } - } -} - -extension UserContentView.ViewModel { - - public enum AccessoryType { - case none - case disclosureIndicator - } - -} - -//extension UserContentView.ViewModel { -// -// func configure() { -// assert(Thread.isMainThread) -// -// switch user { -// case .twitter(let user): -// configure(user: user) -// case .mastodon(let user): -// configure(user: user) -// } -// } -// -//} - -//extension UserContentView.ViewModel { -// private func configure(user: TwitterUser) { -// // platform -// platform = .twitter -// // avatar -// user.publisher(for: \.profileImageURL) -// .map { _ in user.avatarImageURL() } -// .assign(to: &$avatarImageURL) -// // author name -// user.publisher(for: \.name) -// .map { PlaintextMetaContent(string: $0) } -// .assign(to: &$name) -// // author username -// user.publisher(for: \.username) -// .map { PlaintextMetaContent(string: "@" + $0) } -// .assign(to: &$username) -// // acct -// user.publisher(for: \.username) -// .map { PlaintextMetaContent(string: "@" + $0) } -// .assign(to: &$acct) -// // protected -// user.publisher(for: \.protected) -// .assign(to: &$protected) -// } -//} - -//extension UserContentView.ViewModel { -// private func configure(user: MastodonUser) { -// // platform -// platform = .mastodon -// // avatar -// Publishers.CombineLatest3( -// UserDefaults.shared.publisher(for: \.preferredStaticAvatar), -// user.publisher(for: \.avatar), -// user.publisher(for: \.avatarStatic) -// ) -// .map { preferredStaticAvatar, avatar, avatarStatic in -// let string = preferredStaticAvatar ? (avatarStatic ?? avatar) : avatar -// return string.flatMap { URL(string: $0) } -// } -// .assign(to: &$avatarImageURL) -// // author name -// Publishers.CombineLatest( -// user.publisher(for: \.displayName), -// user.publisher(for: \.emojis) -// ) -// .map { name, _ -> MetaContent in -// user.nameMetaContent ?? PlaintextMetaContent(string: name) -// } -// .assign(to: &$name) -// // author username -// user.publisher(for: \.acct) -// .map { PlaintextMetaContent(string: "@" + $0) } -// .assign(to: &$username) -// // acct -// user.publisher(for: \.acct) -// .map { _ in PlaintextMetaContent(string: "@" + user.acctWithDomain) } -// .assign(to: &$acct) -// // protected -// user.publisher(for: \.locked) -// .assign(to: &$protected) -// } -//} diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserContentView.swift b/TwidereSDK/Sources/TwidereUI/Content/UserContentView.swift deleted file mode 100644 index 92d416c7..00000000 --- a/TwidereSDK/Sources/TwidereUI/Content/UserContentView.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// UserContentView.swift -// -// -// Created by MainasuK on 2022-7-12. -// - -import SwiftUI - -public struct UserContentView: View { - - @ObservedObject public var viewModel: ViewModel - - public init(viewModel: ViewModel) { - self.viewModel = viewModel - } - - public var body: some View { - HStack { - let dimension = ProfileAvatarView.Dimension.inline - ProfileAvatarViewRepresentable( - configuration: .init(url: viewModel.avatarImageURL), - dimension: dimension, - badge: .none - ) - .frame( - width: dimension.primitiveAvatarButtonSize.width, - height: dimension.primitiveAvatarButtonSize.height - ) - VStack(alignment: .leading, spacing: .zero) { - Spacer() - MetaLabelRepresentable( - textStyle: .userAuthorName, - metaContent: viewModel.name - ) - MetaLabelRepresentable( - textStyle: .userAuthorUsername, - metaContent: viewModel.acct - ) - Spacer() - } - Spacer() - switch viewModel.accessoryType { - case .none: - EmptyView() - case .disclosureIndicator: - Image(systemName: "chevron.right") - .foregroundColor(Color(.secondaryLabel)) - } // end switch - } // end HStack - } // end body - -} diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView+Configuration.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView+Configuration.swift index 296083b4..728586ca 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView+Configuration.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView+Configuration.swift @@ -14,21 +14,6 @@ import TwidereAsset import Meta import MastodonSDK -//extension UserView { -// public struct ConfigurationContext { -// public let authContext: AuthContext -// public let listMembershipViewModel: ListMembershipViewModel? -// -// public init( -// authContext: AuthContext, -// listMembershipViewModel: ListMembershipViewModel? -// ) { -// self.authContext = authContext -// self.listMembershipViewModel = listMembershipViewModel -// } -// } -//} -// //extension UserView { // public func configure( // user: UserObject, @@ -49,30 +34,6 @@ import MastodonSDK // // viewModel.relationshipViewModel.user = user // viewModel.relationshipViewModel.me = me -// -// viewModel.listMembershipViewModel = configurationContext.listMembershipViewModel -// if let listMembershipViewModel = configurationContext.listMembershipViewModel { -// listMembershipViewModel.$ownerUserIdentifier -// .assign(to: \.listOwnerUserIdentifier, on: viewModel) -// .store(in: &disposeBag) -// } -// -// // accessory -// switch style { -// case .addListMember: -// guard let listMembershipViewModel = configurationContext.listMembershipViewModel else { -// assertionFailure() -// break -// } -// let userRecord = user.asRecord -// listMembershipViewModel.$members -// .map { members in members.contains(userRecord) } -// .assign(to: \.isListMember, on: viewModel) -// .store(in: &disposeBag) -// listMembershipViewModel.$workingMembers -// .map { members in members.contains(userRecord) } -// .assign(to: \.isListMemberCandidate, on: viewModel) -// .store(in: &disposeBag) // default: // break // } diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift index 8481e2ba..0b436941 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift @@ -24,7 +24,7 @@ extension UserView { let relationshipViewModel = RelationshipViewModel() // input - public let user: UserObject + public let user: UserObject? public let authContext: AuthContext? public let kind: Kind public weak var delegate: UserViewDelegate? @@ -40,7 +40,7 @@ extension UserView { @Published public var name: MetaContent = PlaintextMetaContent(string: "") @Published public var username: String = "" -// @Published public var platform: Platform = .none + @Published public var platform: Platform = .none // @Published public var authenticationContext: AuthenticationContext? // me // @Published public var userAuthenticationContext: AuthenticationContext? // @@ -62,22 +62,23 @@ extension UserView { // follow @Published public var followButtonViewModel: FollowButton.ViewModel? -// -// public var listMembershipViewModel: ListMembershipViewModel? -// @Published public var listOwnerUserIdentifier: UserIdentifier? = nil -// @Published public var isListMember = false -// @Published public var isListMemberCandidate = false // a.k.a isBusy -// @Published public var isMyList = false -// -// @Published public var badgeCount: Int = 0 -// + + public var listMembershipViewModel: ListMembershipViewModel? + @Published public var listOwnerUserIdentifier: UserIdentifier? = nil + @Published public var isListMember = false + @Published public var isListMemberCandidate = false // a.k.a isBusy + @Published public var isMyList = false + + // notification count + @Published public var notificationBadgeCount: Int = 0 + // public enum Header { // case none // case notification(info: NotificationHeaderInfo) // } private init( - object user: UserObject, + object user: UserObject?, authContext: AuthContext?, kind: Kind, delegate: UserViewDelegate? @@ -88,10 +89,27 @@ extension UserView { self.delegate = delegate // end init - // notification switch kind { case .notification(let notification): self.notification = notification + + case .listMember(let listMembershipViewModel), .addListMember(let listMembershipViewModel): + if let listMembershipViewModel = listMembershipViewModel, + let userRecord = user?.asRecord + { + self.listMembershipViewModel = listMembershipViewModel + listMembershipViewModel.$ownerUserIdentifier + .assign(to: \.listOwnerUserIdentifier, on: self) + .store(in: &disposeBag) + listMembershipViewModel.$members + .map { members in members.contains(userRecord) } + .assign(to: \.isListMember, on: self) + .store(in: &disposeBag) + listMembershipViewModel.$workingMembers + .map { members in members.contains(userRecord) } + .assign(to: \.isListMemberCandidate, on: self) + .store(in: &disposeBag) + } default: break } @@ -110,7 +128,7 @@ extension UserView { switch kind { case .search: // follow - if let authContext = authContext { + if let authContext = authContext, let user = user { self.followButtonViewModel = .init(user: user, authContext: authContext) } default: @@ -120,33 +138,30 @@ extension UserView { // avatar style UserDefaults.shared.publisher(for: \.avatarStyle) .assign(to: &$avatarStyle) -// // isMyList -// Publishers.CombineLatest( -// $authenticationContext, -// $listOwnerUserIdentifier -// ) -// .map { authenticationContext, userIdentifier -> Bool in -// guard let authenticationContext = authenticationContext else { return false } -// guard let userIdentifier = userIdentifier else { return false } -// return authenticationContext.userIdentifier == userIdentifier -// } -// .assign(to: &$isMyList) -// // badge count -// $userAuthenticationContext -// .map { authenticationContext -> Int in -// switch authenticationContext { -// case .twitter: -// return 0 -// case .mastodon(let authenticationContext): -// let accessToken = authenticationContext.authorization.accessToken -// let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) -// return count -// case .none: -// return 0 -// } -// } -// .assign(to: &$badgeCount) - } + + // isMyList + $listOwnerUserIdentifier + .map { userIdentifier -> Bool in + guard let authenticationContext = authContext?.authenticationContext else { return false } + guard let userIdentifier = userIdentifier else { return false } + return authenticationContext.userIdentifier == userIdentifier + } + .assign(to: &$isMyList) + + // notification badge count + notificationBadgeCount = { + switch authContext?.authenticationContext { + case .twitter: + return 0 + case .mastodon(let authenticationContext): + let accessToken = authenticationContext.authorization.accessToken + let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) + return count + case nil: + return 0 + } + }() + } // end init } } @@ -185,13 +200,14 @@ extension UserView.ViewModel { // headline: name | lock | username // subheadline: follower count - // accessory: membership menu - case listMember + // accessory: membership menu (isMyList) + // menuActions: [ remove ] + case listMember(ListMembershipViewModel?) // headline: name | lock | username // subheadline: follower count // accessory: membership button - case addListMember + case addListMember(ListMembershipViewModel?) // headline: name | lock // subheadline: username @@ -212,7 +228,7 @@ extension UserView.ViewModel { public enum MenuAction: Hashable { case signOut - case remove + case removeListMember } } @@ -320,31 +336,6 @@ extension UserView.ViewModel { // .store(in: &disposeBag) // // // accessory -// switch userView.style { -// case .account: -// $badgeCount -// .sink { count in -// let count = max(0, min(count, 50)) -// userView.badgeImageView.image = UIImage(systemName: "\(count).circle.fill")?.withRenderingMode(.alwaysTemplate) -// userView.badgeImageView.isHidden = count == 0 -// } -// .store(in: &disposeBag) -// userView.menuButton.showsMenuAsPrimaryAction = true -// userView.menuButton.menu = { -// let children = [ -// UIAction( -// title: L10n.Common.Controls.Actions.signOut, -// image: UIImage(systemName: "person.crop.circle.badge.minus"), -// attributes: .destructive, -// state: .off -// ) { [weak userView] _ in -// guard let userView = userView else { return } -// userView.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): sign out user…") -// userView.delegate?.userView(userView, menuActionDidPressed: .signOut, menuButton: userView.menuButton) -// } -// ] -// return UIMenu(title: "", image: nil, options: [], children: children) -// }() // // case .notification: // $isFollowRequestBusy @@ -356,46 +347,6 @@ extension UserView.ViewModel { // } // .store(in: &disposeBag) // -// case .listMember: -// userView.menuButton.showsMenuAsPrimaryAction = true -// userView.menuButton.menu = { -// let children = [ -// UIAction( -// title: L10n.Common.Controls.Actions.remove, -// image: UIImage(systemName: "minus.circle"), -// attributes: .destructive, -// state: .off -// ) { [weak userView] _ in -// guard let userView = userView else { return } -// userView.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): remove user…") -// userView.delegate?.userView(userView, menuActionDidPressed: .remove, menuButton: userView.menuButton) -// } -// ] -// return UIMenu(title: "", image: nil, options: [], children: children) -// }() -// $isMyList -// .map { !$0 } -// .assign(to: \.isHidden, on: userView.menuButton) -// .store(in: &disposeBag) -// case .addListMember: -// Publishers.CombineLatest( -// $isListMember, -// $isListMemberCandidate -// ) -// .receive(on: DispatchQueue.main) -// .sink { [weak userView] isMember, isMemberCandidate in -// guard let userView = userView else { return } -// let image = isMember ? UIImage(systemName: "minus.circle") : UIImage(systemName: "plus.circle") -// let tintColor = isMember ? UIColor.systemRed : Asset.Colors.hightLight.color -// userView.membershipButton.setImage(image, for: .normal) -// userView.membershipButton.tintColor = tintColor -// -// userView.membershipButton.alpha = isMemberCandidate ? 0 : 1 -// userView.activityIndicatorView.isHidden = !isMemberCandidate -// userView.activityIndicatorView.startAnimating() -// } -// .store(in: &disposeBag) -// // default: // userView.menuButton.showsMenuAsPrimaryAction = true // userView.menuButton.menu = nil @@ -449,6 +400,7 @@ extension UserView.ViewModel { // end init // user + platform = .twitter user.publisher(for: \.profileImageURL) .map { _ in user.avatarImageURL() } .assign(to: &$avatarURL) @@ -474,6 +426,7 @@ extension UserView.ViewModel { // end init // user + platform = .mastodon user.publisher(for: \.avatar) .compactMap { $0.flatMap { URL(string: $0) } } .assign(to: &$avatarURL) @@ -485,3 +438,23 @@ extension UserView.ViewModel { .assign(to: &$username) } } + +#if DEBUG +extension UserView.ViewModel { + public convenience init(kind: Kind) { + self.init( + object: nil, + authContext: nil, + kind: kind, + delegate: nil + ) + // end init + + avatarURL = URL(string: "https://pbs.twimg.com/profile_images/1445764922474827784/W2zEPN7U_400x400.jpg") + name = PlaintextMetaContent(string: "Name") + username = "username" + platform = .twitter + notificationBadgeCount = 10 + } +} +#endif diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift index 92ccd0f6..51150a96 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift @@ -18,8 +18,7 @@ import Kingfisher public protocol UserViewDelegate: AnyObject { func userView(_ viewModel: UserView.ViewModel, userAvatarButtonDidPressed user: UserRecord) func userView(_ viewModel: UserView.ViewModel, menuActionDidPressed action: UserView.ViewModel.MenuAction) -// func userView(_ userView: UserView, friendshipButtonDidPressed button: UIButton) -// func userView(_ userView: UserView, membershipButtonDidPressed button: UIButton) + func userView(_ viewModel: UserView.ViewModel, listMembershipButtonDidPressed user: UserRecord) func userView(_ viewModel: UserView.ViewModel, followReqeustButtonDidPressed user: UserRecord, accept: Bool) } @@ -72,8 +71,37 @@ extension UserView { var avatarButton: some View { Button { - viewModel.delegate?.userView(viewModel, userAvatarButtonDidPressed: viewModel.user.asRecord) + guard let user = viewModel.user?.asRecord else { + assertionFailure() + return + } + viewModel.delegate?.userView(viewModel, userAvatarButtonDidPressed: user) } label: { + switch viewModel.kind { + case .account: + BadgeClipContainer { + avatarButtonContentView + } badge: { + switch viewModel.platform { + case .none: + EmptyView() + case .twitter: + Image(uiImage: Asset.Badge.circleTwitter.image) + case .mastodon: + Image(uiImage: Asset.Badge.circleMastodon.image) + } + } + + default: + avatarButtonContentView + } + } + .buttonStyle(.borderless) + .allowsHitTesting(allowsAvatarButtonHitTesting) + } + + var avatarButtonContentView: some View { + Group { let dimension: CGFloat = StatusView.hangingAvatarButtonDimension KFImage(viewModel.avatarURL) .placeholder { progress in @@ -85,8 +113,6 @@ extension UserView { .clipShape(AvatarClipShape(avatarStyle: viewModel.avatarStyle)) .animation(.easeInOut, value: viewModel.avatarStyle) } - .buttonStyle(.borderless) - .allowsHitTesting(allowsAvatarButtonHitTesting) } var nameLabel: some View { @@ -130,7 +156,17 @@ extension UserView { Image(systemName: "person.crop.circle.badge.minus") } } - + case .listMember: + // remove + Button(role: .destructive) { + viewModel.delegate?.userView(viewModel, menuActionDidPressed: .removeListMember) + } label: { + Label { + Text(L10n.Common.Controls.Actions.remove) + } icon: { + Image(systemName: "minus.circle") + } + } default: EmptyView() } @@ -142,10 +178,24 @@ extension UserView { var membershipButton: some View { Button { -// switch + guard !viewModel.isListMemberCandidate else { return } + guard let user = viewModel.user?.asRecord else { return } + viewModel.delegate?.userView(viewModel, listMembershipButtonDidPressed: user) } label: { -// let systemName = -// Image(systemName: "ellipsis.circle") + let tintColor = viewModel.isListMember ? UIColor.systemRed : Asset.Colors.hightLight.color + let systemName = viewModel.isListMember ? "minus.circle" : "plus.circle" + Image(systemName: systemName) + .foregroundColor(Color(uiColor: tintColor)) + .padding() + .opacity(viewModel.isListMemberCandidate ? 0 : 1) + .overlay { + Group { + if viewModel.isListMemberCandidate { + ProgressView() + .progressViewStyle(.circular) + } + } + } // end overlay } } @@ -153,14 +203,22 @@ extension UserView { HStack(spacing: .zero) { Button { guard !viewModel.isFollowRequestBusy else { return } - viewModel.delegate?.userView(viewModel, followReqeustButtonDidPressed: viewModel.user.asRecord, accept: true) + guard let user = viewModel.user?.asRecord else { + assertionFailure() + return + } + viewModel.delegate?.userView(viewModel, followReqeustButtonDidPressed: user, accept: true) } label: { Image(uiImage: Asset.Indices.checkmarkCircle.image.withRenderingMode(.alwaysTemplate)) .padding() } Button { guard !viewModel.isFollowRequestBusy else { return } - viewModel.delegate?.userView(viewModel, followReqeustButtonDidPressed: viewModel.user.asRecord, accept: false) + guard let user = viewModel.user?.asRecord else { + assertionFailure() + return + } + viewModel.delegate?.userView(viewModel, followReqeustButtonDidPressed: user, accept: false) } label: { Image(uiImage: Asset.Indices.xmarkCircle.image.withRenderingMode(.alwaysTemplate)) .padding() @@ -175,6 +233,13 @@ extension UserView { } } } + + var notificationBadgeCountView: some View { + Group { + let count = max(0, min(viewModel.notificationBadgeCount, 50)) + Image(systemName: "\(count).circle.fill") + } + } } extension UserView { @@ -236,7 +301,12 @@ extension UserView { Group { switch viewModel.kind { case .account: - menuView + HStack { + if viewModel.notificationBadgeCount > 0 { + notificationBadgeCountView + } + menuView + } case .search: // TODO: follow button EmptyView() @@ -251,9 +321,11 @@ extension UserView { case .mentionPick: EmptyView() case .listMember: - EmptyView() + if viewModel.isMyList { + menuView + } case .addListMember: - EmptyView() + membershipButton case .settingAccountSection: Image(systemName: "chevron.right") .foregroundColor(Color(.secondaryLabel)) @@ -370,14 +442,6 @@ extension UserView { // return button // }() // -// // add/remove control -// public let membershipButton: HitTestExpandedButton = { -// let button = HitTestExpandedButton() -// button.setImage(UIImage(systemName: "plus.circle"), for: .normal) -// button.tintColor = Asset.Colors.hightLight.color // FIXME: tint color -// return button -// }() -// // // follow request // public let followRequestControlContainerView = UIStackView() // @@ -779,54 +843,36 @@ extension UserView { #if DEBUG -import SwiftUI +import CoreData +import CoreDataStack + struct UserView_Preview: PreviewProvider { + + static var kinds: [UserView.ViewModel.Kind] = [ + .account, + .search, + .friend, + .history, + // .notification, + .mentionPick, + // .listMember, + // .addListMember, + .settingAccountSection, + .plain + ] + static var previews: some View { - EmptyView() -// Group { -// UIViewPreview { -// let userView = UserView() -// userView.setup(style: .account) -// return userView -// } -// .previewLayout(.fixed(width: 375, height: 48)) -// .previewDisplayName("Account") -// UIViewPreview { -// let userView = UserView() -// userView.setup(style: .relationship) -// return userView -// } -// .previewLayout(.fixed(width: 375, height: 48)) -// .previewDisplayName("Relationship") -// UIViewPreview { -// let userView = UserView() -// userView.setup(style: .friendship) -// return userView -// } -// .previewLayout(.fixed(width: 375, height: 48)) -// .previewDisplayName("Friendship") -// UIViewPreview { -// let userView = UserView() -// userView.setup(style: .notification) -// return userView -// } -// .previewLayout(.fixed(width: 375, height: 48)) -// .previewDisplayName("Notification") -// UIViewPreview { -// let userView = UserView() -// userView.setup(style: .mentionPick) -// return userView -// } -// .previewLayout(.fixed(width: 375, height: 48)) -// .previewDisplayName("MentionPick") -// UIViewPreview { -// let userView = UserView() -// userView.setup(style: .addListMember) -// return userView -// } -// .previewLayout(.fixed(width: 375, height: 48)) -// .previewDisplayName("AddListMember") -// } + List { + ForEach(kinds, id: \.self) { kind in + Section(content: { + UserView(viewModel: .init(kind: kind)) + .padding(.horizontal) + }, header: { + Text("\(String(describing: kind).localizedCapitalized)") + }) + .textCase(nil) + } + } } } #endif diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserViewTableViewCellDelegate.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserViewTableViewCellDelegate.swift index 451633dc..1a9a56df 100644 --- a/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserViewTableViewCellDelegate.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/User/UserViewTableViewCellDelegate.swift @@ -23,6 +23,7 @@ public protocol UserViewTableViewCellDelegate: AutoGenerateProtocolDelegate { // sourcery:inline:UserViewTableViewCellDelegate.AutoGenerateProtocolDelegate func tableViewCell(_ cell: UITableViewCell, viewModel: UserView.ViewModel, userAvatarButtonDidPressed user: UserRecord) func tableViewCell(_ cell: UITableViewCell, viewModel: UserView.ViewModel, menuActionDidPressed action: UserView.ViewModel.MenuAction) + func tableViewCell(_ cell: UITableViewCell, viewModel: UserView.ViewModel, listMembershipButtonDidPressed user: UserRecord) func tableViewCell(_ cell: UITableViewCell, viewModel: UserView.ViewModel, followReqeustButtonDidPressed user: UserRecord, accept: Bool) // sourcery:end } @@ -39,6 +40,10 @@ public extension UserViewDelegate where Self: UserViewContainerTableViewCell { userViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, menuActionDidPressed: action) } + func userView(_ viewModel: UserView.ViewModel, listMembershipButtonDidPressed user: UserRecord) { + userViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, listMembershipButtonDidPressed: user) + } + func userView(_ viewModel: UserView.ViewModel, followReqeustButtonDidPressed user: UserRecord, accept: Bool) { userViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, followReqeustButtonDidPressed: user, accept: accept) } diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift index 3bd1d7bf..5648062b 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift @@ -36,14 +36,45 @@ extension UserViewTableViewCellDelegate where Self: DataSourceProvider { switch action { case .signOut: Task { + guard let user = viewModel.user?.asRecord else { + assertionFailure() + return + } try await DataSourceFacade.responseToUserSignOut( dependency: self, - user: viewModel.user.asRecord + user: user ) } // end Task - case .remove: - assertionFailure("Override in view controller") - } + + case .removeListMember: + Task { @MainActor in + guard !viewModel.isListMemberCandidate else { return } + guard let authenticationContext = viewModel.authContext?.authenticationContext else { return } + guard let listMembershipViewModel = viewModel.listMembershipViewModel else { return } + guard let user = viewModel.user?.asRecord else { return } + do { + try await listMembershipViewModel.remove(user: user, authenticationContext: authenticationContext) + + var config = SwiftMessages.defaultConfig + config.duration = .seconds(seconds: 3) + config.interactiveHide = true + let bannerView = NotificationBannerView() + bannerView.configure(style: .success) + bannerView.titleLabel.text = L10n.Common.Alerts.ListMemberRemoved.title + bannerView.messageLabel.isHidden = true + SwiftMessages.show(config: config, view: bannerView) + } catch { + var config = SwiftMessages.defaultConfig + config.duration = .seconds(seconds: 3) + config.interactiveHide = true + let bannerView = NotificationBannerView() + bannerView.configure(style: .warning) + bannerView.titleLabel.text = L10n.Common.Alerts.FailedToRemoveListMember.title + bannerView.messageLabel.text = error.localizedDescription + SwiftMessages.show(config: config, view: bannerView) + } + } // end Task + } // end switch } // func tableViewCell( @@ -92,53 +123,38 @@ extension UserViewTableViewCellDelegate where Self: DataSourceProvider { // MARK: - membership extension UserViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { + func tableViewCell( + _ cell: UITableViewCell, + viewModel: UserView.ViewModel, + listMembershipButtonDidPressed user: UserRecord + ) { + guard !viewModel.isListMemberCandidate else { + return + } -// func tableViewCell( -// _ cell: UITableViewCell, -// userView: UserView, -// membershipButtonDidPressed button: UIButton -// ) { -// guard !userView.viewModel.isListMemberCandidate else { -// return -// } -// -// Task { @MainActor in -// let authenticationContext = self.authContext.authenticationContext -// -// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) -// guard let item = await item(from: source) else { -// assertionFailure() -// return -// } -// guard case let .user(user) = item else { -// assertionFailure("only works for user data") -// return -// } -// -// guard let listMembershipViewModel = userView.viewModel.listMembershipViewModel else { -// assertionFailure() -// return -// } -// -// do { -// if userView.viewModel.isListMember { -// try await listMembershipViewModel.remove(user: user, authenticationContext: authenticationContext) -// } else { -// try await listMembershipViewModel.add(user: user, authenticationContext: authenticationContext) -// } -// } catch { -// var config = SwiftMessages.defaultConfig -// config.duration = .seconds(seconds: 3) -// config.interactiveHide = true -// let bannerView = NotificationBannerView() -// bannerView.configure(style: .warning) -// bannerView.titleLabel.text = L10n.Common.Alerts.FailedToAddListMember.title -// bannerView.messageLabel.text = error.localizedDescription -// SwiftMessages.show(config: config, view: bannerView) -// } -// } // end Task -// } - + Task { @MainActor in + guard !viewModel.isListMemberCandidate else { return } + guard let authenticationContext = viewModel.authContext?.authenticationContext else { return } + guard let listMembershipViewModel = viewModel.listMembershipViewModel else { return } + guard let user = viewModel.user?.asRecord else { return } + do { + if viewModel.isListMember { + try await listMembershipViewModel.remove(user: user, authenticationContext: authenticationContext) + } else { + try await listMembershipViewModel.add(user: user, authenticationContext: authenticationContext) + } + } catch { + var config = SwiftMessages.defaultConfig + config.duration = .seconds(seconds: 3) + config.interactiveHide = true + let bannerView = NotificationBannerView() + bannerView.configure(style: .warning) + bannerView.titleLabel.text = viewModel.isListMember ? L10n.Common.Alerts.FailedToRemoveListMember.title : L10n.Common.Alerts.FailedToAddListMember.title + bannerView.messageLabel.text = error.localizedDescription + SwiftMessages.show(config: config, view: bannerView) + } + } // end Task + } } // MARK: - follow request diff --git a/TwidereX/Scene/List/ListUser/ListUserViewController.swift b/TwidereX/Scene/List/ListUser/ListUserViewController.swift index 92f90c14..a38cc62d 100644 --- a/TwidereX/Scene/List/ListUser/ListUserViewController.swift +++ b/TwidereX/Scene/List/ListUser/ListUserViewController.swift @@ -91,6 +91,8 @@ extension ListUserViewController { self.viewModel.stateMachine.enter(ListUserViewModel.State.Loading.self) } .store(in: &disposeBag) + + viewModel.listMembershipViewModel.delegate = self } @@ -133,75 +135,21 @@ extension ListUserViewController: UITableViewDelegate, AutoGenerateTableViewDele } // MARK: - UserViewTableViewCellDelegate -extension ListUserViewController: UserViewTableViewCellDelegate { -// func tableViewCell( -// _ cell: UITableViewCell, -// userView: UserView, -// menuActionDidPressed action: UserView.MenuAction, -// menuButton button: UIButton -// ) { -// switch action { -// case .remove: -// Task { -// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) -// guard let item = await item(from: source) else { -// assertionFailure() -// return -// } -// guard case let .user(user) = item else { -// assertionFailure("only works for status data provider") -// return -// } -// -// let authenticationContext = self.viewModel.authContext.authenticationContext -// -// do { -// let list = self.viewModel.kind.list -// _ = try await self.context.apiService.removeListMember( -// list: list, -// user: user, -// authenticationContext: authenticationContext -// ) -// await self.viewModel.update(user: user, action: .remove) -// -// var config = SwiftMessages.defaultConfig -// config.duration = .seconds(seconds: 3) -// config.interactiveHide = true -// let bannerView = NotificationBannerView() -// bannerView.configure(style: .success) -// bannerView.titleLabel.text = L10n.Common.Alerts.ListMemberRemoved.title -// bannerView.messageLabel.isHidden = true -// SwiftMessages.show(config: config, view: bannerView) -// } catch { -// var config = SwiftMessages.defaultConfig -// config.duration = .seconds(seconds: 3) -// config.interactiveHide = true -// let bannerView = NotificationBannerView() -// bannerView.configure(style: .warning) -// bannerView.titleLabel.text = L10n.Common.Alerts.FailedToRemoveListMember.title -// bannerView.messageLabel.text = L10n.Common.Alerts.FailedToRemoveListMember.message -// SwiftMessages.show(config: config, view: bannerView) -// } -// } // end Task -// default: -// assertionFailure() -// } // end swtich -// } -} +extension ListUserViewController: UserViewTableViewCellDelegate { } // MARK: - ListMembershipViewModelDelegate extension ListUserViewController: ListMembershipViewModelDelegate { func listMembershipViewModel(_ viewModel: ListMembershipViewModel, didAddUser user: UserRecord) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - Task { + Task { @MainActor in await self.viewModel.update(user: user, action: .add) } // end Task } func listMembershipViewModel(_ viewModel: ListMembershipViewModel, didRemoveUser user: UserRecord) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - Task { + Task { @MainActor in await self.viewModel.update(user: user, action: .remove) } // end Task } diff --git a/TwidereX/Scene/List/ListUser/ListUserViewModel+Diffable.swift b/TwidereX/Scene/List/ListUser/ListUserViewModel+Diffable.swift index cf21595d..bc0af447 100644 --- a/TwidereX/Scene/List/ListUser/ListUserViewModel+Diffable.swift +++ b/TwidereX/Scene/List/ListUser/ListUserViewModel+Diffable.swift @@ -32,7 +32,9 @@ extension ListUserViewModel { snapshot.appendSections([.main]) - let items = records.map { UserItem.user(record: $0, kind: .listMember) } + let items = records.map { + UserItem.user(record: $0, kind: .listMember(self.listMembershipViewModel)) + } snapshot.appendItems(items, toSection: .main) let currentState = await self.stateMachine.currentState diff --git a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+Diffable.swift b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+Diffable.swift index 2014d64d..a1affa0e 100644 --- a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+Diffable.swift +++ b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+Diffable.swift @@ -55,7 +55,7 @@ extension SearchUserViewModel { case .search: return .user(record: record, kind: .search) case .listMember: - return .user(record: record, kind: .addListMember) + return .user(record: record, kind: .addListMember(self.listMembershipViewModel)) } // end switch } snapshot.appendItems(newItems, toSection: .main) From 02b4b598e1b1fa7d5ef505b7058d4cdbfd5c0290 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 9 May 2023 15:28:23 +0800 Subject: [PATCH 073/128] fix: private list cannot be fetching issue --- TwidereSDK/Package.swift | 2 +- TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index 97172674..ab57c355 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -51,7 +51,7 @@ let package = Package( .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.6.0"), .package(url: "https://github.com/tid-kijyun/Kanna.git", from: "5.2.7"), .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"), - .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.5.0"), + .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.6.0"), .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"), .package(name: "CoverFlowStackLayout", path: "../CoverFlowStackLayout"), ], diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2490a8d5..dc04105b 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -258,8 +258,8 @@ "repositoryURL": "https://github.com/TwidereProject/TwitterSDK.git", "state": { "branch": null, - "revision": "3682d6e00f7721bbc31b7e490bf2f63faae684fc", - "version": "0.5.0" + "revision": "1890309577bcb97061348369c50ae4b067e61fa3", + "version": "0.6.0" } }, { From ba69a0085aa2bc504a41cc4753dc78b6b359fd59 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 9 May 2023 15:31:05 +0800 Subject: [PATCH 074/128] chore: update i18n resources --- .../Sources/TwidereLocalization/Generated/Strings.swift | 4 ++++ .../Resources/ar.lproj/Localizable.strings | 1 + .../Resources/ca.lproj/Localizable.strings | 1 + .../Resources/de.lproj/Localizable.strings | 1 + .../Resources/en.lproj/Localizable.strings | 1 + .../Resources/es.lproj/Localizable.strings | 1 + .../Resources/eu.lproj/Localizable.strings | 1 + .../Resources/gl.lproj/Localizable.strings | 1 + .../Resources/ja.lproj/Localizable.strings | 1 + .../Resources/ko.lproj/Localizable.strings | 1 + .../Resources/pt-BR.lproj/Localizable.strings | 1 + .../Resources/tr.lproj/Localizable.strings | 1 + .../Resources/zh-Hans.lproj/Localizable.strings | 1 + 13 files changed, 16 insertions(+) diff --git a/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift b/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift index ac520b3a..86da00ae 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift +++ b/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift @@ -1043,6 +1043,10 @@ public enum L10n { /// Search users public static let searchPlaceholder = L10n.tr("Localizable", "Scene.ComposeUserSearch.SearchPlaceholder", fallback: "Search users") } + public enum Detail { + /// Detail + public static let title = L10n.tr("Localizable", "Scene.Detail.Title", fallback: "Detail") + } public enum Drafts { /// Drafts public static let title = L10n.tr("Localizable", "Scene.Drafts.Title", fallback: "Drafts") diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings index e4ebd371..f0b17c45 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings @@ -322,6 +322,7 @@ "Scene.Compose.Vote.PlaceholderIndex" = "اختر %d"; "Scene.ComposeHashtagSearch.SearchPlaceholder" = "ابحث عن وسم"; "Scene.ComposeUserSearch.SearchPlaceholder" = "ابحث عن مستخدم"; +"Scene.Detail.Title" = "Detail"; "Scene.Drafts.Actions.DeleteDraft" = "احذف المسودة"; "Scene.Drafts.Actions.EditDraft" = "عدّل المسودة"; "Scene.Drafts.Title" = "المسودات"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings index a8454527..c794d577 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings @@ -322,6 +322,7 @@ "Scene.Compose.Vote.PlaceholderIndex" = "Elecció d"; "Scene.ComposeHashtagSearch.SearchPlaceholder" = "Cerca etiquetes"; "Scene.ComposeUserSearch.SearchPlaceholder" = "Cerca usuaris"; +"Scene.Detail.Title" = "Detail"; "Scene.Drafts.Actions.DeleteDraft" = "Suprimeix l'esborrany"; "Scene.Drafts.Actions.EditDraft" = "Edita l'esborrany"; "Scene.Drafts.Title" = "Esborranys"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings index 5f590576..9ab45fe7 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings @@ -322,6 +322,7 @@ "Scene.Compose.Vote.PlaceholderIndex" = "Choice %d"; "Scene.ComposeHashtagSearch.SearchPlaceholder" = "Search hashtag"; "Scene.ComposeUserSearch.SearchPlaceholder" = "Search users"; +"Scene.Detail.Title" = "Detail"; "Scene.Drafts.Actions.DeleteDraft" = "Entwurf löschen"; "Scene.Drafts.Actions.EditDraft" = "Entwurf bearbeiten"; "Scene.Drafts.Title" = "Entwürfe"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings index 3e7e8a05..a921e13e 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings @@ -322,6 +322,7 @@ "Scene.Compose.Vote.PlaceholderIndex" = "Choice %d"; "Scene.ComposeHashtagSearch.SearchPlaceholder" = "Search hashtag"; "Scene.ComposeUserSearch.SearchPlaceholder" = "Search users"; +"Scene.Detail.Title" = "Detail"; "Scene.Drafts.Actions.DeleteDraft" = "Delete draft"; "Scene.Drafts.Actions.EditDraft" = "Edit draft"; "Scene.Drafts.Title" = "Drafts"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings index d4d5287f..6074affc 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings @@ -322,6 +322,7 @@ "Scene.Compose.Vote.PlaceholderIndex" = "Opción %d"; "Scene.ComposeHashtagSearch.SearchPlaceholder" = "Buscar hashtags"; "Scene.ComposeUserSearch.SearchPlaceholder" = "Buscar usuarios"; +"Scene.Detail.Title" = "Detail"; "Scene.Drafts.Actions.DeleteDraft" = "Eliminar borrador"; "Scene.Drafts.Actions.EditDraft" = "Editar borrador"; "Scene.Drafts.Title" = "Borradores"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings index 977d3c09..4fd104dd 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings @@ -322,6 +322,7 @@ "Scene.Compose.Vote.PlaceholderIndex" = "%d aukera"; "Scene.ComposeHashtagSearch.SearchPlaceholder" = "Bilatu traola"; "Scene.ComposeUserSearch.SearchPlaceholder" = "Erabiltzaileak bilatu"; +"Scene.Detail.Title" = "Detail"; "Scene.Drafts.Actions.DeleteDraft" = "Ezabatu zirriborroa"; "Scene.Drafts.Actions.EditDraft" = "Editatu zirriborroa"; "Scene.Drafts.Title" = "Zirriborroak"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings index 730c7590..70addc89 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings @@ -322,6 +322,7 @@ "Scene.Compose.Vote.PlaceholderIndex" = "Opción %d"; "Scene.ComposeHashtagSearch.SearchPlaceholder" = "Buscar cancelo"; "Scene.ComposeUserSearch.SearchPlaceholder" = "Buscar usuarias"; +"Scene.Detail.Title" = "Detail"; "Scene.Drafts.Actions.DeleteDraft" = "Eliminar borrador"; "Scene.Drafts.Actions.EditDraft" = "Editar borrador"; "Scene.Drafts.Title" = "Borradores"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings index fac12b1b..0002ce49 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings @@ -322,6 +322,7 @@ "Scene.Compose.Vote.PlaceholderIndex" = "選択肢 %d"; "Scene.ComposeHashtagSearch.SearchPlaceholder" = "ハッシュタグを検索"; "Scene.ComposeUserSearch.SearchPlaceholder" = "ユーザーを検索"; +"Scene.Detail.Title" = "Detail"; "Scene.Drafts.Actions.DeleteDraft" = "下書きを削除"; "Scene.Drafts.Actions.EditDraft" = "下書きを編集"; "Scene.Drafts.Title" = "下書き"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings index 642ab517..97dcecc0 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings @@ -322,6 +322,7 @@ "Scene.Compose.Vote.PlaceholderIndex" = "%d 고르기"; "Scene.ComposeHashtagSearch.SearchPlaceholder" = "해시태그 찾기"; "Scene.ComposeUserSearch.SearchPlaceholder" = "사용자 찾기"; +"Scene.Detail.Title" = "Detail"; "Scene.Drafts.Actions.DeleteDraft" = "지우기"; "Scene.Drafts.Actions.EditDraft" = "다시 쓰기"; "Scene.Drafts.Title" = "임시 보관함"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings index 7cf9ca13..cb2f1043 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings @@ -322,6 +322,7 @@ "Scene.Compose.Vote.PlaceholderIndex" = "Opção %d"; "Scene.ComposeHashtagSearch.SearchPlaceholder" = "Buscar hashtag"; "Scene.ComposeUserSearch.SearchPlaceholder" = "Buscar usuários"; +"Scene.Detail.Title" = "Detail"; "Scene.Drafts.Actions.DeleteDraft" = "Excluir rascunho"; "Scene.Drafts.Actions.EditDraft" = "Editar rascunho"; "Scene.Drafts.Title" = "Rascunhos"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings index 3f8c7bd6..6b12331f 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings @@ -322,6 +322,7 @@ "Scene.Compose.Vote.PlaceholderIndex" = "Seçim %d"; "Scene.ComposeHashtagSearch.SearchPlaceholder" = "Hashtag ara"; "Scene.ComposeUserSearch.SearchPlaceholder" = "Kullanıcıları ara"; +"Scene.Detail.Title" = "Detail"; "Scene.Drafts.Actions.DeleteDraft" = "Taslağı sil"; "Scene.Drafts.Actions.EditDraft" = "Taslağı düzenle"; "Scene.Drafts.Title" = "Taslaklar"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings index d8ea84d2..630827dd 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings @@ -322,6 +322,7 @@ "Scene.Compose.Vote.PlaceholderIndex" = "选项 %d"; "Scene.ComposeHashtagSearch.SearchPlaceholder" = "搜索标签"; "Scene.ComposeUserSearch.SearchPlaceholder" = "搜索用户"; +"Scene.Detail.Title" = "Detail"; "Scene.Drafts.Actions.DeleteDraft" = "删除草稿"; "Scene.Drafts.Actions.EditDraft" = "编辑草稿"; "Scene.Drafts.Title" = "草稿"; From 8358d30f29fa3fe9cf502b3974ae1380565a4af3 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 9 May 2023 15:31:39 +0800 Subject: [PATCH 075/128] chore: update version to 2.0.0 (126) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 36 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 5350483f..18a059ce 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 125 + 126 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 4b75f20f..fa97696f 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 125 + 126 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index c4b7e5a2..c438fb90 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3431,7 +3431,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3458,7 +3458,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3481,7 +3481,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3505,7 +3505,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3532,7 +3532,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3561,7 +3561,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3590,7 +3590,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3618,7 +3618,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3644,7 +3644,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3671,7 +3671,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3825,7 +3825,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3857,7 +3857,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3884,7 +3884,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3909,7 +3909,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3932,7 +3932,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3955,7 +3955,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3979,7 +3979,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -4006,7 +4006,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 126; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index 697cf1d3..9443ba69 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 125 + 126 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index 051466d7..df103e99 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 125 + 126 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index 2cc5125d..d0970f22 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 125 + 126 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index 2cc5125d..d0970f22 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 125 + 126 From 191c3f6ec9527fde68fb9f3b725c8181f0642c2f Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 10 May 2023 15:32:59 +0800 Subject: [PATCH 076/128] fix: media sensitive not take effect on Twitter post issue. Fix the quote post could not open issue --- TwidereSDK/Package.swift | 2 +- .../CoreDataStack 7.xcdatamodel/contents | 2 + .../Entity/Twitter/TwitterStatus.swift | 22 +++++++- .../Entity/Twitter/TwitterUser.swift | 1 + .../Extension/TwitterStatus+Property.swift | 2 + .../Content/StatusView+ViewModel.swift | 8 +++ .../TwidereUI/Content/StatusView.swift | 8 ++- .../StatusViewTableViewCellDelegate.swift | 5 ++ .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Facade/DataSourceFacade+Status.swift | 4 +- ...ider+StatusViewTableViewCellDelegate.swift | 52 +++++-------------- ...ovider+UserViewTableViewCellDelegate.swift | 32 +----------- 12 files changed, 65 insertions(+), 77 deletions(-) diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index ab57c355..24193981 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -51,7 +51,7 @@ let package = Package( .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.6.0"), .package(url: "https://github.com/tid-kijyun/Kanna.git", from: "5.2.7"), .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"), - .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.6.0"), + .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.7.0"), .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"), .package(name: "CoverFlowStackLayout", path: "../CoverFlowStackLayout"), ], diff --git a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 7.xcdatamodel/contents b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 7.xcdatamodel/contents index 426ef9ee..ff7b27b2 100644 --- a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 7.xcdatamodel/contents +++ b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 7.xcdatamodel/contents @@ -253,6 +253,8 @@ + + diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift index b3d8c125..213195a5 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift @@ -42,7 +42,12 @@ final public class TwitterStatus: NSManagedObject { @NSManaged public private(set) var replyToStatusID: TwitterStatus.ID? // sourcery: autoUpdatableObject, autoGenerateProperty @NSManaged public private(set) var replyToUserID: TwitterUser.ID? - + + // sourcery: autoUpdatableObject, autoGenerateProperty + @NSManaged public private(set) var isMediaSensitive: Bool + // sourcery: autoUpdatableObject + @NSManaged public private(set) var isMediaSensitiveToggled: Bool + // sourcery: autoUpdatableObject, autoGenerateProperty @NSManaged public private(set) var createdAt: Date // sourcery: autoUpdatableObject, autoGenerateProperty @@ -238,6 +243,7 @@ extension TwitterStatus: AutoGenerateProperty { public let source: String? public let replyToStatusID: TwitterStatus.ID? public let replyToUserID: TwitterUser.ID? + public let isMediaSensitive: Bool public let createdAt: Date public let updatedAt: Date @@ -252,6 +258,7 @@ extension TwitterStatus: AutoGenerateProperty { source: String?, replyToStatusID: TwitterStatus.ID?, replyToUserID: TwitterUser.ID?, + isMediaSensitive: Bool, createdAt: Date, updatedAt: Date ) { @@ -265,6 +272,7 @@ extension TwitterStatus: AutoGenerateProperty { self.source = source self.replyToStatusID = replyToStatusID self.replyToUserID = replyToUserID + self.isMediaSensitive = isMediaSensitive self.createdAt = createdAt self.updatedAt = updatedAt } @@ -281,6 +289,7 @@ extension TwitterStatus: AutoGenerateProperty { self.source = property.source self.replyToStatusID = property.replyToStatusID self.replyToUserID = property.replyToUserID + self.isMediaSensitive = property.isMediaSensitive self.createdAt = property.createdAt self.updatedAt = property.updatedAt } @@ -292,6 +301,7 @@ extension TwitterStatus: AutoGenerateProperty { update(source: property.source) update(replyToStatusID: property.replyToStatusID) update(replyToUserID: property.replyToUserID) + update(isMediaSensitive: property.isMediaSensitive) update(createdAt: property.createdAt) update(updatedAt: property.updatedAt) } @@ -377,6 +387,16 @@ extension TwitterStatus: AutoUpdatableObject { self.replyToUserID = replyToUserID } } + public func update(isMediaSensitive: Bool) { + if self.isMediaSensitive != isMediaSensitive { + self.isMediaSensitive = isMediaSensitive + } + } + public func update(isMediaSensitiveToggled: Bool) { + if self.isMediaSensitiveToggled != isMediaSensitiveToggled { + self.isMediaSensitiveToggled = isMediaSensitiveToggled + } + } public func update(createdAt: Date) { if self.createdAt != createdAt { self.createdAt = createdAt diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift index 451e42fe..0627bdf7 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift @@ -170,6 +170,7 @@ extension TwitterUser { public static func predicate(username: String) -> NSPredicate { return NSPredicate(format: "%K == %@", #keyPath(TwitterUser.username), username) + } } diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Extension/TwitterStatus+Property.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Extension/TwitterStatus+Property.swift index a19066a4..92a4be0c 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Extension/TwitterStatus+Property.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Extension/TwitterStatus+Property.swift @@ -37,6 +37,7 @@ extension TwitterStatus.Property { }, replyToStatusID: entity.inReplyToStatusIDStr, replyToUserID: entity.inReplyToUserIDStr, + isMediaSensitive: entity.possiblySensitive ?? false, createdAt: entity.createdAt, updatedAt: networkDate ) @@ -135,6 +136,7 @@ extension TwitterStatus.Property { source: status.source, replyToStatusID: status.repliedToID, replyToUserID: status.inReplyToUserID, + isMediaSensitive: status.possiblySensitive ?? false, createdAt: status.createdAt, updatedAt: networkDate ) diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index 44d8e9e7..285e78bc 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -1135,6 +1135,14 @@ extension StatusView.ViewModel { // media mediaViewModels = MediaView.ViewModel.viewModels(from: status) + // media content warning + isMediaSensitive = status.isMediaSensitive + isMediaSensitiveToggled = status.isMediaSensitiveToggled + status.publisher(for: \.isMediaSensitiveToggled) + .receive(on: DispatchQueue.main) + .assign(to: \.isMediaSensitiveToggled, on: self) + .store(in: &disposeBag) + // poll if let poll = status.poll { self.pollViewModel = PollView.ViewModel( diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index bf1ac3c4..3015c5a1 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -36,7 +36,10 @@ public protocol StatusViewDelegate: AnyObject { func statusView(_ viewModel: StatusView.ViewModel, pollVoteActionForViewModel pollViewModel: PollView.ViewModel) func statusView(_ viewModel: StatusView.ViewModel, pollUpdateIfNeedsForViewModel pollViewModel: PollView.ViewModel) func statusView(_ viewModel: StatusView.ViewModel, pollViewModel: PollView.ViewModel, pollOptionDidSelectForViewModel optionViewModel: PollOptionView.ViewModel) - + + // repost + func statusView(_ viewModel: StatusView.ViewModel, quoteStatusViewDidPressed quoteViewModel: StatusView.ViewModel) + // metric func statusView(_ viewModel: StatusView.ViewModel, statusMetricViewModel: StatusMetricView.ViewModel, statusMetricButtonDidPressed action: StatusMetricView.Action) @@ -182,6 +185,9 @@ public struct StatusView: View { Color(uiColor: .label.withAlphaComponent(0.04)) } .cornerRadius(12) + .onTapGesture { + viewModel.delegate?.statusView(viewModel, quoteStatusViewDidPressed: quoteViewModel) + } } // location (inline) if let location = viewModel.location { diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/Status/StatusViewTableViewCellDelegate.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/Status/StatusViewTableViewCellDelegate.swift index a06d6e8b..d5d663bf 100644 --- a/TwidereSDK/Sources/TwidereUI/TableViewCell/Status/StatusViewTableViewCellDelegate.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/Status/StatusViewTableViewCellDelegate.swift @@ -32,6 +32,7 @@ public protocol StatusViewTableViewCellDelegate: AutoGenerateProtocolDelegate { func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, pollVoteActionForViewModel pollViewModel: PollView.ViewModel) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, pollUpdateIfNeedsForViewModel pollViewModel: PollView.ViewModel) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, pollViewModel: PollView.ViewModel, pollOptionDidSelectForViewModel optionViewModel: PollOptionView.ViewModel) + func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, quoteStatusViewDidPressed quoteViewModel: StatusView.ViewModel) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, statusMetricViewModel: StatusMetricView.ViewModel, statusMetricButtonDidPressed action: StatusMetricView.Action) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, statusToolbarViewModel: StatusToolbarView.ViewModel, statusToolbarButtonDidPressed action: StatusToolbarView.Action) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, viewHeightDidChange: Void) @@ -74,6 +75,10 @@ public extension StatusViewDelegate where Self: StatusViewContainerTableViewCell statusViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, pollViewModel: pollViewModel, pollOptionDidSelectForViewModel: optionViewModel) } + func statusView(_ viewModel: StatusView.ViewModel, quoteStatusViewDidPressed quoteViewModel: StatusView.ViewModel) { + statusViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, quoteStatusViewDidPressed: quoteViewModel) + } + func statusView(_ viewModel: StatusView.ViewModel, statusMetricViewModel: StatusMetricView.ViewModel, statusMetricButtonDidPressed action: StatusMetricView.Action) { statusViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, statusMetricViewModel: statusMetricViewModel, statusMetricButtonDidPressed: action) } diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index dc04105b..049913c7 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -258,8 +258,8 @@ "repositoryURL": "https://github.com/TwidereProject/TwitterSDK.git", "state": { "branch": null, - "revision": "1890309577bcb97061348369c50ae4b067e61fa3", - "version": "0.6.0" + "revision": "cf2f33c96e7b9f86dbee23111cd523f9a9294099", + "version": "0.7.0" } }, { diff --git a/TwidereX/Protocol/Facade/DataSourceFacade+Status.swift b/TwidereX/Protocol/Facade/DataSourceFacade+Status.swift index d14c4c2d..4e789043 100644 --- a/TwidereX/Protocol/Facade/DataSourceFacade+Status.swift +++ b/TwidereX/Protocol/Facade/DataSourceFacade+Status.swift @@ -266,8 +266,8 @@ extension DataSourceFacade { try await managedObjectContext.performChanges { guard let object = status.object(in: managedObjectContext) else { return } switch object { - case .twitter: - break + case .twitter(let status): + status.update(isMediaSensitiveToggled: !status.isMediaSensitiveToggled) case .mastodon(let status): status.update(isMediaSensitiveToggled: !status.isMediaSensitiveToggled) } diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift index 4ed055fd..f3f9d9b6 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift @@ -127,7 +127,6 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC // MARK: - media extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider & MediaPreviewableViewController { - @MainActor func tableViewCell( _ cell: UITableViewCell, @@ -158,26 +157,6 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC ) } // end Task } - - -// func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, mediaGridContainerView containerView: MediaGridContainerView, toggleContentWarningOverlayViewDisplay contentWarningOverlayView: ContentWarningOverlayView) { -// Task { -// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) -// guard let item = await item(from: source) else { -// assertionFailure() -// return -// } -// guard let status = await item.status(in: self.context.managedObjectContext) else { -// assertionFailure("only works for status data provider") -// return -// } -// try await DataSourceFacade.responseToToggleMediaSensitiveAction( -// provider: self, -// target: .status, -// status: status -// ) -// } -// } } // MARK: - poll @@ -275,24 +254,19 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC // MARK: - quote extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { -// func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, quoteStatusViewDidPressed quoteStatusView: StatusView) { -// Task { -// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) -// guard let item = await item(from: source) else { -// return -// } -// guard let status = await item.status(in: self.context.managedObjectContext) else { -// assertionFailure("only works for status data provider") -// return -// } -// -// await DataSourceFacade.coordinateToStatusThreadScene( -// provider: self, -// target: .quote, -// status: status -// ) -// } -// } + func tableViewCell( + _ cell: UITableViewCell, + viewModel: StatusView.ViewModel, + quoteStatusViewDidPressed quoteViewModel: StatusView.ViewModel + ) { + guard let status = quoteViewModel.status?.asRecord else { return } + Task { + await DataSourceFacade.coordinateToStatusThreadScene( + provider: self, + kind: .status(status) + ) + } // end Task + } } // MARK: - metric diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift index 5648062b..c9b37379 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift @@ -75,37 +75,7 @@ extension UserViewTableViewCellDelegate where Self: DataSourceProvider { } } // end Task } // end switch - } - -// func tableViewCell( -// _ cell: UITableViewCell, -// userView: UserView, -// menuActionDidPressed action: UserView.MenuAction, -// menuButton button: UIButton -// ) { -// switch action { -// case .signOut: -// // TODO: move to view controller -// Task { -// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) -// guard let item = await item(from: source) else { -// assertionFailure() -// return -// } -// guard case let .user(user) = item else { -// assertionFailure("only works for user data") -// return -// } -// try await DataSourceFacade.responseToUserSignOut( -// dependency: self, -// user: user -// ) -// } // end Task -// case .remove: -// assertionFailure("Override in view controller") -// } // end swtich -// } - + } } // MARK: - friendship button From f9edf8bb10b01dd22a6fb70002580a479bf33cd8 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 10 May 2023 15:45:42 +0800 Subject: [PATCH 077/128] fix: add post's missing translate button --- .../Protocol/TextStyleConfigurable.swift | 6 ++++++ .../Sources/TwidereUI/Content/StatusView.swift | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/TwidereSDK/Sources/TwidereCore/Protocol/TextStyleConfigurable.swift b/TwidereSDK/Sources/TwidereCore/Protocol/TextStyleConfigurable.swift index d7efd15a..e250ac4d 100644 --- a/TwidereSDK/Sources/TwidereCore/Protocol/TextStyleConfigurable.swift +++ b/TwidereSDK/Sources/TwidereCore/Protocol/TextStyleConfigurable.swift @@ -25,6 +25,7 @@ public enum TextStyle { case statusTimestamp case statusLocation case statusContent + case statusTranslateButton case statusMetrics case userAuthorName case pollOptionTitle @@ -73,6 +74,7 @@ extension TextStyle { case .statusTimestamp: return 1 case .statusLocation: return 1 case .statusContent: return 0 + case .statusTranslateButton: return 1 case .statusMetrics: return 1 case .pollOptionTitle: return 1 case .pollOptionPercentage: return 1 @@ -116,6 +118,8 @@ extension TextStyle { return .preferredFont(forTextStyle: .caption1) case .statusContent: return .preferredFont(forTextStyle: .body) + case .statusTranslateButton: + return .preferredFont(forTextStyle: .headline) case .statusMetrics: return .preferredFont(forTextStyle: .footnote) case .pollOptionTitle: @@ -181,6 +185,8 @@ extension TextStyle { return .secondaryLabel case .statusContent: return .label.withAlphaComponent(0.8) + case .statusTranslateButton: + return .tintColor case .statusMetrics: return .secondaryLabel case .userAuthorName: diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index 3015c5a1..915aa4f0 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -145,6 +145,10 @@ public struct StatusView: View { // content if viewModel.isContentReveal { contentView + + if viewModel.isTranslateButtonDisplay { + translateButton + } } // media if !viewModel.mediaViewModels.isEmpty { @@ -426,6 +430,20 @@ extension StatusView { } } + var translateButton: some View { + Button { + viewModel.delegate?.statusView(viewModel, statusToolbarViewModel: viewModel.toolbarViewModel, statusToolbarButtonDidPressed: .translate) + } label: { + HStack { + Text(L10n.Common.Controls.Status.Actions.translate) + .font(Font(TextStyle.statusTranslateButton.font)) + .foregroundColor(Color(uiColor: TextStyle.statusTranslateButton.textColor)) + Spacer() + } + .padding(.vertical) + } + } + var toolbarView: some View { StatusToolbarView( viewModel: viewModel.toolbarViewModel, From 82dcd9d7b3895c53c0872166b448b20d6ba52b79 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 10 May 2023 15:46:25 +0800 Subject: [PATCH 078/128] chore: update version to 2.0.0 (127) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 36 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 18a059ce..18226129 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 126 + 127 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index fa97696f..759e399f 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 126 + 127 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index c438fb90..1b4edfa7 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3431,7 +3431,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 126; + CURRENT_PROJECT_VERSION = 127; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3458,7 +3458,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 126; + CURRENT_PROJECT_VERSION = 127; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3481,7 +3481,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 126; + CURRENT_PROJECT_VERSION = 127; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3505,7 +3505,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 126; + CURRENT_PROJECT_VERSION = 127; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3532,7 +3532,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 126; + CURRENT_PROJECT_VERSION = 127; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3561,7 +3561,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 126; + CURRENT_PROJECT_VERSION = 127; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3590,7 +3590,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 126; + CURRENT_PROJECT_VERSION = 127; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3618,7 +3618,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 126; + CURRENT_PROJECT_VERSION = 127; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3644,7 +3644,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 126; + CURRENT_PROJECT_VERSION = 127; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3671,7 +3671,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 126; + CURRENT_PROJECT_VERSION = 127; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3825,7 +3825,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 126; + CURRENT_PROJECT_VERSION = 127; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3857,7 +3857,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 126; + CURRENT_PROJECT_VERSION = 127; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3884,7 +3884,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 126; + CURRENT_PROJECT_VERSION = 127; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3909,7 +3909,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 126; + CURRENT_PROJECT_VERSION = 127; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3932,7 +3932,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 126; + CURRENT_PROJECT_VERSION = 127; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3955,7 +3955,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 126; + CURRENT_PROJECT_VERSION = 127; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3979,7 +3979,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 126; + CURRENT_PROJECT_VERSION = 127; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -4006,7 +4006,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 126; + CURRENT_PROJECT_VERSION = 127; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index 9443ba69..bfc815a7 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 126 + 127 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index df103e99..03f8fcda 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 126 + 127 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index d0970f22..64ab8a7e 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 126 + 127 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index d0970f22..64ab8a7e 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 126 + 127 From f5fc878de03d7055864a15a2f0d2790e08d137aa Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 11 May 2023 14:23:34 +0800 Subject: [PATCH 079/128] feat: make content overlay default hidden for media without sensitive mark --- .../Sources/TwidereUI/Content/StatusView+ViewModel.swift | 7 ++++++- TwidereSDK/Sources/TwidereUI/Content/StatusView.swift | 9 +++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index 285e78bc..db740060 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -115,7 +115,12 @@ extension StatusView { public var isMediaContentWarningOverlayReveal: Bool { return isMediaSensitiveToggled ? isMediaSensitive : !isMediaSensitive } - + public var isMediaContentWarningOverlayToggleButtonDisplay: Bool { + switch status { + case .twitter: return isMediaSensitive + default: return true + } + } // @Published public var isRepost = false // @Published public var isRepostEnabled = true diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index 915aa4f0..b638837c 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -160,12 +160,13 @@ public struct StatusView: View { viewModel.delegate?.statusView(viewModel, mediaViewModel: mediaViewModel, action: action) } ) - // .clipShape(RoundedRectangle(cornerRadius: MediaGridContainerView.cornerRadius)) .overlay { - ContentWarningOverlayView(isReveal: viewModel.isMediaContentWarningOverlayReveal) { - viewModel.delegate?.statusView(viewModel, toggleContentWarningOverlayDisplay: !viewModel.isMediaContentWarningOverlayReveal) + if viewModel.isMediaContentWarningOverlayToggleButtonDisplay { + ContentWarningOverlayView(isReveal: viewModel.isMediaContentWarningOverlayReveal) { + viewModel.delegate?.statusView(viewModel, toggleContentWarningOverlayDisplay: !viewModel.isMediaContentWarningOverlayReveal) + } + .cornerRadius(MediaGridContainerView.cornerRadius) } - .cornerRadius(MediaGridContainerView.cornerRadius) } } // poll From 012a26a4aa73ed8686047119f2ebfa5c53ae5541 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 11 May 2023 18:52:51 +0800 Subject: [PATCH 080/128] feat: add multiple window support and open in new window entry --- .../TwidereCore/Model/User/UserObject.swift | 13 ++++ .../Content/UserView+ViewModel.swift | 12 ++++ .../Sources/TwidereUI/Content/UserView.swift | 12 ++++ TwidereX/Info.plist | 2 +- ...ovider+UserViewTableViewCellDelegate.swift | 13 ++++ ...PinBasedAuthenticationViewController.swift | 6 +- .../Welcome/WelcomeViewController.swift | 2 +- TwidereX/Supporting Files/SceneDelegate.swift | 67 ++++++++++++++++++- 8 files changed, 123 insertions(+), 4 deletions(-) diff --git a/TwidereSDK/Sources/TwidereCore/Model/User/UserObject.swift b/TwidereSDK/Sources/TwidereCore/Model/User/UserObject.swift index 28c1376b..ff69aa4d 100644 --- a/TwidereSDK/Sources/TwidereCore/Model/User/UserObject.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/User/UserObject.swift @@ -47,6 +47,19 @@ extension UserObject { } } + public var authenticationIndex: AuthenticationIndex? { + switch self { + case .twitter(let object): + return object.twitterAuthentication.flatMap { + $0.authenticationIndex + } + case .mastodon(let object): + return object.mastodonAuthentication.flatMap { + $0.authenticationIndex + } + } + } + public var notifications: Set { switch self { case .twitter: diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift index 0b436941..9307a5d7 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift @@ -41,6 +41,9 @@ extension UserView { @Published public var username: String = "" @Published public var platform: Platform = .none + + @Published public var isMyself: Bool = false + // @Published public var authenticationContext: AuthenticationContext? // me // @Published public var userAuthenticationContext: AuthenticationContext? // @@ -114,6 +117,14 @@ extension UserView { break } + // isMyself + isMyself = { + guard let authContext = self.authContext, + let user = self.user + else { return false } + return authContext.authenticationContext.userIdentifier == user.userIdentifer + }() + // follow request switch notification { case .twitter: @@ -227,6 +238,7 @@ extension UserView.ViewModel { } public enum MenuAction: Hashable { + case openInNewWindowForAccount case signOut case removeListMember } diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift index 51150a96..1c703746 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift @@ -146,6 +146,18 @@ extension UserView { Menu { switch viewModel.kind { case .account: + // open in new window + if !viewModel.isMyself, UIApplication.shared.supportsMultipleScenes { + Button { + viewModel.delegate?.userView(viewModel, menuActionDidPressed: .openInNewWindowForAccount) + } label: { + Label { + Text("Open in new window") + } icon: { + Image(systemName: "macwindow.badge.plus") + } + } + } // sign out Button(role: .destructive) { viewModel.delegate?.userView(viewModel, menuActionDidPressed: .signOut) diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index bfc815a7..e5fb55e7 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -62,7 +62,7 @@ UIApplicationSceneManifest UIApplicationSupportsMultipleScenes - + UISceneConfigurations UIWindowSceneSessionRoleApplication diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift index c9b37379..6bafb393 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift @@ -7,6 +7,8 @@ // import UIKit +import CoreData +import CoreDataStack import SwiftMessages // MARK: - avatar button @@ -34,6 +36,17 @@ extension UserViewTableViewCellDelegate where Self: DataSourceProvider { menuActionDidPressed action: UserView.ViewModel.MenuAction ) { switch action { + case .openInNewWindowForAccount: + guard let userRecord = viewModel.user?.asRecord else { return } + guard let requestingScene = self.view.window?.windowScene else { return } + Task { @MainActor in + let _record: ManagedObjectRecord? = await context.managedObjectContext.perform { + guard let user = userRecord.object(in: self.context.managedObjectContext) else { return nil } + return user.authenticationIndex?.asRecrod + } + guard let record = _record else { return } + try SceneDelegate.openSceneSessionForAccount(record, fromRequestingScene: requestingScene) + } // end Task case .signOut: Task { guard let user = viewModel.user?.asRecord else { diff --git a/TwidereX/Scene/Onboarding/Welcome/Twitter/PinBased/TwitterPinBasedAuthenticationViewController.swift b/TwidereX/Scene/Onboarding/Welcome/Twitter/PinBased/TwitterPinBasedAuthenticationViewController.swift index e2a1f3fb..f50c6259 100644 --- a/TwidereX/Scene/Onboarding/Welcome/Twitter/PinBased/TwitterPinBasedAuthenticationViewController.swift +++ b/TwidereX/Scene/Onboarding/Welcome/Twitter/PinBased/TwitterPinBasedAuthenticationViewController.swift @@ -19,7 +19,11 @@ final class TwitterPinBasedAuthenticationViewController: UIViewController, Needs var disposeBag = Set() var viewModel: TwitterPinBasedAuthenticationViewModel! - let webView = WKWebView() + lazy var webView: WKWebView = { + let configuration = WKWebViewConfiguration() + configuration.websiteDataStore = .nonPersistent() + return WKWebView(frame: view.bounds, configuration: configuration) + }() deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) diff --git a/TwidereX/Scene/Onboarding/Welcome/WelcomeViewController.swift b/TwidereX/Scene/Onboarding/Welcome/WelcomeViewController.swift index 0459002f..31effd3c 100644 --- a/TwidereX/Scene/Onboarding/Welcome/WelcomeViewController.swift +++ b/TwidereX/Scene/Onboarding/Welcome/WelcomeViewController.swift @@ -202,7 +202,7 @@ extension WelcomeViewController: WelcomeViewModelDelegate { .sink { [weak self] authenticationSession in guard let self = self else { return } guard let authenticationSession = authenticationSession else { return } - authenticationSession.prefersEphemeralWebBrowserSession = false + authenticationSession.prefersEphemeralWebBrowserSession = true authenticationSession.presentationContextProvider = self authenticationSession.start() } diff --git a/TwidereX/Supporting Files/SceneDelegate.swift b/TwidereX/Supporting Files/SceneDelegate.swift index baa2a997..2362cef5 100644 --- a/TwidereX/Supporting Files/SceneDelegate.swift +++ b/TwidereX/Supporting Files/SceneDelegate.swift @@ -11,6 +11,7 @@ import Combine import Intents import FPSIndicator import CoreDataStack +import CoreData class SceneDelegate: UIResponder, UIWindowSceneDelegate { @@ -58,7 +59,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let sceneCoordinator = SceneCoordinator(scene: scene, sceneDelegate: self, context: AppContext.shared) self.coordinator = sceneCoordinator - sceneCoordinator.setup() + let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity + if userActivity?.activityType == UserActivity.openNewWindowActivityType, + let objectIDURI = userActivity?.userInfo?[UserActivity.sessionUserInfoAuthenticationIndexObjectIDKey] as? URL, + let objectID = AppContext.shared.managedObjectContext.persistentStoreCoordinator?.managedObjectID(forURIRepresentation: objectIDURI) + { + sceneCoordinator.setup(authentication: ManagedObjectRecord(objectID: objectID)) + } else { + sceneCoordinator.setup() + } window.makeKeyAndVisible() @@ -213,6 +222,62 @@ extension SceneDelegate { } +extension SceneDelegate { + + public class func openSceneSessionForAccount( + _ record: ManagedObjectRecord, + fromRequestingScene requestingScene: UIWindowScene + ) throws { + let options = UIWindowScene.ActivationRequestOptions() + options.preferredPresentationStyle = .prominent + options.requestingScene = requestingScene + + if let activeSceneSession = Self.activeSceneSessionForAccount(record) { + UIApplication.shared.requestSceneSessionActivation( + activeSceneSession, // reuse old one + userActivity: nil, // ignore for actived session + options: options + ) + } else { + let userActivity = record.openNewWindowUserActivity + UIApplication.shared.requestSceneSessionActivation( + nil, // create new one + userActivity: userActivity, + options: options + ) + } + } + + class func activeSceneSessionForAccount(_ record: ManagedObjectRecord) -> UISceneSession? { + for openSession in UIApplication.shared.openSessions where openSession.configuration.delegateClass == SceneDelegate.self { + guard let userInfo = openSession.userInfo, + let objectIDURI = userInfo[UserActivity.sessionUserInfoAuthenticationIndexObjectIDKey] as? URL, + objectIDURI == record.objectID.uriRepresentation() + else { continue } + return openSession + } // end for … in + + return nil + } +} + +struct UserActivity { + static var openNewWindowActivityType: String { "com.twidere.TwidereX.openNewWindow" } + + static var sessionUserInfoAuthenticationIndexObjectIDKey: String { "authenticationIndex.objectID" } +} + +extension ManagedObjectRecord where T: AuthenticationIndex { + var openNewWindowUserActivity: NSUserActivity { + let userActivity = NSUserActivity(activityType: UserActivity.openNewWindowActivityType) + userActivity.userInfo = [ + UserActivity.sessionUserInfoAuthenticationIndexObjectIDKey: objectID.uriRepresentation() + ] + userActivity.targetContentIdentifier = "\(UserActivity.openNewWindowActivityType)-\(objectID)" + return userActivity + } +} + #if DEBUG extension SceneDelegate { static var isXcodeUnitTest: Bool { From d338de5c792ac7e5552dac3552f9341a4db2cc15 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 11 May 2023 18:53:14 +0800 Subject: [PATCH 081/128] fix: compose shortcut not work issue --- TwidereX/Coordinator/SceneCoordinator.swift | 6 +++ TwidereX/Supporting Files/SceneDelegate.swift | 39 +++++++------------ 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/TwidereX/Coordinator/SceneCoordinator.swift b/TwidereX/Coordinator/SceneCoordinator.swift index 044cace8..8f948a88 100644 --- a/TwidereX/Coordinator/SceneCoordinator.swift +++ b/TwidereX/Coordinator/SceneCoordinator.swift @@ -22,6 +22,8 @@ final public class SceneCoordinator { private(set) weak var sceneDelegate: SceneDelegate! private(set) weak var context: AppContext! + private(set) var authContext: AuthContext? + let id = UUID().uuidString // output @@ -147,8 +149,11 @@ extension SceneCoordinator { transition: .modal(animated: false) ) // entry #1: Welcome } + self.authContext = nil return } + + self.authContext = authContext switch UIDevice.current.userInterfaceIdiom { case .phone: @@ -169,6 +174,7 @@ extension SceneCoordinator { } catch { assertionFailure(error.localizedDescription) + self.authContext = nil Task { try? await Task.sleep(nanoseconds: .second * 2) setup() // entry #3: retry diff --git a/TwidereX/Supporting Files/SceneDelegate.swift b/TwidereX/Supporting Files/SceneDelegate.swift index 2362cef5..e39aa675 100644 --- a/TwidereX/Supporting Files/SceneDelegate.swift +++ b/TwidereX/Supporting Files/SceneDelegate.swift @@ -159,34 +159,25 @@ extension SceneDelegate { switch shortcutItem.type { case "com.twidere.TwidereX.compose": + guard let authContext = coordinator.authContext else { return false } + if let topMost = topMostViewController(), topMost.isModal { topMost.dismiss(animated: false) } let composeViewModel = ComposeViewModel(context: coordinator.context) - assertionFailure("TODO: check authContext and handle alert") -// let composeContentViewModel = ComposeContentViewModel( -// context: .shared, -// authContext: <#T##AuthContext#>, -// kind: .post, -// configurationContext: .init( -// apiService: coordinator.context.apiService, -// authenticationService: coordinator.context.authenticationService, -// mastodonEmojiService: coordinator.context.mastodonEmojiService, -// statusViewConfigureContext: .init( -// dateTimeProvider: DateTimeSwiftProvider(), -// twitterTextProvider: OfficialTwitterTextProvider(), -// authenticationContext: coordinator.context.authenticationService.$activeAuthenticationContext -// ) -// ) -// ) -// coordinator.present( -// scene: .compose( -// viewModel: composeViewModel, -// contentViewModel: composeContentViewModel -// ), -// from: nil, -// transition: .modal(animated: true) -// ) + let composeContentViewModel = ComposeContentViewModel( + context: coordinator.context, + authContext: authContext, + kind: .post + ) + coordinator.present( + scene: .compose( + viewModel: composeViewModel, + contentViewModel: composeContentViewModel + ), + from: nil, + transition: .modal(animated: true) + ) return true case "com.twidere.TwidereX.search": if let topMost = topMostViewController(), topMost.isModal { From 614a8c5162713041a22e02aaf1c1a76ba4e438c2 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 11 May 2023 18:57:38 +0800 Subject: [PATCH 082/128] chore: update for version 2.0.0 (128) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 36 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 18226129..f9aa0a6a 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 127 + 128 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 759e399f..6758123f 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 127 + 128 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 1b4edfa7..6839e4d6 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3431,7 +3431,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 127; + CURRENT_PROJECT_VERSION = 128; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3458,7 +3458,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 127; + CURRENT_PROJECT_VERSION = 128; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3481,7 +3481,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 127; + CURRENT_PROJECT_VERSION = 128; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3505,7 +3505,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 127; + CURRENT_PROJECT_VERSION = 128; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3532,7 +3532,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 127; + CURRENT_PROJECT_VERSION = 128; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3561,7 +3561,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 127; + CURRENT_PROJECT_VERSION = 128; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3590,7 +3590,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 127; + CURRENT_PROJECT_VERSION = 128; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3618,7 +3618,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 127; + CURRENT_PROJECT_VERSION = 128; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3644,7 +3644,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 127; + CURRENT_PROJECT_VERSION = 128; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3671,7 +3671,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 127; + CURRENT_PROJECT_VERSION = 128; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3825,7 +3825,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 127; + CURRENT_PROJECT_VERSION = 128; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3857,7 +3857,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 127; + CURRENT_PROJECT_VERSION = 128; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3884,7 +3884,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 127; + CURRENT_PROJECT_VERSION = 128; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3909,7 +3909,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 127; + CURRENT_PROJECT_VERSION = 128; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3932,7 +3932,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 127; + CURRENT_PROJECT_VERSION = 128; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3955,7 +3955,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 127; + CURRENT_PROJECT_VERSION = 128; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3979,7 +3979,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 127; + CURRENT_PROJECT_VERSION = 128; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -4006,7 +4006,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 127; + CURRENT_PROJECT_VERSION = 128; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index e5fb55e7..33f4967d 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 127 + 128 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index 03f8fcda..57f097f7 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 127 + 128 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index 64ab8a7e..e0d1d83e 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 127 + 128 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index 64ab8a7e..e0d1d83e 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 127 + 128 From 8f7b88f07f6eb189dce0efed81cd47b304e71d47 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 12 May 2023 15:39:53 +0800 Subject: [PATCH 083/128] fix: potential crash issue when get object from objectID --- .../CoreDataStack/Utility/ManagedObjectRecord.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/TwidereSDK/Sources/CoreDataStack/Utility/ManagedObjectRecord.swift b/TwidereSDK/Sources/CoreDataStack/Utility/ManagedObjectRecord.swift index de4915be..63a361be 100644 --- a/TwidereSDK/Sources/CoreDataStack/Utility/ManagedObjectRecord.swift +++ b/TwidereSDK/Sources/CoreDataStack/Utility/ManagedObjectRecord.swift @@ -18,7 +18,12 @@ public class ManagedObjectRecord: Hashable { } public func object(in managedObjectContext: NSManagedObjectContext) -> T? { - return managedObjectContext.object(with: objectID) as? T + do { + return try managedObjectContext.existingObject(with: objectID) as? T + } catch { + assertionFailure(error.localizedDescription) + return nil + } } public static func == (lhs: ManagedObjectRecord, rhs: ManagedObjectRecord) -> Bool { From 0b3a115a3da29f2936e7cf69430141270a2df1f6 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 12 May 2023 18:53:46 +0800 Subject: [PATCH 084/128] fix: add missing lock icon for protected enabled account --- .../TwidereCore/Model/User/UserObject.swift | 9 ++ .../Content/StatusView+ViewModel.swift | 10 +- .../TwidereUI/Content/StatusView.swift | 28 +++-- .../Content/UserView+ViewModel.swift | 11 +- .../Sources/TwidereUI/Content/UserView.swift | 115 +++++++++++------- 5 files changed, 115 insertions(+), 58 deletions(-) diff --git a/TwidereSDK/Sources/TwidereCore/Model/User/UserObject.swift b/TwidereSDK/Sources/TwidereCore/Model/User/UserObject.swift index ff69aa4d..2a1e0705 100644 --- a/TwidereSDK/Sources/TwidereCore/Model/User/UserObject.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/User/UserObject.swift @@ -99,6 +99,15 @@ extension UserObject { return object.avatar.flatMap { URL(string: $0) } } } + + public var protected: Bool { + switch self { + case .twitter(let object): + return object.protected + case .mastodon(let object): + return object.locked + } + } } extension UserObject { diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index db740060..a338aa46 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -56,7 +56,9 @@ extension StatusView { @Published public var authorName: MetaContent = PlaintextMetaContent(string: "") @Published public var authorUsernme = "" @Published public var authorUserIdentifier: UserIdentifier? - + + @Published public var protected: Bool = false + // static let pollOptionOrdinalNumberFormatter: NumberFormatter = { // let formatter = NumberFormatter() // formatter.numberStyle = .ordinal @@ -68,7 +70,7 @@ extension StatusView { // @Published public var authorAvatarImageURL: URL? // @Published public var authorUsername: String? // -// @Published public var protected: Bool = false + // content @Published public var spoilerContent: MetaContent? @@ -1105,6 +1107,8 @@ extension StatusView.ViewModel { .assign(to: &$authorName) status.author.publisher(for: \.username) .assign(to: &$authorUsernme) + status.author.publisher(for: \.protected) + .assign(to: &$protected) authorUserIdentifier = .twitter(.init(id: status.author.id)) // timestamp @@ -1264,6 +1268,8 @@ extension StatusView.ViewModel { status.author.publisher(for: \.username) .map { _ in status.author.acct } .assign(to: &$authorUsernme) + status.author.publisher(for: \.locked) + .assign(to: &$protected) authorUserIdentifier = .mastodon(.init(domain: status.author.domain, id: status.author.id)) // visibility diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index b638837c..ed1a2cf1 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -68,6 +68,7 @@ public struct StatusView: View { @Environment(\.dynamicTypeSize) var dynamicTypeSize @ScaledMetric(relativeTo: .subheadline) private var visibilityIconImageDimension: CGFloat = 16 @ScaledMetric(relativeTo: .headline) private var inlineAvatarButtonDimension: CGFloat = 20 + @ScaledMetric(relativeTo: .headline) private var lockImageDimension: CGFloat = 16 public init(viewModel: StatusView.ViewModel) { self.viewModel = viewModel @@ -313,16 +314,25 @@ extension StatusView { } }() nameLayout { - // name - LabelRepresentable( - metaContent: viewModel.authorName, - textStyle: .statusAuthorName, - setupLabel: { label in - label.setContentHuggingPriority(.defaultHigh, for: .horizontal) - label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + HStack { + // name + LabelRepresentable( + metaContent: viewModel.authorName, + textStyle: .statusAuthorName, + setupLabel: { label in + label.setContentHuggingPriority(.defaultHigh, for: .horizontal) + label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + } + ) + .fixedSize(horizontal: false, vertical: true) + // lock + if viewModel.protected { + Image(uiImage: Asset.ObjectTools.lockMini.image.withRenderingMode(.alwaysTemplate)) + .resizable() + .frame(width: lockImageDimension, height: lockImageDimension) + .foregroundColor(.secondary) } - ) - .fixedSize(horizontal: false, vertical: true) + } .layoutPriority(0.618) // username LabelRepresentable( diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift index 9307a5d7..de83f88f 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift @@ -43,6 +43,7 @@ extension UserView { @Published public var platform: Platform = .none @Published public var isMyself: Bool = false + @Published public var protected: Bool = false // @Published public var authenticationContext: AuthenticationContext? // me // @Published public var userAuthenticationContext: AuthenticationContext? @@ -55,10 +56,9 @@ extension UserView { // @Published public var name: MetaContent? = PlaintextMetaContent(string: " ") // @Published public var username: String? // -// @Published public var protected: Bool = false -// + // @Published public var followerCount: Int? -// + // follow request @Published public var isFollowRequestActionDisplay = false @Published public var isFollowRequestBusy = false @@ -421,6 +421,8 @@ extension UserView.ViewModel { .assign(to: &$name) user.publisher(for: \.username) .assign(to: &$username) + user.publisher(for: \.protected) + .assign(to: &$protected) } public convenience init( @@ -448,6 +450,8 @@ extension UserView.ViewModel { user.publisher(for: \.username) .map { _ in user.acctWithDomain } .assign(to: &$username) + user.publisher(for: \.locked) + .assign(to: &$protected) } } @@ -467,6 +471,7 @@ extension UserView.ViewModel { username = "username" platform = .twitter notificationBadgeCount = 10 + protected = true } } #endif diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift index 1c703746..e4022696 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift @@ -26,38 +26,74 @@ public struct UserView: View { @ObservedObject public private(set) var viewModel: ViewModel + @Environment(\.dynamicTypeSize) var dynamicTypeSize + @ScaledMetric(relativeTo: .headline) private var lockImageDimension: CGFloat = 16 + public init(viewModel: UserView.ViewModel) { self.viewModel = viewModel } public var body: some View { - HStack(alignment: .center, spacing: .zero) { - // avatar - avatarButton - .padding(.trailing, StatusView.hangingAvatarButtonTrailingSpacing) - // info - VStackLayout(alignment: .leading, spacing: .zero) { - headlineView - subheadlineView - } - .frame(alignment: .leading) - Spacer() - // accessory view - accessoryView - } // end HStack - .padding(.vertical, viewModel.verticalMargin) - .overlay { - if viewModel.isSeparateLineDisplay { - HStack(spacing: .zero) { - Color.clear.frame(width: StatusView.hangingAvatarButtonDimension + StatusView.hangingAvatarButtonTrailingSpacing) - VStack(spacing: .zero) { + Group { + if dynamicTypeSize < .accessibility1 { + HStack(alignment: .center, spacing: .zero) { + // avatar + avatarButton + .padding(.trailing, StatusView.hangingAvatarButtonTrailingSpacing) + // info + VStackLayout(alignment: .leading, spacing: .zero) { + headlineView + subheadlineView + } + .frame(alignment: .leading) + Spacer() + // accessory view + accessoryView + } // end HStack + .padding(.vertical, viewModel.verticalMargin) + .overlay { + if viewModel.isSeparateLineDisplay { + HStack(spacing: .zero) { + Color.clear.frame(width: StatusView.hangingAvatarButtonDimension + StatusView.hangingAvatarButtonTrailingSpacing) + VStack(spacing: .zero) { + Spacer() + Divider() + Color.clear.frame(height: 1) + } + } // end HStack + } // end if + } // end .overlay + } else { + VStack(spacing: .zero) { + HStack { + // avatar + avatarButton + .padding(.trailing, StatusView.hangingAvatarButtonTrailingSpacing) Spacer() - Divider() - Color.clear.frame(height: 1) + // accessory view + accessoryView + } + // info + VStackLayout(alignment: .leading, spacing: .zero) { + headlineView + subheadlineView } + .frame(alignment: .leading) } // end HStack - } // end if - } // end .overlay + .padding(.vertical, viewModel.verticalMargin) + .overlay { + if viewModel.isSeparateLineDisplay { + HStack(spacing: .zero) { + VStack(spacing: .zero) { + Spacer() + Divider() + Color.clear.frame(height: 1) + } + } // end HStack + } // end if + } // end .overlay + } + } // Group } } @@ -258,26 +294,17 @@ extension UserView { var headlineView: some View { Group { switch viewModel.kind { - case .account: - nameLabel - case .search: - nameLabel - case .friend: - nameLabel - case .history: - nameLabel - case .notification: - nameLabel - case .mentionPick: - nameLabel - case .listMember: - nameLabel - case .addListMember: - nameLabel - case .settingAccountSection: - nameLabel - case .plain: - nameLabel + default: + HStack(spacing: 6) { + nameLabel + if viewModel.protected { + Image(uiImage: Asset.ObjectTools.lockMini.image.withRenderingMode(.alwaysTemplate)) + .resizable() + .frame(width: lockImageDimension, height: lockImageDimension) + .foregroundColor(.secondary) + Spacer() + } + } } } // end Group } From 609fd2a8160ed56a53ece44f8f29aec71be765f4 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 12 May 2023 18:56:36 +0800 Subject: [PATCH 085/128] fix: make protected Twitter user timeline fetchable --- TwidereSDK/Package.swift | 2 +- .../Error/Twitter/TwitterAPIError.swift | 4 ++++ .../StatusFetchViewModel+Timeline+User.swift | 8 ++++++++ .../Status/StatusFetchViewModel+Timeline.swift | 14 ++++++++------ .../xcshareddata/swiftpm/Package.resolved | 4 ++-- TwidereX/Diffable/Misc/TabBar/TabBarItem.swift | 13 ++++++++++++- TwidereX/Scene/Profile/ProfileViewController.swift | 1 + TwidereX/Scene/Profile/ProfileViewModel.swift | 7 ++++++- .../Segmented/Paging/ProfilePagingViewModel.swift | 4 ++++ .../Root/Drawer/DrawerSidebarViewController.swift | 13 ++++++++++++- 10 files changed, 58 insertions(+), 12 deletions(-) diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index 24193981..007e3ef9 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -51,7 +51,7 @@ let package = Package( .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.6.0"), .package(url: "https://github.com/tid-kijyun/Kanna.git", from: "5.2.7"), .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"), - .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.7.0"), + .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.8.0"), .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"), .package(name: "CoverFlowStackLayout", path: "../CoverFlowStackLayout"), ], diff --git a/TwidereSDK/Sources/TwidereCore/Error/Twitter/TwitterAPIError.swift b/TwidereSDK/Sources/TwidereCore/Error/Twitter/TwitterAPIError.swift index dd4d6123..1bc90779 100644 --- a/TwidereSDK/Sources/TwidereCore/Error/Twitter/TwitterAPIError.swift +++ b/TwidereSDK/Sources/TwidereCore/Error/Twitter/TwitterAPIError.swift @@ -13,6 +13,8 @@ extension Twitter.API.Error.TwitterAPIError: LocalizedError { public var errorDescription: String? { switch self { + case .notAuthorizedToViewTheSpecifiedUser: + return L10n.Common.Alerts.PermissionDeniedNotAuthorized.title case .userHasBeenSuspended: return L10n.Common.Alerts.AccountSuspended.title case .rateLimitExceeded: @@ -32,6 +34,8 @@ extension Twitter.API.Error.TwitterAPIError: LocalizedError { public var failureReason: String? { switch self { + case .notAuthorizedToViewTheSpecifiedUser: + return L10n.Common.Alerts.PermissionDeniedNotAuthorized.message case .userHasBeenSuspended: let twitterRules = L10n.Common.Alerts.AccountSuspended.twitterRules return L10n.Common.Alerts.AccountSuspended.message(twitterRules) diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift index c847bfeb..c18a5d56 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift @@ -26,6 +26,7 @@ extension StatusFetchViewModel.Timeline.User { public struct TwitterFetchContext: Hashable { public let authenticationContext: TwitterAuthenticationContext public let userID: Twitter.Entity.V2.User.ID + public let protected: Bool public let paginationToken: String? public let maxID: Twitter.Entity.V2.Tweet.ID? public let maxResults: Int? @@ -37,6 +38,7 @@ extension StatusFetchViewModel.Timeline.User { public init( authenticationContext: TwitterAuthenticationContext, userID: Twitter.Entity.V2.User.ID, + protected: Bool, paginationToken: String?, maxID: Twitter.Entity.V2.Tweet.ID?, maxResults: Int?, @@ -45,6 +47,7 @@ extension StatusFetchViewModel.Timeline.User { ) { self.authenticationContext = authenticationContext self.userID = userID + self.protected = protected self.paginationToken = paginationToken self.maxID = maxID self.maxResults = maxResults @@ -56,6 +59,7 @@ extension StatusFetchViewModel.Timeline.User { return TwitterFetchContext( authenticationContext: authenticationContext, userID: userID, + protected: protected, paginationToken: paginationToken, maxID: maxID, maxResults: maxResults, @@ -68,6 +72,7 @@ extension StatusFetchViewModel.Timeline.User { return TwitterFetchContext( authenticationContext: authenticationContext, userID: userID, + protected: protected, paginationToken: paginationToken, maxID: maxID, maxResults: maxResults, @@ -157,6 +162,9 @@ extension StatusFetchViewModel.Timeline.User { switch fetchContext.timelineKind { case .status, .media: do { + guard !fetchContext.protected else { + throw Twitter.API.Error.ResponseError(httpResponseStatus: .ok, twitterAPIError: .rateLimitExceeded) + } guard !fetchContext.needsAPIFallback else { throw Twitter.API.Error.ResponseError(httpResponseStatus: .ok, twitterAPIError: .rateLimitExceeded) } diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift index c871bd12..c62fcd5c 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift @@ -50,25 +50,26 @@ extension StatusFetchViewModel.Timeline.Kind { public class UserTimelineContext { public let timelineKind: TimelineKind + @Published public var protected: Bool? @Published public var userIdentifier: UserIdentifier? public init( timelineKind: TimelineKind, - userIdentifier: Published.Publisher? + protected protectedPublisher: Published.Publisher?, + userIdentifier userIdentifierPublisher: Published.Publisher? ) { self.timelineKind = timelineKind - - if let userIdentifier = userIdentifier { - userIdentifier.assign(to: &self.$userIdentifier) - - } + protectedPublisher?.assign(to: &$protected) + userIdentifierPublisher?.assign(to: &$userIdentifier) } public init( timelineKind: TimelineKind, + protected: Bool, userIdentifier: UserIdentifier? ) { self.timelineKind = timelineKind + self.protected = protected self.userIdentifier = userIdentifier } @@ -450,6 +451,7 @@ extension StatusFetchViewModel.Timeline { return .user(.twitter(.init( authenticationContext: authenticationContext, userID: userIdentifier.id, + protected: userTimelineContext.protected ?? false, paginationToken: nil, maxID: nil, maxResults: nil, diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index 049913c7..33da201f 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -258,8 +258,8 @@ "repositoryURL": "https://github.com/TwidereProject/TwitterSDK.git", "state": { "branch": null, - "revision": "cf2f33c96e7b9f86dbee23111cd523f9a9294099", - "version": "0.7.0" + "revision": "1b2998a2fc5b7abc421e61b075ba3807ba694991", + "version": "0.8.0" } }, { diff --git a/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift b/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift index fa9defd3..7c081596 100644 --- a/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift +++ b/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift @@ -121,7 +121,18 @@ extension TabBarItem { fatalError() case .likes: let _viewController = UserLikeTimelineViewController() - _viewController.viewModel = UserLikeTimelineViewModel(context: context, authContext: authContext, timelineContext: .init(timelineKind: .like, userIdentifier: authContext.authenticationContext.userIdentifier)) + _viewController.viewModel = UserLikeTimelineViewModel( + context: context, + authContext: authContext, + timelineContext: .init( + timelineKind: .like, + protected: { + guard let user = authContext.authenticationContext.user(in: context.managedObjectContext) else { return false } + return user.protected + }(), + userIdentifier: authContext.authenticationContext.userIdentifier + ) + ) viewController = _viewController case .history: let _viewController = HistoryViewController() diff --git a/TwidereX/Scene/Profile/ProfileViewController.swift b/TwidereX/Scene/Profile/ProfileViewController.swift index 9e6487dd..18c67e5c 100644 --- a/TwidereX/Scene/Profile/ProfileViewController.swift +++ b/TwidereX/Scene/Profile/ProfileViewController.swift @@ -57,6 +57,7 @@ final class ProfileViewController: UIViewController, NeedsDependency, DrawerSide authContext: authContext, coordinator: coordinator, displayLikeTimeline: viewModel.displayLikeTimeline, + protected: viewModel.$protected, userIdentifier: viewModel.$userIdentifier ) return profilePagingViewController diff --git a/TwidereX/Scene/Profile/ProfileViewModel.swift b/TwidereX/Scene/Profile/ProfileViewModel.swift index 6dfb433c..ffb797e9 100644 --- a/TwidereX/Scene/Profile/ProfileViewModel.swift +++ b/TwidereX/Scene/Profile/ProfileViewModel.swift @@ -30,6 +30,7 @@ class ProfileViewModel: ObservableObject { let displayLikeTimeline: Bool @Published var userRecord: UserRecord? @Published var userIdentifier: UserIdentifier? = nil + @Published var protected: Bool? = nil let relationshipViewModel = RelationshipViewModel() // let suspended = CurrentValueSubject(false) @@ -52,7 +53,7 @@ class ProfileViewModel: ObservableObject { } $user - .map { user in user.flatMap { UserRecord(object: $0) } } + .map { $0?.asRecord } .assign(to: &$userRecord) $user @@ -67,6 +68,10 @@ class ProfileViewModel: ObservableObject { } } .assign(to: &$userIdentifier) + + $user + .map { $0?.protected } + .assign(to: &$protected) // bind active authentication Task { diff --git a/TwidereX/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift b/TwidereX/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift index 87b5d25a..8aa1be67 100644 --- a/TwidereX/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift +++ b/TwidereX/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift @@ -27,6 +27,7 @@ final class ProfilePagingViewModel: NSObject { authContext: AuthContext, coordinator: SceneCoordinator, displayLikeTimeline: Bool, + protected: Published.Publisher?, userIdentifier: Published.Publisher? ) { self.context = context @@ -39,6 +40,7 @@ final class ProfilePagingViewModel: NSObject { authContext: authContext, timelineContext: .init( timelineKind: .status, + protected: protected, userIdentifier: userIdentifier ) ) @@ -55,6 +57,7 @@ final class ProfilePagingViewModel: NSObject { authContext: authContext, timelineContext: .init( timelineKind: .media, + protected: protected, userIdentifier: userIdentifier ) ) @@ -75,6 +78,7 @@ final class ProfilePagingViewModel: NSObject { authContext: authContext, timelineContext: .init( timelineKind: .like, + protected: protected, userIdentifier: userIdentifier ) ) diff --git a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift index e71a8679..e4712687 100644 --- a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift +++ b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift @@ -210,7 +210,18 @@ extension DrawerSidebarViewController: UICollectionViewDelegate { let federatedTimelineViewModel = FederatedTimelineViewModel(context: context, authContext: viewModel.authContext, isLocal: false) coordinator.present(scene: .federatedTimeline(viewModel: federatedTimelineViewModel), from: presentingViewController, transition: .show) case .likes: - let userLikeTimelineViewModel = UserLikeTimelineViewModel(context: context, authContext: viewModel.authContext, timelineContext: .init(timelineKind: .like, userIdentifier: viewModel.authContext.authenticationContext.userIdentifier)) + let userLikeTimelineViewModel = UserLikeTimelineViewModel( + context: context, + authContext: viewModel.authContext, + timelineContext: .init( + timelineKind: .like, + protected: { + guard let user = viewModel.authContext.authenticationContext.user(in: context.managedObjectContext) else { return false } + return user.protected + }(), + userIdentifier: viewModel.authContext.authenticationContext.userIdentifier + ) + ) coordinator.present(scene: .userLikeTimeline(viewModel: userLikeTimelineViewModel), from: presentingViewController, transition: .show) case .history: let historyViewModel = HistoryViewModel(context: context, coordinator: coordinator, authContext: viewModel.authContext) From 360cf430e843afe0bce086f01b55f2100a4362f2 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 12 May 2023 18:56:57 +0800 Subject: [PATCH 086/128] fix: media layout not center alignment issue --- .../Container/MediaStackContainerView.swift | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/TwidereSDK/Sources/TwidereUI/Container/MediaStackContainerView.swift b/TwidereSDK/Sources/TwidereUI/Container/MediaStackContainerView.swift index 1462e880..b843376b 100644 --- a/TwidereSDK/Sources/TwidereUI/Container/MediaStackContainerView.swift +++ b/TwidereSDK/Sources/TwidereUI/Container/MediaStackContainerView.swift @@ -28,19 +28,23 @@ public struct MediaStackContainerView: View { let dimension = min(root.size.width, root.size.height) switch viewModel.items.count { case 1: - MediaView(viewModel: viewModel.items[0]) - .frame(width: dimension, height: dimension) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .overlay(alignment: .bottom) { - MediaMetaIndicatorView(viewModel: viewModel.items[0]) - } - .overlay( - RoundedRectangle(cornerRadius: 12) - .stroke(Color(uiColor: .placeholderText).opacity(0.5), lineWidth: 1) - ) - .onTapGesture { - handler(viewModel.items[0], .preview) - } + VStack { + Spacer() + MediaView(viewModel: viewModel.items[0]) + .frame(width: dimension, height: dimension) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .overlay(alignment: .bottom) { + MediaMetaIndicatorView(viewModel: viewModel.items[0]) + } + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color(uiColor: .placeholderText).opacity(0.5), lineWidth: 1) + ) + .onTapGesture { + handler(viewModel.items[0], .preview) + } + Spacer() + } default: CoverFlowStackScrollView { HStack(spacing: .zero) { From 9babc24dac19e76fb5e46b29fa7b259e69c3095d Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 12 May 2023 20:45:22 +0800 Subject: [PATCH 087/128] fix: status repost not respect user protected and post visibility issue --- .../text.bubble.imageset/Contents.json | 15 +++ .../text.bubble.imageset/message.pdf | Bin 0 -> 4802 bytes .../text.bubble.mini.imageset/Contents.json | 15 +++ .../message.mini.pdf | Bin 0 -> 4795 bytes .../Media/repeat.lock.imageset/Contents.json | 15 +++ .../repeat.lock.imageset/lock and repeat.pdf | Bin 0 -> 5862 bytes .../repeat.lock.mini.imageset/Contents.json | 15 +++ .../lock and repeat.pdf | Bin 0 -> 5818 bytes .../Contents.json | 0 .../repeat-off.pdf | Bin .../repeat.off.mini.imageset/Contents.json | 15 +++ .../repeat.off.mini.imageset/Retweet-Off.pdf | Bin 0 -> 5893 bytes .../TwidereAsset/Generated/Assets.swift | 7 +- .../TwidereUI/Content/StatusToolbarView.swift | 116 +++++++++++++----- .../Content/StatusView+ViewModel.swift | 68 ++++++---- 15 files changed, 214 insertions(+), 52 deletions(-) create mode 100644 TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.imageset/Contents.json create mode 100644 TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.imageset/message.pdf create mode 100644 TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.mini.imageset/Contents.json create mode 100644 TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.mini.imageset/message.mini.pdf create mode 100644 TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.imageset/Contents.json create mode 100644 TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.imageset/lock and repeat.pdf create mode 100644 TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.mini.imageset/Contents.json create mode 100644 TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.mini.imageset/lock and repeat.pdf rename TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/{repeat-off.imageset => repeat.off.imageset}/Contents.json (100%) rename TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/{repeat-off.imageset => repeat.off.imageset}/repeat-off.pdf (100%) create mode 100644 TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.off.mini.imageset/Contents.json create mode 100644 TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.off.mini.imageset/Retweet-Off.pdf diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.imageset/Contents.json new file mode 100644 index 00000000..8b028c9a --- /dev/null +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "message.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.imageset/message.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.imageset/message.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f9a4bb81af12d0e964fdb24551a048d684ec9d4d GIT binary patch literal 4802 zcmdT|O^@6}5WV-W=u4#J5RbdvUzVasWJ3@FMA&ePI1IB+cF}ym&IAR1eO}q^*S%{? zhyw?Fh@9u{s#jI7s(1Ry+t+WNn^wqoif4j5#Lz$)>AKlI`&-uo0Y5r!q`E7SlT(^8V z?~dg&_Z$>#dv9&>njj*~4D9@^hVr+aDxhhci_v&ptkDLk5ZSE|*{fSsVw(LQfMs^v z-OnHCl!%=_?No(j1GL0A>5s^6g@Z6##vsgcBFJoCFk0M~y=z#t_f~ySNA=Uc762pt z-`77ZGI~&9QU6$0Wzi9->R(~G{xW<{4J}xUjMa~#Axw9t3N%oP48{j8vW1p zH$}5x>SE}7eNcu9GDP=kZLPFI!C`D^f?#c-0%5?-8U|I-#S5BZ+feDl`(n{$D{+D&BQ@>d^EAWB4(P9 zqO(c6#`Il;ZPRpc)_NemjZ!t!m>2=Ut!t8PlW8&ddwGPDZM_d3tJ>Bjmw31Husg+Itk)1XCR7BD{Sc)MHbNb+8QO$;TV!JLUYej_P$(->7>&KL!T`+? znwWZ#JRjET0x(+i$<_0T` z#>FT>N#UGH!GsCDGtdF4ho(n4I;4&esf_k=zY=^{WcnU!ob?zQ>yv}@X4$$j|B7zv z0#i&?gG&A;EfSP6OpaDOZ;f*Tp5jkv84XH)ylv2_j_#=^G%ouY%R~Tm1F+ zc&HyRT{C8_|)gi&X)hMp8 z>eyk3)kr0_Umde+IU`o>tp+#IgR)X)A7Z5+NEi*!1DQqEcyxS{HSB{HXPS{QKvBrR z?JdwAC*Q1{rpEw<_E}FJ3Pg4#D)Jh&wD1Zkq37CYoGwi|=RJ3_oSL~3Y^eHY48eVM z>f{#f#@{y6bjyt#aZo=F6zX44n+QL4EQB*w&#H3Vt)mr3?yoQ$VN0tIUCqOOPM5TG zJ`-l@fOvc{#`^S<-+7V?44KsANnbJNiBMszpQLCdaABUMRNM>mtT=%An58V?JNFZn zQ=m#Z<>W+r&#Eh6mgYE@^GUoCbXD2HPfEMu8?@PFg;71jiubBzkVO|=I!?5kbxhDo4f6+(`Ws@ z+&*v^Th6iaHe0$L&ZkfJyWy1XyE+{&!x0|kT4V;_ZtsC#H8 literal 0 HcmV?d00001 diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.mini.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.mini.imageset/Contents.json new file mode 100644 index 00000000..fffdd98b --- /dev/null +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.mini.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "message.mini.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.mini.imageset/message.mini.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.mini.imageset/message.mini.pdf new file mode 100644 index 0000000000000000000000000000000000000000..78dfc7cdcc0dfdbb6635ec19a26549086dae48b3 GIT binary patch literal 4795 zcmdT|OOM+&5WeeI@UlR1h)hy^h(Hja*)&Db7EPVrf*xG2l5F8eYdb~SU!QL%YB*kL zf%ecteOPcmk@KE!B;LGv_4=7P!{LtpZLGvs-w%fmmyfCg zxTam_%W2r`o7Jn~&-?AL{^q5+eJlUlo|``!)9USNp5d`RsZh<3(V;}nB{;`rFKX?!N3NgW;KeY*Ft52{MzXTp@Sd%IxBuKC!E zE_DdjBzF%iy69c3SZH-mt}BFMR4Nd*O<=_Zmc)LtK1Q|+Hjrg=AILJYLR!147)#vM zvr8CzN?P7Ghvuh$oqyj_IBc)}_xX2IxpP$ZjT@^|)P$RggYz#C&cDD){#g>|f%P%6 zg+A`RQmv2We`zOUEb%|fzpaM4fd2g_`8RyX=(Z2Z7!3vhI<+yQJ3>n&>Rt*(#eM5L zA7i`{!Jw2NKnUPw>VyCWVGOz&QXzmr-bogOHqb9BM)wtSt(}esh}W@mim}jkDZ*w# zT;JnSz;+#z2V!F1r>M477Rx8C-PG=;T5VfDQBeuSt`V3G-bdoqX!{U*A}kZE4?t4r zdYwROu)-&sFZ&PweD?NA4V@~6A zZ@Syu+$YeaIQf0lFR|hlw&!cBywBR@>GEZ38?3`=yvHRtw%|`)85L_AgkF z?JstTbXT($LCVGGL5}szuWM~A;#!&2&Z5O?{2UUDNFjV2Xx9dW^g*Uk{mEFo8DtKm zphQgsqeJpw0gfo->EJpmAT}je&fM=@q$PN)8)kDo(v(93XB$h|a@*FrVMrE}Xi)4d zwa6gCP)fFIb@X1t6V5JsER)OjmD((o%(@?A?ZWP?Sq_3lkR_6$IinK?C4?kc5N~1m z9T@B(m+qR_YHvM=7Ogt+>uy0FX<;s9)?MaTo(9Z_R}+F6doGW(Fw3%sf=?MEi6yoo zJbZQu6K4f$Gm3)3jO`s*i80f5Ui*$_XRa_BIK~8H2;n9GGkNbY;9Yr18d88Ir9=4*(u3Y|8w;dFeJeg-xbJoXgA}Va;^8yo^h@YmnJKlyYjl$|b^d<3N`S z<4uG!-tk3*V;C0{tFI1+0{k%C-EUqVKdX1BkKzC? z3_hpxPu0~VX+4~epHAE1tmsN)UBulU50~KpjdUL}fp0b^U<&WA84|Z91o3Uk>bv_p zn5xKwwVL$Y9zWr%nc05^A|!VV&OManJYFuQU6<45em9&N%HFrH5RND3<9>Lm rp4{I(9fPu3ACDJB4(9-`-roN`!1{d6v^ia}JI=?hxw-lJ$Jbu~b_csL literal 0 HcmV?d00001 diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.imageset/Contents.json new file mode 100644 index 00000000..bcacd54f --- /dev/null +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "lock and repeat.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.imageset/lock and repeat.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.imageset/lock and repeat.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6b2ad0cbf1ccfb03553e0c4815c4e46afc803583 GIT binary patch literal 5862 zcmb7I+m0MH5Pk2j@JpoRA-(Ne+bu>#90cu8ZwQZ(hBAs{7D&S`F&>>u;S>&z`C0&%5p6R{q(U7Qgy_d-(A1QCWc3 zvg-73zrERaSFg5z-fgzm-@H&S-?snSoVq_c-4E%fhWhc8U;LY^f1CPgFg7@~*>_yF z{5Wk69heu*rXQv-no;c=Q1{+>>$HM7?~RFGK_IWALLwVjyIe*6R7HdqH;qMtyY6FW zE4f&W?q_VO*lxzSPNem?A`UPNK@U-d-cEzpliGDUBwbbm3{elxsu_yC)6PI(7L9!z zT(CxM){G?EHk%1b>$-T~Y1>;p`Uv%=*ONCMe>eRY5#sbUV`egO!a7RBlur9PPJ&iW zGd7UA>8tcmeT4K1qr{Y}eM6LL{nCX=Pa0Wc@?BI_Pa_q81ki}6hE^wnmBl5L2on?$f>c{b`0YP#77@u0OGZdc0yVV zy$;$3i-elYICzUJlNt(GX#ncrry;1BGPL>VCeuKqfz4Y`%YnHxEQF_Vh<*Z@7{^H) zWqLPGehjc$DwkzNWHXmgQ&!MrCvBY~HozM=7FF!wx}G8$(xs!q_L7&dU0MZOmME!U zcL>sMQ{rn_BaCfiLTjc!Ecsd#ZQZjOu%cmpwNTcyrahous(>+4XcBqW%al*PuC$}2 zPBHbqJ9IyN@h})tRXZf*e+~oWFNVRWQIn|=Y0LtI)*I`_84L5O(U~bFR*~~nZUZ@D z!3A$eNx!TKsV=QgOTA=!L^d5UauC-!YiI#e3Jp&>c~Cy}Mbsv043&cMgih{rF}hSx z8FGCLaYX-|MP2V=)Xt}pEt8No#4>jhEjO>+1FIc7SJa?NgEMtH3R1{XZqs2vXCi3o znFtY6*;39_r#34bgc(4Qpf@{~0Nn z!H~fCN~?;d1}XOH9BVn0OwJb2uoE+(h8PzR6E~qK?-}GuQQ+%ZBzfWk`J6~FMQpdS zE=!9{OKOjm#ELt95g1x!5Tt!w28KLFQ6M315C*2PS1@75u}=%tFYF$%UGWuI?z;GB z&NypWBU{V0)hJd*8_`{7E6f^Dt7}-pj?XaX@xP9Bt}!FFZ>&%>;tZ1OE5RGOuewfowgr@ zrhF7i%vK_`;IScOA$!OL^enaRY@ykQnae6jX&EJJaUn|aGdm@Z87Wo;Jt5L~!kkS5 zEXRP@%osbBlYNHBLhc8JFENQrcIdnl5o`Jw+x8m96p<+>A#L@YS)sHXY065RVy*T@ z%K^aZU;+;R+D&eBKyY}{P7oVy#34bVf=iLZ-3P7PIp;AFr1jwLinjzT#4R@BicP{e zO_<5zyhVg0WU7!vN(Cun3Cg8e84h|FJa(fCi?!hOo5NxSe=7+-P|#jmMFhpWFf_M0WPUA>_Dl z3BmyTew>`gTTUBboKQo=ZLy(;(@B_YzyPNcq%lKH1jtPoFD(n>h-0qxU{ex+LyAe# zAZ5pDyF-k*vvoiLe$m1Qrl zr;_u2mg5YJMO`ib3wG8EDNVE<%g(K^CpA_myM|Gp_>%9A@+4W_8|&kwyhG+kOFZvs zzF9iLc+0%{>TozdoYcEt@V1HH)z5$ZzExLmZa!=a@Wb}@?&ihuvwA0UA^qkLgWS*1 z{8cqwZ%@Zh_nYlW$?NHLQTOtAc-S6bQJzjK_~zywnDPZ`9#{q9!|T;|cekLb#Di;9 z?YumGf~;QfKLKepjc1y~!4nP6j=#KlxY->)oQrzerQkW zOC1Q|b>$rLRwESh<0_m77OOn&y%(!+Y$x<^e{;9n-gmNl-@ZaP9-of;?GyF*?)FIz m%GLGp_@Kn$6yU43cYkleKD|ia+&?6D{OE@6(W9?_eEkomZia0D literal 0 HcmV?d00001 diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.mini.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.mini.imageset/Contents.json new file mode 100644 index 00000000..bcacd54f --- /dev/null +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.mini.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "lock and repeat.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.mini.imageset/lock and repeat.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.mini.imageset/lock and repeat.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f148daea4478627d732b3238ac2ef5d9d23b6bd1 GIT binary patch literal 5818 zcmb7IOOG5y48HfTs7s{eke+%!q$m>E5QG2`Hryf(!|WtkG!K}Wpun%^b5*sw`XQn8 zVb%Lh+5YUZUG94F_Vt_Rst;YKgcRrB|LBBx@j|?O+3imc^lztIeDmY}^zrsd7=ZV* z>Uz8Ex5sYtdjI$1Zh!yXD{=R({cm^e{_0dO^CRT&@|@rJH{E+Y&I{jL?L#nPckFoC z^5wcabx;!=sTQht!Dt=CaRMrrq%s2Qtg>2JFo-&@YZO*w2W!h!tiP&STHH-6RvL%y zQ>O=S-AIlx-lX-8(P&nzsMAs{SgMhnbemn zHY6am9=TK)*^Q;rl#l9lkltA|j#^R@(N-!dAkr9{95tSqqsaXQFWSbXRj}n!WGbjS z2H9>?%4=97+$1ume6~|FYg*m&x;?BG2d6k|B!57&>g2MgT3%-GGShQsS!p**jb!LU zcj|um>SiyI2*x4D{=eBH_wWQObRVe!S@e3_BiUVyBZkPuyvp>{U#+Ax9uRM{+^%EH z_R^z0MxkZyT6!%5Rbn8V61G|AEEg)$8J(4n{^kr}hS zi!MYDg~4fy(GaKFF!w=>F0JGNu$ouG4kO=L_H0tL}H2^R@N11YF&_KmPf3t82e%nyQI+RI0kH$5o%qC zoC5*ImF&K<5 zys~}4Pm*ScnjWlxTK+i06q<(-qXvP=ohaoOnuzAh85xd&si(wtZZ)$liHfQ%PB&K9 zFlJ1#o9invsVGZNtZ0SNEvJ5o5_)GLz=|1Z{NTxr*wqLy#*SlZi}*HcHj_Ez{91V0 zTH>)>vWbi4~)j2_DCP3Q<`{cLhELn~+K}24`vH#|JR| zMCSlLWAGV-&h1T#(qjTc_>5Ok0@F4rNuLI_>wCmHX&f63!nwz6nHR-mX_d|f8ktQ5 z6Bu_^gPXDV#362+Ta-^YD;S|6r((^S0;~9%!@b-I zJGV+~@#Tb`IWoayBhg+>%ReauD*@ry8ti~kp5n-{#g|~z)}@gf&MK#!b0hzymyPEf zjly#2U$O;!Ex2$0n!NA1WFpB`=cGsN5Ygi+jM^|KLtC3pMvRN|L zXe|3XwanPWN*O8Xnz7y?4i}Dew^%MB=}v<0A#ry%-cN46Ii1e8t9bt#?kn+Ze*Ndq zz1Y0ne%x2!r~Sj@_SN}|c#j}tcPJY5Md4x~KVp0vG~Ms7=g*hj{wnB3^uDUQJD+a* z6D-no=m5UmUVsVokPJ1KrUdb2>gI>X2T&E{!Mzyl+?_u|RxS8nfi#*h^fsEv!7Cda z9lzV&wuke_spyx>{zHcf?TWATS9pidZ2`DGn4|KWzzAL+1D^CSdfA0M8Tplt5X=bIphbAUJR9{=5geZFPh SUT)bP=aTQ9Jo)zLH~#_bNQ0XI literal 0 HcmV?d00001 diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat-off.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.off.imageset/Contents.json similarity index 100% rename from TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat-off.imageset/Contents.json rename to TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.off.imageset/Contents.json diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat-off.imageset/repeat-off.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.off.imageset/repeat-off.pdf similarity index 100% rename from TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat-off.imageset/repeat-off.pdf rename to TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.off.imageset/repeat-off.pdf diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.off.mini.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.off.mini.imageset/Contents.json new file mode 100644 index 00000000..1fe64dc8 --- /dev/null +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.off.mini.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Retweet-Off.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.off.mini.imageset/Retweet-Off.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.off.mini.imageset/Retweet-Off.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a4f1818af305724ffb419f2bc74eb49581028150 GIT binary patch literal 5893 zcmd^DOOMn>5Wf3Y_<{rnJpF!1XeD4_6(td5HwPq?!!Q_j$vieQLj?Kt`Kr5J?y)BV z&Y8=4KX*O4y1uG*J$QZj^0DlE)5un|yLX?Owte!XUHty_zHb+AUcLL&ua3a*Tl~^* zKOS#eg@xAzy4vqP-&X?Z|NJh=WWgKg+b_3=`{lmhuD+hUzqlx>Za%*|CZjKR`%S^F z`@`<@e$^ky1h0JdZ1<&o+lo%4-}oTi0F6(7Koukp$9=!tG>gmr@4Hohef6w;epCOq zIy8SZau8?LkNIi-j$f@d4L{Ex4y*0>dh~14jL+lKxsr?t<-FJJw@)3(<1M9VD?978Qoh|Z zH7IQqQn!GNG)f}GUBKaB9iKBJDFRR&`Pc7ZeaLoVSPsbVaYD42X_|hNx)AR+=DyXc-^? zL|QM6q%{$$RFa$eK%SX_hLFwS*fhGc!TR7yU&eH&a&T=I16-7Cg_M1+Ajhjph*%T$ zm^HCIMpkEhNHG&B4V=g3`9vjl79FAmP#6{P02<5Agczcrz-jC8P(wiq=L|tblme%% z^%Ru35wabJrD0gQX+jr;k;1g0)6p3#@L*I_njRRD+G*{9unFGj>L&v@s@oc&*pTXQe8-c zM98FER}?u5GJUBMGv!#ivcuw)9eAWdKCK<`I!&o)YuaNmM<<-?>IIl)LM9-W0fJpmKk2&!8e9(P=!WSyvba zORd8?5kx?N1<`XE8_$ZNR7{jr#-%YP2cS>EJzj6V!nW(owro z&IY_o8%9qOl0a#6Fg8_LuY6LbvCd0^R5YLlR(U7AqHZ+zgLWJ{<05u|#uts!Xqe<( za2BjX%Mnfs0dAvXhiHrkWP%bhI~;;GK!Oh{m}~-X6`4g_wI*O&RIo(=Vq$8Pu@f53 z86X-L4HzWXrcRJ+VdzFoc*=Slj#ZAN^Lekwb>}daC5?RVU^LxW5KxVtQq@+}pVU1y z?Jl>F8Y(>mP!X+4_1s2EEZU*-bXO`(6r}=ZgVeztHCHjZd?@V=npuSu3oN(7HKzC` zhgMS|#alIaM)#Sa^Ui!A&v+%*E4nBZ3*6l+WNLwH5_wkqM>)+6oA+2DB@!ocWtZ#& zdBz;8Y6W8z#YQfl1zEacZG}~Kn9D=~@Z0+U(jx1T!^mV@ctpAe;SP(^Ldb;Jsjf_! zmrAhqAh3oup-hk8M5+og;HF$4u080fi5c@cLmp2#lrBrsm`Ei><07jgN~ez! z_n^!b0ftW5<6wa>i&~51D;Gv$RMf=sgsCVk_NoMwDMC;ZJRwug5|5{T(K#}YnV;1c zC1cfR;E5-t9-qT1*(#4Y&AU znIG)LJSxyuTYS!Z`|FL(N|ZhCJhFr0Rrm`qvDDMG>l{}0F$$*-v7s)ZXa*gkBIoWmC-YPaOllPzW_*+DzZX40oz$s>H6 zbj2TtCLak(ZYOO;6lb4MFJ$_2^Xu~PXL`yb zGy3;rz%|anPM;Q6Ee&7FB<=Lw?7sy@XiwMox65y<9>!!d9R!rQ9`EG8)LmTUZ9~~G zDRL)~DziW|;vAvxny)PovyX>nfzV#=&QGaGVL2wprUQd?NKd!h-SN=ArODp7rPs@k zeF1*$-`_2NbVf*;#&IL1#W3T1zS|!AEi6(52JkhFGLLO6P!%VF@(#$^pYPto)s}W} z-3}f<-+hLxocVtMQfZRnL>2Auz!t}yKVKe~>)ppw*)RM4LqjdI$(1jSp`1nWYk`*^ z8AvZMq0B#nHameVuF|KF2^h9aAxtw)AxjtY38dz$=#D>GWh}x)zjp8oevi72`{mub z-!~NApDq#Hhlky!f7Cv_d;chhcyYbkA-)v8bY6>_yMKC$1%i7xF89Y29c!a%9z1yQ G>gB)Q4YZK} literal 0 HcmV?d00001 diff --git a/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift b/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift index d244bd3b..1730b999 100644 --- a/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift +++ b/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift @@ -87,6 +87,8 @@ public enum Asset { public static let ellipsisBubblePlus = ImageAsset(name: "Communication/ellipsis.bubble.plus") public static let mail = ImageAsset(name: "Communication/mail") public static let mailMiniInline = ImageAsset(name: "Communication/mail.mini.inline") + public static let textBubble = ImageAsset(name: "Communication/text.bubble") + public static let textBubbleMini = ImageAsset(name: "Communication/text.bubble.mini") public static let textBubbleSmall = ImageAsset(name: "Communication/text.bubble.small") } public enum Editing { @@ -148,9 +150,12 @@ public enum Asset { public static let altRectangle = ImageAsset(name: "Media/alt.rectangle") public static let gifRectangle = ImageAsset(name: "Media/gif.rectangle") public static let playerRectangle = ImageAsset(name: "Media/player.rectangle") - public static let repeatOff = ImageAsset(name: "Media/repeat-off") public static let `repeat` = ImageAsset(name: "Media/repeat") + public static let repeatLock = ImageAsset(name: "Media/repeat.lock") + public static let repeatLockMini = ImageAsset(name: "Media/repeat.lock.mini") public static let repeatMini = ImageAsset(name: "Media/repeat.mini") + public static let repeatOff = ImageAsset(name: "Media/repeat.off") + public static let repeatOffMini = ImageAsset(name: "Media/repeat.off.mini") } public enum ObjectTools { public static let bell = ImageAsset(name: "Object&Tools/bell") diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift index 00d5c0ea..17992bac 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift @@ -68,9 +68,9 @@ extension StatusToolbarView { image: { switch viewModel.style { case .inline: - return Asset.Arrows.arrowTurnUpLeftMini.image.withRenderingMode(.alwaysTemplate) + return Asset.Communication.textBubbleMini.image.withRenderingMode(.alwaysTemplate) case .plain: - return Asset.Arrows.arrowTurnUpLeft.image.withRenderingMode(.alwaysTemplate) + return Asset.Communication.textBubble.image.withRenderingMode(.alwaysTemplate) } }(), count: isMetricCountDisplay ? viewModel.replyCount : nil, @@ -78,50 +78,99 @@ extension StatusToolbarView { ) } + enum RepostButtonImage { + case repost + case repostOff + case repostLock + + func image(style: StatusToolbarView.Style) -> UIImage { + switch self { + case .repost: + switch style { + case .inline: return Asset.Media.repeatMini.image.withRenderingMode(.alwaysTemplate) + case .plain: return Asset.Media.repeat.image.withRenderingMode(.alwaysTemplate) + } + case .repostOff: + switch style { + case .inline: return Asset.Media.repeatOffMini.image.withRenderingMode(.alwaysTemplate) + case .plain: return Asset.Media.repeatOff.image.withRenderingMode(.alwaysTemplate) + } + case .repostLock: + switch style { + case .inline: return Asset.Media.repeatLockMini.image.withRenderingMode(.alwaysTemplate) + case .plain: return Asset.Media.repeatLock.image.withRenderingMode(.alwaysTemplate) + } + } // end switch + } // end func + + static func kind( + platform: Platform, + isReposeRestricted: Bool, + isMyself: Bool + ) -> Self { + switch platform { + case .twitter: + if isMyself { return .repost } + if isReposeRestricted { return .repostOff } + return .repost + case .mastodon: + if isReposeRestricted { + return isMyself ? .repostLock : .repostOff + } + return .repost + case .none: + return .repost + } // end switch + } + } + public var repostButton: some View { ToolbarButton( handler: { action in + guard viewModel.isRepostable else { return } logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): repost") handler(action) }, action: .repost, image: { - switch viewModel.style { - case .inline: - return Asset.Media.repeatMini.image.withRenderingMode(.alwaysTemplate) - case .plain: - return Asset.Media.repeat.image.withRenderingMode(.alwaysTemplate) - } + return RepostButtonImage.kind( + platform: viewModel.platform, + isReposeRestricted: viewModel.isReposeRestricted, + isMyself: viewModel.isMyself + ).image(style: viewModel.style) }(), count: isMetricCountDisplay ? viewModel.repostCount : nil, tintColor: viewModel.isReposted ? Asset.Scene.Status.Toolbar.repost.color : nil ) + .opacity(viewModel.isRepostable ? 1 : 0.5) } public var repostMenu: some View { Menu { - // repost - Button { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): repost") - handler(.repost) - } label: { - Label { - let text = viewModel.isReposted ? L10n.Common.Controls.Status.Actions.undoRetweet : L10n.Common.Controls.Status.Actions.retweet - Text(text) - } icon: { - let image = viewModel.isReposted ? Asset.Media.repeatOff.image.withRenderingMode(.alwaysTemplate) : Asset.Media.repeat.image.withRenderingMode(.alwaysTemplate) - Image(uiImage: image) + if viewModel.isRepostable { + // repost + Button { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): repost") + handler(.repost) + } label: { + Label { + let text = viewModel.isReposted ? L10n.Common.Controls.Status.Actions.undoRetweet : L10n.Common.Controls.Status.Actions.retweet + Text(text) + } icon: { + let image = viewModel.isReposted ? Asset.Media.repeatOff.image.withRenderingMode(.alwaysTemplate) : Asset.Media.repeat.image.withRenderingMode(.alwaysTemplate) + Image(uiImage: image) + } } - } - // quote - Button { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): quote") - handler(.quote) - } label: { - Label { - Text(L10n.Common.Controls.Status.Actions.quote) - } icon: { - Image(uiImage: Asset.TextFormatting.textQuote.image.withRenderingMode(.alwaysTemplate)) + // quote + Button { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): quote") + handler(.quote) + } label: { + Label { + Text(L10n.Common.Controls.Status.Actions.quote) + } icon: { + Image(uiImage: Asset.TextFormatting.textQuote.image.withRenderingMode(.alwaysTemplate)) + } } } } label: { @@ -190,6 +239,7 @@ extension StatusToolbarView { // input @Published var platform: Platform = .none @Published var style: Style = .inline + @Published var replyCount: Int? @Published var repostCount: Int? @Published var likeCount: Int? @@ -197,6 +247,13 @@ extension StatusToolbarView { @Published var isReposted: Bool = false @Published var isLiked: Bool = false + @Published var isReposeRestricted: Bool = false + @Published var isMyself: Bool = false + + var isRepostable: Bool { + return isMyself || !isReposeRestricted + } + public init() { // end init } @@ -208,6 +265,7 @@ extension StatusToolbarView { case inline case plain } + public enum Action: Hashable, CaseIterable { case reply case repost diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index a338aa46..6357e160 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -55,9 +55,10 @@ extension StatusView { @Published public var authorName: MetaContent = PlaintextMetaContent(string: "") @Published public var authorUsernme = "" - @Published public var authorUserIdentifier: UserIdentifier? + public let authorUserIdentifier: UserIdentifier? @Published public var protected: Bool = false + public let isMyself: Bool // static let pollOptionOrdinalNumberFormatter: NumberFormatter = { // let formatter = NumberFormatter() @@ -65,7 +66,6 @@ extension StatusView { // return formatter // }() // -// @Published public var userIdentifier: UserIdentifier? // @Published public var authorAvatarImage: UIImage? // @Published public var authorAvatarImageURL: URL? // @Published public var authorUsername: String? @@ -123,9 +123,6 @@ extension StatusView { default: return true } } - -// @Published public var isRepost = false -// @Published public var isRepostEnabled = true // poll @Published public var pollViewModel: PollView.ViewModel? @@ -149,9 +146,7 @@ extension StatusView { return nil } } -// @Published public var replySettings: Twitter.Entity.V2.Tweet.ReplySettings? -////// // @Published public var groupedAccessibilityLabel = "" // timestamp @@ -170,6 +165,9 @@ extension StatusView { guard let authorUserIdentifier = self.authorUserIdentifier else { return false } return authContext.authenticationContext.userIdentifier == authorUserIdentifier } + + // reply settings banner + @Published public var replySettingBannerViewModel: ReplySettingBannerView.ViewModel? // conversation link @Published public var isTopConversationLinkLineViewDisplay = false @@ -188,22 +186,23 @@ extension StatusView { self.authContext = authContext self.kind = kind self.delegate = delegate + let _authorUserIdentifier: UserIdentifier = { + switch author { + case .twitter(let author): + return .twitter(.init(id: author.id)) + case .mastodon(let author): + return .mastodon(.init(domain: author.domain, id: author.id)) + } + }() + self.authorUserIdentifier = _authorUserIdentifier + self.isMyself = { + guard let myUserIdentifier = authContext?.authenticationContext.userIdentifier else { return false } + return myUserIdentifier == _authorUserIdentifier + }() // end init viewLayoutFramePublisher?.assign(to: &$viewLayoutFrame) -// // isMyself -// Publishers.CombineLatest( -// $authenticationContext, -// $userIdentifier -// ) -// .map { authenticationContext, userIdentifier in -// guard let authenticationContext = authenticationContext, -// let userIdentifier = userIdentifier -// else { return false } -// return authenticationContext.userIdentifier == userIdentifier -// } -// .assign(to: &$isMyself) // // isContentSensitive // Publishers.CombineLatest( // $platform, @@ -273,6 +272,8 @@ extension StatusView { self.author = nil self.authContext = nil self.kind = .timeline + self.authorUserIdentifier = nil + self.isMyself = false // end init viewLayoutFramePublisher?.assign(to: &$viewLayoutFrame) @@ -1097,6 +1098,16 @@ extension StatusView.ViewModel { viewLayoutFramePublisher: viewLayoutFramePublisher ) } + + // reply settings + replySettingBannerViewModel = status.replySettings + .flatMap { object in Twitter.Entity.V2.Tweet.ReplySettings(rawValue: object.value) } + .flatMap { replaySettings in + ReplySettingBannerView.ViewModel( + replaySettings: replaySettings, + authorUsername: status.author.username + ) + } // author status.author.publisher(for: \.profileImageURL) @@ -1109,7 +1120,6 @@ extension StatusView.ViewModel { .assign(to: &$authorUsernme) status.author.publisher(for: \.protected) .assign(to: &$protected) - authorUserIdentifier = .twitter(.init(id: status.author.id)) // timestamp switch kind { @@ -1185,6 +1195,7 @@ extension StatusView.ViewModel { // toolbar toolbarViewModel.platform = .twitter + toolbarViewModel.isMyself = isMyself status.publisher(for: \.replyCount) .map { Int($0) } .assign(to: &toolbarViewModel.$replyCount) @@ -1194,6 +1205,8 @@ extension StatusView.ViewModel { status.publisher(for: \.likeCount) .map { Int($0) } .assign(to: &toolbarViewModel.$likeCount) + status.author.publisher(for: \.protected) + .assign(to: &toolbarViewModel.$isReposeRestricted) if case let .twitter(authenticationContext) = authContext?.authenticationContext { status.publisher(for: \.likeBy) .map { users -> Bool in @@ -1270,8 +1283,7 @@ extension StatusView.ViewModel { .assign(to: &$authorUsernme) status.author.publisher(for: \.locked) .assign(to: &$protected) - authorUserIdentifier = .mastodon(.init(domain: status.author.domain, id: status.author.id)) - + // visibility visibility = status.visibility @@ -1337,6 +1349,7 @@ extension StatusView.ViewModel { // toolbar toolbarViewModel.platform = .mastodon + toolbarViewModel.isMyself = isMyself status.publisher(for: \.replyCount) .map { Int($0) } .assign(to: &toolbarViewModel.$replyCount) @@ -1346,6 +1359,17 @@ extension StatusView.ViewModel { status.publisher(for: \.likeCount) .map { Int($0) } .assign(to: &toolbarViewModel.$likeCount) + toolbarViewModel.isReposeRestricted = { + switch status.visibility { + case .public: return false + case .unlisted: return false + case .direct: return true + case .private: return true + case ._other: + assertionFailure() + return false + } + }() if case let .mastodon(authenticationContext) = authContext?.authenticationContext { status.publisher(for: \.likeBy) .map { users -> Bool in From 0a770283486c6059591b3164e7033fd3ae2378b3 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 12 May 2023 20:52:48 +0800 Subject: [PATCH 088/128] feat: add missing Twitter post reply setting banner --- .../TwidereUI/Content/StatusView.swift | 55 ++++--- .../Control/ReplySettingBannerView.swift | 139 ++++++++++-------- 2 files changed, 117 insertions(+), 77 deletions(-) diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index ed1a2cf1..d318ecd0 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -209,28 +209,50 @@ public struct StatusView: View { // metric if let metricViewModel = viewModel.metricViewModel { StatusMetricView(viewModel: metricViewModel) { action in - + // TODO: } .padding(.vertical, 8) } // toolbar if viewModel.hasToolbar { - toolbarView - .overlay(alignment: .top) { - switch viewModel.kind { - case .conversationRoot: - VStack(spacing: .zero) { - Color.clear - .frame(height: 1) - Divider() - .frame(width: viewModel.viewLayoutFrame.safeAreaLayoutFrame.width) - .fixedSize() - Spacer() + VStack(spacing: .zero) { + toolbarView + .overlay(alignment: .top) { + switch viewModel.kind { + case .conversationRoot: + // toolbar top divider + VStack(spacing: .zero) { + Color.clear + .frame(height: 1) + Divider() + .frame(width: viewModel.viewLayoutFrame.safeAreaLayoutFrame.width) + .fixedSize() + Spacer() + } + default: + EmptyView() } - default: - EmptyView() + } + if viewModel.kind == .conversationRoot, + let replySettingBannerViewModel = viewModel.replySettingBannerViewModel, + !replySettingBannerViewModel.shouldHidden + { + HStack { + ReplySettingBannerView(viewModel: replySettingBannerViewModel) + Spacer() + } + .background { + Color(uiColor: Asset.Colors.hightLight.color.withAlphaComponent(0.6)) + .frame(width: viewModel.viewLayoutFrame.safeAreaLayoutFrame.width) + .overlay(alignment: .top) { + // reply settings banner top divider + VStack(spacing: .zero) { + Divider() + } + } } } + } // end VStack } } // end VStack .padding(.top, viewModel.margin) // container margin @@ -247,13 +269,12 @@ public struct StatusView: View { .frame(height: 1) } case .conversationRoot: + // cell bottom divider VStack(spacing: .zero) { Spacer() Divider() .frame(width: viewModel.viewLayoutFrame.safeAreaLayoutFrame.width) .fixedSize() - Color.clear - .frame(height: 1) } default: EmptyView() @@ -314,7 +335,7 @@ extension StatusView { } }() nameLayout { - HStack { + HStack(spacing: 4) { // name LabelRepresentable( metaContent: viewModel.authorName, diff --git a/TwidereSDK/Sources/TwidereUI/Control/ReplySettingBannerView.swift b/TwidereSDK/Sources/TwidereUI/Control/ReplySettingBannerView.swift index f8acb018..aedfc955 100644 --- a/TwidereSDK/Sources/TwidereUI/Control/ReplySettingBannerView.swift +++ b/TwidereSDK/Sources/TwidereUI/Control/ReplySettingBannerView.swift @@ -6,76 +6,95 @@ // import UIKit +import TwitterSDK import TwidereAsset +import TwidereLocalization -final public class ReplySettingBannerView: UIView { +public struct ReplySettingBannerView: View { - let topSeparator = SeparatorLineView() - - let overflowBackgroundView = UIView() - - let stackView = UIStackView() - - public let imageView = UIImageView() - - public let label: UILabel = { - let label = UILabel() - label.font = .preferredFont(forTextStyle: .callout) - label.numberOfLines = 0 - return label - }() - - public override init(frame: CGRect) { - super.init(frame: frame) - _init() - } + public let viewModel: ViewModel + + @ScaledMetric(relativeTo: .callout) private var imageDimension: CGFloat = 16 + - public required init?(coder: NSCoder) { - super.init(coder: coder) - _init() + public var body: some View { + HStack(spacing: 4) { + Image(uiImage: viewModel.icon) + .resizable() + .frame(width: imageDimension, height: imageDimension) + Text(viewModel.title) + } + .font(.callout) + .foregroundColor(.white) + .padding(.vertical, 8) } } extension ReplySettingBannerView { - private func _init() { - // Hack the background view to fill the table width - overflowBackgroundView.translatesAutoresizingMaskIntoConstraints = false - addSubview(overflowBackgroundView) - NSLayoutConstraint.activate([ - overflowBackgroundView.topAnchor.constraint(equalTo: topAnchor), - leadingAnchor.constraint(equalTo: overflowBackgroundView.leadingAnchor, constant: 400), - overflowBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 400), - overflowBackgroundView.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - - // Hack the line to fill the table width - topSeparator.translatesAutoresizingMaskIntoConstraints = false - addSubview(topSeparator) - NSLayoutConstraint.activate([ - topSeparator.topAnchor.constraint(equalTo: topSeparator.topAnchor), - leadingAnchor.constraint(equalTo: topSeparator.leadingAnchor, constant: 400), - topSeparator.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 400), - ]) + public class ViewModel: ObservableObject { + // input + public let replaySettings: Twitter.Entity.V2.Tweet.ReplySettings + public let authorUsername: String - // stackView: H - [ icon | label ] - stackView.axis = .horizontal - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.alignment = .center - stackView.spacing = 4 - addSubview(stackView) - NSLayoutConstraint.activate([ - stackView.topAnchor.constraint(equalTo: topAnchor, constant: 8), - stackView.leadingAnchor.constraint(equalTo: leadingAnchor), - stackView.trailingAnchor.constraint(equalTo: trailingAnchor), - bottomAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 8), - ]) + // output + public let icon: UIImage + public let title: String + public let shouldHidden: Bool - stackView.addArrangedSubview(imageView) - stackView.addArrangedSubview(label) - - overflowBackgroundView.backgroundColor = Asset.Colors.hightLight.color.withAlphaComponent(0.6) - imageView.tintColor = .white - label.textColor = .white + public init( + replaySettings: Twitter.Entity.V2.Tweet.ReplySettings, + authorUsername: String + ) { + self.replaySettings = replaySettings + self.authorUsername = authorUsername + self.icon = { + switch replaySettings { + case .everyone: + fallthrough + case .following: + return Asset.Communication.at.image.withRenderingMode(.alwaysTemplate) + case .mentionedUsers: + return Asset.Human.personCheckMini.image.withRenderingMode(.alwaysTemplate) + } + }() + self.title = { + switch replaySettings { + case .everyone: + return "" + case .following: + return L10n.Common.Controls.Status.ReplySettings.peopleUserFollowsOrMentionedCanReply("@\(authorUsername)") + case .mentionedUsers: + return L10n.Common.Controls.Status.ReplySettings.peopleUserMentionedCanReply("@\(authorUsername)") + } + }() + self.shouldHidden = replaySettings == .everyone + // end init + } } } + +#if canImport(SwiftUI) && DEBUG +import SwiftUI + +@available(iOS 13.0, *) +struct ReplySettingBannerView_Previews: PreviewProvider { + + static var previews: some View { + Group { + ReplySettingBannerView(viewModel: .init( + replaySettings: .following, + authorUsername: "alice" + )) + ReplySettingBannerView(viewModel: .init( + replaySettings: .mentionedUsers, + authorUsername: "alice" + )) + } + .background(Color.black) + } + +} + +#endif + From e8e7a9d17ebeeb90bf399073cec57da900484382 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 12 May 2023 20:53:37 +0800 Subject: [PATCH 089/128] chore: update version to 2.0.0 (129) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- .../Control/ReplySettingBannerView.swift | 1 + TwidereX.xcodeproj/project.pbxproj | 36 +++++++++---------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 8 files changed, 25 insertions(+), 24 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index f9aa0a6a..4f6e87f4 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 128 + 129 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 6758123f..8810f44f 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 128 + 129 NSExtension NSExtensionAttributes diff --git a/TwidereSDK/Sources/TwidereUI/Control/ReplySettingBannerView.swift b/TwidereSDK/Sources/TwidereUI/Control/ReplySettingBannerView.swift index aedfc955..b8b347ee 100644 --- a/TwidereSDK/Sources/TwidereUI/Control/ReplySettingBannerView.swift +++ b/TwidereSDK/Sources/TwidereUI/Control/ReplySettingBannerView.swift @@ -6,6 +6,7 @@ // import UIKit +import SwiftUI import TwitterSDK import TwidereAsset import TwidereLocalization diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 6839e4d6..9840a86c 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3431,7 +3431,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3458,7 +3458,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3481,7 +3481,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3505,7 +3505,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3532,7 +3532,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3561,7 +3561,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3590,7 +3590,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3618,7 +3618,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3644,7 +3644,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3671,7 +3671,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3825,7 +3825,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3857,7 +3857,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3884,7 +3884,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3909,7 +3909,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3932,7 +3932,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3955,7 +3955,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3979,7 +3979,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -4006,7 +4006,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index 33f4967d..5f7a8ad6 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 128 + 129 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index 57f097f7..14cabbb5 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 128 + 129 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index e0d1d83e..f33f97dc 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 128 + 129 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index e0d1d83e..f33f97dc 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 128 + 129 From fa88cbcc96688a6fc37bc44f9c44511940a6fd55 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 15 May 2023 10:44:00 +0800 Subject: [PATCH 090/128] fix: duplicated trend items may raise crash issue --- TwidereSDK/Sources/TwidereCore/Service/TrendService.swift | 4 ++-- TwidereX/Scene/Search/Trend/TrendViewModel+Diffable.swift | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/TwidereSDK/Sources/TwidereCore/Service/TrendService.swift b/TwidereSDK/Sources/TwidereCore/Service/TrendService.swift index 7841b62e..3bf926bf 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/TrendService.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/TrendService.swift @@ -87,7 +87,7 @@ extension TrendService { for data in response.value { guard let location = data.locations.first else { continue } trendGroupRecords[.twitter(placeID: location.woeid)] = TrendGroup( - trends: data.trends.map { TrendObject.twitter(trend: $0) }, + trends: data.trends.map { TrendObject.twitter(trend: $0) }.removingDuplicates(), timestamp: data.asOf ) } @@ -97,7 +97,7 @@ extension TrendService { authenticationContext: authenticationContext ) trendGroupRecords[.mastodon(domain: domain)] = TrendGroup( - trends: response.value.map { TrendObject.mastodon(tag: $0) }, + trends: response.value.map { TrendObject.mastodon(tag: $0) }.removingDuplicates(), timestamp: response.networkDate ) logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch trends \(response.value.count) ") diff --git a/TwidereX/Scene/Search/Trend/TrendViewModel+Diffable.swift b/TwidereX/Scene/Search/Trend/TrendViewModel+Diffable.swift index 1cb529d7..1897af57 100644 --- a/TwidereX/Scene/Search/Trend/TrendViewModel+Diffable.swift +++ b/TwidereX/Scene/Search/Trend/TrendViewModel+Diffable.swift @@ -31,7 +31,9 @@ extension TrendViewModel { .map { trendGroupRecords, trendGroupIndex in let trendItems: [SearchItem] = trendGroupRecords[trendGroupIndex] .flatMap { group in - return group.trends.map { .trend(trend: $0) } + return group.trends + .removingDuplicates() + .map { .trend(trend: $0) } } ?? [] return trendItems } From fac7dbc46ede3617775a7c0dd982a2dcaa2eab45 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 16 May 2023 17:10:58 +0800 Subject: [PATCH 091/128] chore: update version to 2.0.0 (130) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 36 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 4f6e87f4..685ee726 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 129 + 130 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 8810f44f..1d95e087 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 129 + 130 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 9840a86c..106f8f28 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3431,7 +3431,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3458,7 +3458,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3481,7 +3481,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3505,7 +3505,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3532,7 +3532,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3561,7 +3561,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3590,7 +3590,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3618,7 +3618,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3644,7 +3644,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3671,7 +3671,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3825,7 +3825,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3857,7 +3857,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3884,7 +3884,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3909,7 +3909,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3932,7 +3932,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3955,7 +3955,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3979,7 +3979,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -4006,7 +4006,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index 5f7a8ad6..ca828d48 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 129 + 130 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index 14cabbb5..09edc087 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 129 + 130 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index f33f97dc..45d40d23 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 129 + 130 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index f33f97dc..45d40d23 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 129 + 130 From b4ecefadc0ce4149aa259e628f2f449838a2cc88 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 16 May 2023 17:11:43 +0800 Subject: [PATCH 092/128] fix: restore v1 lookup for Home timeline --- .../Timeline/APIService+Timeline+Home.swift | 23 +++ .../Timeline/APIService+Timeline.swift | 174 +++++++++--------- 2 files changed, 110 insertions(+), 87 deletions(-) diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Home.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Home.swift index e88f1bcb..d0a93ac7 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Home.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Home.swift @@ -129,6 +129,18 @@ extension APIService { return TwitterHomeTimelineTaskResult.content(response) } } + // fetch lookup + group.addTask { + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch lookup") + let responses = await self.twitterBatchLookupResponses( + content: response.value, + authenticationContext: authenticationContext + ) + #if DEBUG + response.logRateLimit(category: "HomeLookup") + #endif + return TwitterHomeTimelineTaskResult.lookup(responses) + } case .lookup: break case .persist: @@ -159,6 +171,17 @@ extension APIService { let statusArray = statusRecords.compactMap { $0.object(in: managedObjectContext) } assert(statusArray.count == statusRecords.count) + // amend the v2 missing properties + if let me = me { + var batchLookupResponse = TwitterBatchLookupResponse() + for lookupResult in lookupResults { + for status in lookupResult.value { + batchLookupResponse.lookupDict[status.idStr] = status + } + } + batchLookupResponse.update(statuses: statusArray, me: me) + } + // locate anchor status let anchorStatus: TwitterStatus? = { guard let untilID = query.untilID else { return nil } diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline.swift index 127bf5ac..736a3909 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline.swift @@ -127,90 +127,90 @@ extension APIService { } // end func } -//// Fetch v1 API again to update v2 missing properies -//extension APIService { -// -// public struct TwitterBatchLookupResponse { -// let logger = Logger(subsystem: "APIService", category: "TwitterBatchLookupResponse") -// -// public var lookupDict: [Twitter.Entity.Tweet.ID: Twitter.Entity.Tweet] = [:] -// -// public func update(status: TwitterStatus, me: TwitterUser) { -// guard let lookupStatus = lookupDict[status.id] else { return } -// -// // like state -// lookupStatus.favorited.flatMap { -// status.update(isLike: $0, by: me) -// } -// // repost state -// lookupStatus.retweeted.flatMap { -// status.update(isRepost: $0, by: me) -// } -// // media -// if let twitterAttachments = lookupStatus.twitterAttachments { -// // gif -// let isGIF = twitterAttachments.contains(where: { $0.kind == .animatedGIF }) -// if isGIF { -// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fix GIF missing") -// status.update(attachments: twitterAttachments) -// return -// } -// // media missing bug -// if status.attachments.isEmpty { -// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fix media missing") -// status.update(attachments: twitterAttachments) -// return -// } -// } -// } -// -// public func update(statuses: [TwitterStatus], me: TwitterUser) { -// for status in statuses { -// update(status: status, me: me) -// } -// } -// } -// -// public func twitterBatchLookupResponses( -// statusIDs: [Twitter.Entity.Tweet.ID], -// authenticationContext: TwitterAuthenticationContext -// ) async -> [Twitter.Response.Content<[Twitter.Entity.Tweet]>] { -// let chunks = stride(from: 0, to: statusIDs.count, by: 100).map { -// statusIDs[$0.. Twitter.Response.Content<[Twitter.Entity.Tweet]>? in -// let query = Twitter.API.Lookup.LookupQuery(ids: Array(chunk)) -// let response = try? await Twitter.API.Lookup.tweets( -// session: self.session, -// query: query, -// authorization: authenticationContext.authorization -// ) -// return response -// } -// -// return _responses.compactMap { $0 } -// } -// -// public func twitterBatchLookupResponses( -// content: Twitter.API.V2.User.Timeline.HomeContent, -// authenticationContext: TwitterAuthenticationContext -// ) async -> [Twitter.Response.Content<[Twitter.Entity.Tweet]>] { -// let statusIDs: [Twitter.Entity.Tweet.ID] = { -// var ids: [Twitter.Entity.Tweet.ID] = [] -// ids.append(contentsOf: content.data?.map { $0.id } ?? []) -// ids.append(contentsOf: content.includes?.tweets?.map { $0.id } ?? []) -// return ids -// }() -// -// let responses = await twitterBatchLookupResponses( -// statusIDs: statusIDs, -// authenticationContext: authenticationContext -// ) -// -// return responses -// } -// +// Fetch v1 API again to update v2 missing properies +extension APIService { + + public struct TwitterBatchLookupResponse { + let logger = Logger(subsystem: "APIService", category: "TwitterBatchLookupResponse") + + public var lookupDict: [Twitter.Entity.Tweet.ID: Twitter.Entity.Tweet] = [:] + + public func update(status: TwitterStatus, me: TwitterUser) { + guard let lookupStatus = lookupDict[status.id] else { return } + + // like state + lookupStatus.favorited.flatMap { + status.update(isLike: $0, by: me) + } + // repost state + lookupStatus.retweeted.flatMap { + status.update(isRepost: $0, by: me) + } + // media + if let twitterAttachments = lookupStatus.twitterAttachments { + // gif + let isGIF = twitterAttachments.contains(where: { $0.kind == .animatedGIF }) + if isGIF { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fix GIF missing") + status.update(attachmentsTransient: twitterAttachments) + return + } + // media missing bug + if status.attachmentsTransient.isEmpty { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fix media missing") + status.update(attachmentsTransient: twitterAttachments) + return + } + } + } + + public func update(statuses: [TwitterStatus], me: TwitterUser) { + for status in statuses { + update(status: status, me: me) + } + } + } + + public func twitterBatchLookupResponses( + statusIDs: [Twitter.Entity.Tweet.ID], + authenticationContext: TwitterAuthenticationContext + ) async -> [Twitter.Response.Content<[Twitter.Entity.Tweet]>] { + let chunks = stride(from: 0, to: statusIDs.count, by: 100).map { + statusIDs[$0.. Twitter.Response.Content<[Twitter.Entity.Tweet]>? in + let query = Twitter.API.Lookup.LookupQuery(ids: Array(chunk)) + let response = try? await Twitter.API.Lookup.tweets( + session: self.session, + query: query, + authorization: authenticationContext.authorization + ) + return response + } + + return _responses.compactMap { $0 } + } + + public func twitterBatchLookupResponses( + content: Twitter.API.V2.User.Timeline.HomeContent, + authenticationContext: TwitterAuthenticationContext + ) async -> [Twitter.Response.Content<[Twitter.Entity.Tweet]>] { + let statusIDs: [Twitter.Entity.Tweet.ID] = { + var ids: [Twitter.Entity.Tweet.ID] = [] + ids.append(contentsOf: content.data?.map { $0.id } ?? []) + ids.append(contentsOf: content.includes?.tweets?.map { $0.id } ?? []) + return ids + }() + + let responses = await twitterBatchLookupResponses( + statusIDs: statusIDs, + authenticationContext: authenticationContext + ) + + return responses + } + // public func twitterBatchLookup( // statusIDs: [Twitter.Entity.Tweet.ID], // authenticationContext: TwitterAuthenticationContext @@ -229,9 +229,9 @@ extension APIService { // // return .init(lookupDict: lookupDict) // } -// -//} -// + +} + //// Fetch v2 API again to update v2 only properies //extension APIService { // From 1c8ee710273a19ee632bf927a1f2043e91ce7ea5 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 16 May 2023 17:12:44 +0800 Subject: [PATCH 093/128] fix: add missing like relationship when fetch like timeline --- .../APIService/Timeline/APIService+Timeline+Like.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Like.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Like.swift index a028b6d8..0119dfd0 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Like.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Like.swift @@ -55,7 +55,7 @@ extension APIService { let me = authenticationContext.authenticationRecord.object(in: managedObjectContext)?.user // persist [TwitterStatus] - _ = Persistence.Twitter.persist( + let results = Persistence.Twitter.persist( in: managedObjectContext, context: Persistence.Twitter.PersistContextV2( dictionary: dictionary, @@ -63,6 +63,11 @@ extension APIService { networkDate: response.networkDate ) ) + if let me = me, userID == me.id { + for result in results { + result.update(isLike: true, by: me) + } + } } // end try await managedObjectContext.performChanges return response From d5d0a3e64efb14f79b1ecd26046cf492a9cd68e0 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 16 May 2023 17:14:04 +0800 Subject: [PATCH 094/128] feat: update the TrendView --- .../Entity/Mastodon+Entity+History.swift | 6 + .../Entity/Mastodon+Entity+Tag.swift | 1 + .../Sources/TwidereUI/Content/TrendView.swift | 192 ++++++++++++++++++ .../Diffable/Misc/Search/SearchSection.swift | 74 ++++--- .../SavedSearchViewController.swift | 23 +++ .../SavedSearchViewModel+Diffable.swift | 4 +- .../SavedSearch/SavedSearchViewModel.swift | 2 + .../Search/Cell/TrendTableViewCell.swift | 181 +++++++++-------- .../Search/Search/SearchViewController.swift | 23 +++ .../Search/SearchViewModel+Diffable.swift | 4 +- .../Scene/Search/Search/SearchViewModel.swift | 2 + .../Search/Trend/TrendViewController.swift | 23 +++ .../Trend/TrendViewModel+Diffable.swift | 4 +- .../Scene/Search/Trend/TrendViewModel.swift | 2 + 14 files changed, 416 insertions(+), 125 deletions(-) create mode 100644 TwidereSDK/Sources/TwidereUI/Content/TrendView.swift diff --git a/TwidereSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+History.swift b/TwidereSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+History.swift index 4e3a6640..5374bd08 100644 --- a/TwidereSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+History.swift +++ b/TwidereSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+History.swift @@ -21,5 +21,11 @@ extension Mastodon.Entity { public let day: Date public let uses: String public let accounts: String + + public init(day: Date, uses: String, accounts: String) { + self.day = day + self.uses = uses + self.accounts = accounts + } } } diff --git a/TwidereSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Tag.swift b/TwidereSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Tag.swift index 8d8e0d81..5518b6e6 100644 --- a/TwidereSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Tag.swift +++ b/TwidereSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Tag.swift @@ -22,6 +22,7 @@ extension Mastodon.Entity { public let url: String public let history: [History]? + enum CodingKeys: String, CodingKey { case name case url diff --git a/TwidereSDK/Sources/TwidereUI/Content/TrendView.swift b/TwidereSDK/Sources/TwidereUI/Content/TrendView.swift new file mode 100644 index 00000000..e6cad025 --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Content/TrendView.swift @@ -0,0 +1,192 @@ +// +// SwiftUIView.swift +// +// +// Created by MainasuK on 2023/5/15. +// + +import SwiftUI +import Combine +import Meta +import MetaTextKit +import TwitterSDK +import MastodonSDK + +public struct TrendView: View { + + @ObservedObject public var viewModel: ViewModel + + public init(viewModel: ViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + HStack { + VStack(alignment: .leading, spacing: .zero) { + titleLabel + descriptionLabel + } + Spacer() + HStack { + chartDescriptionLabel + chartView + } + } + } +} + +extension TrendView { + var titleLabel: some View { + LabelRepresentable( + metaContent: viewModel.title, + textStyle: .searchTrendTitle, + setupLabel: { label in + label.setContentHuggingPriority(.defaultHigh, for: .horizontal) + label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + } + ) + .fixedSize(horizontal: false, vertical: true) + } + + var descriptionLabel: some View { + LabelRepresentable( + metaContent: viewModel.description, + textStyle: .searchTrendSubtitle, + setupLabel: { label in + label.setContentHuggingPriority(.defaultHigh, for: .horizontal) + label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + } + ) + .fixedSize(horizontal: false, vertical: true) + } + + var chartDescriptionLabel: some View { + Text(viewModel.chartDescription) + .font(.footnote) + .foregroundColor(.secondary) + } + + var chartView: some View { + EmptyView() +// Chart { +// +// } + } +} + +extension TrendView { + public class ViewModel: ObservableObject { + + @Published public var viewLayoutFrame = ViewLayoutFrame() + + // input + public let kind: Kind + public let title: MetaContent + public let description: MetaContent + public let chartDescription: String + + // output + @Published var historyData: [Mastodon.Entity.History]? + + public init( + kind: Kind, + title: MetaContent, + description: MetaContent, + chartDescription: String, + viewLayoutFramePublisher: Published.Publisher? + ) { + self.kind = kind + self.title = title + self.description = description + self.chartDescription = chartDescription + // end init + + viewLayoutFramePublisher?.assign(to: &$viewLayoutFrame) + } + + } +} + +extension TrendView.ViewModel { + public enum Kind: Hashable { + case twitter + case mastodon + } +} + +extension TrendView.ViewModel { + public convenience init( + object: TrendObject, + viewLayoutFramePublisher: Published.Publisher? + ) { + switch object { + case .twitter(let trend): + self.init(trend: trend, viewLayoutFramePublisher: viewLayoutFramePublisher) + case .mastodon(let tag): + self.init(tag: tag, viewLayoutFramePublisher: viewLayoutFramePublisher) + } + } + + public convenience init( + trend: Twitter.Entity.Trend, + viewLayoutFramePublisher: Published.Publisher? + ) { + self.init( + kind: .twitter, + title: PlaintextMetaContent(string: "\(trend.name)"), + description: PlaintextMetaContent(string: ""), + chartDescription: "", + viewLayoutFramePublisher: viewLayoutFramePublisher + ) + } + + public convenience init( + tag: Mastodon.Entity.Tag, + viewLayoutFramePublisher: Published.Publisher? + ) { + self.init( + kind: .mastodon, + title: Meta.convert(document: .plaintext(string: "#" + tag.name)), + description: PlaintextMetaContent(string: L10n.Scene.Trends.accounts(tag.talkingPeopleCount ?? 0)), + chartDescription: tag.history?.first?.uses ?? " ", + viewLayoutFramePublisher: viewLayoutFramePublisher + ) + + self.historyData = tag.history + } +} + + +#if DEBUG +extension TrendView.ViewModel { + convenience init(kind: Kind) { + self.init( + kind: kind, + title: PlaintextMetaContent(string: "#Name"), + description: PlaintextMetaContent(string: "500 people talking"), + chartDescription: "123", + viewLayoutFramePublisher: nil + ) + + historyData = [ + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 1), uses: "123", accounts: "123"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 2), uses: "123", accounts: "123"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 3), uses: "123", accounts: "123"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 4), uses: "123", accounts: "123"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 5), uses: "123", accounts: "123"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 6), uses: "123", accounts: "123"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 7), uses: "123", accounts: "123"), + ] + } +} +#endif + +#if canImport(SwiftUI) && DEBUG +struct TrendView_Previews: PreviewProvider { + static var previews: some View { + TrendView(viewModel: .init(kind: .twitter)) + TrendView(viewModel: .init(kind: .mastodon)) + } +} +#endif + diff --git a/TwidereX/Diffable/Misc/Search/SearchSection.swift b/TwidereX/Diffable/Misc/Search/SearchSection.swift index 34b30ca6..315934d9 100644 --- a/TwidereX/Diffable/Misc/Search/SearchSection.swift +++ b/TwidereX/Diffable/Misc/Search/SearchSection.swift @@ -7,8 +7,10 @@ // import UIKit +import SwiftUI import Meta import TwidereCore +import TwidereUI enum SearchSection: Hashable, CaseIterable { case history @@ -17,7 +19,9 @@ enum SearchSection: Hashable, CaseIterable { extension SearchSection { - struct Configuration { } + struct Configuration { + let viewLayoutFramePublisher: Published.Publisher? + } static func diffableDataSource( tableView: UITableView, @@ -41,7 +45,13 @@ extension SearchSection { return cell case .trend(let object): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TrendTableViewCell.self), for: indexPath) as! TrendTableViewCell - configure(cell: cell, object: object) + let trendViewModel = TrendView.ViewModel( + object: object, + viewLayoutFramePublisher: configuration.viewLayoutFramePublisher + ) + cell.contentConfiguration = UIHostingConfiguration { + TrendView(viewModel: trendViewModel) + } return cell case .loader: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell @@ -84,34 +94,34 @@ extension SearchSection { } } - private static func configure( - cell: TrendTableViewCell, - object: TrendObject - ) { - switch object { - case .twitter(let trend): - let metaContent = Meta.convert(document: .plaintext(string: trend.name)) - cell.primaryLabel.configure(content: metaContent) - cell.accessoryType = .disclosureIndicator - case .mastodon(let tag): - let metaContent = Meta.convert(document: .plaintext(string: "#" + tag.name)) - - cell.primaryLabel.configure(content: metaContent) - cell.secondaryLabel.text = L10n.Scene.Trends.accounts(tag.talkingPeopleCount ?? 0) - cell.setSecondaryLabelDisplay() - - cell.supplementaryLabel.text = tag.history?.first?.uses ?? " " - cell.setSupplementaryLabelDisplay() - - cell.lineChartView.data = (tag.history ?? []) - .sorted(by: { $0.day < $1.day }) // latest last - .map { entry in - guard let point = Int(entry.accounts) else { - return .zero - } - return CGFloat(point) - } - cell.setLineChartViewDisplay() - } - } +// private static func configure( +// cell: TrendTableViewCell, +// object: TrendObject +// ) { +// switch object { +// case .twitter(let trend): +// let metaContent = Meta.convert(document: .plaintext(string: trend.name)) +// cell.primaryLabel.configure(content: metaContent) +// cell.accessoryType = .disclosureIndicator +// case .mastodon(let tag): +// let metaContent = Meta.convert(document: .plaintext(string: "#" + tag.name)) +// +// cell.primaryLabel.configure(content: metaContent) +// cell.secondaryLabel.text = L10n.Scene.Trends.accounts(tag.talkingPeopleCount ?? 0) +// cell.setSecondaryLabelDisplay() +// +// cell.supplementaryLabel.text = tag.history?.first?.uses ?? " " +// cell.setSupplementaryLabelDisplay() +// +// cell.lineChartView.data = (tag.history ?? []) +// .sorted(by: { $0.day < $1.day }) // latest last +// .map { entry in +// guard let point = Int(entry.accounts) else { +// return .zero +// } +// return CGFloat(point) +// } +// cell.setLineChartViewDisplay() +// } +// } } diff --git a/TwidereX/Scene/Search/SavedSearch/SavedSearchViewController.swift b/TwidereX/Scene/Search/SavedSearch/SavedSearchViewController.swift index 92af6493..f0a4b06a 100644 --- a/TwidereX/Scene/Search/SavedSearch/SavedSearchViewController.swift +++ b/TwidereX/Scene/Search/SavedSearch/SavedSearchViewController.swift @@ -62,6 +62,29 @@ extension SavedSearchViewController { tableView.deselectRow(with: transitionCoordinator, animated: animated) } + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + viewModel.viewLayoutFrame.update(view: view) + } + + override func viewSafeAreaInsetsDidChange() { + super.viewSafeAreaInsetsDidChange() + + viewModel.viewLayoutFrame.update(view: view) + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + coordinator.animate {[weak self] _ in + guard let self = self else { return } + self.viewModel.viewLayoutFrame.update(view: self.view) + } completion: { _ in + // do nothing + } + } + } // MARK: - AuthContextProvider diff --git a/TwidereX/Scene/Search/SavedSearch/SavedSearchViewModel+Diffable.swift b/TwidereX/Scene/Search/SavedSearch/SavedSearchViewModel+Diffable.swift index 98b252c0..dfcdc305 100644 --- a/TwidereX/Scene/Search/SavedSearch/SavedSearchViewModel+Diffable.swift +++ b/TwidereX/Scene/Search/SavedSearch/SavedSearchViewModel+Diffable.swift @@ -16,7 +16,9 @@ extension SavedSearchViewModel { diffableDataSource = SearchSection.diffableDataSource( tableView: tableView, context: context, - configuration: SearchSection.Configuration() + configuration: SearchSection.Configuration( + viewLayoutFramePublisher: $viewLayoutFrame + ) ) var snapshot = NSDiffableDataSourceSnapshot() diff --git a/TwidereX/Scene/Search/SavedSearch/SavedSearchViewModel.swift b/TwidereX/Scene/Search/SavedSearch/SavedSearchViewModel.swift index afa96c39..2252bc6e 100644 --- a/TwidereX/Scene/Search/SavedSearch/SavedSearchViewModel.swift +++ b/TwidereX/Scene/Search/SavedSearch/SavedSearchViewModel.swift @@ -22,6 +22,8 @@ final class SavedSearchViewModel { let authContext: AuthContext let savedSearchService: SavedSearchService let savedSearchFetchedResultController: SavedSearchFetchedResultController + + @Published public var viewLayoutFrame = ViewLayoutFrame() // output var diffableDataSource: UITableViewDiffableDataSource? diff --git a/TwidereX/Scene/Search/Search/Cell/TrendTableViewCell.swift b/TwidereX/Scene/Search/Search/Cell/TrendTableViewCell.swift index 7aca31b3..988919cb 100644 --- a/TwidereX/Scene/Search/Search/Cell/TrendTableViewCell.swift +++ b/TwidereX/Scene/Search/Search/Cell/TrendTableViewCell.swift @@ -13,35 +13,36 @@ import TwidereCore final class TrendTableViewCell: UITableViewCell { - let container: UIStackView = { - let stackView = UIStackView() - stackView.axis = .horizontal - stackView.spacing = 16 - return stackView - }() - - let infoContainer: UIStackView = { - let stackView = UIStackView() - stackView.axis = .vertical - return stackView - }() - - let lineChartContainer: UIStackView = { - let stackView = UIStackView() - stackView.axis = .vertical - return stackView - }() - - let primaryLabel = MetaLabel(style: .searchTrendTitle) - let secondaryLabel = PlainLabel(style: .searchTrendSubtitle) - let supplementaryLabel = PlainLabel(style: .searchTrendCount) - let lineChartView = LineChartView() +// let container: UIStackView = { +// let stackView = UIStackView() +// stackView.axis = .horizontal +// stackView.spacing = 16 +// return stackView +// }() +// +// let infoContainer: UIStackView = { +// let stackView = UIStackView() +// stackView.axis = .vertical +// return stackView +// }() +// +// let lineChartContainer: UIStackView = { +// let stackView = UIStackView() +// stackView.axis = .vertical +// return stackView +// }() +// +// let primaryLabel = MetaLabel(style: .searchTrendTitle) +// let secondaryLabel = PlainLabel(style: .searchTrendSubtitle) +// let supplementaryLabel = PlainLabel(style: .searchTrendCount) +// let lineChartView = LineChartView() override func prepareForReuse() { super.prepareForReuse() - accessoryType = .none - resetDisplay() + contentConfiguration = nil +// accessoryType = .none +// resetDisplay() } override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -59,72 +60,72 @@ final class TrendTableViewCell: UITableViewCell { extension TrendTableViewCell { private func _init() { - container.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(container) - NSLayoutConstraint.activate([ - container.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 11), - container.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - container.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), - contentView.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: 11), - ]) - - // container: H - [ info container | padding | supplementary | line chart container ] - container.addArrangedSubview(infoContainer) - - // info container: V - [ primary | secondary ] - infoContainer.addArrangedSubview(primaryLabel) - infoContainer.addArrangedSubview(secondaryLabel) - - // padding - let padding = UIView() - container.addArrangedSubview(padding) - - // supplementary - container.addArrangedSubview(supplementaryLabel) - supplementaryLabel.setContentHuggingPriority(.required - 1, for: .horizontal) - - // line chart - container.addArrangedSubview(lineChartContainer) - - let lineChartViewTopPadding = UIView() - let lineChartViewBottomPadding = UIView() - lineChartViewTopPadding.translatesAutoresizingMaskIntoConstraints = false - lineChartViewBottomPadding.translatesAutoresizingMaskIntoConstraints = false - lineChartView.translatesAutoresizingMaskIntoConstraints = false - lineChartContainer.addArrangedSubview(lineChartViewTopPadding) - lineChartContainer.addArrangedSubview(lineChartView) - lineChartContainer.addArrangedSubview(lineChartViewBottomPadding) - NSLayoutConstraint.activate([ - lineChartView.widthAnchor.constraint(equalToConstant: 66), - lineChartView.heightAnchor.constraint(equalToConstant: 27), - lineChartViewTopPadding.heightAnchor.constraint(equalTo: lineChartViewBottomPadding.heightAnchor), - ]) - - primaryLabel.isUserInteractionEnabled = false - - resetDisplay() +// container.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(container) +// NSLayoutConstraint.activate([ +// container.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 11), +// container.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), +// container.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), +// contentView.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: 11), +// ]) +// +// // container: H - [ info container | padding | supplementary | line chart container ] +// container.addArrangedSubview(infoContainer) +// +// // info container: V - [ primary | secondary ] +// infoContainer.addArrangedSubview(primaryLabel) +// infoContainer.addArrangedSubview(secondaryLabel) +// +// // padding +// let padding = UIView() +// container.addArrangedSubview(padding) +// +// // supplementary +// container.addArrangedSubview(supplementaryLabel) +// supplementaryLabel.setContentHuggingPriority(.required - 1, for: .horizontal) +// +// // line chart +// container.addArrangedSubview(lineChartContainer) +// +// let lineChartViewTopPadding = UIView() +// let lineChartViewBottomPadding = UIView() +// lineChartViewTopPadding.translatesAutoresizingMaskIntoConstraints = false +// lineChartViewBottomPadding.translatesAutoresizingMaskIntoConstraints = false +// lineChartView.translatesAutoresizingMaskIntoConstraints = false +// lineChartContainer.addArrangedSubview(lineChartViewTopPadding) +// lineChartContainer.addArrangedSubview(lineChartView) +// lineChartContainer.addArrangedSubview(lineChartViewBottomPadding) +// NSLayoutConstraint.activate([ +// lineChartView.widthAnchor.constraint(equalToConstant: 66), +// lineChartView.heightAnchor.constraint(equalToConstant: 27), +// lineChartViewTopPadding.heightAnchor.constraint(equalTo: lineChartViewBottomPadding.heightAnchor), +// ]) +// +// primaryLabel.isUserInteractionEnabled = false +// +// resetDisplay() } } -extension TrendTableViewCell { - - func resetDisplay() { - secondaryLabel.isHidden = true - supplementaryLabel.isHidden = true - lineChartContainer.isHidden = true - } - - func setSecondaryLabelDisplay() { - secondaryLabel.isHidden = false - } - - func setSupplementaryLabelDisplay() { - supplementaryLabel.isHidden = false - } - - func setLineChartViewDisplay() { - lineChartContainer.isHidden = false - } - -} +//extension TrendTableViewCell { +// +// func resetDisplay() { +// secondaryLabel.isHidden = true +// supplementaryLabel.isHidden = true +// lineChartContainer.isHidden = true +// } +// +// func setSecondaryLabelDisplay() { +// secondaryLabel.isHidden = false +// } +// +// func setSupplementaryLabelDisplay() { +// supplementaryLabel.isHidden = false +// } +// +// func setLineChartViewDisplay() { +// lineChartContainer.isHidden = false +// } +// +//} diff --git a/TwidereX/Scene/Search/Search/SearchViewController.swift b/TwidereX/Scene/Search/Search/SearchViewController.swift index 3d9853bf..30c52b4a 100644 --- a/TwidereX/Scene/Search/Search/SearchViewController.swift +++ b/TwidereX/Scene/Search/Search/SearchViewController.swift @@ -167,6 +167,29 @@ extension SearchViewController { viewModel.viewDidAppear.send() } + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + viewModel.viewLayoutFrame.update(view: view) + } + + override func viewSafeAreaInsetsDidChange() { + super.viewSafeAreaInsetsDidChange() + + viewModel.viewLayoutFrame.update(view: view) + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + coordinator.animate {[weak self] _ in + guard let self = self else { return } + self.viewModel.viewLayoutFrame.update(view: self.view) + } completion: { _ in + // do nothing + } + } + } extension SearchViewController { diff --git a/TwidereX/Scene/Search/Search/SearchViewModel+Diffable.swift b/TwidereX/Scene/Search/Search/SearchViewModel+Diffable.swift index 55602440..1774973f 100644 --- a/TwidereX/Scene/Search/Search/SearchViewModel+Diffable.swift +++ b/TwidereX/Scene/Search/Search/SearchViewModel+Diffable.swift @@ -17,7 +17,9 @@ extension SearchViewModel { diffableDataSource = SearchSection.diffableDataSource( tableView: tableView, context: context, - configuration: SearchSection.Configuration() + configuration: SearchSection.Configuration( + viewLayoutFramePublisher: $viewLayoutFrame + ) ) var snapshot = NSDiffableDataSourceSnapshot() diff --git a/TwidereX/Scene/Search/Search/SearchViewModel.swift b/TwidereX/Scene/Search/Search/SearchViewModel.swift index 9a89fcd0..2233dbcf 100644 --- a/TwidereX/Scene/Search/Search/SearchViewModel.swift +++ b/TwidereX/Scene/Search/Search/SearchViewModel.swift @@ -26,6 +26,8 @@ final class SearchViewModel { let trendViewModel: TrendViewModel let viewDidAppear = PassthroughSubject() + @Published public var viewLayoutFrame = ViewLayoutFrame() + // output var diffableDataSource: UITableViewDiffableDataSource? @Published var savedSearchTexts = Set() diff --git a/TwidereX/Scene/Search/Trend/TrendViewController.swift b/TwidereX/Scene/Search/Trend/TrendViewController.swift index cc3b33a0..605dd07d 100644 --- a/TwidereX/Scene/Search/Trend/TrendViewController.swift +++ b/TwidereX/Scene/Search/Trend/TrendViewController.swift @@ -68,6 +68,29 @@ extension TrendViewController { tableView.deselectRow(with: transitionCoordinator, animated: animated) } + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + viewModel.viewLayoutFrame.update(view: view) + } + + override func viewSafeAreaInsetsDidChange() { + super.viewSafeAreaInsetsDidChange() + + viewModel.viewLayoutFrame.update(view: view) + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + coordinator.animate {[weak self] _ in + guard let self = self else { return } + self.viewModel.viewLayoutFrame.update(view: self.view) + } completion: { _ in + // do nothing + } + } + } // MARK: - AuthContextProvider diff --git a/TwidereX/Scene/Search/Trend/TrendViewModel+Diffable.swift b/TwidereX/Scene/Search/Trend/TrendViewModel+Diffable.swift index 1897af57..675b69ea 100644 --- a/TwidereX/Scene/Search/Trend/TrendViewModel+Diffable.swift +++ b/TwidereX/Scene/Search/Trend/TrendViewModel+Diffable.swift @@ -16,7 +16,9 @@ extension TrendViewModel { diffableDataSource = SearchSection.diffableDataSource( tableView: tableView, context: context, - configuration: SearchSection.Configuration() + configuration: SearchSection.Configuration( + viewLayoutFramePublisher: $viewLayoutFrame + ) ) var snapshot = NSDiffableDataSourceSnapshot() diff --git a/TwidereX/Scene/Search/Trend/TrendViewModel.swift b/TwidereX/Scene/Search/Trend/TrendViewModel.swift index ad08cfb1..4e5d6c0f 100644 --- a/TwidereX/Scene/Search/Trend/TrendViewModel.swift +++ b/TwidereX/Scene/Search/Trend/TrendViewModel.swift @@ -24,6 +24,8 @@ final class TrendViewModel: ObservableObject { let trendService: TrendService @Published var trendGroupIndex: TrendService.TrendGroupIndex = .none @Published var searchText = "" + + @Published public var viewLayoutFrame = ViewLayoutFrame() // output var diffableDataSource: UITableViewDiffableDataSource? From 69762a0d2fc2dbb3740198e3d9506c8847995f18 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 16 May 2023 17:14:57 +0800 Subject: [PATCH 095/128] feat: use transient property to lazy load the JSON data from objects --- .../.xccurrentversion | 2 +- .../CoreDataStack 8.xcdatamodel/contents | 322 ++++++++++++++++++ .../Entity/Twitter/TwitterStatus.swift | 202 +++++++---- .../CoreDataStack/TwitterStatus.swift | 6 +- .../Model/Status/StatusObject.swift | 2 +- .../Persistence+TwitterStatus+V2.swift | 10 +- .../Twitter/Persistence+TwitterStatus.swift | 8 +- .../Content/MediaView+ViewModel.swift | 2 +- .../Content/StatusView+ViewModel.swift | 4 +- .../ComposeContentViewModel.swift | 4 +- .../Facade/DataSourceFacade+Profile.swift | 2 +- ...meTimelineViewController+DebugAction.swift | 6 +- 12 files changed, 476 insertions(+), 94 deletions(-) create mode 100644 TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 8.xcdatamodel/contents diff --git a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/.xccurrentversion b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/.xccurrentversion index eaddb16f..c7412cd8 100644 --- a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/.xccurrentversion +++ b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - CoreDataStack 7.xcdatamodel + CoreDataStack 8.xcdatamodel diff --git a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 8.xcdatamodel/contents b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 8.xcdatamodel/contents new file mode 100644 index 00000000..1459ebff --- /dev/null +++ b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 8.xcdatamodel/contents @@ -0,0 +1,322 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift index 213195a5..dbc50a0f 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift @@ -83,107 +83,167 @@ final public class TwitterStatus: NSManagedObject { } extension TwitterStatus { + @NSManaged private var attachments: Data? + @NSManaged private var primitiveAttachmentsTransient: [TwitterAttachment]? // sourcery: autoUpdatableObject - @objc public var attachments: [TwitterAttachment] { + @objc public private(set) var attachmentsTransient: [TwitterAttachment] { get { - let keyPath = #keyPath(TwitterStatus.attachments) + let keyPath = #keyPath(attachmentsTransient) willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data + let attachments = primitiveAttachmentsTransient didAccessValue(forKey: keyPath) - do { - guard let data = _data else { return [] } - let attachments = try JSONDecoder().decode([TwitterAttachment].self, from: data) + if let attachments = attachments { return attachments - } catch { - assertionFailure(error.localizedDescription) - return [] + } else { + do { + let _data = self.attachments + guard let data = _data else { + primitiveAttachmentsTransient = [] + return [] + } + let attachments = try JSONDecoder().decode([TwitterAttachment].self, from: data) + primitiveAttachmentsTransient = attachments + return attachments + } catch { + assertionFailure(error.localizedDescription) + return [] + } } } set { - let keyPath = #keyPath(TwitterStatus.attachments) - let data = try? JSONEncoder().encode(newValue) - willChangeValue(forKey: keyPath) - setPrimitiveValue(data, forKey: keyPath) - didChangeValue(forKey: keyPath) + let keyPath = #keyPath(attachmentsTransient) + do { + let data = try JSONEncoder().encode(newValue) + willChangeValue(forKey: keyPath) + primitiveAttachmentsTransient = newValue + didChangeValue(forKey: keyPath) + attachments = data + } catch { + assertionFailure() + } } } + @NSManaged private var location: Data? + @NSManaged private var primitiveLocationTransient: TwitterLocation? // sourcery: autoUpdatableObject - @objc public var location: TwitterLocation? { + @objc public private(set) var locationTransient: TwitterLocation? { get { - let keyPath = #keyPath(TwitterStatus.location) + let keyPath = #keyPath(locationTransient) willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data + let location = primitiveLocationTransient didAccessValue(forKey: keyPath) - do { - guard let data = _data else { return nil } - let location = try JSONDecoder().decode(TwitterLocation.self, from: data) + if let location = location { return location - } catch { - assertionFailure(error.localizedDescription) - return nil + } else { + do { + let _data = self.location + guard let data = _data else { + primitiveLocationTransient = nil + return nil + } + let location = try JSONDecoder().decode(TwitterLocation.self, from: data) + primitiveLocationTransient = location + return location + } catch { + assertionFailure(error.localizedDescription) + return nil + } } } set { - let keyPath = #keyPath(TwitterStatus.location) - let data = try? JSONEncoder().encode(newValue) - willChangeValue(forKey: keyPath) - setPrimitiveValue(data, forKey: keyPath) - didChangeValue(forKey: keyPath) + let keyPath = #keyPath(locationTransient) + do { + let data = try newValue.flatMap { try JSONEncoder().encode($0) } + willChangeValue(forKey: keyPath) + primitiveLocationTransient = newValue + didChangeValue(forKey: keyPath) + location = data + } catch { + assertionFailure() + } } } + @NSManaged private var entities: Data? + @NSManaged private var primitiveEntitiesTransient: TwitterEntity? // sourcery: autoUpdatableObject - @objc public var entities: TwitterEntity? { + @objc public private(set) var entitiesTransient: TwitterEntity? { get { - let keyPath = #keyPath(TwitterStatus.entities) + let keyPath = #keyPath(entitiesTransient) willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data + let entities = primitiveEntitiesTransient didAccessValue(forKey: keyPath) - do { - guard let data = _data else { return nil } - let entities = try JSONDecoder().decode(TwitterEntity.self, from: data) + if let entities = entities { return entities - } catch { - assertionFailure(error.localizedDescription) - return nil + } else { + do { + let _data = self.entities + guard let data = _data else { + primitiveEntitiesTransient = nil + return nil + } + let entities = try JSONDecoder().decode(TwitterEntity.self, from: data) + primitiveEntitiesTransient = entities + return entities + } catch { + assertionFailure(error.localizedDescription) + return nil + } } } set { - let keyPath = #keyPath(TwitterStatus.entities) - willChangeValue(forKey: keyPath) - if let newValue = newValue { - let data = try? JSONEncoder().encode(newValue) - setPrimitiveValue(data, forKey: keyPath) - } else { - setPrimitiveValue(nil, forKey: keyPath) + let keyPath = #keyPath(entitiesTransient) + do { + let data = try newValue.flatMap { try JSONEncoder().encode($0) } + willChangeValue(forKey: keyPath) + primitiveEntitiesTransient = newValue + didChangeValue(forKey: keyPath) + entities = data + } catch { + assertionFailure() } - didChangeValue(forKey: keyPath) } } + @NSManaged private var replySettings: Data? + @NSManaged private var primitiveReplySettingsTransient: TwitterReplySettings? // sourcery: autoUpdatableObject - @objc public var replySettings: TwitterReplySettings? { + @objc public private(set) var replySettingsTransient: TwitterReplySettings? { get { - let keyPath = #keyPath(TwitterStatus.replySettings) + let keyPath = #keyPath(replySettingsTransient) willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data + let replySettings = primitiveReplySettingsTransient didAccessValue(forKey: keyPath) - do { - guard let data = _data else { return nil } - let replySettings = try JSONDecoder().decode(TwitterReplySettings.self, from: data) + if let replySettings = replySettings { return replySettings - } catch { - assertionFailure(error.localizedDescription) - return nil + } else { + do { + let _data = self.replySettings + guard let data = _data else { + primitiveReplySettingsTransient = nil + return nil + } + let replySettings = try JSONDecoder().decode(TwitterReplySettings.self, from: data) + primitiveReplySettingsTransient = replySettings + return replySettings + } catch { + assertionFailure(error.localizedDescription) + return nil + } } } set { - let keyPath = #keyPath(TwitterStatus.replySettings) - let data = try? JSONEncoder().encode(newValue) - willChangeValue(forKey: keyPath) - setPrimitiveValue(data, forKey: keyPath) - didChangeValue(forKey: keyPath) + let keyPath = #keyPath(replySettingsTransient) + do { + let data = try newValue.flatMap { try JSONEncoder().encode($0) } + willChangeValue(forKey: keyPath) + primitiveReplySettingsTransient = newValue + didChangeValue(forKey: keyPath) + replySettings = data + } catch { + assertionFailure() + } } } } @@ -412,24 +472,24 @@ extension TwitterStatus: AutoUpdatableObject { self.replyTo = replyTo } } - public func update(attachments: [TwitterAttachment]) { - if self.attachments != attachments { - self.attachments = attachments + public func update(attachmentsTransient: [TwitterAttachment]) { + if self.attachmentsTransient != attachmentsTransient { + self.attachmentsTransient = attachmentsTransient } } - public func update(location: TwitterLocation?) { - if self.location != location { - self.location = location + public func update(locationTransient: TwitterLocation?) { + if self.locationTransient != locationTransient { + self.locationTransient = locationTransient } } - public func update(entities: TwitterEntity?) { - if self.entities != entities { - self.entities = entities + public func update(entitiesTransient: TwitterEntity?) { + if self.entitiesTransient != entitiesTransient { + self.entitiesTransient = entitiesTransient } } - public func update(replySettings: TwitterReplySettings?) { - if self.replySettings != replySettings { - self.replySettings = replySettings + public func update(replySettingsTransient: TwitterReplySettings?) { + if self.replySettingsTransient != replySettingsTransient { + self.replySettingsTransient = replySettingsTransient } } // sourcery:end diff --git a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterStatus.swift b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterStatus.swift index b350b999..90197b0a 100644 --- a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterStatus.swift +++ b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterStatus.swift @@ -16,7 +16,7 @@ extension TwitterStatus { public var displayText: String { var text = self.text - for url in entities?.urls ?? [] { + for url in entitiesTransient?.urls ?? [] { let shortURL = url.url guard let displayURL = url.displayURL, let expandedURL = url.expandedURL @@ -43,7 +43,7 @@ extension TwitterStatus { } public var urlEntities: [TwitterContent.URLEntity] { - let results = entities?.urls?.map { entity in + let results = entitiesTransient?.urls?.map { entity in TwitterContent.URLEntity(url: entity.url, expandedURL: entity.expandedURL, displayURL: entity.displayURL) } return results ?? [] @@ -53,7 +53,7 @@ extension TwitterStatus { extension TwitterStatus { /// The tweet more then 240 characters public var hasMore: Bool { - for url in entities?.urls ?? [] { + for url in entitiesTransient?.urls ?? [] { guard text.localizedCaseInsensitiveContains("… " + url.url) else { continue } guard let expandedURL = url.expandedURL else { continue } guard expandedURL.hasPrefix("https://twitter.com/") else { continue } diff --git a/TwidereSDK/Sources/TwidereCore/Model/Status/StatusObject.swift b/TwidereSDK/Sources/TwidereCore/Model/Status/StatusObject.swift index e341565d..da28157b 100644 --- a/TwidereSDK/Sources/TwidereCore/Model/Status/StatusObject.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/Status/StatusObject.swift @@ -65,7 +65,7 @@ extension StatusObject { switch self { case .twitter(let status): let status = status.repost ?? status - return status.attachments.map { .twitter($0) } + return status.attachmentsTransient.map { .twitter($0) } case .mastodon(let status): return status.attachments.map { .mastodon($0) } } diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterStatus+V2.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterStatus+V2.swift index fab0c575..b46b6e84 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterStatus+V2.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterStatus+V2.swift @@ -274,14 +274,14 @@ extension Persistence.TwitterStatus { context: PersistContextV2 ) { // entities - status.update(entities: TwitterEntity(entity: context.entity.status.entities)) + status.update(entitiesTransient: TwitterEntity(entity: context.entity.status.entities)) // replySettings (v2 only) let _replySettings = context.entity.status.replySettings.flatMap { TwitterReplySettings(value: $0.rawValue) } if let replySettings = _replySettings { - status.update(replySettings: replySettings) + status.update(replySettingsTransient: replySettings) } // conversationID (v2 only) @@ -301,20 +301,20 @@ extension Persistence.TwitterStatus { // do not update video & GIFV attachments except isEmpty let isVideo = media.contains(where: { $0.type == TwitterAttachment.Kind.animatedGIF.rawValue || $0.type == TwitterAttachment.Kind.video.rawValue }) if isVideo { - let isEmpty = status.attachments.isEmpty + let isEmpty = status.attachmentsTransient.isEmpty if !isEmpty { return } } let attachments = media.compactMap { $0.twitterAttachment } - status.update(attachments: attachments) + status.update(attachmentsTransient: attachments) } // place (not stable: geo may erased) context.dictionary.place(for: context.entity.status) .flatMap { place in - status.update(location: place.twitterLocation) + status.update(locationTransient: place.twitterLocation) } } diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterStatus.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterStatus.swift index f5c40b13..4761af5c 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterStatus.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterStatus.swift @@ -203,15 +203,15 @@ extension Persistence.TwitterStatus { context: PersistContext ) { // prefer use V2 entities. only update entities when not exist - if status.entities == nil { - status.update(entities: TwitterEntity( + if status.entitiesTransient == nil { + status.update(entitiesTransient: TwitterEntity( entity: context.entity.entities, extendedEntity: context.entity.extendedEntities )) } - context.entity.twitterAttachments.flatMap { status.update(attachments: $0) } - context.entity.twitterLocation.flatMap { status.update(location:$0) } + context.entity.twitterAttachments.flatMap { status.update(attachmentsTransient: $0) } + context.entity.twitterLocation.flatMap { status.update(locationTransient: $0) } // update relationship if let me = context.me { diff --git a/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift index 931df219..3254e529 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift @@ -251,7 +251,7 @@ extension MediaView.ViewModel { } public static func viewModels(from status: TwitterStatus) -> [MediaView.ViewModel] { - return status.attachments.map { attachment -> MediaView.ViewModel in + return status.attachmentsTransient.map { attachment -> MediaView.ViewModel in MediaView.ViewModel( mediaKind: { switch attachment.kind { diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index 6357e160..c02f4510 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -1100,7 +1100,7 @@ extension StatusView.ViewModel { } // reply settings - replySettingBannerViewModel = status.replySettings + replySettingBannerViewModel = status.replySettingsTransient .flatMap { object in Twitter.Entity.V2.Tweet.ReplySettings(rawValue: object.value) } .flatMap { replaySettings in ReplySettingBannerView.ViewModel( @@ -1171,7 +1171,7 @@ extension StatusView.ViewModel { } // location - location = status.location?.fullName + location = status.locationTransient?.fullName // metric switch kind { diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift index 9d0b1609..2d999258 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -213,7 +213,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { ) self.secondaryMentionPickItems = { var items: [MentionPickViewModel.Item] = [] - for mention in status.entities?.mentions ?? [] { + for mention in status.entitiesTransient?.mentions ?? [] { let username = mention.username let item = MentionPickViewModel.Item.twitterUser( username: username, @@ -376,7 +376,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { } excludeUsernames.insert(author.username) - for mention in status.entities?.mentions ?? [] { + for mention in status.entitiesTransient?.mentions ?? [] { guard !excludeUsernames.contains(mention.username) else { continue } usernames.append(mention.username) } diff --git a/TwidereX/Protocol/Facade/DataSourceFacade+Profile.swift b/TwidereX/Protocol/Facade/DataSourceFacade+Profile.swift index f413c261..75269411 100644 --- a/TwidereX/Protocol/Facade/DataSourceFacade+Profile.swift +++ b/TwidereX/Protocol/Facade/DataSourceFacade+Profile.swift @@ -72,7 +72,7 @@ extension DataSourceFacade { switch object { case .twitter(let status): let status = status.repost ?? status - let mentions = status.entities?.mentions ?? [] + let mentions = status.entitiesTransient?.mentions ?? [] let _userID: TwitterUser.ID? = mentions.first(where: { $0.username == mention })?.id if let userID = _userID { diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift index b878b79f..44917b6b 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift @@ -335,13 +335,13 @@ extension HomeTimelineViewController { case .quote: return status.quote != nil case .gif: - return status.attachments.contains(where: { attachment in attachment.kind == .animatedGIF }) + return status.attachmentsTransient.contains(where: { attachment in attachment.kind == .animatedGIF }) case .video: - return status.attachments.contains(where: { attachment in attachment.kind == .video }) + return status.attachmentsTransient.contains(where: { attachment in attachment.kind == .video }) case .poll: return status.poll != nil case .location: - return status.location != nil + return status.locationTransient != nil case .followsYouAuthor: guard case let .twitter(authenticationContext) = authenticationContext else { return false } guard let me = authenticationContext.authenticationRecord.object(in: AppContext.shared.managedObjectContext)?.user else { return false } From ee6b1a0e1b25466f8e6ba48af6e4754969a263a3 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 16 May 2023 18:27:40 +0800 Subject: [PATCH 096/128] feat: make others entity object's JSON data access via transient --- .../CoreDataStack 8.xcdatamodel/contents | 8 + .../Entity/Mastodon/MastodonList.swift | 26 --- .../MastodonNotificationSubscription.swift | 60 ++++-- .../Entity/Mastodon/MastodonStatus.swift | 194 ++++++++++++------ .../Entity/Mastodon/MastodonUser.swift | 128 ++++++++---- .../Entity/Twitter/TwitterStatus.swift | 22 +- .../Entity/Twitter/TwitterUser.swift | 116 +++++++---- .../AutoGenerateProperty.generated.swift | 36 ++++ .../AutoGenerateRelationship.generated.swift | 25 +++ .../AutoUpdatableObject.generated.swift | 24 +++ .../CoreDataStack/MastodonUser.swift | 4 +- .../Extension/CoreDataStack/TwitterUser.swift | 4 +- .../Notification/NotificationHeaderInfo.swift | 2 +- .../Model/Status/StatusObject.swift | 4 +- .../Extension/MastodonStatus+Property.swift | 6 +- .../Extension/MastodonUser+Property.swift | 4 +- .../Twitter/Persistence+TwitterUser+V2.swift | 4 +- .../Twitter/Persistence+TwitterUser.swift | 4 +- ...cationService+NotificationSubscriber.swift | 2 +- .../Content/MediaView+ViewModel.swift | 2 +- .../TwidereUI/Content/PollOptionView.swift | 2 +- .../Content/StatusView+ViewModel.swift | 6 +- .../ComposeContentViewModel.swift | 2 +- .../Facade/DataSourceFacade+Profile.swift | 4 +- .../View/ProfileHeaderView+ViewModel.swift | 13 +- .../MastodonNotificationSectionView.swift | 2 +- ...MastodonNotificationSectionViewModel.swift | 4 +- 27 files changed, 464 insertions(+), 244 deletions(-) create mode 100644 TwidereSDK/Sources/CoreDataStack/Generated/AutoGenerateProperty.generated.swift create mode 100644 TwidereSDK/Sources/CoreDataStack/Generated/AutoGenerateRelationship.generated.swift create mode 100644 TwidereSDK/Sources/CoreDataStack/Generated/AutoUpdatableObject.generated.swift diff --git a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 8.xcdatamodel/contents b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 8.xcdatamodel/contents index 1459ebff..7e268763 100644 --- a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 8.xcdatamodel/contents +++ b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 8.xcdatamodel/contents @@ -80,6 +80,7 @@ + @@ -119,10 +120,12 @@ + + @@ -130,6 +133,7 @@ + @@ -160,7 +164,9 @@ + + @@ -286,6 +292,7 @@ + @@ -301,6 +308,7 @@ + diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonList.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonList.swift index 46978fcb..fb0d8e17 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonList.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonList.swift @@ -169,30 +169,4 @@ extension MastodonList: AutoUpdatableObject { } } // sourcery:end - -// public func update(`private`: Bool) { -// if self.`private` != `private` { -// self.`private` = `private` -// } -// } -// public func update(memberCount: Int64) { -// if self.memberCount != memberCount { -// self.memberCount = memberCount -// } -// } -// public func update(followerCount: Int64) { -// if self.followerCount != followerCount { -// self.followerCount = followerCount -// } -// } -// public func update(theDescription: String?) { -// if self.theDescription != theDescription { -// self.theDescription = theDescription -// } -// } -// public func update(createdAt: Date?) { -// if self.createdAt != createdAt { -// self.createdAt = createdAt -// } -// } } diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonNotificationSubscription.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonNotificationSubscription.swift index 1f1c50a1..120ca42f 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonNotificationSubscription.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonNotificationSubscription.swift @@ -48,28 +48,44 @@ final public class MastodonNotificationSubscription: NSManagedObject { } extension MastodonNotificationSubscription { + @NSManaged private var mentionPreference: Data? + @NSManaged private var primitiveMentionPreferenceTransient: MentionPreference? // sourcery: autoUpdatableObject, autoGenerateProperty - @objc public var mentionPreference: MentionPreference { + @objc public private(set) var mentionPreferenceTransient: MentionPreference { get { - let keyPath = #keyPath(MastodonNotificationSubscription.mentionPreference) + let keyPath = #keyPath(mentionPreferenceTransient) willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data + let mentionPreference = primitiveMentionPreferenceTransient didAccessValue(forKey: keyPath) - do { - guard let data = _data, !data.isEmpty else { return MentionPreference() } - let mentionPreference = try JSONDecoder().decode(MentionPreference.self, from: data) + if let mentionPreference = mentionPreference { return mentionPreference - } catch { - assertionFailure(error.localizedDescription) - return MentionPreference() + } else { + do { + let _data = self.mentionPreference + guard let data = _data, !data.isEmpty else { + primitiveMentionPreferenceTransient = MentionPreference() + return MentionPreference() + } + let mentionPreference = try JSONDecoder().decode(MentionPreference.self, from: data) + primitiveMentionPreferenceTransient = mentionPreference + return mentionPreference + } catch { + assertionFailure(error.localizedDescription) + return MentionPreference() + } } } set { - let keyPath = #keyPath(MastodonNotificationSubscription.mentionPreference) - let data = try? JSONEncoder().encode(newValue) - willChangeValue(forKey: keyPath) - setPrimitiveValue(data, forKey: keyPath) - didChangeValue(forKey: keyPath) + let keyPath = #keyPath(mentionPreferenceTransient) + do { + let data = try JSONEncoder().encode(newValue) + mentionPreference = data + willChangeValue(forKey: keyPath) + primitiveMentionPreferenceTransient = newValue + didChangeValue(forKey: keyPath) + } catch { + assertionFailure() + } } } } @@ -124,7 +140,7 @@ extension MastodonNotificationSubscription: AutoGenerateProperty { public let poll: Bool public let createdAt: Date public let updatedAt: Date - public let mentionPreference: MentionPreference + public let mentionPreferenceTransient: MentionPreference public init( id: ID?, @@ -140,7 +156,7 @@ extension MastodonNotificationSubscription: AutoGenerateProperty { poll: Bool, createdAt: Date, updatedAt: Date, - mentionPreference: MentionPreference + mentionPreferenceTransient: MentionPreference ) { self.id = id self.domain = domain @@ -155,7 +171,7 @@ extension MastodonNotificationSubscription: AutoGenerateProperty { self.poll = poll self.createdAt = createdAt self.updatedAt = updatedAt - self.mentionPreference = mentionPreference + self.mentionPreferenceTransient = mentionPreferenceTransient } } @@ -173,7 +189,7 @@ extension MastodonNotificationSubscription: AutoGenerateProperty { self.poll = property.poll self.createdAt = property.createdAt self.updatedAt = property.updatedAt - self.mentionPreference = property.mentionPreference + self.mentionPreferenceTransient = property.mentionPreferenceTransient } public func update(property: Property) { @@ -190,7 +206,7 @@ extension MastodonNotificationSubscription: AutoGenerateProperty { update(poll: property.poll) update(createdAt: property.createdAt) update(updatedAt: property.updatedAt) - update(mentionPreference: property.mentionPreference) + update(mentionPreferenceTransient: property.mentionPreferenceTransient) } // sourcery:end @@ -290,9 +306,9 @@ extension MastodonNotificationSubscription: AutoUpdatableObject { self.updatedAt = updatedAt } } - public func update(mentionPreference: MentionPreference) { - if self.mentionPreference != mentionPreference { - self.mentionPreference = mentionPreference + public func update(mentionPreferenceTransient: MentionPreference) { + if self.mentionPreferenceTransient != mentionPreferenceTransient { + self.mentionPreferenceTransient = mentionPreferenceTransient } } diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonStatus.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonStatus.swift index 41f48716..953d87e9 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonStatus.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonStatus.swift @@ -94,78 +94,138 @@ final public class MastodonStatus: NSManagedObject { } extension MastodonStatus { + @NSManaged private var attachments: Data? + @NSManaged private var primitiveAttachmentsTransient: [MastodonAttachment]? // sourcery: autoUpdatableObject, autoGenerateProperty - @objc public var attachments: [MastodonAttachment] { + @objc public private(set) var attachmentsTransient: [MastodonAttachment] { get { - let keyPath = #keyPath(MastodonStatus.attachments) + let keyPath = #keyPath(attachmentsTransient) willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data + let attachments = primitiveAttachmentsTransient didAccessValue(forKey: keyPath) - do { - guard let data = _data, !data.isEmpty else { return [] } - let attachments = try JSONDecoder().decode([MastodonAttachment].self, from: data) + if let attachments = attachments { return attachments - } catch { - assertionFailure(error.localizedDescription) - return [] + } else { + do { + let _data = self.attachments + guard let data = _data, !data.isEmpty else { + primitiveAttachmentsTransient = [] + return [] + } + let attachments = try JSONDecoder().decode([MastodonAttachment].self, from: data) + primitiveAttachmentsTransient = attachments + return attachments + } catch { + assertionFailure(error.localizedDescription) + return [] + } } } set { - let keyPath = #keyPath(MastodonStatus.attachments) - let data = try? JSONEncoder().encode(newValue) - willChangeValue(forKey: keyPath) - setPrimitiveValue(data, forKey: keyPath) - didChangeValue(forKey: keyPath) + let keyPath = #keyPath(attachmentsTransient) + do { + if newValue.isEmpty { + attachments = nil + } else { + let data = try JSONEncoder().encode(newValue) + attachments = data + } + willChangeValue(forKey: keyPath) + primitiveAttachmentsTransient = newValue + didChangeValue(forKey: keyPath) + } catch { + assertionFailure() + } } } + @NSManaged private var emojis: Data? + @NSManaged private var primitiveEmojisTransient: [MastodonEmoji]? // sourcery: autoUpdatableObject, autoGenerateProperty - @objc public var emojis: [MastodonEmoji] { + @objc public private(set) var emojisTransient: [MastodonEmoji] { get { - let keyPath = #keyPath(MastodonStatus.emojis) + let keyPath = #keyPath(emojisTransient) willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data + let emojis = primitiveEmojisTransient didAccessValue(forKey: keyPath) - do { - guard let data = _data else { return [] } - let emojis = try JSONDecoder().decode([MastodonEmoji].self, from: data) + if let emojis = emojis { return emojis - } catch { - assertionFailure(error.localizedDescription) - return [] + } else { + do { + let _data = self.emojis + guard let data = _data, !data.isEmpty else { + primitiveEmojisTransient = [] + return [] + } + let emojis = try JSONDecoder().decode([MastodonEmoji].self, from: data) + primitiveEmojisTransient = emojis + return emojis + } catch { + assertionFailure(error.localizedDescription) + return [] + } } } set { - let keyPath = #keyPath(MastodonStatus.emojis) - let data = try? JSONEncoder().encode(newValue) - willChangeValue(forKey: keyPath) - setPrimitiveValue(data, forKey: keyPath) - didChangeValue(forKey: keyPath) + let keyPath = #keyPath(emojisTransient) + do { + if newValue.isEmpty { + emojis = nil + } else { + let data = try JSONEncoder().encode(newValue) + emojis = data + } + willChangeValue(forKey: keyPath) + primitiveEmojisTransient = newValue + didChangeValue(forKey: keyPath) + } catch { + assertionFailure() + } } } + @NSManaged private var mentions: Data? + @NSManaged private var primitiveMentionsTransient: [MastodonMention]? // sourcery: autoUpdatableObject, autoGenerateProperty - @objc public var mentions: [MastodonMention] { + @objc public private(set) var mentionsTransient: [MastodonMention] { get { - let keyPath = #keyPath(MastodonStatus.mentions) + let keyPath = #keyPath(mentionsTransient) willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data + let mentions = primitiveMentionsTransient didAccessValue(forKey: keyPath) - do { - guard let data = _data else { return [] } - let emojis = try JSONDecoder().decode([MastodonMention].self, from: data) - return emojis - } catch { - assertionFailure(error.localizedDescription) - return [] + if let mentions = mentions { + return mentions + } else { + do { + let _data = self.mentions + guard let data = _data, !data.isEmpty else { + primitiveMentionsTransient = [] + return [] + } + let mentions = try JSONDecoder().decode([MastodonMention].self, from: data) + primitiveMentionsTransient = mentions + return mentions + } catch { + assertionFailure(error.localizedDescription) + return [] + } } } set { - let keyPath = #keyPath(MastodonStatus.mentions) - let data = try? JSONEncoder().encode(newValue) - willChangeValue(forKey: keyPath) - setPrimitiveValue(data, forKey: keyPath) - didChangeValue(forKey: keyPath) + let keyPath = #keyPath(mentionsTransient) + do { + if newValue.isEmpty { + mentions = nil + } else { + let data = try JSONEncoder().encode(newValue) + mentions = data + } + willChangeValue(forKey: keyPath) + primitiveMentionsTransient = newValue + didChangeValue(forKey: keyPath) + } catch { + assertionFailure() + } } } } @@ -249,9 +309,9 @@ extension MastodonStatus: AutoGenerateProperty { public let replyToUserID: MastodonStatus.ID? public let createdAt: Date public let updatedAt: Date - public let attachments: [MastodonAttachment] - public let emojis: [MastodonEmoji] - public let mentions: [MastodonMention] + public let attachmentsTransient: [MastodonAttachment] + public let emojisTransient: [MastodonEmoji] + public let mentionsTransient: [MastodonMention] public init( id: ID, @@ -272,9 +332,9 @@ extension MastodonStatus: AutoGenerateProperty { replyToUserID: MastodonStatus.ID?, createdAt: Date, updatedAt: Date, - attachments: [MastodonAttachment], - emojis: [MastodonEmoji], - mentions: [MastodonMention] + attachmentsTransient: [MastodonAttachment], + emojisTransient: [MastodonEmoji], + mentionsTransient: [MastodonMention] ) { self.id = id self.domain = domain @@ -294,9 +354,9 @@ extension MastodonStatus: AutoGenerateProperty { self.replyToUserID = replyToUserID self.createdAt = createdAt self.updatedAt = updatedAt - self.attachments = attachments - self.emojis = emojis - self.mentions = mentions + self.attachmentsTransient = attachmentsTransient + self.emojisTransient = emojisTransient + self.mentionsTransient = mentionsTransient } } @@ -319,9 +379,9 @@ extension MastodonStatus: AutoGenerateProperty { self.replyToUserID = property.replyToUserID self.createdAt = property.createdAt self.updatedAt = property.updatedAt - self.attachments = property.attachments - self.emojis = property.emojis - self.mentions = property.mentions + self.attachmentsTransient = property.attachmentsTransient + self.emojisTransient = property.emojisTransient + self.mentionsTransient = property.mentionsTransient } public func update(property: Property) { @@ -340,9 +400,9 @@ extension MastodonStatus: AutoGenerateProperty { update(replyToUserID: property.replyToUserID) update(createdAt: property.createdAt) update(updatedAt: property.updatedAt) - update(attachments: property.attachments) - update(emojis: property.emojis) - update(mentions: property.mentions) + update(attachmentsTransient: property.attachmentsTransient) + update(emojisTransient: property.emojisTransient) + update(mentionsTransient: property.mentionsTransient) } // sourcery:end } @@ -468,19 +528,19 @@ extension MastodonStatus: AutoUpdatableObject { self.updatedAt = updatedAt } } - public func update(attachments: [MastodonAttachment]) { - if self.attachments != attachments { - self.attachments = attachments + public func update(attachmentsTransient: [MastodonAttachment]) { + if self.attachmentsTransient != attachmentsTransient { + self.attachmentsTransient = attachmentsTransient } } - public func update(emojis: [MastodonEmoji]) { - if self.emojis != emojis { - self.emojis = emojis + public func update(emojisTransient: [MastodonEmoji]) { + if self.emojisTransient != emojisTransient { + self.emojisTransient = emojisTransient } } - public func update(mentions: [MastodonMention]) { - if self.mentions != mentions { - self.mentions = mentions + public func update(mentionsTransient: [MastodonMention]) { + if self.mentionsTransient != mentionsTransient { + self.mentionsTransient = mentionsTransient } } // sourcery:end diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift index 3940e6c9..1bcc78fc 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift @@ -97,53 +97,93 @@ final public class MastodonUser: NSManagedObject { } extension MastodonUser { + @NSManaged private var emojis: Data? + @NSManaged private var primitiveEmojisTransient: [MastodonEmoji]? // sourcery: autoUpdatableObject, autoGenerateProperty - @objc public var emojis: [MastodonEmoji] { + @objc public private(set) var emojisTransient: [MastodonEmoji] { get { - let keyPath = #keyPath(MastodonUser.emojis) + let keyPath = #keyPath(emojisTransient) willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data + let emojis = primitiveEmojisTransient didAccessValue(forKey: keyPath) - do { - guard let data = _data else { return [] } - let emojis = try JSONDecoder().decode([MastodonEmoji].self, from: data) + if let emojis = emojis { return emojis - } catch { - assertionFailure(error.localizedDescription) - return [] + } else { + do { + let _data = self.emojis + guard let data = _data, !data.isEmpty else { + primitiveEmojisTransient = [] + return [] + } + let emojis = try JSONDecoder().decode([MastodonEmoji].self, from: data) + primitiveEmojisTransient = emojis + return emojis + } catch { + assertionFailure(error.localizedDescription) + return [] + } } } set { - let keyPath = #keyPath(MastodonUser.emojis) - let data = try? JSONEncoder().encode(newValue) - willChangeValue(forKey: keyPath) - setPrimitiveValue(data, forKey: keyPath) - didChangeValue(forKey: keyPath) + let keyPath = #keyPath(emojisTransient) + do { + if newValue.isEmpty { + emojis = nil + } else { + let data = try JSONEncoder().encode(newValue) + emojis = data + } + willChangeValue(forKey: keyPath) + primitiveEmojisTransient = newValue + didChangeValue(forKey: keyPath) + } catch { + assertionFailure() + } } } + @NSManaged private var fields: Data? + @NSManaged private var primitiveFieldsTransient: [MastodonField]? // sourcery: autoUpdatableObject, autoGenerateProperty - @objc public var fields: [MastodonField] { + @objc public private(set) var fieldsTransient: [MastodonField] { get { - let keyPath = #keyPath(MastodonUser.fields) + let keyPath = #keyPath(fieldsTransient) willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data + let fields = primitiveFieldsTransient didAccessValue(forKey: keyPath) - do { - guard let data = _data else { return [] } - let fields = try JSONDecoder().decode([MastodonField].self, from: data) + if let fields = fields { return fields - } catch { - assertionFailure(error.localizedDescription) - return [] + } else { + do { + let _data = self.fields + guard let data = _data, !data.isEmpty else { + primitiveFieldsTransient = [] + return [] + } + let fields = try JSONDecoder().decode([MastodonField].self, from: data) + primitiveFieldsTransient = fields + return fields + } catch { + assertionFailure(error.localizedDescription) + return [] + } } } set { - let keyPath = #keyPath(MastodonUser.fields) - let data = try? JSONEncoder().encode(newValue) - willChangeValue(forKey: keyPath) - setPrimitiveValue(data, forKey: keyPath) - didChangeValue(forKey: keyPath) + let keyPath = #keyPath(fieldsTransient) + do { + if newValue.isEmpty { + fields = nil + } else { + let data = try JSONEncoder().encode(newValue) + fields = data + } + willChangeValue(forKey: keyPath) + primitiveFieldsTransient = newValue + didChangeValue(forKey: keyPath) + } catch { + assertionFailure() + } } } } @@ -235,8 +275,8 @@ extension MastodonUser: AutoGenerateProperty { public let suspended: Bool public let createdAt: Date public let updatedAt: Date - public let emojis: [MastodonEmoji] - public let fields: [MastodonField] + public let emojisTransient: [MastodonEmoji] + public let fieldsTransient: [MastodonField] public init( domain: String, @@ -258,8 +298,8 @@ extension MastodonUser: AutoGenerateProperty { suspended: Bool, createdAt: Date, updatedAt: Date, - emojis: [MastodonEmoji], - fields: [MastodonField] + emojisTransient: [MastodonEmoji], + fieldsTransient: [MastodonField] ) { self.domain = domain self.id = id @@ -280,8 +320,8 @@ extension MastodonUser: AutoGenerateProperty { self.suspended = suspended self.createdAt = createdAt self.updatedAt = updatedAt - self.emojis = emojis - self.fields = fields + self.emojisTransient = emojisTransient + self.fieldsTransient = fieldsTransient } } @@ -305,8 +345,8 @@ extension MastodonUser: AutoGenerateProperty { self.suspended = property.suspended self.createdAt = property.createdAt self.updatedAt = property.updatedAt - self.emojis = property.emojis - self.fields = property.fields + self.emojisTransient = property.emojisTransient + self.fieldsTransient = property.fieldsTransient } public func update(property: Property) { @@ -327,8 +367,8 @@ extension MastodonUser: AutoGenerateProperty { update(suspended: property.suspended) update(createdAt: property.createdAt) update(updatedAt: property.updatedAt) - update(emojis: property.emojis) - update(fields: property.fields) + update(emojisTransient: property.emojisTransient) + update(fieldsTransient: property.fieldsTransient) } // sourcery:end } @@ -424,14 +464,14 @@ extension MastodonUser: AutoUpdatableObject { self.updatedAt = updatedAt } } - public func update(emojis: [MastodonEmoji]) { - if self.emojis != emojis { - self.emojis = emojis + public func update(emojisTransient: [MastodonEmoji]) { + if self.emojisTransient != emojisTransient { + self.emojisTransient = emojisTransient } } - public func update(fields: [MastodonField]) { - if self.fields != fields { - self.fields = fields + public func update(fieldsTransient: [MastodonField]) { + if self.fieldsTransient != fieldsTransient { + self.fieldsTransient = fieldsTransient } } // sourcery:end diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift index dbc50a0f..e83d7811 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift @@ -97,7 +97,7 @@ extension TwitterStatus { } else { do { let _data = self.attachments - guard let data = _data else { + guard let data = _data, !data.isEmpty else { primitiveAttachmentsTransient = [] return [] } @@ -113,11 +113,15 @@ extension TwitterStatus { set { let keyPath = #keyPath(attachmentsTransient) do { - let data = try JSONEncoder().encode(newValue) + if newValue.isEmpty { + attachments = nil + } else { + let data = try JSONEncoder().encode(newValue) + attachments = data + } willChangeValue(forKey: keyPath) primitiveAttachmentsTransient = newValue didChangeValue(forKey: keyPath) - attachments = data } catch { assertionFailure() } @@ -138,7 +142,7 @@ extension TwitterStatus { } else { do { let _data = self.location - guard let data = _data else { + guard let data = _data, !data.isEmpty else { primitiveLocationTransient = nil return nil } @@ -155,10 +159,10 @@ extension TwitterStatus { let keyPath = #keyPath(locationTransient) do { let data = try newValue.flatMap { try JSONEncoder().encode($0) } + location = data willChangeValue(forKey: keyPath) primitiveLocationTransient = newValue didChangeValue(forKey: keyPath) - location = data } catch { assertionFailure() } @@ -179,7 +183,7 @@ extension TwitterStatus { } else { do { let _data = self.entities - guard let data = _data else { + guard let data = _data, !data.isEmpty else { primitiveEntitiesTransient = nil return nil } @@ -196,10 +200,10 @@ extension TwitterStatus { let keyPath = #keyPath(entitiesTransient) do { let data = try newValue.flatMap { try JSONEncoder().encode($0) } + entities = data willChangeValue(forKey: keyPath) primitiveEntitiesTransient = newValue didChangeValue(forKey: keyPath) - entities = data } catch { assertionFailure() } @@ -220,7 +224,7 @@ extension TwitterStatus { } else { do { let _data = self.replySettings - guard let data = _data else { + guard let data = _data, !data.isEmpty else { primitiveReplySettingsTransient = nil return nil } @@ -237,10 +241,10 @@ extension TwitterStatus { let keyPath = #keyPath(replySettingsTransient) do { let data = try newValue.flatMap { try JSONEncoder().encode($0) } + replySettings = data willChangeValue(forKey: keyPath) primitiveReplySettingsTransient = newValue didChangeValue(forKey: keyPath) - replySettings = data } catch { assertionFailure() } diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift index 0627bdf7..c5a5fb13 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift @@ -79,61 +79,93 @@ public final class TwitterUser: NSManagedObject { } extension TwitterUser { + @NSManaged private var bioEntities: Data? + @NSManaged private var primitiveBioEntitiesTransient: TwitterEntity? // sourcery: autoUpdatableObject - @objc public var bioEntities: TwitterEntity? { + @objc public private(set) var bioEntitiesTransient: TwitterEntity? { get { - let keyPath = #keyPath(TwitterUser.bioEntities) + let keyPath = #keyPath(bioEntitiesTransient) willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data + let bioEntities = primitiveBioEntitiesTransient didAccessValue(forKey: keyPath) - do { - guard let data = _data else { return nil } - let entities = try JSONDecoder().decode(TwitterEntity.self, from: data) - return entities - } catch { - assertionFailure(error.localizedDescription) - return nil + if let bioEntities = bioEntities { + return bioEntities + } else { + do { + let _data = self.bioEntities + guard let data = _data, !data.isEmpty else { + primitiveBioEntitiesTransient = nil + return nil + } + let entities = try JSONDecoder().decode(TwitterEntity.self, from: data) + primitiveBioEntitiesTransient = entities + return entities + } catch { + assertionFailure(error.localizedDescription) + return nil + } } } set { - let keyPath = #keyPath(TwitterUser.bioEntities) - willChangeValue(forKey: keyPath) - if let newValue = newValue { - let data = try? JSONEncoder().encode(newValue) - setPrimitiveValue(data, forKey: keyPath) - } else { - setPrimitiveValue(nil, forKey: keyPath) + let keyPath = #keyPath(bioEntitiesTransient) + do { + if let newValue = newValue { + let data = try JSONEncoder().encode(newValue) + bioEntities = data + } else { + bioEntities = nil + } + willChangeValue(forKey: keyPath) + primitiveBioEntitiesTransient = newValue + didChangeValue(forKey: keyPath) + } catch { + assertionFailure() } - didChangeValue(forKey: keyPath) } } + @NSManaged private var urlEntities: Data? + @NSManaged private var primitiveUrlEntitiesTransient: TwitterEntity? // sourcery: autoUpdatableObject - @objc public var urlEntities: TwitterEntity? { + @objc public private(set) var urlEntitiesTransient: TwitterEntity? { get { - let keyPath = #keyPath(TwitterUser.urlEntities) + let keyPath = #keyPath(urlEntitiesTransient) willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data + let urlEntities = primitiveUrlEntitiesTransient didAccessValue(forKey: keyPath) - do { - guard let data = _data else { return nil } - let entities = try JSONDecoder().decode(TwitterEntity.self, from: data) - return entities - } catch { - assertionFailure(error.localizedDescription) - return nil + if let urlEntities = urlEntities { + return urlEntities + } else { + do { + let _data = self.urlEntities + guard let data = _data, !data.isEmpty else { + primitiveUrlEntitiesTransient = nil + return nil + } + let entities = try JSONDecoder().decode(TwitterEntity.self, from: data) + primitiveUrlEntitiesTransient = entities + return entities + } catch { + assertionFailure(error.localizedDescription) + return nil + } } } set { - let keyPath = #keyPath(TwitterUser.urlEntities) - willChangeValue(forKey: keyPath) - if let newValue = newValue { - let data = try? JSONEncoder().encode(newValue) - setPrimitiveValue(data, forKey: keyPath) - } else { - setPrimitiveValue(nil, forKey: keyPath) + let keyPath = #keyPath(urlEntitiesTransient) + do { + if let newValue = newValue { + let data = try JSONEncoder().encode(newValue) + urlEntities = data + } else { + urlEntities = nil + } + willChangeValue(forKey: keyPath) + primitiveUrlEntitiesTransient = newValue + didChangeValue(forKey: keyPath) + } catch { + assertionFailure() } - didChangeValue(forKey: keyPath) } } } @@ -357,14 +389,14 @@ extension TwitterUser: AutoUpdatableObject { self.updatedAt = updatedAt } } - public func update(bioEntities: TwitterEntity?) { - if self.bioEntities != bioEntities { - self.bioEntities = bioEntities + public func update(bioEntitiesTransient: TwitterEntity?) { + if self.bioEntitiesTransient != bioEntitiesTransient { + self.bioEntitiesTransient = bioEntitiesTransient } } - public func update(urlEntities: TwitterEntity?) { - if self.urlEntities != urlEntities { - self.urlEntities = urlEntities + public func update(urlEntitiesTransient: TwitterEntity?) { + if self.urlEntitiesTransient != urlEntitiesTransient { + self.urlEntitiesTransient = urlEntitiesTransient } } // sourcery:end diff --git a/TwidereSDK/Sources/CoreDataStack/Generated/AutoGenerateProperty.generated.swift b/TwidereSDK/Sources/CoreDataStack/Generated/AutoGenerateProperty.generated.swift new file mode 100644 index 00000000..eed4b7e0 --- /dev/null +++ b/TwidereSDK/Sources/CoreDataStack/Generated/AutoGenerateProperty.generated.swift @@ -0,0 +1,36 @@ +// Generated using Sourcery 1.8.1 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT + + + + + + + + + +// sourcery:inline:MastodonStatus.MastodonStatus.AutoGenerateProperty + +// Generated using Sourcery +// DO NOT EDIT +public struct Property { + + public init( + ) { + } +} + +public func configure(property: Property) { +} + +public func update(property: Property) { +} +// sourcery:end + + + + + + + + diff --git a/TwidereSDK/Sources/CoreDataStack/Generated/AutoGenerateRelationship.generated.swift b/TwidereSDK/Sources/CoreDataStack/Generated/AutoGenerateRelationship.generated.swift new file mode 100644 index 00000000..29fddaeb --- /dev/null +++ b/TwidereSDK/Sources/CoreDataStack/Generated/AutoGenerateRelationship.generated.swift @@ -0,0 +1,25 @@ +// Generated using Sourcery 1.8.1 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT + + + + +// sourcery:inline:MastodonStatus.MastodonStatus.AutoGenerateRelationship + +// Generated using Sourcery +// DO NOT EDIT +public struct Relationship { + + public init( + ) { + } +} + +public func configure(relationship: Relationship) { +} + +// sourcery:end + + + + diff --git a/TwidereSDK/Sources/CoreDataStack/Generated/AutoUpdatableObject.generated.swift b/TwidereSDK/Sources/CoreDataStack/Generated/AutoUpdatableObject.generated.swift new file mode 100644 index 00000000..48c23e3d --- /dev/null +++ b/TwidereSDK/Sources/CoreDataStack/Generated/AutoUpdatableObject.generated.swift @@ -0,0 +1,24 @@ +// Generated using Sourcery 1.8.1 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT + + + + + + + + + +// sourcery:inline:MastodonStatus.MastodonStatus.AutoUpdatableObject + +// Generated using Sourcery +// DO NOT EDIT +// sourcery:end + + + + + + + + diff --git a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/MastodonUser.swift b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/MastodonUser.swift index 1da6c9ca..383a4af6 100644 --- a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/MastodonUser.swift +++ b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/MastodonUser.swift @@ -32,7 +32,7 @@ extension MastodonUser { extension MastodonUser { public var nameMetaContent: MastodonMetaContent? { do { - let content = MastodonContent(content: name, emojis: emojis.asDictionary) + let content = MastodonContent(content: name, emojis: emojisTransient.asDictionary) let metaContent = try MastodonMetaContent.convert(document: content) return metaContent } catch { @@ -44,7 +44,7 @@ extension MastodonUser { public var bioMetaContent: MastodonMetaContent? { guard let note = note else { return nil } do { - let content = MastodonContent(content: note, emojis: emojis.asDictionary) + let content = MastodonContent(content: note, emojis: emojisTransient.asDictionary) let metaContent = try MastodonMetaContent.convert(document: content) return metaContent } catch { diff --git a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterUser.swift b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterUser.swift index a6bcab08..ba4b022c 100644 --- a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterUser.swift +++ b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterUser.swift @@ -81,7 +81,7 @@ extension String { extension TwitterUser { public var bioURLEntities: [TwitterContent.URLEntity] { - let results = bioEntities?.urls?.map { entity in + let results = bioEntitiesTransient?.urls?.map { entity in TwitterContent.URLEntity(url: entity.url, expandedURL: entity.expandedURL, displayURL: entity.displayURL) } return results ?? [] @@ -99,7 +99,7 @@ extension TwitterUser { } public var urlEntity: [TwitterContent.URLEntity] { - let results = urlEntities?.urls?.map { entity in + let results = urlEntitiesTransient?.urls?.map { entity in TwitterContent.URLEntity(url: entity.url, expandedURL: entity.expandedURL, displayURL: entity.displayURL) } return results ?? [] diff --git a/TwidereSDK/Sources/TwidereCore/Model/Notification/NotificationHeaderInfo.swift b/TwidereSDK/Sources/TwidereCore/Model/Notification/NotificationHeaderInfo.swift index f7c05754..216b7ec6 100644 --- a/TwidereSDK/Sources/TwidereCore/Model/Notification/NotificationHeaderInfo.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/Notification/NotificationHeaderInfo.swift @@ -101,7 +101,7 @@ extension NotificationHeaderInfo { // assertionFailure() return nil } - let content = MastodonContent(content: text, emojis: user.emojis.asDictionary) + let content = MastodonContent(content: text, emojis: user.emojisTransient.asDictionary) return Meta.convert(document: .mastodon(content: content)) } } diff --git a/TwidereSDK/Sources/TwidereCore/Model/Status/StatusObject.swift b/TwidereSDK/Sources/TwidereCore/Model/Status/StatusObject.swift index da28157b..1cf6af9e 100644 --- a/TwidereSDK/Sources/TwidereCore/Model/Status/StatusObject.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/Status/StatusObject.swift @@ -43,7 +43,7 @@ extension StatusObject { return status.displayText case .mastodon(let status): do { - let content = MastodonContent(content: status.content, emojis: status.emojis.asDictionary) + let content = MastodonContent(content: status.content, emojis: status.emojisTransient.asDictionary) let metaContent = try MastodonMetaContent.convert(document: content) return metaContent.original } catch { @@ -67,7 +67,7 @@ extension StatusObject { let status = status.repost ?? status return status.attachmentsTransient.map { .twitter($0) } case .mastodon(let status): - return status.attachments.map { .mastodon($0) } + return status.attachmentsTransient.map { .mastodon($0) } } } } diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Mastodon/Extension/MastodonStatus+Property.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Mastodon/Extension/MastodonStatus+Property.swift index 0f651d20..764555f7 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Mastodon/Extension/MastodonStatus+Property.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Mastodon/Extension/MastodonStatus+Property.swift @@ -37,9 +37,9 @@ extension MastodonStatus.Property { replyToUserID: entity.inReplyToAccountID, createdAt: entity.createdAt, updatedAt: networkDate, - attachments: entity.mastodonAttachments, - emojis: entity.mastodonEmojis, - mentions: entity.mastodonMentions + attachmentsTransient: entity.mastodonAttachments, + emojisTransient: entity.mastodonEmojis, + mentionsTransient: entity.mastodonMentions ) } } diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Mastodon/Extension/MastodonUser+Property.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Mastodon/Extension/MastodonUser+Property.swift index 199e3bc5..55df28cf 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Mastodon/Extension/MastodonUser+Property.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Mastodon/Extension/MastodonUser+Property.swift @@ -32,8 +32,8 @@ extension MastodonUser.Property { suspended: entity.suspended ?? false, createdAt: entity.createdAt, updatedAt: networkDate, - emojis: entity.mastodonEmojis, - fields: entity.mastodonFields + emojisTransient: entity.mastodonEmojis, + fieldsTransient: entity.mastodonFields ) } } diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser+V2.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser+V2.swift index 5feac52a..4ace8978 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser+V2.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser+V2.swift @@ -102,8 +102,8 @@ extension Persistence.TwitterUser { twitterUser user: TwitterUser, context: PersistContextV2 ) { - user.update(bioEntities: TwitterEntity(entity: context.entity.entities?.description)) - user.update(urlEntities: TwitterEntity(entity: context.entity.entities?.url)) + user.update(bioEntitiesTransient: TwitterEntity(entity: context.entity.entities?.description)) + user.update(urlEntitiesTransient: TwitterEntity(entity: context.entity.entities?.url)) // V2 entity not contains relationship flags } diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser.swift index df83510a..0c01df9c 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser.swift @@ -115,8 +115,8 @@ extension Persistence.TwitterUser { context: PersistContext ) { user.update(profileBannerURL: context.entity.profileBannerURL) - user.update(bioEntities: TwitterEntity(entity: context.entity.entities?.description)) - user.update(urlEntities: TwitterEntity(entity: context.entity.entities?.url)) + user.update(bioEntitiesTransient: TwitterEntity(entity: context.entity.entities?.description)) + user.update(urlEntitiesTransient: TwitterEntity(entity: context.entity.entities?.url)) // relationship if let me = context.me { diff --git a/TwidereSDK/Sources/TwidereCore/Service/NotificationService+NotificationSubscriber.swift b/TwidereSDK/Sources/TwidereCore/Service/NotificationService+NotificationSubscriber.swift index bc10c883..f0ed7187 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/NotificationService+NotificationSubscriber.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/NotificationService+NotificationSubscriber.swift @@ -204,7 +204,7 @@ extension NotificationService.MastodonNotificationSubscriber { poll: true, createdAt: now, updatedAt: now, - mentionPreference: MastodonNotificationSubscription.MentionPreference() + mentionPreferenceTransient: MastodonNotificationSubscription.MentionPreference() ), relationship: .init(authentication: authentication) ) diff --git a/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift index 3254e529..be129b78 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift @@ -271,7 +271,7 @@ extension MediaView.ViewModel { } public static func viewModels(from status: MastodonStatus) -> [MediaView.ViewModel] { - return status.attachments.map { attachment -> MediaView.ViewModel in + return status.attachmentsTransient.map { attachment -> MediaView.ViewModel in MediaView.ViewModel( mediaKind: { switch attachment.kind { diff --git a/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift b/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift index f942b359..d3af2b19 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift @@ -178,7 +178,7 @@ extension PollOptionView { index = Int(option.index) content = { do { - let content = MastodonContent(content: option.title, emojis: option.poll.status.emojis.asDictionary) + let content = MastodonContent(content: option.title, emojis: option.poll.status.emojisTransient.asDictionary) let metaContent = try MastodonMetaContent.convert(document: content) return metaContent } catch { diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index c02f4510..5fdd7763 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -1262,7 +1262,7 @@ extension StatusView.ViewModel { label: { let name = status.author.name let userRepostText = L10n.Common.Controls.Status.userBoosted(name) - let text = MastodonContent(content: userRepostText, emojis: status.author.emojis.asDictionary) + let text = MastodonContent(content: userRepostText, emojis: status.author.emojisTransient.asDictionary) let label = MastodonMetaContent.convert(text: text) return label }() @@ -1298,7 +1298,7 @@ extension StatusView.ViewModel { // spoiler content if let spoilerText = status.spoilerText, !spoilerText.isEmpty { do { - let content = MastodonContent(content: spoilerText, emojis: status.emojis.asDictionary) + let content = MastodonContent(content: spoilerText, emojis: status.emojisTransient.asDictionary) let metaContent = try MastodonMetaContent.convert(document: content, useParagraphMark: true) self.spoilerContent = metaContent } catch { @@ -1309,7 +1309,7 @@ extension StatusView.ViewModel { // content do { - let content = MastodonContent(content: status.content, emojis: status.emojis.asDictionary) + let content = MastodonContent(content: status.content, emojis: status.emojisTransient.asDictionary) let metaContent = try MastodonMetaContent.convert(document: content, useParagraphMark: true) self.content = metaContent } catch { diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift index 2d999258..3704d244 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -248,7 +248,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { if _authorUserIdentifier?.id != status.author.id { mentionAccts.append("@" + status.author.acct) } - for mention in status.mentions { + for mention in status.mentionsTransient { let acct = "@" + mention.acct guard !mentionAccts.contains(acct) else { continue } guard mention.id != _authorUserIdentifier?.id else { continue } diff --git a/TwidereX/Protocol/Facade/DataSourceFacade+Profile.swift b/TwidereX/Protocol/Facade/DataSourceFacade+Profile.swift index 75269411..f1ceef63 100644 --- a/TwidereX/Protocol/Facade/DataSourceFacade+Profile.swift +++ b/TwidereX/Protocol/Facade/DataSourceFacade+Profile.swift @@ -92,7 +92,7 @@ extension DataSourceFacade { case .mastodon(let status): let status = status.repost ?? status - guard let mention = status.mentions.first(where: { mention == $0.username }) else { + guard let mention = status.mentionsTransient.first(where: { mention == $0.username }) else { return nil } @@ -138,7 +138,7 @@ extension DataSourceFacade { guard let object = user.object(in: provider.context.managedObjectContext) else { return nil } switch object { case .twitter(let user): - let mentions = user.bioEntities?.mentions ?? [] + let mentions = user.bioEntitiesTransient?.mentions ?? [] let _userID = mentions.first(where: { $0.username == mention })?.id if let userID = _userID { diff --git a/TwidereX/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift b/TwidereX/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift index 0bde2fcd..972d7711 100644 --- a/TwidereX/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift +++ b/TwidereX/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift @@ -12,6 +12,7 @@ import CoreDataStack import TwitterMeta import MastodonMeta import AlamofireImage +import TwidereCore extension ProfileHeaderView { class ViewModel: ObservableObject { @@ -36,7 +37,7 @@ extension ProfileHeaderView { @Published var username: String = "" @Published var isFollowsYou: Bool = false - @Published var relationship: Relationship? + @Published var relationship: TwidereCore.Relationship? @Published var bioMetaContent: MetaContent? @Published var fields: [ProfileFieldListView.Item]? @@ -265,7 +266,7 @@ extension ProfileHeaderView { private func configureContent(twitterUser user: TwitterUser) { Publishers.CombineLatest3( user.publisher(for: \.bio), - user.publisher(for: \.bioEntities), + user.publisher(for: \.bioEntitiesTransient), UIContentSizeCategory.publisher ) .map { _, _, _ in user.bioMetaContent(provider: SwiftTwitterTextProvider()) } @@ -349,7 +350,7 @@ extension ProfileHeaderView { // name Publishers.CombineLatest3( user.publisher(for: \.displayName), - user.publisher(for: \.emojis), + user.publisher(for: \.emojisTransient), UIContentSizeCategory.publisher ) .map { _, emojis, _ -> MetaContent in @@ -382,7 +383,7 @@ extension ProfileHeaderView { private func configureContent(mastodonUser user: MastodonUser) { Publishers.CombineLatest3( user.publisher(for: \.note), - user.publisher(for: \.emojis), + user.publisher(for: \.emojisTransient), UIContentSizeCategory.publisher ) .map { _, _, _ in user.bioMetaContent } @@ -392,8 +393,8 @@ extension ProfileHeaderView { private func configureField(mastodonUser user: MastodonUser) { Publishers.CombineLatest3( - user.publisher(for: \.fields), - user.publisher(for: \.emojis), + user.publisher(for: \.fieldsTransient), + user.publisher(for: \.emojisTransient), UIContentSizeCategory.publisher ) .map { fields, emojis, _ -> [ProfileFieldListView.Item]? in diff --git a/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionView.swift b/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionView.swift index 2fc5c9f8..ac6a588a 100644 --- a/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionView.swift +++ b/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionView.swift @@ -117,7 +117,7 @@ extension MastodonNotificationSectionView { viewModel.mentionPreference = newValue viewModel.updateNotificationSubscription { notificationSubscription in let mentionPreference = MastodonNotificationSubscription.MentionPreference(preference: newValue) - notificationSubscription.update(mentionPreference: mentionPreference) + notificationSubscription.update(mentionPreferenceTransient: mentionPreference) } } )) { diff --git a/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionViewModel.swift b/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionViewModel.swift index fe351dcc..4ff5da7d 100644 --- a/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionViewModel.swift +++ b/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionViewModel.swift @@ -43,7 +43,7 @@ final class MastodonNotificationSectionViewModel: ObservableObject { self.isFavoriteEnabled = notificationSubscription.favourite self.isPollEnabled = notificationSubscription.poll self.isMentionEnabled = notificationSubscription.mention - self.mentionPreference = notificationSubscription.mentionPreference.preference + self.mentionPreference = notificationSubscription.mentionPreferenceTransient.preference // end init notificationSubscription.publisher(for: \.isActive) @@ -66,7 +66,7 @@ final class MastodonNotificationSectionViewModel: ObservableObject { .receive(on: DispatchQueue.main) .assign(to: &$isMentionEnabled) - notificationSubscription.publisher(for: \.mentionPreference) + notificationSubscription.publisher(for: \.mentionPreferenceTransient) .receive(on: DispatchQueue.main) .map { $0.preference } .assign(to: &$mentionPreference) From fa004c3497fc6b3d0d7b04e7034195fb407885a5 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 18 May 2023 19:49:09 +0800 Subject: [PATCH 097/128] feat: add new SwiftUI TrendView --- .../Extension/CoreDataStack/TwitterUser.swift | 1 - .../Sources/TwidereUI/Content/TrendView.swift | 89 +++++++++++++++---- 2 files changed, 71 insertions(+), 19 deletions(-) diff --git a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterUser.swift b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterUser.swift index ba4b022c..5831ef39 100644 --- a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterUser.swift +++ b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterUser.swift @@ -51,7 +51,6 @@ extension TwitterUser { } extension TwitterUser { - public enum SizeKind: String { case small case medium diff --git a/TwidereSDK/Sources/TwidereUI/Content/TrendView.swift b/TwidereSDK/Sources/TwidereUI/Content/TrendView.swift index e6cad025..2eded997 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/TrendView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/TrendView.swift @@ -11,6 +11,7 @@ import Meta import MetaTextKit import TwitterSDK import MastodonSDK +import Charts public struct TrendView: View { @@ -21,15 +22,33 @@ public struct TrendView: View { } public var body: some View { - HStack { - VStack(alignment: .leading, spacing: .zero) { - titleLabel - descriptionLabel + switch viewModel.kind { + case .twitter: + HStack { + VStack(alignment: .leading, spacing: .zero) { + titleLabel + } + .alignmentGuide(.listRowSeparatorLeading) { viewDimensions in + viewDimensions[.leading] + } + Spacer() } - Spacer() + + case .mastodon: HStack { - chartDescriptionLabel - chartView + VStack(alignment: .leading, spacing: .zero) { + titleLabel + descriptionLabel + } + .alignmentGuide(.listRowSeparatorLeading) { viewDimensions in + viewDimensions[.leading] + } + Spacer() + HStack { + chartDescriptionLabel + chartView + .frame(width: 66, height: 40) + } } } } @@ -66,11 +85,41 @@ extension TrendView { .foregroundColor(.secondary) } + var chartLineGradient: LinearGradient { + LinearGradient( + gradient: Gradient ( + colors: [ + Color(uiColor: Asset.Colors.hightLight.color).opacity(0.5), + Color(uiColor: Asset.Colors.hightLight.color).opacity(0.0), + ] + ), + startPoint: .top, + endPoint: .bottom + ) + } + var chartView: some View { - EmptyView() -// Chart { -// -// } + Group { + if let historyData = viewModel.historyData { + Chart(historyData, id: \.self) { data in + LineMark( + x: .value("Day", data.day), + y: .value("Accounts", Int(data.accounts) ?? 0) + ) + .interpolationMethod(.catmullRom) + + AreaMark( + x: .value("Day", data.day), + y: .value("Accounts", Int(data.accounts) ?? 0) + ) + .interpolationMethod(.catmullRom) + .foregroundStyle(chartLineGradient) + } + .chartLegend(.hidden) + .chartXAxis(.hidden) + .chartYAxis(.hidden) + } + } // Group } } @@ -169,13 +218,13 @@ extension TrendView.ViewModel { ) historyData = [ - Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 1), uses: "123", accounts: "123"), - Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 2), uses: "123", accounts: "123"), - Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 3), uses: "123", accounts: "123"), - Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 4), uses: "123", accounts: "123"), - Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 5), uses: "123", accounts: "123"), - Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 6), uses: "123", accounts: "123"), - Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 7), uses: "123", accounts: "123"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 1), uses: "123", accounts: "12"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 2), uses: "123", accounts: "23"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 3), uses: "123", accounts: "33"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 4), uses: "123", accounts: "23"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 5), uses: "123", accounts: "13"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 6), uses: "123", accounts: "23"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 7), uses: "123", accounts: "53"), ] } } @@ -185,7 +234,11 @@ extension TrendView.ViewModel { struct TrendView_Previews: PreviewProvider { static var previews: some View { TrendView(viewModel: .init(kind: .twitter)) + .previewDisplayName("Twitter") + .previewLayout(.fixed(width: 475, height: 88)) TrendView(viewModel: .init(kind: .mastodon)) + .previewDisplayName("Mastodon") + .previewLayout(.fixed(width: 475, height: 88)) } } #endif From 181202f5d491b651b7c26cc44c67aaf25545c93d Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 18 May 2023 19:49:50 +0800 Subject: [PATCH 098/128] chore: update version to 2.0.0 (131) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 36 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 685ee726..a535b3bd 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 130 + 131 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 1d95e087..74c8e822 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 130 + 131 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 106f8f28..5d26665b 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3431,7 +3431,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3458,7 +3458,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3481,7 +3481,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3505,7 +3505,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3532,7 +3532,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3561,7 +3561,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3590,7 +3590,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3618,7 +3618,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3644,7 +3644,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3671,7 +3671,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3825,7 +3825,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3857,7 +3857,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3884,7 +3884,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3909,7 +3909,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3932,7 +3932,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3955,7 +3955,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3979,7 +3979,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -4006,7 +4006,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index ca828d48..b8160ed3 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 130 + 131 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index 09edc087..f312c6c0 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 130 + 131 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index 45d40d23..25b47773 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 130 + 131 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index 45d40d23..25b47773 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 130 + 131 From f079d216638da222f067877d198036b6b63e70fb Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 23 May 2023 20:31:53 +0800 Subject: [PATCH 099/128] feat: add multi column for iPadOS --- .../Extension/NSLayoutConstraint.swift | 7 + .../Content/StatusView+ViewModel.swift | 1 + TwidereX.xcodeproj/project.pbxproj | 36 +++ TwidereX/Coordinator/SceneCoordinator.swift | 2 +- TwidereX/Diffable/Status/StatusSection.swift | 2 - ...HomeListStatusTimelineViewController.swift | 6 + .../Scene/Profile/ProfileViewController.swift | 6 + .../Root/ContentSplitViewController.swift | 41 +++- .../Scene/Root/NewColumn/NewColumnView.swift | 40 ++++ .../NewColumn/NewColumnViewController.swift | 55 +++++ .../Root/NewColumn/NewColumnViewModel.swift | 73 ++++++ .../SecondaryContainerViewController.swift | 224 ++++++++++++++++++ .../SecondaryContainerViewModel.swift | 146 ++++++++++++ .../Base/Common/TimelineViewController.swift | 6 + 14 files changed, 641 insertions(+), 4 deletions(-) create mode 100644 TwidereX/Scene/Root/NewColumn/NewColumnView.swift create mode 100644 TwidereX/Scene/Root/NewColumn/NewColumnViewController.swift create mode 100644 TwidereX/Scene/Root/NewColumn/NewColumnViewModel.swift create mode 100644 TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewController.swift create mode 100644 TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewModel.swift diff --git a/TwidereSDK/Sources/TwidereCommon/Extension/NSLayoutConstraint.swift b/TwidereSDK/Sources/TwidereCommon/Extension/NSLayoutConstraint.swift index 60a169ae..10524dfb 100644 --- a/TwidereSDK/Sources/TwidereCommon/Extension/NSLayoutConstraint.swift +++ b/TwidereSDK/Sources/TwidereCommon/Extension/NSLayoutConstraint.swift @@ -13,3 +13,10 @@ extension NSLayoutConstraint { return self } } + +extension NSLayoutConstraint { + public func identifier(_ identifier: String) -> Self { + self.identifier = identifier + return self + } +} diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index 5fdd7763..ab809e0e 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -940,6 +940,7 @@ extension StatusView.ViewModel { case .timeline, .referenceReplyTo: width += StatusView.hangingAvatarButtonDimension width += StatusView.hangingAvatarButtonTrailingSpacing + width += 2 * 16 // cell margin default: break } diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 5d26665b..8486256b 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -25,6 +25,8 @@ DB02C77527351DA2007EA0BF /* HashtagTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02C77427351DA2007EA0BF /* HashtagTableViewCell+ViewModel.swift */; }; DB02C777273520B8007EA0BF /* SearchHashtagViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02C776273520B8007EA0BF /* SearchHashtagViewModel+State.swift */; }; DB02C77927352217007EA0BF /* SearchHashtagViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02C77827352217007EA0BF /* SearchHashtagViewModel+Diffable.swift */; }; + DB0455B62A1CA2F3009A00EF /* NewColumnViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0455B52A1CA2F3009A00EF /* NewColumnViewModel.swift */; }; + DB0455B82A1CA510009A00EF /* NewColumnView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0455B72A1CA510009A00EF /* NewColumnView.swift */; }; DB05E13729C318530055BF3F /* TwidereSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB05E13629C318530055BF3F /* TwidereSDK */; }; DB05E13929C318590055BF3F /* TwidereSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB05E13829C318590055BF3F /* TwidereSDK */; }; DB05E13B29C3185E0055BF3F /* TwidereSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB05E13A29C3185E0055BF3F /* TwidereSDK */; }; @@ -331,6 +333,9 @@ DBE6357D2885557C001C114B /* PushNotificationScratchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE6357C2885557C001C114B /* PushNotificationScratchViewModel.swift */; }; DBE6357F288555AE001C114B /* PushNotificationScratchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE6357E288555AE001C114B /* PushNotificationScratchView.swift */; }; DBE76D1E2500E65D00DEB0FC /* HomeTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE76D1D2500E65D00DEB0FC /* HomeTimelineViewModel.swift */; }; + DBEA97632A1B556C00C8B75B /* SecondaryContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBEA97622A1B556C00C8B75B /* SecondaryContainerViewController.swift */; }; + DBEA97662A1B58E200C8B75B /* SecondaryContainerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBEA97652A1B58E200C8B75B /* SecondaryContainerViewModel.swift */; }; + DBEA97682A1B6D2300C8B75B /* NewColumnViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBEA97672A1B6D2300C8B75B /* NewColumnViewController.swift */; }; DBED96D8253F5D7800C5383A /* NamingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBED96D7253F5D7800C5383A /* NamingState.swift */; }; DBF167FD27C394830001F75E /* NeedsDependency+AvatarBarButtonItemDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF167FC27C394830001F75E /* NeedsDependency+AvatarBarButtonItemDelegate.swift */; }; DBF3309125B96E0B00A678FB /* WKNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF3309025B96E0B00A678FB /* WKNavigationDelegateShim.swift */; }; @@ -487,6 +492,8 @@ DB02C77427351DA2007EA0BF /* HashtagTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTableViewCell+ViewModel.swift"; sourceTree = ""; }; DB02C776273520B8007EA0BF /* SearchHashtagViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHashtagViewModel+State.swift"; sourceTree = ""; }; DB02C77827352217007EA0BF /* SearchHashtagViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHashtagViewModel+Diffable.swift"; sourceTree = ""; }; + DB0455B52A1CA2F3009A00EF /* NewColumnViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewColumnViewModel.swift; sourceTree = ""; }; + DB0455B72A1CA510009A00EF /* NewColumnView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewColumnView.swift; sourceTree = ""; }; DB06180F2786EC870030EE79 /* LineChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineChartView.swift; sourceTree = ""; }; DB0AD4DE285872BE0002ABDB /* UserMediaTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMediaTimelineViewController.swift; sourceTree = ""; }; DB0AD4E12858734A0002ABDB /* UserMediaTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMediaTimelineViewModel.swift; sourceTree = ""; }; @@ -828,6 +835,9 @@ DBE76CF82500B29500DEB0FC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DBE76CFE2500B43300DEB0FC /* StubMixer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubMixer.swift; sourceTree = ""; }; DBE76D1D2500E65D00DEB0FC /* HomeTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineViewModel.swift; sourceTree = ""; }; + DBEA97622A1B556C00C8B75B /* SecondaryContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryContainerViewController.swift; sourceTree = ""; }; + DBEA97652A1B58E200C8B75B /* SecondaryContainerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryContainerViewModel.swift; sourceTree = ""; }; + DBEA97672A1B6D2300C8B75B /* NewColumnViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewColumnViewController.swift; sourceTree = ""; }; DBED2A7125B8006400BE6941 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = ""; }; DBED96D7253F5D7800C5383A /* NamingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NamingState.swift; sourceTree = ""; }; DBEFDC8725591F5C0086F268 /* DrawerSidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawerSidebarViewController.swift; sourceTree = ""; }; @@ -1062,6 +1072,8 @@ DB148B01281A729600B596C7 /* Sidebar */, DBDA8E9624FE075E006750DC /* MainTab */, DB66DB8E2823A7CC0071F5F3 /* SecondaryTab */, + DBEA97642A1B557100C8B75B /* SecondaryContainer */, + DBEA97692A1B6D2C00C8B75B /* NewColumn */, DB148B02281A7AB300B596C7 /* ContentSplitViewController.swift */, ); path = Root; @@ -2245,6 +2257,25 @@ path = View; sourceTree = ""; }; + DBEA97642A1B557100C8B75B /* SecondaryContainer */ = { + isa = PBXGroup; + children = ( + DBEA97622A1B556C00C8B75B /* SecondaryContainerViewController.swift */, + DBEA97652A1B58E200C8B75B /* SecondaryContainerViewModel.swift */, + ); + path = SecondaryContainer; + sourceTree = ""; + }; + DBEA97692A1B6D2C00C8B75B /* NewColumn */ = { + isa = PBXGroup; + children = ( + DBEA97672A1B6D2300C8B75B /* NewColumnViewController.swift */, + DB0455B52A1CA2F3009A00EF /* NewColumnViewModel.swift */, + DB0455B72A1CA510009A00EF /* NewColumnView.swift */, + ); + path = NewColumn; + sourceTree = ""; + }; DBED96DC253F5D7B00C5383A /* Protocol */ = { isa = PBXGroup; children = ( @@ -2888,6 +2919,7 @@ DBA7DD74256B96450008A95A /* UIFont.swift in Sources */, DB1D3DF228938CDF008F0BD0 /* StatusHistoryViewModel.swift in Sources */, DB76A652275F1C3200A50673 /* MediaPreviewTransitionItem.swift in Sources */, + DBEA97632A1B556C00C8B75B /* SecondaryContainerViewController.swift in Sources */, DB697DF3278FDDF7004EF2F7 /* TimelineViewModel.swift in Sources */, DB235EF42834DD0900398FCA /* SettingListViewModel.swift in Sources */, DB83020D273D160400BF5224 /* NotificationTimelineViewModel+LoadOldestState.swift in Sources */, @@ -2900,6 +2932,7 @@ DB71C7D3271EB71800BE3819 /* ProfileViewController+DataSourceProvider.swift in Sources */, DB8301FA273CED2E00BF5224 /* NotificationTimelineViewModel.swift in Sources */, DB36F35E257F74C10028F81E /* ScrollViewContainer.swift in Sources */, + DBEA97662A1B58E200C8B75B /* SecondaryContainerViewModel.swift in Sources */, DB76A65D275F52D500A50673 /* MediaInfoDescriptionView+ViewModel.swift in Sources */, DB66DB902823AC3C0071F5F3 /* TabBarItem.swift in Sources */, DB697E0927904BFB004EF2F7 /* FederatedTimelineViewController.swift in Sources */, @@ -2981,7 +3014,9 @@ DB1D3DED28938ACD008F0BD0 /* HistoryViewModel.swift in Sources */, DB522F172886A08F0088017C /* UIStatusBarManager+HandleTapAction.m in Sources */, DBC8E04B2576337F00401E20 /* DisposeBagCollectable.swift in Sources */, + DB0455B62A1CA2F3009A00EF /* NewColumnViewModel.swift in Sources */, DBAA898E2758CF01001C273B /* DrawerSidebarHeaderView+ViewModel.swift in Sources */, + DBEA97682A1B6D2300C8B75B /* NewColumnViewController.swift in Sources */, DBADCDC82826658700D1CA4E /* MediaPreviewableViewController.swift in Sources */, DBF167FD27C394830001F75E /* NeedsDependency+AvatarBarButtonItemDelegate.swift in Sources */, DB02C77927352217007EA0BF /* SearchHashtagViewModel+Diffable.swift in Sources */, @@ -3186,6 +3221,7 @@ DB76A64D275DFA0600A50673 /* TwitterStatusThreadReplyViewModel+State.swift in Sources */, DBF87AD92892A67D0029A7C7 /* TranslateButtonPreferenceView.swift in Sources */, DB76A66A27606F6900A50673 /* MediaPreviewVideoViewController.swift in Sources */, + DB0455B82A1CA510009A00EF /* NewColumnView.swift in Sources */, DB148B0C281A837E00B596C7 /* SidebarView.swift in Sources */, DB88E6292890DF67009A01F5 /* BehaviorsPreferenceViewController.swift in Sources */, DBC8E050257653E100401E20 /* SavePhotoActivity.swift in Sources */, diff --git a/TwidereX/Coordinator/SceneCoordinator.swift b/TwidereX/Coordinator/SceneCoordinator.swift index 8f948a88..c7a4d8cd 100644 --- a/TwidereX/Coordinator/SceneCoordinator.swift +++ b/TwidereX/Coordinator/SceneCoordinator.swift @@ -271,7 +271,7 @@ extension SceneCoordinator { } -private extension SceneCoordinator { +extension SceneCoordinator { func get(scene: Scene) -> UIViewController? { let viewController: UIViewController? diff --git a/TwidereX/Diffable/Status/StatusSection.swift b/TwidereX/Diffable/Status/StatusSection.swift index 387b5a17..bb90adba 100644 --- a/TwidereX/Diffable/Status/StatusSection.swift +++ b/TwidereX/Diffable/Status/StatusSection.swift @@ -96,9 +96,7 @@ extension StatusSection { // ) // } return cell - - case .topLoader: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell cell.activityIndicatorView.startAnimating() diff --git a/TwidereX/Scene/List/HomeList/HomeListStatusTimelineViewController.swift b/TwidereX/Scene/List/HomeList/HomeListStatusTimelineViewController.swift index 122d8d9c..7503f3ce 100644 --- a/TwidereX/Scene/List/HomeList/HomeListStatusTimelineViewController.swift +++ b/TwidereX/Scene/List/HomeList/HomeListStatusTimelineViewController.swift @@ -47,6 +47,12 @@ extension HomeListStatusTimelineViewController { .receive(on: DispatchQueue.main) .sink { [weak self] needsSetupAvatarBarButtonItem in guard let self = self else { return } + if let leftBarButtonItem = self.navigationItem.leftBarButtonItem, + leftBarButtonItem !== self.avatarBarButtonItem + { + // allow override + return + } self.navigationItem.leftBarButtonItem = needsSetupAvatarBarButtonItem ? self.avatarBarButtonItem : nil } .store(in: &disposeBag) diff --git a/TwidereX/Scene/Profile/ProfileViewController.swift b/TwidereX/Scene/Profile/ProfileViewController.swift index 18c67e5c..8e9d91fe 100644 --- a/TwidereX/Scene/Profile/ProfileViewController.swift +++ b/TwidereX/Scene/Profile/ProfileViewController.swift @@ -104,6 +104,12 @@ extension ProfileViewController { .receive(on: DispatchQueue.main) .sink { [weak self] needsSetupAvatarBarButtonItem in guard let self = self else { return } + if let leftBarButtonItem = self.navigationItem.leftBarButtonItem, + leftBarButtonItem !== self.avatarBarButtonItem + { + // allow override + return + } self.navigationItem.leftBarButtonItem = needsSetupAvatarBarButtonItem ? self.avatarBarButtonItem : nil } .store(in: &disposeBag) diff --git a/TwidereX/Scene/Root/ContentSplitViewController.swift b/TwidereX/Scene/Root/ContentSplitViewController.swift index 8f37139d..bdad4f28 100644 --- a/TwidereX/Scene/Root/ContentSplitViewController.swift +++ b/TwidereX/Scene/Root/ContentSplitViewController.swift @@ -37,12 +37,19 @@ final class ContentSplitViewController: UIViewController, NeedsDependency { return mainTabBarController }() + private(set) lazy var secondaryContainerViewController: SecondaryContainerViewController = { + let viewController = SecondaryContainerViewController(context: context, coordinator: coordinator, authContext: authContext) + return viewController + }() + private(set) lazy var secondaryTabBarController: SecondaryTabBarController = { let secondaryTabBarController = SecondaryTabBarController(context: context, coordinator: coordinator, authContext: authContext) return secondaryTabBarController }() var mainTabBarViewLeadingLayoutConstraint: NSLayoutConstraint! + var mainTabBarViewTrailingLayoutConstraint: NSLayoutConstraint! + var mainTabBarViewWidthLayoutConstraint: NSLayoutConstraint! @Published var isSidebarDisplay = false @Published var isSecondaryTabBarControllerActive = false @@ -101,14 +108,27 @@ extension ContentSplitViewController { view.addSubview(mainTabBarController.view) mainTabBarController.didMove(toParent: self) mainTabBarViewLeadingLayoutConstraint = mainTabBarController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor) + mainTabBarViewTrailingLayoutConstraint = mainTabBarController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor) + mainTabBarViewWidthLayoutConstraint = mainTabBarController.view.widthAnchor.constraint(equalToConstant: 428).priority(.required - 1) NSLayoutConstraint.activate([ mainTabBarController.view.topAnchor.constraint(equalTo: view.topAnchor), mainTabBarViewLeadingLayoutConstraint, mainTabBarController.view.leadingAnchor.constraint(equalTo: sidebarViewController.view.trailingAnchor, constant: UIView.separatorLineHeight(of: view)).priority(.required - 1), - mainTabBarController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + mainTabBarViewTrailingLayoutConstraint, mainTabBarController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) + addChild(secondaryContainerViewController) + secondaryContainerViewController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(secondaryContainerViewController.view) + secondaryContainerViewController.didMove(toParent: self) + NSLayoutConstraint.activate([ + secondaryContainerViewController.view.topAnchor.constraint(equalTo: view.topAnchor), + secondaryContainerViewController.view.leadingAnchor.constraint(equalTo: mainTabBarController.view.trailingAnchor, constant: UIView.separatorLineHeight(of: view)), // 1pt for divider + secondaryContainerViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + secondaryContainerViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + addChild(secondaryTabBarController) secondaryTabBarController.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(secondaryTabBarController.view) @@ -163,9 +183,28 @@ extension ContentSplitViewController { case .regular: isSidebarDisplay = true mainTabBarViewLeadingLayoutConstraint.isActive = false + let width: CGFloat = { + var minWidth = UIScreen.main.bounds.width + if UIScreen.main.bounds.height < minWidth { + minWidth = UIScreen.main.bounds.height + } + if let window = view.window, window.frame.width < minWidth { + minWidth = window.frame.width + } + return minWidth - ContentSplitViewController.sidebarWidth + }() + let mainWidth = width / 100 * 55 + let secondaryWidth = width / 100 * 45 + secondaryContainerViewController.viewModel.update(width: floor(secondaryWidth)) + mainTabBarViewTrailingLayoutConstraint.isActive = false + mainTabBarViewWidthLayoutConstraint.constant = floor(mainWidth) + mainTabBarViewWidthLayoutConstraint.isActive = true + default: isSidebarDisplay = false mainTabBarViewLeadingLayoutConstraint.isActive = true + mainTabBarViewWidthLayoutConstraint.isActive = false + mainTabBarViewTrailingLayoutConstraint.isActive = true } guard let previousTraitCollection = previousTraitCollection else { return } diff --git a/TwidereX/Scene/Root/NewColumn/NewColumnView.swift b/TwidereX/Scene/Root/NewColumn/NewColumnView.swift new file mode 100644 index 00000000..a2f01e71 --- /dev/null +++ b/TwidereX/Scene/Root/NewColumn/NewColumnView.swift @@ -0,0 +1,40 @@ +// +// NewColumnView.swift +// TwidereX +// +// Created by MainasuK on 2023/5/23. +// Copyright © 2023 Twidere. All rights reserved. +// + +import UIKit +import SwiftUI + +protocol NewColumnViewDelegate: AnyObject { + func newColumnView(_ viewModel: NewColumnViewModel, tabBarItemDidPressed tab: TabBarItem) +} + +struct NewColumnView: View { + @ObservedObject var viewModel: NewColumnViewModel + + var body: some View { + List { + ForEach(viewModel.tabs, id: \.self) { tab in + Button { + viewModel.delegate?.newColumnView(viewModel, tabBarItemDidPressed: tab) + } label: { + HStack { + Image(uiImage: tab.image) + Text("\(tab.title)") + Spacer() + Image(systemName: "chevron.right") + } // end HStack + .font(.subheadline) + .foregroundColor(Color.primary) + } + .buttonStyle(.borderless) + } // end ForEach + } // end List + .listStyle(.plain) + } // end body +} + diff --git a/TwidereX/Scene/Root/NewColumn/NewColumnViewController.swift b/TwidereX/Scene/Root/NewColumn/NewColumnViewController.swift new file mode 100644 index 00000000..779d9049 --- /dev/null +++ b/TwidereX/Scene/Root/NewColumn/NewColumnViewController.swift @@ -0,0 +1,55 @@ +// +// NewColumnViewController.swift +// TwidereX +// +// Created by MainasuK on 2023/5/22. +// Copyright © 2023 Twidere. All rights reserved. +// + +import UIKit +import SwiftUI + +final class NewColumnViewController: UIViewController { + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + let authContext: AuthContext + + public private(set) lazy var viewModel = NewColumnViewModel(context: context, auth: authContext) + private lazy var contentView = NewColumnView(viewModel: viewModel) + + init( + context: AppContext, + coordinator: SceneCoordinator, + authContext: AuthContext + ) { + self.context = context + self.coordinator = coordinator + self.authContext = authContext + super.init(nibName: nil, bundle: nil) + // end init + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension NewColumnViewController { + override func viewDidLoad() { + super.viewDidLoad() + + title = "New Column" + + let hostingViewController = UIHostingController(rootView: contentView) + hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false + addChild(hostingViewController) + view.addSubview(hostingViewController.view) + hostingViewController.didMove(toParent: self) + NSLayoutConstraint.activate([ + hostingViewController.view.topAnchor.constraint(equalTo: view.topAnchor), + hostingViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + hostingViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + hostingViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } +} diff --git a/TwidereX/Scene/Root/NewColumn/NewColumnViewModel.swift b/TwidereX/Scene/Root/NewColumn/NewColumnViewModel.swift new file mode 100644 index 00000000..5af7bd74 --- /dev/null +++ b/TwidereX/Scene/Root/NewColumn/NewColumnViewModel.swift @@ -0,0 +1,73 @@ +// +// NewColumnViewModel.swift +// TwidereX +// +// Created by MainasuK on 2023/5/23. +// Copyright © 2023 Twidere. All rights reserved. +// + +import UIKit +import Combine + +final class NewColumnViewModel: ObservableObject { + + weak var delegate: NewColumnViewDelegate? + + // input + let context: AppContext + let auth: AuthContext + + @Published var preferredEnableHistory: Bool = false + + // output + var tabs: [TabBarItem] { + switch auth.authenticationContext { + case .twitter: + var results: [TabBarItem] = [ + .home, + .notification, + .search, + .me, + .likes, + ] + if preferredEnableHistory { + results.append(.history) + } + results.append(.lists) + results.append(.trends) + return results + case .mastodon: + var results: [TabBarItem] = [ + .home, + .notification, + .search, + .me, + .local, + .federated, + .likes, + ] + if preferredEnableHistory { + results.append(.history) + } + results.append(.lists) + results.append(.trends) + return results + } + } + + // output + + init( + context: AppContext, + auth: AuthContext + ) { + self.context = context + self.auth = auth + // end init + + UserDefaults.shared.publisher(for: \.preferredEnableHistory).removeDuplicates() + .receive(on: DispatchQueue.main) + .assign(to: &$preferredEnableHistory) + } + +} diff --git a/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewController.swift b/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewController.swift new file mode 100644 index 00000000..9aca7a88 --- /dev/null +++ b/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewController.swift @@ -0,0 +1,224 @@ +// +// SecondaryContainerViewController.swift +// TwidereX +// +// Created by MainasuK on 2023/5/22. +// Copyright © 2023 Twidere. All rights reserved. +// + +import UIKit + +final class SecondaryContainerViewController: UIViewController, NeedsDependency { + + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + let authContext: AuthContext + + public private(set) lazy var viewModel = SecondaryContainerViewModel(context: context, auth: authContext) + + let containerScrollView = UIScrollView() + let stack = UIStackView() + + init( + context: AppContext, + coordinator: SceneCoordinator, + authContext: AuthContext + ) { + self.context = context + self.coordinator = coordinator + self.authContext = authContext + super.init(nibName: nil, bundle: nil) + // end init + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension SecondaryContainerViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + containerScrollView.frame = view.bounds + containerScrollView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(containerScrollView) + NSLayoutConstraint.activate([ + containerScrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.topAnchor), + containerScrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor), + containerScrollView.frameLayoutGuide.trailingAnchor.constraint(equalTo: view.trailingAnchor), + containerScrollView.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + stack.axis = .horizontal + stack.spacing = UIView.separatorLineHeight(of: view) + stack.alignment = .leading + + stack.translatesAutoresizingMaskIntoConstraints = false + containerScrollView.addSubview(stack) + NSLayoutConstraint.activate([ + stack.topAnchor.constraint(equalTo: containerScrollView.contentLayoutGuide.topAnchor), + stack.leadingAnchor.constraint(equalTo: containerScrollView.contentLayoutGuide.leadingAnchor), + stack.trailingAnchor.constraint(equalTo: containerScrollView.contentLayoutGuide.trailingAnchor), + stack.bottomAnchor.constraint(equalTo: containerScrollView.contentLayoutGuide.bottomAnchor), + stack.heightAnchor.constraint(equalTo: containerScrollView.frameLayoutGuide.heightAnchor), + ]) + + setupNewColumn() + } + +} + +extension SecondaryContainerViewController { + private func setupNewColumn() { + let newColumnViewController = NewColumnViewController( + context: context, + coordinator: coordinator, + authContext: authContext + ) + newColumnViewController.viewModel.delegate = self + viewModel.addColumn(in: stack, viewController: newColumnViewController, setupColumnMenu: false) + } +} + +// MARK: - NewColumnViewDelegate +extension SecondaryContainerViewController: NewColumnViewDelegate { + func newColumnView(_ viewModel: NewColumnViewModel, tabBarItemDidPressed tab: TabBarItem) { + switch tab { + case .home: + let homeTimelineViewController = HomeTimelineViewController() + configure(viewController: homeTimelineViewController) + homeTimelineViewController.viewModel = HomeTimelineViewModel( + context: context, + authContext: authContext + ) + self.viewModel.addColumn( + in: stack, + viewController: homeTimelineViewController + ) + case .homeList: + assertionFailure() + case .notification: + let notificationViewController = NotificationViewController() + configure(viewController: notificationViewController) + notificationViewController.viewModel = NotificationViewModel( + context: context, + authContext: authContext, + coordinator: coordinator + ) + self.viewModel.addColumn( + in: stack, + viewController: notificationViewController + ) + case .search: + let searchViewController = SearchViewController() + configure(viewController: searchViewController) + searchViewController.viewModel = SearchViewModel( + context: context, + authContext: authContext + ) + self.viewModel.addColumn( + in: stack, + viewController: searchViewController + ) + case .me: + let profileViewController = ProfileViewController() + configure(viewController: profileViewController) + profileViewController.viewModel = MeProfileViewModel( + context: context, + authContext: authContext + ) + self.viewModel.addColumn( + in: stack, + viewController: profileViewController + ) + case .local: + let federatedTimelineViewModel = FederatedTimelineViewModel( + context: context, + authContext: authContext, + isLocal: true + ) + guard let rootViewController = coordinator.get(scene: .federatedTimeline(viewModel: federatedTimelineViewModel)) else { return } + self.viewModel.addColumn( + in: stack, + viewController: rootViewController + ) + case .federated: + let federatedTimelineViewModel = FederatedTimelineViewModel( + context: context, + authContext: authContext, + isLocal: false + ) + guard let rootViewController = coordinator.get(scene: .federatedTimeline(viewModel: federatedTimelineViewModel)) else { return } + self.viewModel.addColumn( + in: stack, + viewController: rootViewController + ) + case .messages: + assertionFailure() + case .likes: + let userLikeTimelineViewModel = UserLikeTimelineViewModel( + context: context, + authContext: authContext, + timelineContext: .init( + timelineKind: .like, + protected: { + guard let user = authContext.authenticationContext.user(in: context.managedObjectContext) else { return false } + return user.protected + }(), + userIdentifier: authContext.authenticationContext.userIdentifier + ) + ) + userLikeTimelineViewModel.isFloatyButtonDisplay = false + guard let rootViewController = coordinator.get(scene: .userLikeTimeline(viewModel: userLikeTimelineViewModel)) else { return } + self.viewModel.addColumn( + in: stack, + viewController: rootViewController + ) + case .history: + let historyViewModel = HistoryViewModel( + context: context, + coordinator: coordinator, + authContext: authContext + ) + guard let rootViewController = coordinator.get(scene: .history(viewModel: historyViewModel)) else { return } + self.viewModel.addColumn( + in: stack, + viewController: rootViewController + ) + case .lists: + guard let me = authContext.authenticationContext.user(in: context.managedObjectContext)?.asRecord else { return } + + let compositeListViewModel = CompositeListViewModel( + context: context, + authContext: authContext, + kind: .lists(me) + ) + guard let rootViewController = coordinator.get(scene: .compositeList(viewModel: compositeListViewModel)) else { return } + self.viewModel.addColumn( + in: stack, + viewController: rootViewController + ) + case .trends: + let trendViewModel = TrendViewModel( + context: context, + authContext: authContext + ) + guard let rootViewController = coordinator.get(scene: .trend(viewModel: trendViewModel)) else { return } + self.viewModel.addColumn( + in: stack, + viewController: rootViewController + ) + case .drafts: + assertionFailure() + case .settings: + assertionFailure() + } + } + + private func configure(viewController: NeedsDependency) { + viewController.context = context + viewController.coordinator = coordinator + } +} diff --git a/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewModel.swift b/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewModel.swift new file mode 100644 index 00000000..6964eeee --- /dev/null +++ b/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewModel.swift @@ -0,0 +1,146 @@ +// +// SecondaryContainerViewModel.swift +// TwidereX +// +// Created by MainasuK on 2023/5/22. +// Copyright © 2023 Twidere. All rights reserved. +// + +import UIKit +import TwidereCommon + +class SecondaryContainerViewModel: ObservableObject { + + // input + let context: AppContext + let auth: AuthContext + + @Published private(set) var width: CGFloat = 375 + + // output + @Published private var viewControllers: [UINavigationController] = [] + + init( + context: AppContext, + auth: AuthContext + ) { + self.context = context + self.auth = auth + // end init + } + +} + +extension SecondaryContainerViewModel { + func addColumn( + in stack: UIStackView, + viewController: UIViewController, + setupColumnMenu: Bool = true + ) { + let navigationController = UINavigationController(rootViewController: viewController) + viewControllers.append(navigationController) + + let count = stack.arrangedSubviews.count + if count == 0 { + stack.addArrangedSubview(navigationController.view) + } else { + stack.insertArrangedSubview(navigationController.view, at: count - 1) + } + + navigationController.view.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + navigationController.view.widthAnchor.constraint(equalToConstant: width).identifier("width"), + navigationController.view.heightAnchor.constraint(equalTo: stack.heightAnchor), + ]) + + if setupColumnMenu { + setupColumnMenuBarButtonItem( + in: stack, + viewController: viewController, + navigationController: navigationController + ) + } + } + + func update(width: CGFloat) { + for viewController in viewControllers { + guard let constraint = viewController.view.constraints.first(where: { $0.identifier == "width" }) else { + continue + } + constraint.constant = width + } + + self.width = width + } +} + +extension SecondaryContainerViewModel { + private func setupColumnMenuBarButtonItem( + in stack: UIStackView, + viewController: UIViewController, + navigationController: UINavigationController + ) { + let barButtonItem = UIBarButtonItem() + barButtonItem.image = UIImage(systemName: "slider.horizontal.3") + let deferredMenuElement = UIDeferredMenuElement.uncached { [weak self, weak stack, weak viewController, weak navigationController] handler in + guard let self = self, + let stack = stack, + let viewController = viewController, + let navigationController = navigationController + else { + handler([]) + return + } + + var menuElements: [UIMenuElement] = [] + + let closeColumnAction = UIAction(title: "Close column", image: UIImage(systemName: "xmark.square")) { [weak self, weak stack, weak navigationController] _ in + guard let self = self else { return } + guard let stack = stack else { return } + guard let navigationController = navigationController else { return } + stack.removeArrangedSubview(navigationController.view) + navigationController.view.removeFromSuperview() + navigationController.view.isHidden = true + self.viewControllers.removeAll(where: { $0 === navigationController }) + } + let closeMenu = UIMenu(title: "", options: .displayInline, children: [closeColumnAction]) + menuElements.append(closeMenu) + + let _index: Int? = stack.arrangedSubviews.firstIndex(where: { view in + return navigationController.view === view + }) + if let index = _index { + var moveMenuElements: [UIMenuElement] = [] + if index > 0 { + let moveLeftMenuAction = UIAction(title: "Move left", image: UIImage(systemName: "arrow.left.square")) { [weak self, weak stack, weak navigationController] _ in + guard let self = self else { return } + guard let stack = stack else { return } + guard let navigationController = navigationController else { return } + stack.removeArrangedSubview(navigationController.view) + stack.insertArrangedSubview(navigationController.view, at: index - 1) + } + moveMenuElements.append(moveLeftMenuAction) + } + if index < stack.arrangedSubviews.count - 2 { + let moveRightMenuAction = UIAction(title: "Move Right", image: UIImage(systemName: "arrow.right.square")) { [weak self, weak stack, weak navigationController] _ in + guard let self = self else { return } + guard let stack = stack else { return } + guard let navigationController = navigationController else { return } + stack.removeArrangedSubview(navigationController.view) + stack.insertArrangedSubview(navigationController.view, at: index + 1) + } + moveMenuElements.append(moveRightMenuAction) + } + if !moveMenuElements.isEmpty { + let moveMenu = UIMenu(title: "", options: .displayInline, children: moveMenuElements) + menuElements.append(moveMenu) + } + } + + let menu = UIMenu(title: "", options: .displayInline, children: menuElements) + handler([menu]) + } + barButtonItem.menu = UIMenu(title: "", options: .displayInline, children: [deferredMenuElement]) + viewController.navigationItem.leftBarButtonItem = barButtonItem + } +} diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift index b27a6799..546e56b7 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift @@ -87,6 +87,12 @@ extension TimelineViewController { .receive(on: DispatchQueue.main) .sink { [weak self] needsSetupAvatarBarButtonItem in guard let self = self else { return } + if let leftBarButtonItem = self.navigationItem.leftBarButtonItem, + leftBarButtonItem !== self.avatarBarButtonItem + { + // allow override + return + } self.navigationItem.leftBarButtonItem = needsSetupAvatarBarButtonItem ? self.avatarBarButtonItem : nil } .store(in: &disposeBag) From 3daa983e726862ea2be7cc5b1a4c078968dbf7f5 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 23 May 2023 20:32:26 +0800 Subject: [PATCH 100/128] chore: update to version 2.0.0 (132) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 36 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index a535b3bd..fc2ffac4 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 131 + 132 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 74c8e822..f7b5ede6 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 131 + 132 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 8486256b..a8899289 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3467,7 +3467,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3494,7 +3494,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3517,7 +3517,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3541,7 +3541,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3568,7 +3568,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3597,7 +3597,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3626,7 +3626,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3654,7 +3654,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3680,7 +3680,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3707,7 +3707,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3861,7 +3861,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3893,7 +3893,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3920,7 +3920,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3945,7 +3945,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3968,7 +3968,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3991,7 +3991,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -4015,7 +4015,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -4042,7 +4042,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index b8160ed3..6e57623d 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 131 + 132 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index f312c6c0..e5e4a38b 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 131 + 132 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index 25b47773..04af1923 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 131 + 132 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index 25b47773..04af1923 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 131 + 132 From 2f76328209c5a382d74448f723db4019a533256d Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 2 Jun 2023 22:09:28 +0800 Subject: [PATCH 101/128] feat: add tweet detail and update layout margin for iPad column. Fix the top conversation link not display issue --- TwidereSDK/Package.swift | 6 +- .../CoreDataStack/TwitterStatus.swift | 16 +- .../TwitterSDK/Twitter+Entity+V2+Tweet.swift | 19 ++ .../Vendor/SwiftTwitterTextProvider.swift | 40 --- .../StatusFetchViewModel+Timeline+Home.swift | 63 ++--- .../Content/StatusView+ViewModel.swift | 237 ++++++------------ .../TextViewRepresentable.swift | 107 ++++---- .../xcshareddata/swiftpm/Package.resolved | 12 +- .../NotificationViewController.swift | 6 + .../Scene/Profile/ProfileViewController.swift | 1 - .../StatusThreadViewModel+Diffable.swift | 1 + 11 files changed, 219 insertions(+), 289 deletions(-) create mode 100644 TwidereSDK/Sources/TwidereCore/Extension/TwitterSDK/Twitter+Entity+V2+Tweet.swift delete mode 100644 TwidereSDK/Sources/TwidereCore/Vendor/SwiftTwitterTextProvider.swift diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index 007e3ef9..c5656ca6 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -29,7 +29,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-nio.git", from: "2.34.0"), .package(url: "https://github.com/Flipboard/FLAnimatedImage.git", from: "1.0.0"), .package(url: "https://github.com/MainasuK/CommonOSLog", from: "0.1.1"), - .package(url: "https://github.com/TwidereProject/MetaTextKit.git", exact: "4.5.2"), + .package(url: "https://github.com/TwidereProject/MetaTextKit.git", exact: "4.6.0"), .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.5.0"), .package(url: "https://github.com/Alamofire/AlamofireImage.git", from: "4.1.0"), .package(url: "https://github.com/Alamofire/AlamofireNetworkActivityIndicator.git", from: "3.1.0"), @@ -43,7 +43,6 @@ let package = Package( .package(url: "https://github.com/aheze/Popovers.git", from: "1.3.2"), .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", from: "4.2.2"), .package(url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.4"), - .package(url: "https://github.com/TwidereProject/twitter-text.git", exact: "0.0.3"), .package(url: "https://github.com/MainasuK/DateTools", branch: "master"), .package(url: "https://github.com/kciter/Floaty.git", branch: "master"), .package(url: "https://github.com/MainasuK/FPSIndicator.git", from: "1.1.0"), @@ -51,7 +50,7 @@ let package = Package( .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.6.0"), .package(url: "https://github.com/tid-kijyun/Kanna.git", from: "5.2.7"), .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"), - .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.8.0"), + .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.9.1"), .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"), .package(name: "CoverFlowStackLayout", path: "../CoverFlowStackLayout"), ], @@ -106,7 +105,6 @@ let package = Package( .product(name: "AlamofireImage", package: "AlamofireImage"), .product(name: "AlamofireNetworkActivityIndicator", package: "AlamofireNetworkActivityIndicator"), .product(name: "MetaTextKit", package: "MetaTextKit"), - .product(name: "TwitterText", package: "twitter-text"), .product(name: "DateToolsSwift", package: "DateTools"), .product(name: "CryptoSwift", package: "CryptoSwift"), .product(name: "Kanna", package: "Kanna"), diff --git a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterStatus.swift b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterStatus.swift index 90197b0a..4728b3a1 100644 --- a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterStatus.swift +++ b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterStatus.swift @@ -28,15 +28,15 @@ extension TwitterStatus { continue } - // drop quote URL - if let quote = quote { - let quoteID = quote.id - guard !displayURL.hasPrefix("twitter.com"), - !expandedURL.hasPrefix(quoteID) - else { - text = text.replacingOccurrences(of: shortURL, with: "") - continue + // drop twitter URL + // - quote URL: remove URL + // - long tweet self URL suffix: replace "… URL" with "…" + if displayURL.hasPrefix("twitter.com") && expandedURL.localizedCaseInsensitiveContains("/status/") { + if expandedURL.localizedCaseInsensitiveContains(self.id) { + text = text.replacingOccurrences(of: "… " + shortURL, with: "…") } + text = text.replacingOccurrences(of: shortURL, with: "") + continue } } return text diff --git a/TwidereSDK/Sources/TwidereCore/Extension/TwitterSDK/Twitter+Entity+V2+Tweet.swift b/TwidereSDK/Sources/TwidereCore/Extension/TwitterSDK/Twitter+Entity+V2+Tweet.swift new file mode 100644 index 00000000..dff3e777 --- /dev/null +++ b/TwidereSDK/Sources/TwidereCore/Extension/TwitterSDK/Twitter+Entity+V2+Tweet.swift @@ -0,0 +1,19 @@ +// +// Twitter+Entity+V2+Tweet.swift +// +// +// Created by MainasuK on 2023/6/2. +// + +import Foundation +import TwitterSDK +import TwitterMeta + +extension Twitter.Entity.V2.Tweet { + public var urlEntities: [TwitterContent.URLEntity] { + let results = entities?.urls?.map { entity in + TwitterContent.URLEntity(url: entity.url, expandedURL: entity.expandedURL, displayURL: entity.displayURL) + } + return results ?? [] + } +} diff --git a/TwidereSDK/Sources/TwidereCore/Vendor/SwiftTwitterTextProvider.swift b/TwidereSDK/Sources/TwidereCore/Vendor/SwiftTwitterTextProvider.swift deleted file mode 100644 index 1bb9ab29..00000000 --- a/TwidereSDK/Sources/TwidereCore/Vendor/SwiftTwitterTextProvider.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// SwiftTwitterTextProvider.swift -// -// -// Created by MainasuK on 2023/2/3. -// - -import Foundation -import TwitterText -import TwitterMeta - -public class SwiftTwitterTextProvider: TwitterTextProvider { - - public func parse(text: String) -> TwitterMeta.ParseResult { - let result = Parser.defaultParser.parseTweet(text: text) - return .init( - isValid: result.isValid, - weightedLength: result.weightedLength, - maxWeightedLength: Parser.defaultParser.maxWeightedTweetLength(), - entities: self.entities(in: text) - ) - } - - public func entities(in text: String) -> [TwitterMeta.TwitterTextProviderEntity] { - return TwitterText.entities(in: text).compactMap { entity in - switch entity.type { - case .url: return .url(range: entity.range) - case .screenName: return .screenName(range: entity.range) - case .hashtag: return .hashtag(range: entity.range) - case .listname: return .listName(range: entity.range) - case .symbol: return .symbol(range: entity.range) - case .tweetChar: return .tweetChar(range: entity.range) - case .tweetEmojiChar: return .tweetEmojiChar(range: entity.range) - } - } - } - - public init() { } - -} diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Home.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Home.swift index 5ea4cd75..1682e3c7 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Home.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Home.swift @@ -94,35 +94,40 @@ extension StatusFetchViewModel.Timeline.Home { public static func fetch(api: APIService, input: Input) async throws -> StatusFetchViewModel.Timeline.Output { switch input { case .twitter(let fetchContext): - let responses = try await api.twitterHomeTimeline( - query: .init( - sinceID: fetchContext.sinceID, - untilID: fetchContext.untilID, - paginationToken: nil, - maxResults: fetchContext.maxResults ?? 100, - onlyMedia: fetchContext.filter.rule.contains(.onlyMedia) - ), - authenticationContext: fetchContext.authenticationContext - ) - let nextInput: Input? = { - guard let last = responses.last, - last.value.meta.nextToken != nil, - let oldestID = last.value.meta.oldestID - else { return nil } - let fetchContext = fetchContext.map(untilID: oldestID) - return .twitter(fetchContext) - }() - return .init( - result: { - let statuses = responses - .map { $0.value.data } - .compactMap{ $0 } - .flatMap { $0 } - return .twitterV2(statuses) - }(), - backInput: nil, - nextInput: nextInput.flatMap { .home($0) } - ) + do { + let responses = try await api.twitterHomeTimeline( + query: .init( + sinceID: fetchContext.sinceID, + untilID: fetchContext.untilID, + paginationToken: nil, + maxResults: fetchContext.maxResults ?? 100, + onlyMedia: fetchContext.filter.rule.contains(.onlyMedia) + ), + authenticationContext: fetchContext.authenticationContext + ) + let nextInput: Input? = { + guard let last = responses.last, + last.value.meta.nextToken != nil, + let oldestID = last.value.meta.oldestID + else { return nil } + let fetchContext = fetchContext.map(untilID: oldestID) + return .twitter(fetchContext) + }() + return .init( + result: { + let statuses = responses + .map { $0.value.data } + .compactMap{ $0 } + .flatMap { $0 } + return .twitterV2(statuses) + }(), + backInput: nil, + nextInput: nextInput.flatMap { .home($0) } + ) + } catch { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(error.localizedDescription)") + throw error + } case .mastodon(let fetchContext): let authenticationContext = fetchContext.authenticationContext let responses = try await api.mastodonHomeTimeline( diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index ab809e0e..0b831841 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -297,6 +297,31 @@ extension StatusView { } } +extension StatusView.ViewModel { + @MainActor + public func updateTwitterStatusContent(statusID: TwitterStatus.ID) async throws { + do { + guard case let .twitter(authenticationContext) = authContext?.authenticationContext else { return } + let response = try await Twitter.API.V2.Status.detail( + session: URLSession(configuration: .ephemeral), + query: .init(statusID: statusID), + authorization: authenticationContext.authorization + ) + let metaContent = TwitterMetaContent.convert( + document: TwitterContent(content: response.value.text, urlEntities: response.value.urlEntities), + urlMaximumLength: .max, + twitterTextProvider: SwiftTwitterTextProvider(), + useParagraphMark: true + ) + self.content = metaContent + // delegate?.statusView(self, translateContentDidChange: status) + } catch { + debugPrint(error.localizedDescription) + throw error + } + } +} + //extension StatusView.ViewModel { // func bind(statusView: StatusView) { // bindHeader(statusView: statusView) @@ -936,14 +961,46 @@ extension StatusView.ViewModel { var containerMargin: CGFloat { var width: CGFloat = 0 + + // container margin switch kind { - case .timeline, .referenceReplyTo: + case .conversationThread: + fallthrough + case .referenceReplyTo: + fallthrough + case .referenceQuote: + fallthrough + case .timeline: width += StatusView.hangingAvatarButtonDimension width += StatusView.hangingAvatarButtonTrailingSpacing - width += 2 * 16 // cell margin - default: + case .repost: + break + case .quote: + break + case .conversationRoot: break } + + // manually readable margin (iPad multi-column layout) + switch kind { + case .timeline: + fallthrough + case .conversationThread: + fallthrough + case .conversationRoot: + if viewLayoutFrame.layoutFrame.width == viewLayoutFrame.readableContentLayoutFrame.width { + width += 2 * 16 + } + case .referenceReplyTo: + break + case .referenceQuote: + break + case .repost: + break + case .quote: + break + } + return width } @@ -1131,14 +1188,25 @@ extension StatusView.ViewModel { } // content - let content = TwitterContent(content: status.displayText, urlEntities: status.urlEntities) - let metaContent = TwitterMetaContent.convert( - document: content, - urlMaximumLength: .max, - twitterTextProvider: SwiftTwitterTextProvider(), - useParagraphMark: true - ) - self.content = metaContent + switch kind { + case .conversationRoot where status.hasMore: + let statusID = status.id + defer { + Task { + try? await self.updateTwitterStatusContent(statusID: statusID) + } + } + fallthrough + default: + let content = TwitterContent(content: status.displayText, urlEntities: status.urlEntities) + let metaContent = TwitterMetaContent.convert( + document: content, + urlMaximumLength: .max, + twitterTextProvider: SwiftTwitterTextProvider(), + useParagraphMark: true + ) + self.content = metaContent + } // language status.publisher(for: \.language) @@ -1426,152 +1494,5 @@ extension StatusView.ViewModel { ) return viewModel -// -// if let repost = status.repost { -// let _repostViewModel = StatusView.ViewModel( -// status: repost, -// authContext: authContext, -// kind: kind, -// delegate: delegate, -// parentViewModel: self, -// viewLayoutFramePublisher: viewLayoutFramePublisher -// ) -// repostViewModel = _repostViewModel -// -// // header - repost -// let _statusHeaderViewModel = StatusHeaderView.ViewModel( -// image: Asset.Media.repeat.image.withRenderingMode(.alwaysTemplate), -// label: { -// let name = status.author.name -// let userRepostText = L10n.Common.Controls.Status.userBoosted(name) -// let text = MastodonContent(content: userRepostText, emojis: status.author.emojis.asDictionary) -// let label = MastodonMetaContent.convert(text: text) -// return label -// }() -// ) -// _statusHeaderViewModel.hasHangingAvatar = _repostViewModel.hasHangingAvatar -// _repostViewModel.statusHeaderViewModel = _statusHeaderViewModel -// } -// -// // author -// status.author.publisher(for: \.avatar) -// .compactMap { $0.flatMap { URL(string: $0) } } -// .assign(to: &$avatarURL) -// status.author.publisher(for: \.displayName) -// .compactMap { _ in status.author.nameMetaContent } -// .assign(to: &$authorName) -// status.author.publisher(for: \.username) -// .map { _ in status.author.acct } -// .assign(to: &$authorUsernme) -// authorUserIdentifier = .mastodon(.init(domain: status.author.domain, id: status.author.id)) -// -// // visibility -// visibility = status.visibility -// -// // timestamp -// switch kind { -// case .conversationRoot: -// break -// default: -// timestampLabelViewModel = TimestampLabelView.ViewModel(timestamp: status.createdAt) -// } -// -// // spoiler content -// if let spoilerText = status.spoilerText, !spoilerText.isEmpty { -// do { -// let content = MastodonContent(content: spoilerText, emojis: status.emojis.asDictionary) -// let metaContent = try MastodonMetaContent.convert(document: content, useParagraphMark: true) -// self.spoilerContent = metaContent -// } catch { -// assertionFailure(error.localizedDescription) -// self.spoilerContent = nil -// } -// } -// -// // content -// do { -// let content = MastodonContent(content: status.content, emojis: status.emojis.asDictionary) -// let metaContent = try MastodonMetaContent.convert(document: content, useParagraphMark: true) -// self.content = metaContent -// } catch { -// assertionFailure(error.localizedDescription) -// self.content = PlaintextMetaContent(string: "") -// } -// -// // language -// status.publisher(for: \.language) -// .assign(to: &$language) -// -// // content warning -// isContentSensitiveToggled = status.isContentSensitiveToggled -// status.publisher(for: \.isContentSensitiveToggled) -// .receive(on: DispatchQueue.main) -// .assign(to: \.isContentSensitiveToggled, on: self) -// .store(in: &disposeBag) -// -// // media -// mediaViewModels = MediaView.ViewModel.viewModels(from: status) -// -// // poll -// if let poll = status.poll { -// self.pollViewModel = PollView.ViewModel( -// authContext: authContext, -// poll: .mastodon(object: poll) -// ) -// } -// -// // media content warning -// isMediaSensitive = status.isMediaSensitive -// isMediaSensitiveToggled = status.isMediaSensitiveToggled -// status.publisher(for: \.isMediaSensitiveToggled) -// .receive(on: DispatchQueue.main) -// .assign(to: \.isMediaSensitiveToggled, on: self) -// .store(in: &disposeBag) -// -// // toolbar -// toolbarViewModel.platform = .mastodon -// status.publisher(for: \.replyCount) -// .map { Int($0) } -// .assign(to: &toolbarViewModel.$replyCount) -// status.publisher(for: \.repostCount) -// .map { Int($0) } -// .assign(to: &toolbarViewModel.$repostCount) -// status.publisher(for: \.likeCount) -// .map { Int($0) } -// .assign(to: &toolbarViewModel.$likeCount) -// if case let .mastodon(authenticationContext) = authContext?.authenticationContext { -// status.publisher(for: \.likeBy) -// .map { users -> Bool in -// let ids = users.map { $0.id } -// return ids.contains(authenticationContext.userID) -// } -// .assign(to: &toolbarViewModel.$isLiked) -// status.publisher(for: \.repostBy) -// .map { users -> Bool in -// let ids = users.map { $0.id } -// return ids.contains(authenticationContext.userID) -// } -// .assign(to: &toolbarViewModel.$isReposted) -// } else { -// // do nothing -// } -// -// // metric -// switch kind { -// case .conversationRoot: -// let _metricViewModel = StatusMetricView.ViewModel(platform: .mastodon, timestamp: status.createdAt) -// metricViewModel = _metricViewModel -// status.publisher(for: \.replyCount) -// .map { Int($0) } -// .assign(to: &_metricViewModel.$replyCount) -// status.publisher(for: \.repostCount) -// .map { Int($0) } -// .assign(to: &_metricViewModel.$repostCount) -// status.publisher(for: \.likeCount) -// .map { Int($0) } -// .assign(to: &_metricViewModel.$likeCount) -// default: -// break -// } } } diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift index f1886334..c1e09180 100644 --- a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift @@ -13,19 +13,8 @@ import MetaTextKit import MetaTextArea public struct TextViewRepresentable: UIViewRepresentable { - - let textView: WrappedTextView = { - let textView = WrappedTextView() - textView.backgroundColor = .clear - textView.isScrollEnabled = false - textView.isEditable = false - textView.isSelectable = false - textView.textContainerInset = .zero - textView.textContainer.lineFragmentPadding = 0 - textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - textView.setContentHuggingPriority(.defaultHigh, for: .vertical) - return textView - }() + // let logger = Logger(subsystem: "TextViewRepresentable", category: "View") + let logger = Logger(.disabled) // input let metaContent: MetaContent @@ -34,6 +23,9 @@ public struct TextViewRepresentable: UIViewRepresentable { let isSelectable: Bool let handler: (Meta) -> Void + // output + let attributedString: NSAttributedString + public init( metaContent: MetaContent, textStyle: TextStyle, @@ -46,53 +38,82 @@ public struct TextViewRepresentable: UIViewRepresentable { self.width = width self.isSelectable = isSelectable self.handler = handler + self.attributedString = { + let attributedString = NSMutableAttributedString(string: metaContent.string) + let textAttributes: [NSAttributedString.Key: Any] = [ + .font: textStyle.font, + .foregroundColor: textStyle.textColor, + ] + let linkAttributes: [NSAttributedString.Key: Any] = [ + .font: textStyle.font, + .foregroundColor: UIColor.tintColor, + ] + let paragraphStyle: NSMutableParagraphStyle = { + let style = NSMutableParagraphStyle() + style.lineSpacing = 3 + style.paragraphSpacing = 8 + return style + }() + + MetaText.setAttributes( + for: attributedString, + textAttributes: textAttributes, + linkAttributes: linkAttributes, + paragraphStyle: paragraphStyle, + content: metaContent + ) + + return attributedString + }() } public func makeUIView(context: Context) -> UITextView { - let textView = self.textView + let textView: WrappedTextView = { + let textView = WrappedTextView() + textView.backgroundColor = .clear + textView.isScrollEnabled = false + textView.isEditable = false + textView.isSelectable = false + textView.textContainerInset = .zero + textView.textContainer.lineFragmentPadding = 0 + textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + textView.setContentHuggingPriority(.defaultHigh, for: .vertical) + return textView + }() textView.isSelectable = isSelectable textView.delegate = context.coordinator textView.textViewDelegate = context.coordinator - - let attributedString = NSMutableAttributedString(string: metaContent.string) - let textAttributes: [NSAttributedString.Key: Any] = [ - .font: textStyle.font, - .foregroundColor: textStyle.textColor, - ] - let linkAttributes: [NSAttributedString.Key: Any] = [ - .font: textStyle.font, - .foregroundColor: UIColor.tintColor, - ] - let paragraphStyle: NSMutableParagraphStyle = { - let style = NSMutableParagraphStyle() - style.lineSpacing = 3 - style.paragraphSpacing = 8 - return style - }() - - MetaText.setAttributes( - for: attributedString, - textAttributes: textAttributes, - linkAttributes: linkAttributes, - paragraphStyle: paragraphStyle, - content: metaContent - ) - textView.frame.size.width = width textView.textStorage.setAttributedString(attributedString) textView.invalidateIntrinsicContentSize() textView.setNeedsLayout() textView.layoutIfNeeded() - return textView } public func updateUIView(_ view: UITextView, context: Context) { + let textView = view + + var needsLayout = false + defer { + if needsLayout { + textView.invalidateIntrinsicContentSize() + textView.setNeedsLayout() + textView.layoutIfNeeded() + } + } + if textView.frame.size.width != width { textView.frame.size.width = width - textView.invalidateIntrinsicContentSize() - textView.setNeedsLayout() - textView.layoutIfNeeded() + needsLayout = true + } + if textView.attributedText != attributedString { + textView.textStorage.setAttributedString(attributedString) + needsLayout = true + + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): update textView \(view.hashValue): \(metaContent.string)") + } else { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): reuse textView content") } } diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index 33da201f..9d88ff9e 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -141,8 +141,8 @@ "repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git", "state": { "branch": null, - "revision": "addb2b4875a2a5712e1c50add410c560a9cf42f2", - "version": "4.5.2" + "revision": "8877640bba088661e768fc531b5fc9c5976a6e48", + "version": "4.6.0" } }, { @@ -249,8 +249,8 @@ "repositoryURL": "https://github.com/TwidereProject/twitter-text.git", "state": { "branch": null, - "revision": "c88fa4ed8dd7441f827942b83d0564101bbdc508", - "version": "0.0.3" + "revision": "aaa067a823a61f27780692a836ec78fd0c0706fc", + "version": "0.0.4" } }, { @@ -258,8 +258,8 @@ "repositoryURL": "https://github.com/TwidereProject/TwitterSDK.git", "state": { "branch": null, - "revision": "1b2998a2fc5b7abc421e61b075ba3807ba694991", - "version": "0.8.0" + "revision": "8148362dbf900b6f6416e3d64b3332fc660bd844", + "version": "0.9.1" } }, { diff --git a/TwidereX/Scene/Notification/NotificationViewController.swift b/TwidereX/Scene/Notification/NotificationViewController.swift index 8aae0660..45716a4d 100644 --- a/TwidereX/Scene/Notification/NotificationViewController.swift +++ b/TwidereX/Scene/Notification/NotificationViewController.swift @@ -65,6 +65,12 @@ extension NotificationViewController { .receive(on: DispatchQueue.main) .sink { [weak self] needsSetupAvatarBarButtonItem in guard let self = self else { return } + if let leftBarButtonItem = self.navigationItem.leftBarButtonItem, + leftBarButtonItem !== self.avatarBarButtonItem + { + // allow override + return + } self.navigationItem.leftBarButtonItem = needsSetupAvatarBarButtonItem ? self.avatarBarButtonItem : nil } .store(in: &disposeBag) diff --git a/TwidereX/Scene/Profile/ProfileViewController.swift b/TwidereX/Scene/Profile/ProfileViewController.swift index 8e9d91fe..381c674a 100644 --- a/TwidereX/Scene/Profile/ProfileViewController.swift +++ b/TwidereX/Scene/Profile/ProfileViewController.swift @@ -124,7 +124,6 @@ extension ProfileViewController { self.avatarBarButtonItem.configure(user: user) } .store(in: &disposeBag) - } addChild(tabBarPagerController) diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift index 6c727763..b79a65ba 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift @@ -34,6 +34,7 @@ extension StatusThreadViewModel { let viewModel = StatusView.ViewModel( status: status, authContext: self.authContext, + kind: .conversationThread, delegate: cell, viewLayoutFramePublisher: self.$viewLayoutFrame ) From e615958da4acb987e6c1dd2807dea51ad77c61ca Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 2 Jun 2023 22:10:26 +0800 Subject: [PATCH 102/128] chore: update version to 2.0.0 (133) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 36 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index fc2ffac4..d24aea0b 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 132 + 133 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index f7b5ede6..557f0357 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 132 + 133 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index a8899289..48e8a626 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3467,7 +3467,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3494,7 +3494,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3517,7 +3517,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3541,7 +3541,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3568,7 +3568,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3597,7 +3597,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3626,7 +3626,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3654,7 +3654,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3680,7 +3680,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3707,7 +3707,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3861,7 +3861,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3893,7 +3893,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3920,7 +3920,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3945,7 +3945,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3968,7 +3968,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3991,7 +3991,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -4015,7 +4015,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -4042,7 +4042,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index 6e57623d..a82269fb 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 132 + 133 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index e5e4a38b..4d893e66 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 132 + 133 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index 04af1923..712d01e3 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 132 + 133 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index 04af1923..712d01e3 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 132 + 133 From 3cc0d14e214f7d0f69b177f5ea1ccdf60febd6d5 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 2 Jun 2023 23:02:20 +0800 Subject: [PATCH 103/128] feat: add open tab menu actions --- .../Scene/Root/NewColumn/NewColumnView.swift | 18 +++ .../SecondaryContainerViewController.swift | 117 ++++++++++-------- .../SecondaryContainerViewModel.swift | 51 +++++++- 3 files changed, 130 insertions(+), 56 deletions(-) diff --git a/TwidereX/Scene/Root/NewColumn/NewColumnView.swift b/TwidereX/Scene/Root/NewColumn/NewColumnView.swift index a2f01e71..a6504df9 100644 --- a/TwidereX/Scene/Root/NewColumn/NewColumnView.swift +++ b/TwidereX/Scene/Root/NewColumn/NewColumnView.swift @@ -11,6 +11,7 @@ import SwiftUI protocol NewColumnViewDelegate: AnyObject { func newColumnView(_ viewModel: NewColumnViewModel, tabBarItemDidPressed tab: TabBarItem) + func newColumnView(_ viewModel: NewColumnViewModel, source: UINavigationController, openTabMenuAction tab: TabBarItem) } struct NewColumnView: View { @@ -38,3 +39,20 @@ struct NewColumnView: View { } // end body } +extension NewColumnView { + static func menu( + tabs: [TabBarItem], + viewModel: NewColumnViewModel, + source: UINavigationController + ) -> UIMenu { + let actions: [UIAction] = tabs.map { tab in + UIAction(title: tab.title, image: tab.image) { [weak viewModel, weak source] _ in + guard let source = source, + let viewModel = viewModel + else { return } + viewModel.delegate?.newColumnView(viewModel, source: source, openTabMenuAction: tab) + } + } + return UIMenu(options: .displayInline, children: actions) + } +} diff --git a/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewController.swift b/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewController.swift index 9aca7a88..d4f1adb9 100644 --- a/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewController.swift +++ b/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewController.swift @@ -78,13 +78,43 @@ extension SecondaryContainerViewController { authContext: authContext ) newColumnViewController.viewModel.delegate = self - viewModel.addColumn(in: stack, viewController: newColumnViewController, setupColumnMenu: false) + viewModel.addColumn( + in: stack, + tab: nil, + viewController: newColumnViewController, + setupColumnMenu: false + ) } } // MARK: - NewColumnViewDelegate extension SecondaryContainerViewController: NewColumnViewDelegate { func newColumnView(_ viewModel: NewColumnViewModel, tabBarItemDidPressed tab: TabBarItem) { + guard let viewController = self.viewController(for: tab) else { + assertionFailure() + return + } + + self.viewModel.addColumn( + in: stack, + tab: tab, + viewController: viewController, + newColumnViewModel: viewModel + ) + } + + private func menuActionForOpenTabs(tabs: [TabBarItem], exclude: TabBarItem) -> [TabBarItem] { + var tabs = tabs + tabs.removeAll(where: { $0 == exclude }) + return tabs + } + + private func configure(viewController: NeedsDependency) { + viewController.context = context + viewController.coordinator = coordinator + } + + private func viewController(for tab: TabBarItem) -> UIViewController? { switch tab { case .home: let homeTimelineViewController = HomeTimelineViewController() @@ -93,12 +123,10 @@ extension SecondaryContainerViewController: NewColumnViewDelegate { context: context, authContext: authContext ) - self.viewModel.addColumn( - in: stack, - viewController: homeTimelineViewController - ) + return homeTimelineViewController case .homeList: assertionFailure() + return nil case .notification: let notificationViewController = NotificationViewController() configure(viewController: notificationViewController) @@ -107,10 +135,7 @@ extension SecondaryContainerViewController: NewColumnViewDelegate { authContext: authContext, coordinator: coordinator ) - self.viewModel.addColumn( - in: stack, - viewController: notificationViewController - ) + return notificationViewController case .search: let searchViewController = SearchViewController() configure(viewController: searchViewController) @@ -118,10 +143,7 @@ extension SecondaryContainerViewController: NewColumnViewDelegate { context: context, authContext: authContext ) - self.viewModel.addColumn( - in: stack, - viewController: searchViewController - ) + return searchViewController case .me: let profileViewController = ProfileViewController() configure(viewController: profileViewController) @@ -129,34 +151,26 @@ extension SecondaryContainerViewController: NewColumnViewDelegate { context: context, authContext: authContext ) - self.viewModel.addColumn( - in: stack, - viewController: profileViewController - ) + return profileViewController case .local: let federatedTimelineViewModel = FederatedTimelineViewModel( context: context, authContext: authContext, isLocal: true ) - guard let rootViewController = coordinator.get(scene: .federatedTimeline(viewModel: federatedTimelineViewModel)) else { return } - self.viewModel.addColumn( - in: stack, - viewController: rootViewController - ) + guard let rootViewController = coordinator.get(scene: .federatedTimeline(viewModel: federatedTimelineViewModel)) else { return nil } + return rootViewController case .federated: let federatedTimelineViewModel = FederatedTimelineViewModel( context: context, authContext: authContext, isLocal: false ) - guard let rootViewController = coordinator.get(scene: .federatedTimeline(viewModel: federatedTimelineViewModel)) else { return } - self.viewModel.addColumn( - in: stack, - viewController: rootViewController - ) + guard let rootViewController = coordinator.get(scene: .federatedTimeline(viewModel: federatedTimelineViewModel)) else { return nil } + return rootViewController case .messages: assertionFailure() + return nil case .likes: let userLikeTimelineViewModel = UserLikeTimelineViewModel( context: context, @@ -171,54 +185,55 @@ extension SecondaryContainerViewController: NewColumnViewDelegate { ) ) userLikeTimelineViewModel.isFloatyButtonDisplay = false - guard let rootViewController = coordinator.get(scene: .userLikeTimeline(viewModel: userLikeTimelineViewModel)) else { return } - self.viewModel.addColumn( - in: stack, - viewController: rootViewController - ) + guard let rootViewController = coordinator.get(scene: .userLikeTimeline(viewModel: userLikeTimelineViewModel)) else { return nil } + return rootViewController case .history: let historyViewModel = HistoryViewModel( context: context, coordinator: coordinator, authContext: authContext ) - guard let rootViewController = coordinator.get(scene: .history(viewModel: historyViewModel)) else { return } - self.viewModel.addColumn( - in: stack, - viewController: rootViewController - ) + guard let rootViewController = coordinator.get(scene: .history(viewModel: historyViewModel)) else { return nil } + return rootViewController case .lists: - guard let me = authContext.authenticationContext.user(in: context.managedObjectContext)?.asRecord else { return } + guard let me = authContext.authenticationContext.user(in: context.managedObjectContext)?.asRecord else { return nil } let compositeListViewModel = CompositeListViewModel( context: context, authContext: authContext, kind: .lists(me) ) - guard let rootViewController = coordinator.get(scene: .compositeList(viewModel: compositeListViewModel)) else { return } - self.viewModel.addColumn( - in: stack, - viewController: rootViewController - ) + guard let rootViewController = coordinator.get(scene: .compositeList(viewModel: compositeListViewModel)) else { return nil } + return rootViewController case .trends: let trendViewModel = TrendViewModel( context: context, authContext: authContext ) - guard let rootViewController = coordinator.get(scene: .trend(viewModel: trendViewModel)) else { return } - self.viewModel.addColumn( - in: stack, - viewController: rootViewController - ) + guard let rootViewController = coordinator.get(scene: .trend(viewModel: trendViewModel)) else { return nil } + return rootViewController case .drafts: assertionFailure() + return nil case .settings: assertionFailure() + return nil } } - private func configure(viewController: NeedsDependency) { - viewController.context = context - viewController.coordinator = coordinator + func newColumnView( + _ viewModel: NewColumnViewModel, + source: UINavigationController, + openTabMenuAction tab: TabBarItem + ) { + guard let index = self.viewModel.removeColumn(in: stack, navigationController: source) else { return } + guard let viewController = self.viewController(for: tab) else { return } + self.viewModel.addColumn( + in: stack, + at: index, + tab: tab, + viewController: viewController, + newColumnViewModel: viewModel + ) } } diff --git a/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewModel.swift b/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewModel.swift index 6964eeee..df4b05c7 100644 --- a/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewModel.swift +++ b/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewModel.swift @@ -34,8 +34,11 @@ class SecondaryContainerViewModel: ObservableObject { extension SecondaryContainerViewModel { func addColumn( in stack: UIStackView, + at index: Int? = nil, + tab: TabBarItem?, viewController: UIViewController, - setupColumnMenu: Bool = true + setupColumnMenu: Bool = true, + newColumnViewModel: NewColumnViewModel? = nil ) { let navigationController = UINavigationController(rootViewController: viewController) viewControllers.append(navigationController) @@ -44,7 +47,8 @@ extension SecondaryContainerViewModel { if count == 0 { stack.addArrangedSubview(navigationController.view) } else { - stack.insertArrangedSubview(navigationController.view, at: count - 1) + let at = min(count - 1, index ?? count - 1) + stack.insertArrangedSubview(navigationController.view, at: at) } navigationController.view.translatesAutoresizingMaskIntoConstraints = false @@ -56,8 +60,10 @@ extension SecondaryContainerViewModel { if setupColumnMenu { setupColumnMenuBarButtonItem( in: stack, + tab: tab, viewController: viewController, - navigationController: navigationController + navigationController: navigationController, + newColumnViewModel: newColumnViewModel ) } } @@ -72,13 +78,32 @@ extension SecondaryContainerViewModel { self.width = width } + + func removeColumn( + in stack: UIStackView, + navigationController: UINavigationController + ) -> Int? { + let _index: Int? = stack.arrangedSubviews.firstIndex(where: { view in + navigationController.view === view + }) + guard let index = _index else { return nil } + + stack.removeArrangedSubview(navigationController.view) + navigationController.view.removeFromSuperview() + navigationController.view.isHidden = true + self.viewControllers.removeAll(where: { $0 === navigationController }) + + return index + } } extension SecondaryContainerViewModel { private func setupColumnMenuBarButtonItem( in stack: UIStackView, + tab: TabBarItem?, viewController: UIViewController, - navigationController: UINavigationController + navigationController: UINavigationController, + newColumnViewModel: NewColumnViewModel? = nil ) { let barButtonItem = UIBarButtonItem() barButtonItem.image = UIImage(systemName: "slider.horizontal.3") @@ -140,7 +165,23 @@ extension SecondaryContainerViewModel { let menu = UIMenu(title: "", options: .displayInline, children: menuElements) handler([menu]) } - barButtonItem.menu = UIMenu(title: "", options: .displayInline, children: [deferredMenuElement]) + + var children: [UIMenuElement] = [deferredMenuElement] + + if let newColumnViewModel = newColumnViewModel, let tab = tab { + var tabs = newColumnViewModel.tabs + tabs.removeAll(where: { $0 == tab }) + if !tabs.isEmpty { + let openTabsMenu: UIMenu = NewColumnView.menu( + tabs: tabs, + viewModel: newColumnViewModel, + source: navigationController + ) + children.append(openTabsMenu) + } + } + + barButtonItem.menu = UIMenu(title: "", options: .displayInline, children: children) viewController.navigationItem.leftBarButtonItem = barButtonItem } } From 046f7e85a3fc457697304f51a3c8405d28ca8a32 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 2 Jun 2023 23:02:58 +0800 Subject: [PATCH 104/128] chore: update version to 2.0.0 (134) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 36 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index d24aea0b..92b7b1fc 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 133 + 134 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 557f0357..6cc92700 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 133 + 134 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 48e8a626..18ebe929 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3467,7 +3467,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3494,7 +3494,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3517,7 +3517,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3541,7 +3541,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3568,7 +3568,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3597,7 +3597,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3626,7 +3626,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3654,7 +3654,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3680,7 +3680,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3707,7 +3707,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3861,7 +3861,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3893,7 +3893,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3920,7 +3920,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3945,7 +3945,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3968,7 +3968,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3991,7 +3991,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -4015,7 +4015,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -4042,7 +4042,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index a82269fb..3f104d0b 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 133 + 134 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index 4d893e66..6553c720 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 133 + 134 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index 712d01e3..c2b59739 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 133 + 134 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index 712d01e3..c2b59739 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 133 + 134 From 90c4ce8b7ff5bbd6d42db2dbf12efff93e3a05f2 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 2 Jun 2023 23:15:40 +0800 Subject: [PATCH 105/128] fix: layout loop issue --- .../UIViewRepresentable/TextViewRepresentable.swift | 2 +- .../Sources/TwidereUI/Utility/ViewLayoutFrame.swift | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift index c1e09180..a600fbd7 100644 --- a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift @@ -107,7 +107,7 @@ public struct TextViewRepresentable: UIViewRepresentable { textView.frame.size.width = width needsLayout = true } - if textView.attributedText != attributedString { + if textView.attributedText.string != attributedString.string { textView.textStorage.setAttributedString(attributedString) needsLayout = true diff --git a/TwidereSDK/Sources/TwidereUI/Utility/ViewLayoutFrame.swift b/TwidereSDK/Sources/TwidereUI/Utility/ViewLayoutFrame.swift index 817651eb..db92e782 100644 --- a/TwidereSDK/Sources/TwidereUI/Utility/ViewLayoutFrame.swift +++ b/TwidereSDK/Sources/TwidereUI/Utility/ViewLayoutFrame.swift @@ -37,21 +37,19 @@ extension ViewLayoutFrame { let layoutFrame = view.frame if self.layoutFrame != layoutFrame { self.layoutFrame = layoutFrame + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): layoutFrame: \(layoutFrame.debugDescription)") } let safeAreaLayoutFrame = view.safeAreaLayoutGuide.layoutFrame if self.safeAreaLayoutFrame != safeAreaLayoutFrame { self.safeAreaLayoutFrame = safeAreaLayoutFrame + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): safeAreaLayoutFrame: \(safeAreaLayoutFrame.debugDescription)") } let readableContentLayoutFrame = view.readableContentGuide.layoutFrame if self.readableContentLayoutFrame != readableContentLayoutFrame { self.readableContentLayoutFrame = readableContentLayoutFrame + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): readableContentLayoutFrame: \(readableContentLayoutFrame.debugDescription)") } - - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): layoutFrame: \(layoutFrame.debugDescription)") - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): safeAreaLayoutFrame: \(safeAreaLayoutFrame.debugDescription)") - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): readableContentLayoutFrame: \(readableContentLayoutFrame.debugDescription)") - } } From 3f56a9f53a675c43fdf152c4a3d187229a736f2c Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 2 Jun 2023 23:16:10 +0800 Subject: [PATCH 106/128] chore: update version to 2.0.0 (135) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 36 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 92b7b1fc..ea7e4eed 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 134 + 135 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 6cc92700..16f087d4 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 134 + 135 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 18ebe929..094934e4 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3467,7 +3467,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3494,7 +3494,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3517,7 +3517,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3541,7 +3541,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3568,7 +3568,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3597,7 +3597,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3626,7 +3626,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3654,7 +3654,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3680,7 +3680,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3707,7 +3707,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3861,7 +3861,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3893,7 +3893,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3920,7 +3920,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3945,7 +3945,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3968,7 +3968,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3991,7 +3991,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -4015,7 +4015,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -4042,7 +4042,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index 3f104d0b..8149c3ff 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 134 + 135 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index 6553c720..06462e58 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 134 + 135 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index c2b59739..256cbafa 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 134 + 135 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index c2b59739..256cbafa 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 134 + 135 From b34950e03ae5fe2944239fb2f0b67b873c6305bc Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 5 Jun 2023 14:03:13 +0800 Subject: [PATCH 107/128] chore: limit the UI layout update in function scope --- .../UIViewRepresentable/TextViewRepresentable.swift | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift index a600fbd7..dd447026 100644 --- a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift @@ -95,13 +95,6 @@ public struct TextViewRepresentable: UIViewRepresentable { let textView = view var needsLayout = false - defer { - if needsLayout { - textView.invalidateIntrinsicContentSize() - textView.setNeedsLayout() - textView.layoutIfNeeded() - } - } if textView.frame.size.width != width { textView.frame.size.width = width @@ -115,6 +108,12 @@ public struct TextViewRepresentable: UIViewRepresentable { } else { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): reuse textView content") } + + if needsLayout { + textView.invalidateIntrinsicContentSize() + textView.setNeedsLayout() + textView.layoutIfNeeded() + } } public func makeCoordinator() -> Coordinator { From facf942795dd613a91f407ea795a204122bb0e86 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 15 Jun 2023 19:36:49 +0800 Subject: [PATCH 108/128] feat: move to Twitter Basic API --- TwidereSDK/Package.swift | 2 +- ...TwitterStatusFetchedResultController.swift | 2 +- .../Timeline/APIService+Timeline+Home.swift | 4 ++ .../StatusFetchViewModel+Timeline+User.swift | 52 +++++++++---------- .../Status/StatusFetchViewModel.swift | 2 + .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Diffable/Misc/TabBar/TabBarItem.swift | 1 + .../HostListStatusTimelineViewModel.swift | 9 +++- .../Profile/RemoteProfileViewModel.swift | 36 +++++++++---- .../Root/MainTab/MainTabBarController.swift | 2 +- .../Root/NewColumn/NewColumnViewModel.swift | 10 ++-- .../TimelineViewModel+LoadOldestState.swift | 2 + .../Base/Common/TimelineViewModel.swift | 7 +++ 13 files changed, 82 insertions(+), 51 deletions(-) diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index c5656ca6..c5c64465 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -50,7 +50,7 @@ let package = Package( .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.6.0"), .package(url: "https://github.com/tid-kijyun/Kanna.git", from: "5.2.7"), .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"), - .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.9.1"), + .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.10.0"), .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"), .package(name: "CoverFlowStackLayout", path: "../CoverFlowStackLayout"), ], diff --git a/TwidereSDK/Sources/TwidereCore/FetchedResultsController/Status/TwitterStatusFetchedResultController.swift b/TwidereSDK/Sources/TwidereCore/FetchedResultsController/Status/TwitterStatusFetchedResultController.swift index 5f6e033b..5568530b 100644 --- a/TwidereSDK/Sources/TwidereCore/FetchedResultsController/Status/TwitterStatusFetchedResultController.swift +++ b/TwidereSDK/Sources/TwidereCore/FetchedResultsController/Status/TwitterStatusFetchedResultController.swift @@ -103,7 +103,7 @@ extension TwitterStatusFetchedResultController { } self.statusIDs.value = result } - + } // MARK: - NSFetchedResultsControllerDelegate diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Home.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Home.swift index d0a93ac7..4cbbcaff 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Home.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Home.swift @@ -26,10 +26,14 @@ extension APIService { case persist([ManagedObjectRecord]) } + @available(*, deprecated, message: "") public func twitterHomeTimeline( query: Twitter.API.V2.User.Timeline.HomeQuery, authenticationContext: TwitterAuthenticationContext ) async throws -> [Twitter.Response.Content] { + assertionFailure() + throw Twitter.API.Error.InternalError(message: "API Deprecated") + #if DEBUG // log time cost let start = CACurrentMediaTime() diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift index c18a5d56..cd30f904 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift @@ -9,6 +9,8 @@ import os.log import Foundation import TwitterSDK import MastodonSDK +import CoreData +import CoreDataStack extension StatusFetchViewModel.Timeline { public enum User { } @@ -125,6 +127,7 @@ extension StatusFetchViewModel.Timeline.User { enum TwitterResponse { case v2(Twitter.Response.Content) case v1(Twitter.Response.Content<[Twitter.Entity.Tweet]>) + case local([TwitterStatus.ID]) func filter(fetchContext: TwitterFetchContext) -> StatusFetchViewModel.Result { switch self { @@ -135,6 +138,8 @@ extension StatusFetchViewModel.Timeline.User { case .v1(let response): let result = response.value.filter(fetchContext.filter.isIncluded) return .twitter(result) + case .local(let statusIDs): + return .twitterIDs(statusIDs) } } @@ -151,6 +156,8 @@ extension StatusFetchViewModel.Timeline.User { var fetchContext = fetchContext.map(maxID: maxID) fetchContext.needsAPIFallback = true return .twitter(fetchContext) + case .local: + return nil } } } @@ -200,35 +207,24 @@ extension StatusFetchViewModel.Timeline.User { } case .like: do { - guard !fetchContext.needsAPIFallback else { - throw Twitter.API.Error.ResponseError(httpResponseStatus: .ok, twitterAPIError: .rateLimitExceeded) + if fetchContext.paginationToken != nil { + throw AppError.implicit(.badRequest) } - let response = try await api.twitterLikeTimeline( - userID: fetchContext.userID, - query: .init( - sinceID: nil, - untilID: nil, - paginationToken: fetchContext.paginationToken, - maxResults: fetchContext.maxResults ?? 20, - onlyMedia: fetchContext.filter.rule.contains(.onlyMedia) - ), - authenticationContext: fetchContext.authenticationContext - ) - return .v2(response) - } catch let error as Twitter.API.Error.ResponseError where error.twitterAPIError == .rateLimitExceeded { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Rate Limit] fallback to v1") - let response = try await api.twitterLikeTimelineV1( - query: .init( - count: fetchContext.maxResults ?? 20, - userID: fetchContext.userID, - maxID: fetchContext.maxID, - sinceID: nil, - excludeReplies: false, - query: nil - ), - authenticationContext: fetchContext.authenticationContext - ) - return .v1(response) + + let managedObjectContext = api.coreDataStack.persistentContainer.viewContext + let statusIDs: [TwitterStatus.ID] = try await managedObjectContext.perform { + let userRequest = TwitterUser.sortedFetchRequest + userRequest.predicate = TwitterUser.predicate(id: fetchContext.userID) + guard let user = try managedObjectContext.fetch(userRequest).first else { + throw AppError.implicit(.badRequest) + } + let statusIDs = user.like.map { $0.id } + let statusRequest = TwitterStatus.sortedFetchRequest + statusRequest.predicate = TwitterStatus.predicate(ids: statusIDs) + let results = try managedObjectContext.fetch(statusRequest) + return results.map { $0.id } + } + return .local(statusIDs) } catch { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch failure: \(error.localizedDescription)") throw error diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel.swift index 758a9be5..05f830d4 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel.swift @@ -19,12 +19,14 @@ public enum StatusFetchViewModel { public enum Result { case twitter([Twitter.Entity.Tweet]) // v1 case twitterV2([Twitter.Entity.V2.Tweet]) // v2 + case twitterIDs([TwitterStatus.ID]) case mastodon([Mastodon.Entity.Status]) public var count: Int { switch self { case .twitter(let array): return array.count case .twitterV2(let array): return array.count + case .twitterIDs(let array): return array.count case .mastodon(let array): return array.count } } diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9d88ff9e..e45523af 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -258,8 +258,8 @@ "repositoryURL": "https://github.com/TwidereProject/TwitterSDK.git", "state": { "branch": null, - "revision": "8148362dbf900b6f6416e3d64b3332fc660bd844", - "version": "0.9.1" + "revision": "e6e0d3f1806f3eb04b8592f469c66d3dd0e1e969", + "version": "0.10.0" } }, { diff --git a/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift b/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift index 7c081596..7558aef4 100644 --- a/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift +++ b/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift @@ -133,6 +133,7 @@ extension TabBarItem { userIdentifier: authContext.authenticationContext.userIdentifier ) ) + _viewController.viewModel.isFloatyButtonDisplay = false viewController = _viewController case .history: let _viewController = HistoryViewController() diff --git a/TwidereX/Scene/List/HomeList/HostListStatusTimelineViewModel.swift b/TwidereX/Scene/List/HomeList/HostListStatusTimelineViewModel.swift index 93d5961b..02624645 100644 --- a/TwidereX/Scene/List/HomeList/HostListStatusTimelineViewModel.swift +++ b/TwidereX/Scene/List/HomeList/HostListStatusTimelineViewModel.swift @@ -50,7 +50,14 @@ final class HomeListStatusTimelineViewModel: ObservableObject { } self.homeTimelineMenuActionViewModels = { guard let authenticationIndex = authContext.authenticationContext.authenticationIndex(in: context.managedObjectContext) else { return [] } - return [HomeListMenuActionViewModel(timeline: .home(authenticationIndex))] + switch authContext.authenticationContext.platform { + case .twitter: + return [] + case .mastodon: + return [HomeListMenuActionViewModel(timeline: .home(authenticationIndex))] + case .none: + return [] + } }() // end init diff --git a/TwidereX/Scene/Profile/RemoteProfileViewModel.swift b/TwidereX/Scene/Profile/RemoteProfileViewModel.swift index b957a07f..6033842d 100644 --- a/TwidereX/Scene/Profile/RemoteProfileViewModel.swift +++ b/TwidereX/Scene/Profile/RemoteProfileViewModel.swift @@ -113,12 +113,16 @@ extension RemoteProfileViewModel { } extension RemoteProfileViewModel { - func findTwitterUser(userID: TwitterUser.ID) -> ManagedObjectRecord? { - let request = TwitterUser.sortedFetchRequest - request.predicate = TwitterUser.predicate(id: userID) - request.fetchLimit = 1 - guard let user = try? context.managedObjectContext.fetch(request).first else { return nil } - return .init(objectID: user.objectID) + func findTwitterUser(userID: TwitterUser.ID) async -> ManagedObjectRecord? { + let managedObjectContext = context.managedObjectContext + let _record: ManagedObjectRecord? = await managedObjectContext.perform { + let request = TwitterUser.sortedFetchRequest + request.predicate = TwitterUser.predicate(id: userID) + request.fetchLimit = 1 + guard let user = try? managedObjectContext.fetch(request).first else { return nil } + return user.asRecrod + } + return _record } func findMastodonUser(domain: String, userID: MastodonUser.ID) -> ManagedObjectRecord? { @@ -151,7 +155,7 @@ extension RemoteProfileViewModel { } // end switch }() guard let entity = response.value.data?.first else { return nil } - let record = findTwitterUser(userID: entity.id) + let record = await findTwitterUser(userID: entity.id) return record } @@ -186,8 +190,10 @@ extension RemoteProfileViewModel { let managedObjectContext = context.managedObjectContext let result: Bool = managedObjectContext.performAndWait { switch record { - case .twitter: - return true + case .twitter(let record): + guard case let .twitter(authenticationContext) = authContext.authenticationContext else { return false } + guard let object = record.object(in: managedObjectContext) else { return false } + return object.id == authenticationContext.userID case .mastodon(let record): guard case let .mastodon(authenticationContext) = authContext.authenticationContext else { return false } guard let object = record.object(in: managedObjectContext) else { return false } @@ -195,8 +201,16 @@ extension RemoteProfileViewModel { } } return result - case .twitter: - return true + case .twitter(let twitterContext): + guard case let .twitter(authenticationContext) = authContext.authenticationContext else { return false } + switch twitterContext { + case .userID(let userID): + return userID == authenticationContext.userID + case .username(let username): + let managedObjectContext = context.managedObjectContext + guard let object = authenticationContext.authenticationRecord.object(in: managedObjectContext) else { return false } + return username == object.user.username + } case .mastodon(let mastodonContext): guard case let .mastodon(authenticationContext) = authContext.authenticationContext else { return false } switch mastodonContext { diff --git a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift index a45cafae..ef878254 100644 --- a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift +++ b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift @@ -47,7 +47,7 @@ final class MainTabBarController: UITabBarController, NeedsDependency { case .twitter: return [ .homeList, - .notification, + // .notification, .search, .me, ] diff --git a/TwidereX/Scene/Root/NewColumn/NewColumnViewModel.swift b/TwidereX/Scene/Root/NewColumn/NewColumnViewModel.swift index 5af7bd74..3c41d7ec 100644 --- a/TwidereX/Scene/Root/NewColumn/NewColumnViewModel.swift +++ b/TwidereX/Scene/Root/NewColumn/NewColumnViewModel.swift @@ -24,8 +24,8 @@ final class NewColumnViewModel: ObservableObject { switch auth.authenticationContext { case .twitter: var results: [TabBarItem] = [ - .home, - .notification, + // .homeList, + // .notification, .search, .me, .likes, @@ -34,11 +34,10 @@ final class NewColumnViewModel: ObservableObject { results.append(.history) } results.append(.lists) - results.append(.trends) return results case .mastodon: var results: [TabBarItem] = [ - .home, + // .home, .notification, .search, .me, @@ -50,9 +49,8 @@ final class NewColumnViewModel: ObservableObject { results.append(.history) } results.append(.lists) - results.append(.trends) return results - } + } // end switch } // output diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift index 3b5bb6c9..83097e58 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift @@ -195,6 +195,8 @@ extension TimelineViewModel.LoadOldestState { case .twitterV2(let statuses): let statusIDs = statuses.map { $0.id } viewModel.statusRecordFetchedResultController.twitterStatusFetchedResultController.append(statusIDs: statusIDs) + case .twitterIDs(let statusIDs): + viewModel.statusRecordFetchedResultController.twitterStatusFetchedResultController.append(statusIDs: statusIDs) case .mastodon(let statuses): let statusIDs = statuses.map { $0.id } viewModel.statusRecordFetchedResultController.mastodonStatusFetchedResultController.append(statusIDs: statusIDs) diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift index 23bd88f7..f7378bff 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift @@ -12,6 +12,7 @@ import Combine import CoreDataStack import GameplayKit import TwidereCore +import TwitterSDK import func QuartzCore.CACurrentMediaTime class TimelineViewModel: TimelineViewModelDriver { @@ -120,6 +121,8 @@ extension TimelineViewModel { case .twitterV2(let array): let statusIDs = array.map { $0.id } statusRecordFetchedResultController.twitterStatusFetchedResultController.prepend(statusIDs: statusIDs) + case .twitterIDs(let statusIDs): + statusRecordFetchedResultController.twitterStatusFetchedResultController.prepend(statusIDs: statusIDs) case .mastodon(let array): let statusIDs = array.map { $0.id } statusRecordFetchedResultController.mastodonStatusFetchedResultController.prepend(statusIDs: statusIDs) @@ -184,6 +187,10 @@ extension TimelineViewModel { case .public, .hashtag, .list, .search, .user: await prepend(result: output.result) } + } catch let error as Twitter.API.Error.ResponseError where error.twitterAPIError == .rateLimitExceeded { + self.didLoadLatest.send() + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(error.localizedDescription)") + context.apiService.error.send(.explicit(.twitterResponseError(error))) } catch { self.didLoadLatest.send() logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(error.localizedDescription)") From bf5583df2e267ad7e8cb7b4dddcd4fa1c3f5dd71 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 15 Jun 2023 19:37:24 +0800 Subject: [PATCH 109/128] chore: update version to 2.0.0 (136) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 36 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index ea7e4eed..63c72766 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 135 + 136 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 16f087d4..78cf58d7 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 135 + 136 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 094934e4..7bd07508 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3467,7 +3467,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 135; + CURRENT_PROJECT_VERSION = 136; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3494,7 +3494,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 135; + CURRENT_PROJECT_VERSION = 136; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3517,7 +3517,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 135; + CURRENT_PROJECT_VERSION = 136; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3541,7 +3541,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 135; + CURRENT_PROJECT_VERSION = 136; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3568,7 +3568,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 135; + CURRENT_PROJECT_VERSION = 136; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3597,7 +3597,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 135; + CURRENT_PROJECT_VERSION = 136; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3626,7 +3626,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 135; + CURRENT_PROJECT_VERSION = 136; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3654,7 +3654,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 135; + CURRENT_PROJECT_VERSION = 136; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3680,7 +3680,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 135; + CURRENT_PROJECT_VERSION = 136; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3707,7 +3707,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 135; + CURRENT_PROJECT_VERSION = 136; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3861,7 +3861,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 135; + CURRENT_PROJECT_VERSION = 136; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3893,7 +3893,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 135; + CURRENT_PROJECT_VERSION = 136; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3920,7 +3920,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 135; + CURRENT_PROJECT_VERSION = 136; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3945,7 +3945,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 135; + CURRENT_PROJECT_VERSION = 136; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3968,7 +3968,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 135; + CURRENT_PROJECT_VERSION = 136; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3991,7 +3991,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 135; + CURRENT_PROJECT_VERSION = 136; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -4015,7 +4015,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 135; + CURRENT_PROJECT_VERSION = 136; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -4042,7 +4042,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 135; + CURRENT_PROJECT_VERSION = 136; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index 8149c3ff..ab71b5e2 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 135 + 136 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index 06462e58..e4be7a0d 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 135 + 136 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index 256cbafa..76ab8f26 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 135 + 136 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index 256cbafa..76ab8f26 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 135 + 136 From b284277697a9ec70dd48490a4e69c065af377eee Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 20 Jun 2023 19:12:38 +0800 Subject: [PATCH 110/128] fix: double tap on quote status issue. --- .../Sources/TwidereUI/Content/StatusView.swift | 9 ++++++++- .../Status/StatusViewTableViewCellDelegate.swift | 4 ++-- .../UIViewRepresentable/TextViewRepresentable.swift | 12 ++++++------ 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index d318ecd0..34d45795 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -26,7 +26,7 @@ public protocol StatusViewDelegate: AnyObject { func statusView(_ viewModel: StatusView.ViewModel, toggleContentDisplay isReveal: Bool) // meta - func statusView(_ viewModel: StatusView.ViewModel, textViewDidSelectMeta meta: Meta) + func statusView(_ viewModel: StatusView.ViewModel, textViewDidSelectMeta meta: Meta?) // media func statusView(_ viewModel: StatusView.ViewModel, mediaViewModel: MediaView.ViewModel, action: MediaView.ViewModel.Action) @@ -191,6 +191,7 @@ public struct StatusView: View { Color(uiColor: .label.withAlphaComponent(0.04)) } .cornerRadius(12) + .contentShape(Rectangle()) .onTapGesture { viewModel.delegate?.statusView(viewModel, quoteStatusViewDidPressed: quoteViewModel) } @@ -444,6 +445,9 @@ extension StatusView { } ) .frame(width: viewModel.contentWidth) + .onTapGesture { + // ignore tap + } } } @@ -459,6 +463,9 @@ extension StatusView { } ) .frame(width: viewModel.contentWidth) + .onTapGesture { + // ignore tap + } } } diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/Status/StatusViewTableViewCellDelegate.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/Status/StatusViewTableViewCellDelegate.swift index d5d663bf..dc336cfd 100644 --- a/TwidereSDK/Sources/TwidereUI/TableViewCell/Status/StatusViewTableViewCellDelegate.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/Status/StatusViewTableViewCellDelegate.swift @@ -26,7 +26,7 @@ public protocol StatusViewTableViewCellDelegate: AutoGenerateProtocolDelegate { // sourcery:inline:StatusViewTableViewCellDelegate.AutoGenerateProtocolDelegate func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, userAvatarButtonDidPressed user: UserRecord) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, toggleContentDisplay isReveal: Bool) - func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, textViewDidSelectMeta meta: Meta) + func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, textViewDidSelectMeta meta: Meta?) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, mediaViewModel: MediaView.ViewModel, action: MediaView.ViewModel.Action) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, toggleContentWarningOverlayDisplay isReveal: Bool) func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, pollVoteActionForViewModel pollViewModel: PollView.ViewModel) @@ -51,7 +51,7 @@ public extension StatusViewDelegate where Self: StatusViewContainerTableViewCell statusViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, toggleContentDisplay: isReveal) } - func statusView(_ viewModel: StatusView.ViewModel, textViewDidSelectMeta meta: Meta) { + func statusView(_ viewModel: StatusView.ViewModel, textViewDidSelectMeta meta: Meta?) { statusViewTableViewCellDelegate?.tableViewCell(self, viewModel: viewModel, textViewDidSelectMeta: meta) } diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift index dd447026..3c066b79 100644 --- a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift @@ -21,7 +21,7 @@ public struct TextViewRepresentable: UIViewRepresentable { let textStyle: TextStyle let width: CGFloat let isSelectable: Bool - let handler: (Meta) -> Void + let handler: (Meta?) -> Void // output let attributedString: NSAttributedString @@ -31,7 +31,7 @@ public struct TextViewRepresentable: UIViewRepresentable { textStyle: TextStyle, width: CGFloat, isSelectable: Bool, - handler: @escaping (Meta) -> Void + handler: @escaping (Meta?) -> Void ) { self.metaContent = metaContent self.textStyle = textStyle @@ -147,14 +147,14 @@ extension TextViewRepresentable.Coordinator: UITextViewDelegate { // MARK: - WrappedTextViewDelegate extension TextViewRepresentable.Coordinator: WrappedTextViewDelegate { - public func wrappedTextView(_ wrappedTextView: WrappedTextView, didSelectMeta meta: Meta) { + public func wrappedTextView(_ wrappedTextView: WrappedTextView, didSelectMeta meta: Meta?) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): meta: \(meta.debugDescription)") view.handler(meta) } } public protocol WrappedTextViewDelegate: AnyObject { - func wrappedTextView(_ wrappedTextView: WrappedTextView, didSelectMeta meta: Meta) + func wrappedTextView(_ wrappedTextView: WrappedTextView, didSelectMeta meta: Meta?) } public class WrappedTextView: UITextView { @@ -208,7 +208,7 @@ extension WrappedTextView { switch sender.state { case .ended: let point = sender.location(in: self) - guard let meta = meta(at: point) else { return } + let meta = meta(at: point) textViewDelegate?.wrappedTextView(self, didSelectMeta: meta) default: break @@ -219,7 +219,7 @@ extension WrappedTextView { extension WrappedTextView { public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { - return meta(at: point) != nil || isSelectable + return true } func meta(at point: CGPoint) -> Meta? { From 01bc32f4bfe985c21a050e80183c6666c562318f Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 20 Jun 2023 19:14:36 +0800 Subject: [PATCH 111/128] fix: Twitter mention pick --- .../Content/UserView+ViewModel.swift | 46 ++++++++----- .../Sources/TwidereUI/Content/UserView.swift | 13 +++- .../ComposeContent/ComposeContentView.swift | 23 ++----- .../ComposeContentViewController.swift | 57 ++++++++--------- .../ComposeContentViewModel.swift | 6 +- .../MentionPickViewController.swift | 52 ++++++++------- .../MentionPickViewModel+Diffable.swift | 64 +++++++++++-------- .../MentionPick/MentionPickViewModel.swift | 30 +++++---- ...ider+StatusViewTableViewCellDelegate.swift | 61 ++++-------------- 9 files changed, 177 insertions(+), 175 deletions(-) diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift index de83f88f..9fcbd7af 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift @@ -45,19 +45,7 @@ extension UserView { @Published public var isMyself: Bool = false @Published public var protected: Bool = false -// @Published public var authenticationContext: AuthenticationContext? // me -// @Published public var userAuthenticationContext: AuthenticationContext? -// -// @Published public var userIdentifier: UserIdentifier? = nil -// @Published public var avatarImageURL: URL? -// @Published public var avatarBadge: AvatarBadge = .none -// // TODO: verified | bot -// -// @Published public var name: MetaContent? = PlaintextMetaContent(string: " ") -// @Published public var username: String? -// - -// @Published public var followerCount: Int? + // TODO: verified | bot // follow request @Published public var isFollowRequestActionDisplay = false @@ -75,10 +63,8 @@ extension UserView { // notification count @Published public var notificationBadgeCount: Int = 0 -// public enum Header { -// case none -// case notification(info: NotificationHeaderInfo) -// } + @Published public var isSelectable: Bool = true + @Published public var isSelect: Bool = false private init( object user: UserObject?, @@ -257,6 +243,7 @@ extension UserView.ViewModel { case .notification: return false case .settingAccountSection: return false case .plain: return false + case .mentionPick: return false default: return true } } @@ -455,6 +442,31 @@ extension UserView.ViewModel { } } +extension UserView.ViewModel { + public convenience init( + item: MentionPickViewModel.Item, + delegate: UserViewDelegate? + ) { + self.init( + object: nil, + authContext: nil, + kind: .mentionPick, + delegate: delegate + ) + // end init + + // user + switch item { + case .twitterUser(let username, let attribute): + self.avatarURL = attribute.avatarImageURL + self.name = PlaintextMetaContent(string: attribute.name ?? "") + self.username = username + self.isSelectable = !attribute.disabled + self.isSelect = attribute.selected + } // switch + } +} + #if DEBUG extension UserView.ViewModel { public convenience init(kind: Kind) { diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift index e4022696..3c1f2017 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift @@ -288,6 +288,17 @@ extension UserView { Image(systemName: "\(count).circle.fill") } } + + var checkmarkView: some View { + Button { + // do nothing + } label: { + let name = viewModel.isSelect ? "checkmark.circle.fill" : "circle" + Image(systemName: name) + } + .buttonStyle(.borderless) + .disabled(!viewModel.isSelectable) + } } extension UserView { @@ -358,7 +369,7 @@ extension UserView { followRequestActionView } case .mentionPick: - EmptyView() + checkmarkView case .listMember: if viewModel.isMyList { menuView diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift index 33680088..f60b6532 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentView.swift @@ -22,7 +22,6 @@ public struct ComposeContentView: View { @ObservedObject var viewModel: ComposeContentViewModel - @State var mentionTextHeight: CGFloat = 0 @State var toolbarHeight: CGFloat = 0 @State var isPollExpireConfigurationPopoverPresent = false @@ -142,22 +141,12 @@ extension ComposeContentView { viewModel.mentionPickPublisher.send() } label: { HStack(spacing: .zero) { - VectorImageView( - image: Asset.Communication.textBubbleSmall.image.withRenderingMode(.alwaysTemplate), - tintColor: .tintColor - ) - .frame(width: mentionTextHeight, height: mentionTextHeight, alignment: .center) - Text(viewModel.mentionPickButtonTitle) - .font(.footnote) - .background(GeometryReader { geometry in - Color.clear.preference( - key: SizeDimensionPreferenceKey.self, - value: geometry.size.height - ) - }) - .onPreferenceChange(SizeDimensionPreferenceKey.self) { - mentionTextHeight = $0 - } + Label { + Text(viewModel.mentionPickButtonTitle) + } icon: { + Image(systemName: "text.bubble") + } + .font(.footnote) Spacer() } } diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewController.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewController.swift index 52de0679..0867b372 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewController.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -81,35 +81,34 @@ extension ComposeContentViewController { hostingViewController.didMove(toParent: self) // mention - pick action - // FIXME: TODO -// viewModel.mentionPickPublisher -// .receive(on: DispatchQueue.main) -// .sink { [weak self] _ in -// guard let self = self else { return } -// guard let authContext = self.viewModel.authContext else { return } -// guard let primaryItem = self.viewModel.primaryMentionPickItem else { return } -// -// let mentionPickViewModel = MentionPickViewModel( -// context: self.viewModel.context, -// authContext: authContext, -// primaryItem: primaryItem, -// secondaryItems: self.viewModel.secondaryMentionPickItems -// ) -// let mentionPickViewController = MentionPickViewController() -// mentionPickViewController.viewModel = mentionPickViewModel -// mentionPickViewController.delegate = self -// -// let navigationController = AdaptiveStatusBarStyleNavigationController(rootViewController: mentionPickViewController) -// navigationController.modalPresentationStyle = .pageSheet -// if let sheetPresentationController = navigationController.sheetPresentationController { -// sheetPresentationController.detents = [.medium(), .large()] -// sheetPresentationController.selectedDetentIdentifier = .medium -// sheetPresentationController.prefersScrollingExpandsWhenScrolledToEdge = false -// sheetPresentationController.prefersGrabberVisible = true -// } -// self.present(navigationController, animated: true, completion: nil) -// } -// .store(in: &disposeBag) + viewModel.mentionPickPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + guard let authContext = self.viewModel.authContext else { return } + guard let primaryItem = self.viewModel.primaryMentionPickItem else { return } + + let mentionPickViewModel = MentionPickViewModel( + context: self.viewModel.context, + authContext: authContext, + primaryItem: primaryItem, + secondaryItems: self.viewModel.secondaryMentionPickItems + ) + let mentionPickViewController = MentionPickViewController() + mentionPickViewController.viewModel = mentionPickViewModel + mentionPickViewController.delegate = self + + let navigationController = AdaptiveStatusBarStyleNavigationController(rootViewController: mentionPickViewController) + navigationController.modalPresentationStyle = .pageSheet + if let sheetPresentationController = navigationController.sheetPresentationController { + sheetPresentationController.detents = [.medium(), .large()] + sheetPresentationController.selectedDetentIdentifier = .medium + sheetPresentationController.prefersScrollingExpandsWhenScrolledToEdge = false + sheetPresentationController.prefersGrabberVisible = true + } + self.present(navigationController, animated: true, completion: nil) + } + .store(in: &disposeBag) // attachment - preview action viewModel.mediaPreviewPublisher diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift index 3704d244..3acb420a 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -200,6 +200,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { switch status { case .twitter(let status): // set mention + var usernames: [String] = [] self.primaryMentionPickItem = .twitterUser( username: status.author.username, attribute: MentionPickViewModel.Item.Attribute( @@ -211,10 +212,12 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { state: .finish ) ) + usernames.append(status.author.username) self.secondaryMentionPickItems = { var items: [MentionPickViewModel.Item] = [] for mention in status.entitiesTransient?.mentions ?? [] { let username = mention.username + guard !usernames.contains(username) else { continue } let item = MentionPickViewModel.Item.twitterUser( username: username, attribute: .init( @@ -226,6 +229,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { state: .loading ) ) + usernames.append(username) items.append(item) } return items @@ -385,7 +389,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { break } - let names = usernames.map { "@" + $0 } + let names = usernames.removingDuplicates().map { "@" + $0 } return ListFormatter.localizedString(byJoining: names) } .assign(to: &$mentionPickButtonTitle) diff --git a/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewController.swift b/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewController.swift index 9d9cd87d..a3b4d38b 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewController.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewController.swift @@ -49,12 +49,14 @@ extension MentionPickViewController { ]) tableView.delegate = self -// viewModel.setupDiffableDataSource( -// for: tableView, -// configuration: MentionPickViewModel.DataSourceConfiguration( -// userTableViewCellDelegate: self -// ) -// ) + viewModel.setupDiffableDataSource( + tableView: tableView, + context: viewModel.context, + authContext: viewModel.authContext, + configuration: .init( + userViewTableViewCellDelegate: self + ) + ) } } @@ -100,24 +102,20 @@ extension MentionPickViewController: UITableViewDelegate { } // MARK: - UserTableViewCellDelegate -//extension MentionPickViewController: UserViewTableViewCellDelegate { -// public func tableViewCell(_ cell: UITableViewCell, userView: UserView, menuActionDidPressed action: UserView.MenuAction, menuButton button: UIButton) { -// // do nothing -// } -// -// public func tableViewCell(_ cell: UITableViewCell, userView: UserView, friendshipButtonDidPressed button: UIButton) { -// // do nothing -// } -// -// public func tableViewCell(_ cell: UITableViewCell, userView: UserView, membershipButtonDidPressed button: UIButton) { -// // do nothing -// } -// -// public func tableViewCell(_ cell: UITableViewCell, userView: UserView, acceptFollowReqeustButtonDidPressed button: UIButton) { -// // do nothing -// } -// -// public func tableViewCell(_ cell: UITableViewCell, userView: UserView, rejectFollowReqeustButtonDidPressed button: UIButton) { -// // do nothing -// } -//} +extension MentionPickViewController: UserViewTableViewCellDelegate { + public func tableViewCell(_ cell: UITableViewCell, viewModel: UserView.ViewModel, userAvatarButtonDidPressed user: UserRecord) { + + } + + public func tableViewCell(_ cell: UITableViewCell, viewModel: UserView.ViewModel, menuActionDidPressed action: UserView.ViewModel.MenuAction) { + + } + + public func tableViewCell(_ cell: UITableViewCell, viewModel: UserView.ViewModel, listMembershipButtonDidPressed user: UserRecord) { + + } + + public func tableViewCell(_ cell: UITableViewCell, viewModel: UserView.ViewModel, followReqeustButtonDidPressed user: UserRecord, accept: Bool) { + + } +} diff --git a/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewModel+Diffable.swift b/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewModel+Diffable.swift index 556a4942..f24761e8 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewModel+Diffable.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewModel+Diffable.swift @@ -6,38 +6,52 @@ // import UIKit +import SwiftUI import CoreData import MetaTextKit extension MentionPickViewModel { - struct DataSourceConfiguration { - weak var userTableViewCellDelegate: UserViewTableViewCellDelegate? - } + struct Configuration { + weak var userViewTableViewCellDelegate: UserViewTableViewCellDelegate? + } + func setupDiffableDataSource( - for tableView: UITableView, - configuration: DataSourceConfiguration + tableView: UITableView, + context: AppContext, + authContext: AuthContext, + configuration: Configuration ) { -// tableView.register(UserMentionPickStyleTableViewCell.self, forCellReuseIdentifier: String(describing: UserMentionPickStyleTableViewCell.self)) -// -// diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in -// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserMentionPickStyleTableViewCell.self), for: indexPath) as! UserMentionPickStyleTableViewCell -// MentionPickViewModel.configure( -// cell: cell, -// item: item, -// configuration: configuration -// ) -// return cell -// } -// -// var snapshot = NSDiffableDataSourceSnapshot() -// snapshot.appendSections([.primary]) -// snapshot.appendItems([primaryItem], toSection: .primary) -// if !secondaryItems.isEmpty { -// snapshot.appendSections([.secondary]) -// snapshot.appendItems(secondaryItems, toSection: .secondary) -// } -// diffableDataSource?.apply(snapshot) + tableView.register(UserTableViewCell.self, forCellReuseIdentifier: String(describing: UserTableViewCell.self)) + + diffableDataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell + cell.userViewTableViewCellDelegate = configuration.userViewTableViewCellDelegate + + let viewModel = UserView.ViewModel( + item: item, + delegate: cell + ) + cell.contentConfiguration = UIHostingConfiguration { + UserView(viewModel: viewModel) + } + .margins(.vertical, 0) // remove vertical margins + switch item { + case .twitterUser(_, let attribute): + cell.selectionStyle = attribute.disabled ? .none : .default + } + + return cell + } + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.primary]) + snapshot.appendItems([primaryItem], toSection: .primary) + if !secondaryItems.isEmpty { + snapshot.appendSections([.secondary]) + snapshot.appendItems(secondaryItems, toSection: .secondary) + } + diffableDataSource?.apply(snapshot) } } diff --git a/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewModel.swift b/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewModel.swift index 5248ff05..2d6c3246 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/MentionPick/MentionPickViewModel.swift @@ -35,7 +35,7 @@ public final class MentionPickViewModel { self.context = context self.authContext = authContext self.primaryItem = primaryItem - self.secondaryItems = secondaryItems + self.secondaryItems = secondaryItems.removingDuplicates() // end init switch authContext.authenticationContext { @@ -62,14 +62,27 @@ extension MentionPickViewModel { public enum Item: Hashable { case twitterUser(username: String, attribute: Attribute) + + var id: String { + switch self { + case .twitterUser(let username, _): + return username + } + } + + public static func == (lhs: MentionPickViewModel.Item, rhs: MentionPickViewModel.Item) -> Bool { + return lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } } } extension MentionPickViewModel.Item { - public class Attribute: Hashable { + public class Attribute { - public let id = UUID() - public var state: State = .loading // input @@ -101,14 +114,9 @@ extension MentionPickViewModel.Item { return lhs.state == rhs.state && lhs.disabled == rhs.disabled && lhs.selected == rhs.selected && - lhs.avatarImageURL == rhs.avatarImageURL && - lhs.userID == rhs.userID - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(id) + lhs.avatarImageURL == rhs.avatarImageURL } - + } } diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift index f3f9d9b6..c49e27ac 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift @@ -54,50 +54,6 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC // MARK: - content extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { -// func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) { -// Task { -// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) -// guard let item = await item(from: source) else { -// assertionFailure() -// return -// } -// guard let status = await item.status(in: self.context.managedObjectContext) else { -// assertionFailure("only works for status data provider") -// return -// } -// -// await DataSourceFacade.responseToMetaTextAreaView( -// provider: self, -// target: .status, -// status: status, -// metaTextAreaView: metaTextAreaView, -// didSelectMeta: meta -// ) -// } -// } -// -// func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, quoteStatusView: StatusView, metaTextAreaView: MetaTextAreaView, didSelectMeta meta: Meta) { -// Task { -// let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) -// guard let item = await item(from: source) else { -// assertionFailure() -// return -// } -// guard let status = await item.status(in: self.context.managedObjectContext) else { -// assertionFailure("only works for status data provider") -// return -// } -// -// await DataSourceFacade.responseToMetaTextAreaView( -// provider: self, -// target: .quote, -// status: status, -// metaTextAreaView: metaTextAreaView, -// didSelectMeta: meta -// ) -// } -// } - func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, toggleContentDisplay isReveal: Bool) { Task { @MainActor in guard let status = viewModel.status?.asRecord else { return } @@ -110,10 +66,23 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC } // end Task } - func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, textViewDidSelectMeta meta: Meta) { + func tableViewCell(_ cell: UITableViewCell, viewModel: StatusView.ViewModel, textViewDidSelectMeta meta: Meta?) { Task { @MainActor in guard let status = viewModel.status?.asRecord else { return } + guard let meta = meta else { + switch viewModel.kind { + case .conversationRoot: + return + default: + await DataSourceFacade.coordinateToStatusThreadScene( + provider: self, + kind: .status(status) + ) + return + } + } + await DataSourceFacade.responseToMetaText( provider: self, status: status, @@ -121,10 +90,8 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthC ) } // end Task } - } - // MARK: - media extension StatusViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider & MediaPreviewableViewController { @MainActor From e485962e2ed5c144ab8056381ed61b405223bab6 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 20 Jun 2023 19:17:14 +0800 Subject: [PATCH 112/128] fix: Twitter list fetch logic and add new EmptyStateView for private list --- TwidereSDK/Package.swift | 2 +- .../TwidereCore/Error/EmptyState.swift | 43 ++ .../Model}/Hashtag/HashtagData.swift | 2 +- .../APIService/APIService+Status+List.swift | 1 + .../StatusFetchViewModel+Timeline+List.swift | 29 +- .../TwidereUI/Content/EmptyStateView.swift | 82 ++++ .../Extension/FLAnimatedImageView.swift | 0 .../Extension/PhotoLibraryService.swift | 0 .../Extension/SwiftMessages.swift | 0 .../Extension/UIAlertController.swift | 0 .../Extension/UIApplication.swift | 0 .../{ => Diffable}/Extension/UIImage.swift | 0 .../Extension/UITableView.swift | 0 .../Extension/UIViewController.swift | 0 .../HashtagTableViewCell+ViewModel.swift | 5 +- .../Hashtag}/HashtagTableViewCell.swift | 2 +- TwidereX.xcodeproj/project.pbxproj | 408 ++++++++---------- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../FollowerListViewController.swift | 34 +- .../FollowingListViewController.swift | 33 +- .../SecondaryContainerViewModel.swift | 21 +- .../Share/View/Container/EmptyStateView.swift | 133 ------ .../Base/Common/TimelineViewController.swift | 1 - .../Base/Common/TimelineViewModel.swift | 5 + .../Home/HomeTimelineViewController.swift | 17 +- .../List/ListStatusTimelineViewModel.swift | 2 +- 26 files changed, 399 insertions(+), 425 deletions(-) create mode 100644 TwidereSDK/Sources/TwidereCore/Error/EmptyState.swift rename {TwidereX/Diffable/Misc => TwidereSDK/Sources/TwidereCore/Model}/Hashtag/HashtagData.swift (92%) create mode 100644 TwidereSDK/Sources/TwidereUI/Content/EmptyStateView.swift rename TwidereSDK/Sources/TwidereUI/{ => Diffable}/Extension/FLAnimatedImageView.swift (100%) rename TwidereSDK/Sources/TwidereUI/{ => Diffable}/Extension/PhotoLibraryService.swift (100%) rename TwidereSDK/Sources/TwidereUI/{ => Diffable}/Extension/SwiftMessages.swift (100%) rename TwidereSDK/Sources/TwidereUI/{ => Diffable}/Extension/UIAlertController.swift (100%) rename TwidereSDK/Sources/TwidereUI/{ => Diffable}/Extension/UIApplication.swift (100%) rename TwidereSDK/Sources/TwidereUI/{ => Diffable}/Extension/UIImage.swift (100%) rename TwidereSDK/Sources/TwidereUI/{ => Diffable}/Extension/UITableView.swift (100%) rename TwidereSDK/Sources/TwidereUI/{ => Diffable}/Extension/UIViewController.swift (100%) rename {TwidereX/Scene/Search/SearchResult/Hashtag/Cell => TwidereSDK/Sources/TwidereUI/TableViewCell/Hashtag}/HashtagTableViewCell+ViewModel.swift (93%) rename {TwidereX/Scene/Search/SearchResult/Hashtag/Cell => TwidereSDK/Sources/TwidereUI/TableViewCell/Hashtag}/HashtagTableViewCell.swift (97%) delete mode 100644 TwidereX/Scene/Share/View/Container/EmptyStateView.swift diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index c5c64465..6c48eb1b 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -50,7 +50,7 @@ let package = Package( .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.6.0"), .package(url: "https://github.com/tid-kijyun/Kanna.git", from: "5.2.7"), .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"), - .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.10.0"), + .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.11.0"), .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"), .package(name: "CoverFlowStackLayout", path: "../CoverFlowStackLayout"), ], diff --git a/TwidereSDK/Sources/TwidereCore/Error/EmptyState.swift b/TwidereSDK/Sources/TwidereCore/Error/EmptyState.swift new file mode 100644 index 00000000..ac0e263f --- /dev/null +++ b/TwidereSDK/Sources/TwidereCore/Error/EmptyState.swift @@ -0,0 +1,43 @@ +// +// EmptyState.swift +// +// +// Created by MainasuK on 2023-06-20. +// + +import Foundation +import TwidereLocalization + +public enum EmptyState: Swift.Error { + case noResults + case unableToAccess +} + +extension EmptyState { + public var iconSystemName: String { + switch self { + case .noResults: + return "eye.slash" + case .unableToAccess: + return "exclamationmark.triangle" + } + } + + public var title: String { + switch self { + case .noResults: + return L10n.Common.Controls.List.noResults + case .unableToAccess: + return "Unable to access" + } + } + + public var subtitle: String? { + switch self { + case .noResults: + return nil + case .unableToAccess: + return nil + } + } +} diff --git a/TwidereX/Diffable/Misc/Hashtag/HashtagData.swift b/TwidereSDK/Sources/TwidereCore/Model/Hashtag/HashtagData.swift similarity index 92% rename from TwidereX/Diffable/Misc/Hashtag/HashtagData.swift rename to TwidereSDK/Sources/TwidereCore/Model/Hashtag/HashtagData.swift index c81cf27f..e8759410 100644 --- a/TwidereX/Diffable/Misc/Hashtag/HashtagData.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/Hashtag/HashtagData.swift @@ -13,7 +13,7 @@ import MastodonSDK // Maybe configure an in-memory CoreData persist coordinator better here // But a simple solution should also works -enum HashtagData: Hashable { +public enum HashtagData: Hashable { // case twitter(record: ManagedObjectRecord) case mastodon(data: Mastodon.Entity.Tag) } diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+List.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+List.swift index a6e793d6..5da1df49 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+List.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+List.swift @@ -23,6 +23,7 @@ extension APIService { let _listID: TwitterList.ID? = await managedObjectContext.perform { guard let list = list.object(in: managedObjectContext) else { return nil } + return list.id } guard let listID = _listID else { diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+List.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+List.swift index 063051b5..4aa5f6bc 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+List.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+List.swift @@ -190,25 +190,18 @@ extension StatusFetchViewModel.Timeline.List { query: query, authenticationContext: fetchContext.authenticationContext ) - return .v2(response) - } catch let error as Twitter.API.Error.ResponseError where error.twitterAPIError == .rateLimitExceeded { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Rate Limit] fallback to v1") - let managedObjectContext = api.backgroundManagedObjectContext - let _listID: TwitterList.ID? = await managedObjectContext.perform { - guard let list = fetchContext.list.object(in: managedObjectContext) else { return nil } - return list.id - } - guard let listID = _listID else { - throw AppError.implicit(.badRequest) + + let data = response.value.data ?? [] + if data.isEmpty { + try await fetchContext.managedObjectContext.perform { + guard let list = fetchContext.list.object(in: fetchContext.managedObjectContext) else { return } + if list.private { + throw EmptyState.unableToAccess + } + } } - let response = try await api.twitterListStatusesV1( - query: .init( - id: listID, - maxID: fetchContext.maxID - ), - authenticationContext: fetchContext.authenticationContext - ) - return .v1(response) + + return .v2(response) } catch { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch failure: \(error.localizedDescription)") throw error diff --git a/TwidereSDK/Sources/TwidereUI/Content/EmptyStateView.swift b/TwidereSDK/Sources/TwidereUI/Content/EmptyStateView.swift new file mode 100644 index 00000000..78a39873 --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Content/EmptyStateView.swift @@ -0,0 +1,82 @@ +// +// EmptyStateView.swift +// +// +// Created by MainasuK on 2023-06-20. +// + +import UIKit +import SwiftUI +import TwidereCore + +public struct EmptyStateView: View { + @ObservedObject public var viewModel: ViewModel + + public init(viewModel: EmptyStateView.ViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + VStack { + Spacer() + Spacer() + Spacer() + VStack { + if let iconSystemName = viewModel.iconSystemName { + Image(systemName: iconSystemName) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 120, height: 120) + .foregroundColor(.secondary) + .font(.title) + .opacity(0.5) + } + if let title = viewModel.title { + Text(verbatim: title) + .foregroundColor(.secondary) + .font(.headline) + } + if let subtitle = viewModel.subtitle { + Text(verbatim: subtitle) + .foregroundColor(.secondary) + .font(.subheadline) + } + } + Spacer() + Spacer() + Spacer() + Spacer() + } + } +} + +extension EmptyStateView { + public class ViewModel: ObservableObject { + // input + @Published public var emptyState: EmptyState? + + // ouptut + var iconSystemName: String? { + emptyState?.iconSystemName + } + var title: String? { + emptyState?.title + } + var subtitle: String? { + emptyState?.subtitle + } + + public init(emptyState: EmptyState? = nil) { + self.emptyState = emptyState + // end init + } + } +} + +struct EmptyStateView_Previews: PreviewProvider { + static var previews: some View { + EmptyStateView(viewModel: .init(emptyState: .noResults)) + EmptyStateView(viewModel: .init(emptyState: .unableToAccess)) + + } +} diff --git a/TwidereSDK/Sources/TwidereUI/Extension/FLAnimatedImageView.swift b/TwidereSDK/Sources/TwidereUI/Diffable/Extension/FLAnimatedImageView.swift similarity index 100% rename from TwidereSDK/Sources/TwidereUI/Extension/FLAnimatedImageView.swift rename to TwidereSDK/Sources/TwidereUI/Diffable/Extension/FLAnimatedImageView.swift diff --git a/TwidereSDK/Sources/TwidereUI/Extension/PhotoLibraryService.swift b/TwidereSDK/Sources/TwidereUI/Diffable/Extension/PhotoLibraryService.swift similarity index 100% rename from TwidereSDK/Sources/TwidereUI/Extension/PhotoLibraryService.swift rename to TwidereSDK/Sources/TwidereUI/Diffable/Extension/PhotoLibraryService.swift diff --git a/TwidereSDK/Sources/TwidereUI/Extension/SwiftMessages.swift b/TwidereSDK/Sources/TwidereUI/Diffable/Extension/SwiftMessages.swift similarity index 100% rename from TwidereSDK/Sources/TwidereUI/Extension/SwiftMessages.swift rename to TwidereSDK/Sources/TwidereUI/Diffable/Extension/SwiftMessages.swift diff --git a/TwidereSDK/Sources/TwidereUI/Extension/UIAlertController.swift b/TwidereSDK/Sources/TwidereUI/Diffable/Extension/UIAlertController.swift similarity index 100% rename from TwidereSDK/Sources/TwidereUI/Extension/UIAlertController.swift rename to TwidereSDK/Sources/TwidereUI/Diffable/Extension/UIAlertController.swift diff --git a/TwidereSDK/Sources/TwidereUI/Extension/UIApplication.swift b/TwidereSDK/Sources/TwidereUI/Diffable/Extension/UIApplication.swift similarity index 100% rename from TwidereSDK/Sources/TwidereUI/Extension/UIApplication.swift rename to TwidereSDK/Sources/TwidereUI/Diffable/Extension/UIApplication.swift diff --git a/TwidereSDK/Sources/TwidereUI/Extension/UIImage.swift b/TwidereSDK/Sources/TwidereUI/Diffable/Extension/UIImage.swift similarity index 100% rename from TwidereSDK/Sources/TwidereUI/Extension/UIImage.swift rename to TwidereSDK/Sources/TwidereUI/Diffable/Extension/UIImage.swift diff --git a/TwidereSDK/Sources/TwidereUI/Extension/UITableView.swift b/TwidereSDK/Sources/TwidereUI/Diffable/Extension/UITableView.swift similarity index 100% rename from TwidereSDK/Sources/TwidereUI/Extension/UITableView.swift rename to TwidereSDK/Sources/TwidereUI/Diffable/Extension/UITableView.swift diff --git a/TwidereSDK/Sources/TwidereUI/Extension/UIViewController.swift b/TwidereSDK/Sources/TwidereUI/Diffable/Extension/UIViewController.swift similarity index 100% rename from TwidereSDK/Sources/TwidereUI/Extension/UIViewController.swift rename to TwidereSDK/Sources/TwidereUI/Diffable/Extension/UIViewController.swift diff --git a/TwidereX/Scene/Search/SearchResult/Hashtag/Cell/HashtagTableViewCell+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/Hashtag/HashtagTableViewCell+ViewModel.swift similarity index 93% rename from TwidereX/Scene/Search/SearchResult/Hashtag/Cell/HashtagTableViewCell+ViewModel.swift rename to TwidereSDK/Sources/TwidereUI/TableViewCell/Hashtag/HashtagTableViewCell+ViewModel.swift index 7cdd6311..1e3e13b3 100644 --- a/TwidereX/Scene/Search/SearchResult/Hashtag/Cell/HashtagTableViewCell+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/Hashtag/HashtagTableViewCell+ViewModel.swift @@ -10,6 +10,7 @@ import UIKit import Combine import Meta import MastodonSDK +import TwidereCore extension HashtagTableViewCell { final class ViewModel: ObservableObject { @@ -38,14 +39,14 @@ extension HashtagTableViewCell.ViewModel { } extension HashtagTableViewCell { - func configure(hashtagData: HashtagData) { + public func configure(hashtagData: HashtagData) { switch hashtagData { case .mastodon(let tag): configure(tag: tag) } } - func configure(tag: Mastodon.Entity.Tag) { + public func configure(tag: Mastodon.Entity.Tag) { // primary let primaryContent = Meta.convert(document: .plaintext(string: "#" + tag.name)) viewModel.primaryContent = primaryContent diff --git a/TwidereX/Scene/Search/SearchResult/Hashtag/Cell/HashtagTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/Hashtag/HashtagTableViewCell.swift similarity index 97% rename from TwidereX/Scene/Search/SearchResult/Hashtag/Cell/HashtagTableViewCell.swift rename to TwidereSDK/Sources/TwidereUI/TableViewCell/Hashtag/HashtagTableViewCell.swift index 428d436d..508e4847 100644 --- a/TwidereX/Scene/Search/SearchResult/Hashtag/Cell/HashtagTableViewCell.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/Hashtag/HashtagTableViewCell.swift @@ -11,7 +11,7 @@ import TwidereCore import MetaTextKit import MetaLabel -final class HashtagTableViewCell: UITableViewCell { +final public class HashtagTableViewCell: UITableViewCell { let primaryLabel = MetaLabel(style: .hashtagTitle) let secondaryLabel = MetaLabel(style: .hashtagDescription) diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 7bd07508..3c2ba9fc 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -21,8 +21,6 @@ DB01A9A427637FE60055FABC /* DataSourceFacade+Meta.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB01A9A327637FE60055FABC /* DataSourceFacade+Meta.swift */; }; DB02C76D27350D71007EA0BF /* SearchHashtagViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02C76C27350D71007EA0BF /* SearchHashtagViewController.swift */; }; DB02C77027350D8A007EA0BF /* SearchHashtagViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02C76F27350D8A007EA0BF /* SearchHashtagViewModel.swift */; }; - DB02C77227351B7D007EA0BF /* HashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02C77127351B7D007EA0BF /* HashtagTableViewCell.swift */; }; - DB02C77527351DA2007EA0BF /* HashtagTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02C77427351DA2007EA0BF /* HashtagTableViewCell+ViewModel.swift */; }; DB02C777273520B8007EA0BF /* SearchHashtagViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02C776273520B8007EA0BF /* SearchHashtagViewModel+State.swift */; }; DB02C77927352217007EA0BF /* SearchHashtagViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02C77827352217007EA0BF /* SearchHashtagViewModel+Diffable.swift */; }; DB0455B62A1CA2F3009A00EF /* NewColumnViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0455B52A1CA2F3009A00EF /* NewColumnViewModel.swift */; }; @@ -45,6 +43,8 @@ DB148B03281A7AB300B596C7 /* ContentSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB148B02281A7AB300B596C7 /* ContentSplitViewController.swift */; }; DB148B05281A81AE00B596C7 /* SidebarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB148B04281A81AE00B596C7 /* SidebarViewModel.swift */; }; DB148B0C281A837E00B596C7 /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB148B0B281A837E00B596C7 /* SidebarView.swift */; }; + DB1B80462A403B0A00C90A7D /* UserItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1B80442A403B0A00C90A7D /* UserItem.swift */; }; + DB1B80472A403B0A00C90A7D /* UserSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1B80452A403B0A00C90A7D /* UserSection.swift */; }; DB1D3DEA289388CD008F0BD0 /* HistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D3DE9289388CD008F0BD0 /* HistoryViewController.swift */; }; DB1D3DED28938ACD008F0BD0 /* HistoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D3DEC28938ACD008F0BD0 /* HistoryViewModel.swift */; }; DB1D3DEF28938CD1008F0BD0 /* StatusHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D3DEE28938CD1008F0BD0 /* StatusHistoryViewController.swift */; }; @@ -53,8 +53,6 @@ DB1D7B5525B5A87200397DCD /* TwitterAuthenticationOptionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D7B5425B5A87200397DCD /* TwitterAuthenticationOptionViewModel.swift */; }; DB1E48122772CE380074F6A0 /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB36F375257F79DB0028F81E /* SearchViewModel.swift */; }; DB1E48142772CE850074F6A0 /* SearchViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E48132772CE850074F6A0 /* SearchViewModel+Diffable.swift */; }; - DB1E48162772CEC20074F6A0 /* SearchSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E48152772CEC20074F6A0 /* SearchSection.swift */; }; - DB1E48192772CECC0074F6A0 /* SearchItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E48182772CECC0074F6A0 /* SearchItem.swift */; }; DB235EF42834DD0900398FCA /* SettingListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB235EF32834DD0900398FCA /* SettingListViewModel.swift */; }; DB235EF62834DDD200398FCA /* AboutViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB235EF52834DDD200398FCA /* AboutViewModel.swift */; }; DB25C4C527798E1A00EC1435 /* DataSourceFacade+SavedSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB25C4C427798E1A00EC1435 /* DataSourceFacade+SavedSearch.swift */; }; @@ -123,8 +121,6 @@ DB47AB1A27CCB7EC00CD73C7 /* ListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47AB1927CCB7EC00CD73C7 /* ListViewController.swift */; }; DB47AB1D27CCB88000CD73C7 /* ListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47AB1C27CCB88000CD73C7 /* ListViewModel.swift */; }; DB47AB2D27CE085900CD73C7 /* ListViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47AB2C27CE085800CD73C7 /* ListViewModel+Diffable.swift */; }; - DB47AB2E27CE097C00CD73C7 /* ListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47AB1F27CCC18500CD73C7 /* ListItem.swift */; }; - DB47AB2F27CE097C00CD73C7 /* ListSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47AB2027CCC18500CD73C7 /* ListSection.swift */; }; DB51DC1A2715581E00A0D8FB /* ProfileDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51DC192715581E00A0D8FB /* ProfileDashboardView.swift */; }; DB51DC1C2715588E00A0D8FB /* ProfileDashboardMeterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51DC1B2715588E00A0D8FB /* ProfileDashboardMeterView.swift */; }; DB51DC3B2716F82000A0D8FB /* CellFrameCacheContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51DC3A2716F82000A0D8FB /* CellFrameCacheContainer.swift */; }; @@ -167,7 +163,6 @@ DB5FD9B726D8EFF700CF5439 /* UserBriefInfoView+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5FD9B626D8EFF700CF5439 /* UserBriefInfoView+ViewModel.swift */; }; DB657A7B25AD574D001339B6 /* UIBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB657A7A25AD574D001339B6 /* UIBarButtonItem.swift */; }; DB66DB8D2823A7C80071F5F3 /* SecondaryTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66DB8C2823A7C80071F5F3 /* SecondaryTabBarController.swift */; }; - DB66DB902823AC3C0071F5F3 /* TabBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66DB8F2823AC3C0071F5F3 /* TabBarItem.swift */; }; DB67610D254AD681006C6798 /* ActivityIndicatorCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB67610C254AD681006C6798 /* ActivityIndicatorCollectionViewCell.swift */; }; DB676161254BE580006C6798 /* MediaPreviewCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB676160254BE580006C6798 /* MediaPreviewCollectionViewCell.swift */; }; DB697DF3278FDDF7004EF2F7 /* TimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB697DF2278FDDF7004EF2F7 /* TimelineViewModel.swift */; }; @@ -228,19 +223,6 @@ DB83020D273D160400BF5224 /* NotificationTimelineViewModel+LoadOldestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB83020C273D160400BF5224 /* NotificationTimelineViewModel+LoadOldestState.swift */; }; DB8302142742180C00BF5224 /* NotificationTimelineViewController+DebugAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8302132742180C00BF5224 /* NotificationTimelineViewController+DebugAction.swift */; }; DB86433C26E898C5000C9879 /* DataSourceFacade+Like.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB86433B26E898C5000C9879 /* DataSourceFacade+Like.swift */; }; - DB8761BE274552F800BA7EE2 /* StatusMediaGallerySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8761AA2745515F00BA7EE2 /* StatusMediaGallerySection.swift */; }; - DB8761BF274552F800BA7EE2 /* StatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8761AD2745515F00BA7EE2 /* StatusItem.swift */; }; - DB8761C0274552F800BA7EE2 /* StatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8761AE2745515F00BA7EE2 /* StatusSection.swift */; }; - DB8761C1274552F800BA7EE2 /* UserItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8761A42745515F00BA7EE2 /* UserItem.swift */; }; - DB8761C2274552F800BA7EE2 /* UserSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8761A82745515F00BA7EE2 /* UserSection.swift */; }; - DB8761C3274552FB00BA7EE2 /* HashtagSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8761B22745515F00BA7EE2 /* HashtagSection.swift */; }; - DB8761C4274552FB00BA7EE2 /* HashtagData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8761B12745515F00BA7EE2 /* HashtagData.swift */; }; - DB8761C5274552FB00BA7EE2 /* HashtagItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8761B32745515F00BA7EE2 /* HashtagItem.swift */; }; - DB8761C6274552FF00BA7EE2 /* NotificationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8761B72745515F00BA7EE2 /* NotificationItem.swift */; }; - DB8761C7274552FF00BA7EE2 /* NotificationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8761B52745515F00BA7EE2 /* NotificationSection.swift */; }; - DB8761CB2745530200BA7EE2 /* CoverFlowStackSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8761BB2745515F00BA7EE2 /* CoverFlowStackSection.swift */; }; - DB8761CC2745530200BA7EE2 /* CoverFlowStackItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8761BC2745515F00BA7EE2 /* CoverFlowStackItem.swift */; }; - DB8761D02745553700BA7EE2 /* MediaSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8761CF2745553600BA7EE2 /* MediaSection.swift */; }; DB88AC3C250B26F40009E562 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB88AC3B250B26F40009E562 /* Preview Assets.xcassets */; }; DB88E626288EA1F7009A01F5 /* DisplayPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB88E625288EA1F7009A01F5 /* DisplayPreferenceView.swift */; }; DB88E6292890DF67009A01F5 /* BehaviorsPreferenceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB88E6282890DF67009A01F5 /* BehaviorsPreferenceViewController.swift */; }; @@ -291,6 +273,25 @@ DBB47DA926EB3AA2001590F7 /* ProfileFieldListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB47DA826EB3AA2001590F7 /* ProfileFieldListView.swift */; }; DBB47DAB26EB3BF8001590F7 /* ProfileFieldContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB47DAA26EB3BF8001590F7 /* ProfileFieldContentView.swift */; }; DBB47DAD26EB440C001590F7 /* ProfileFieldCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB47DAC26EB440C001590F7 /* ProfileFieldCollectionViewCell.swift */; }; + DBBA21152A403BAA00CEBCF8 /* TabBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1B802D2A4030AA00C90A7D /* TabBarItem.swift */; }; + DBBA21162A403BAF00CEBCF8 /* SidebarSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1B80232A4030AA00C90A7D /* SidebarSection.swift */; }; + DBBA21172A403BB100CEBCF8 /* SidebarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1B80242A4030AA00C90A7D /* SidebarItem.swift */; }; + DBBA21182A403BB600CEBCF8 /* HashtagItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1B80282A4030AA00C90A7D /* HashtagItem.swift */; }; + DBBA21192A403BB600CEBCF8 /* HashtagSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1B80272A4030AA00C90A7D /* HashtagSection.swift */; }; + DBBA211A2A403BBA00CEBCF8 /* NotificationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1B802B2A4030AA00C90A7D /* NotificationItem.swift */; }; + DBBA211B2A403BBA00CEBCF8 /* NotificationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1B802A2A4030AA00C90A7D /* NotificationSection.swift */; }; + DBBA211C2A403BBE00CEBCF8 /* CoverFlowStackSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1B802F2A4030AA00C90A7D /* CoverFlowStackSection.swift */; }; + DBBA211D2A403BBE00CEBCF8 /* CoverFlowStackItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1B80302A4030AA00C90A7D /* CoverFlowStackItem.swift */; }; + DBBA211E2A403BC500CEBCF8 /* SearchSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1B80322A4030AA00C90A7D /* SearchSection.swift */; }; + DBBA211F2A403BC500CEBCF8 /* SearchItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1B80332A4030AA00C90A7D /* SearchItem.swift */; }; + DBBA21202A403BC900CEBCF8 /* ListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1B80352A4030AA00C90A7D /* ListItem.swift */; }; + DBBA21212A403BC900CEBCF8 /* ListSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1B80362A4030AA00C90A7D /* ListSection.swift */; }; + DBBA21222A403BCC00CEBCF8 /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1B80392A4030AA00C90A7D /* HistoryItem.swift */; }; + DBBA21232A403BCC00CEBCF8 /* HistorySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1B80382A4030AA00C90A7D /* HistorySection.swift */; }; + DBBA21242A403BCF00CEBCF8 /* MediaSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1B803B2A4030AA00C90A7D /* MediaSection.swift */; }; + DBBA21252A403BE100CEBCF8 /* StatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1B80422A4030AA00C90A7D /* StatusSection.swift */; }; + DBBA21262A403BE100CEBCF8 /* StatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1B80412A4030AA00C90A7D /* StatusItem.swift */; }; + DBBA21272A403BE100CEBCF8 /* StatusMediaGallerySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1B80402A4030AA00C90A7D /* StatusMediaGallerySection.swift */; }; DBBBBE592744E800007ACB4B /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBBBE582744E800007ACB4B /* ComposeViewController.swift */; }; DBBBBE612744E8CC007ACB4B /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBBBE602744E8CC007ACB4B /* ComposeViewController.swift */; }; DBBBBE642744E8CC007ACB4B /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DBBBBE622744E8CC007ACB4B /* MainInterface.storyboard */; }; @@ -314,8 +315,6 @@ DBD0B4992758B58F0015A388 /* DrawerSidebarAnimatedTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB761E3E25592AA20050DC01 /* DrawerSidebarAnimatedTransitioning.swift */; }; DBD0B49A2758B5A60015A388 /* DrawerSidebarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB761E69255946160050DC01 /* DrawerSidebarViewModel.swift */; }; DBD0B49B2758B5A60015A388 /* DrawerSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBEFDC8725591F5C0086F268 /* DrawerSidebarViewController.swift */; }; - DBD0B49D2758B5F50015A388 /* SidebarSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD0B49C2758B5F50015A388 /* SidebarSection.swift */; }; - DBD0B4A02758B6010015A388 /* SidebarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD0B49F2758B6010015A388 /* SidebarItem.swift */; }; DBD40B852599B9C2006E4ABC /* CombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD40B842599B9C2006E4ABC /* CombineTests.swift */; }; DBDA7F1A27D1CBCA00BA6BE1 /* TwidereXUITests+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBDA7F1927D1CBCA00BA6BE1 /* TwidereXUITests+Onboarding.swift */; }; DBDA7F1E27D2256400BA6BE1 /* ListViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBDA7F1D27D2256400BA6BE1 /* ListViewModel+State.swift */; }; @@ -342,7 +341,6 @@ DBF3309925B988A500A678FB /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DBF3309825B988A500A678FB /* Settings.bundle */; }; DBF639F2259B13BB009E12C8 /* TimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF639F1259B13BB009E12C8 /* TimelineHeaderView.swift */; }; DBF639F7259B1728009E12C8 /* TimelineHeaderCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF639F6259B1728009E12C8 /* TimelineHeaderCollectionViewCell.swift */; }; - DBF639FC259B333A009E12C8 /* EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF639FB259B333A009E12C8 /* EmptyStateView.swift */; }; DBF63A04259B4811009E12C8 /* TouchBlockingCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF63A03259B4811009E12C8 /* TouchBlockingCollectionView.swift */; }; DBF63A38259D7CCB009E12C8 /* CornerSmoothPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF63A37259D7CCB009E12C8 /* CornerSmoothPreviewViewController.swift */; }; DBF69EE02549705A00E2A915 /* ViewControllerAnimatedTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF69EDE2549705A00E2A915 /* ViewControllerAnimatedTransitioning.swift */; }; @@ -365,8 +363,6 @@ DBFCEF1528939CAC00EEBFB1 /* UserHistoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCEF1428939CAC00EEBFB1 /* UserHistoryViewModel.swift */; }; DBFCEF172893B92B00EEBFB1 /* StatusHistoryViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCEF162893B92B00EEBFB1 /* StatusHistoryViewModel+Diffable.swift */; }; DBFCEF192893BA8A00EEBFB1 /* StatusHistoryViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCEF182893BA8A00EEBFB1 /* StatusHistoryViewController+DataSourceProvider.swift */; }; - DBFCEF1B2893D5C500EEBFB1 /* HistorySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCEF1A2893D5C500EEBFB1 /* HistorySection.swift */; }; - DBFCEF1E2893D5D400EEBFB1 /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCEF1D2893D5D400EEBFB1 /* HistoryItem.swift */; }; DBFCEF202893E18400EEBFB1 /* DataSourceFacade+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCEF1F2893E18400EEBFB1 /* DataSourceFacade+History.swift */; }; DBFDCE4127F450FC00BE99E3 /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DBFDCE0327F446BB00BE99E3 /* Intents.framework */; }; DBFDCE4427F450FC00BE99E3 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFDCE4327F450FC00BE99E3 /* IntentHandler.swift */; }; @@ -488,8 +484,6 @@ DB01A9A327637FE60055FABC /* DataSourceFacade+Meta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Meta.swift"; sourceTree = ""; }; DB02C76C27350D71007EA0BF /* SearchHashtagViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHashtagViewController.swift; sourceTree = ""; }; DB02C76F27350D8A007EA0BF /* SearchHashtagViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHashtagViewModel.swift; sourceTree = ""; }; - DB02C77127351B7D007EA0BF /* HashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTableViewCell.swift; sourceTree = ""; }; - DB02C77427351DA2007EA0BF /* HashtagTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTableViewCell+ViewModel.swift"; sourceTree = ""; }; DB02C776273520B8007EA0BF /* SearchHashtagViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHashtagViewModel+State.swift"; sourceTree = ""; }; DB02C77827352217007EA0BF /* SearchHashtagViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHashtagViewModel+Diffable.swift"; sourceTree = ""; }; DB0455B52A1CA2F3009A00EF /* NewColumnViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewColumnViewModel.swift; sourceTree = ""; }; @@ -507,6 +501,27 @@ DB148B02281A7AB300B596C7 /* ContentSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentSplitViewController.swift; sourceTree = ""; }; DB148B04281A81AE00B596C7 /* SidebarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarViewModel.swift; sourceTree = ""; }; DB148B0B281A837E00B596C7 /* SidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarView.swift; sourceTree = ""; }; + DB1B80232A4030AA00C90A7D /* SidebarSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarSection.swift; sourceTree = ""; }; + DB1B80242A4030AA00C90A7D /* SidebarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarItem.swift; sourceTree = ""; }; + DB1B80272A4030AA00C90A7D /* HashtagSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagSection.swift; sourceTree = ""; }; + DB1B80282A4030AA00C90A7D /* HashtagItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagItem.swift; sourceTree = ""; }; + DB1B802A2A4030AA00C90A7D /* NotificationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSection.swift; sourceTree = ""; }; + DB1B802B2A4030AA00C90A7D /* NotificationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItem.swift; sourceTree = ""; }; + DB1B802D2A4030AA00C90A7D /* TabBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarItem.swift; sourceTree = ""; }; + DB1B802F2A4030AA00C90A7D /* CoverFlowStackSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverFlowStackSection.swift; sourceTree = ""; }; + DB1B80302A4030AA00C90A7D /* CoverFlowStackItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverFlowStackItem.swift; sourceTree = ""; }; + DB1B80322A4030AA00C90A7D /* SearchSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSection.swift; sourceTree = ""; }; + DB1B80332A4030AA00C90A7D /* SearchItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchItem.swift; sourceTree = ""; }; + DB1B80352A4030AA00C90A7D /* ListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListItem.swift; sourceTree = ""; }; + DB1B80362A4030AA00C90A7D /* ListSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListSection.swift; sourceTree = ""; }; + DB1B80382A4030AA00C90A7D /* HistorySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistorySection.swift; sourceTree = ""; }; + DB1B80392A4030AA00C90A7D /* HistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryItem.swift; sourceTree = ""; }; + DB1B803B2A4030AA00C90A7D /* MediaSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSection.swift; sourceTree = ""; }; + DB1B80402A4030AA00C90A7D /* StatusMediaGallerySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMediaGallerySection.swift; sourceTree = ""; }; + DB1B80412A4030AA00C90A7D /* StatusItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusItem.swift; sourceTree = ""; }; + DB1B80422A4030AA00C90A7D /* StatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusSection.swift; sourceTree = ""; }; + DB1B80442A403B0A00C90A7D /* UserItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserItem.swift; sourceTree = ""; }; + DB1B80452A403B0A00C90A7D /* UserSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserSection.swift; sourceTree = ""; }; DB1D3DE9289388CD008F0BD0 /* HistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewController.swift; sourceTree = ""; }; DB1D3DEC28938ACD008F0BD0 /* HistoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryViewModel.swift; sourceTree = ""; }; DB1D3DEE28938CD1008F0BD0 /* StatusHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusHistoryViewController.swift; sourceTree = ""; }; @@ -514,8 +529,6 @@ DB1D7B4225B5938300397DCD /* TwitterAuthenticationOptionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterAuthenticationOptionViewController.swift; sourceTree = ""; }; DB1D7B5425B5A87200397DCD /* TwitterAuthenticationOptionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterAuthenticationOptionViewModel.swift; sourceTree = ""; }; DB1E48132772CE850074F6A0 /* SearchViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewModel+Diffable.swift"; sourceTree = ""; }; - DB1E48152772CEC20074F6A0 /* SearchSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSection.swift; sourceTree = ""; }; - DB1E48182772CECC0074F6A0 /* SearchItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchItem.swift; sourceTree = ""; }; DB235EF32834DD0900398FCA /* SettingListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingListViewModel.swift; sourceTree = ""; }; DB235EF52834DDD200398FCA /* AboutViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewModel.swift; sourceTree = ""; }; DB25C4C427798E1A00EC1435 /* DataSourceFacade+SavedSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+SavedSearch.swift"; sourceTree = ""; }; @@ -586,8 +599,6 @@ DB46D12227DB6223003B8BA1 /* ListUserViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ListUserViewController+DataSourceProvider.swift"; sourceTree = ""; }; DB47AB1927CCB7EC00CD73C7 /* ListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewController.swift; sourceTree = ""; }; DB47AB1C27CCB88000CD73C7 /* ListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewModel.swift; sourceTree = ""; }; - DB47AB1F27CCC18500CD73C7 /* ListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListItem.swift; sourceTree = ""; }; - DB47AB2027CCC18500CD73C7 /* ListSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListSection.swift; sourceTree = ""; }; DB47AB2C27CE085800CD73C7 /* ListViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ListViewModel+Diffable.swift"; sourceTree = ""; }; DB51DC192715581E00A0D8FB /* ProfileDashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDashboardView.swift; sourceTree = ""; }; DB51DC1B2715588E00A0D8FB /* ProfileDashboardMeterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDashboardMeterView.swift; sourceTree = ""; }; @@ -628,7 +639,6 @@ DB60C1A82762394E00628235 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; }; DB657A7A25AD574D001339B6 /* UIBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIBarButtonItem.swift; sourceTree = ""; }; DB66DB8C2823A7C80071F5F3 /* SecondaryTabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryTabBarController.swift; sourceTree = ""; }; - DB66DB8F2823AC3C0071F5F3 /* TabBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarItem.swift; sourceTree = ""; }; DB67610C254AD681006C6798 /* ActivityIndicatorCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorCollectionViewCell.swift; sourceTree = ""; }; DB676160254BE580006C6798 /* MediaPreviewCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewCollectionViewCell.swift; sourceTree = ""; }; DB67616B254C021A006C6798 /* SearchUserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchUserViewController.swift; sourceTree = ""; }; @@ -683,19 +693,6 @@ DB83020C273D160400BF5224 /* NotificationTimelineViewModel+LoadOldestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationTimelineViewModel+LoadOldestState.swift"; sourceTree = ""; }; DB8302132742180C00BF5224 /* NotificationTimelineViewController+DebugAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationTimelineViewController+DebugAction.swift"; sourceTree = ""; }; DB86433B26E898C5000C9879 /* DataSourceFacade+Like.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Like.swift"; sourceTree = ""; }; - DB8761A42745515F00BA7EE2 /* UserItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserItem.swift; sourceTree = ""; }; - DB8761A82745515F00BA7EE2 /* UserSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSection.swift; sourceTree = ""; }; - DB8761AA2745515F00BA7EE2 /* StatusMediaGallerySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMediaGallerySection.swift; sourceTree = ""; }; - DB8761AD2745515F00BA7EE2 /* StatusItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusItem.swift; sourceTree = ""; }; - DB8761AE2745515F00BA7EE2 /* StatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusSection.swift; sourceTree = ""; }; - DB8761B12745515F00BA7EE2 /* HashtagData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagData.swift; sourceTree = ""; }; - DB8761B22745515F00BA7EE2 /* HashtagSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagSection.swift; sourceTree = ""; }; - DB8761B32745515F00BA7EE2 /* HashtagItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagItem.swift; sourceTree = ""; }; - DB8761B52745515F00BA7EE2 /* NotificationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSection.swift; sourceTree = ""; }; - DB8761B72745515F00BA7EE2 /* NotificationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItem.swift; sourceTree = ""; }; - DB8761BB2745515F00BA7EE2 /* CoverFlowStackSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverFlowStackSection.swift; sourceTree = ""; }; - DB8761BC2745515F00BA7EE2 /* CoverFlowStackItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverFlowStackItem.swift; sourceTree = ""; }; - DB8761CF2745553600BA7EE2 /* MediaSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSection.swift; sourceTree = ""; }; DB88AC3B250B26F40009E562 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; DB88E625288EA1F7009A01F5 /* DisplayPreferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayPreferenceView.swift; sourceTree = ""; }; DB88E6282890DF67009A01F5 /* BehaviorsPreferenceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BehaviorsPreferenceViewController.swift; sourceTree = ""; }; @@ -794,8 +791,6 @@ DBCE2EB12591F38100926D09 /* FriendshipListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FriendshipListViewModel.swift; sourceTree = ""; }; DBCE2EC72591F74300926D09 /* FollowingListViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowingListViewModel+Diffable.swift"; sourceTree = ""; }; DBCE2ED72591FDF000926D09 /* FollowingListViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowingListViewModel+State.swift"; sourceTree = ""; }; - DBD0B49C2758B5F50015A388 /* SidebarSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarSection.swift; sourceTree = ""; }; - DBD0B49F2758B6010015A388 /* SidebarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarItem.swift; sourceTree = ""; }; DBD40B842599B9C2006E4ABC /* CombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineTests.swift; sourceTree = ""; }; DBD40BEE2599CB89006E4ABC /* FollowerListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowerListViewController.swift; sourceTree = ""; }; DBD8B53D25E3C3C5006299ED /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -847,7 +842,6 @@ DBF3309825B988A500A678FB /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; DBF639F1259B13BB009E12C8 /* TimelineHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineHeaderView.swift; sourceTree = ""; }; DBF639F6259B1728009E12C8 /* TimelineHeaderCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineHeaderCollectionViewCell.swift; sourceTree = ""; }; - DBF639FB259B333A009E12C8 /* EmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStateView.swift; sourceTree = ""; }; DBF63A03259B4811009E12C8 /* TouchBlockingCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchBlockingCollectionView.swift; sourceTree = ""; }; DBF63A37259D7CCB009E12C8 /* CornerSmoothPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CornerSmoothPreviewViewController.swift; sourceTree = ""; }; DBF69E8E2549601900E2A915 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; @@ -876,8 +870,6 @@ DBFCEF1428939CAC00EEBFB1 /* UserHistoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserHistoryViewModel.swift; sourceTree = ""; }; DBFCEF162893B92B00EEBFB1 /* StatusHistoryViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusHistoryViewModel+Diffable.swift"; sourceTree = ""; }; DBFCEF182893BA8A00EEBFB1 /* StatusHistoryViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusHistoryViewController+DataSourceProvider.swift"; sourceTree = ""; }; - DBFCEF1A2893D5C500EEBFB1 /* HistorySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistorySection.swift; sourceTree = ""; }; - DBFCEF1D2893D5D400EEBFB1 /* HistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryItem.swift; sourceTree = ""; }; DBFCEF1F2893E18400EEBFB1 /* DataSourceFacade+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+History.swift"; sourceTree = ""; }; DBFDCE0327F446BB00BE99E3 /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; }; DBFDCE2227F44C7100BE99E3 /* IntentsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IntentsUI.framework; path = System/Library/Frameworks/IntentsUI.framework; sourceTree = SDKROOT; }; @@ -980,7 +972,6 @@ DB02C76E27350D75007EA0BF /* Hashtag */ = { isa = PBXGroup; children = ( - DB02C77327351B80007EA0BF /* Cell */, DB02C76C27350D71007EA0BF /* SearchHashtagViewController.swift */, DB02C76F27350D8A007EA0BF /* SearchHashtagViewModel.swift */, DB02C77827352217007EA0BF /* SearchHashtagViewModel+Diffable.swift */, @@ -989,15 +980,6 @@ path = Hashtag; sourceTree = ""; }; - DB02C77327351B80007EA0BF /* Cell */ = { - isa = PBXGroup; - children = ( - DB02C77127351B7D007EA0BF /* HashtagTableViewCell.swift */, - DB02C77427351DA2007EA0BF /* HashtagTableViewCell+ViewModel.swift */, - ); - path = Cell; - sourceTree = ""; - }; DB0491FF25904CE400CCD50E /* Toolbar */ = { isa = PBXGroup; children = ( @@ -1006,14 +988,6 @@ path = Toolbar; sourceTree = ""; }; - DB04920325904CFA00CCD50E /* Container */ = { - isa = PBXGroup; - children = ( - DBF639FB259B333A009E12C8 /* EmptyStateView.swift */, - ); - path = Container; - sourceTree = ""; - }; DB04920A25904D4900CCD50E /* Content */ = { isa = PBXGroup; children = ( @@ -1097,6 +1071,130 @@ path = View; sourceTree = ""; }; + DB1B80202A4030AA00C90A7D /* Diffable */ = { + isa = PBXGroup; + children = ( + DB1B80212A4030AA00C90A7D /* Misc */, + DB1B80432A403B0A00C90A7D /* User */, + DB1B803F2A4030AA00C90A7D /* Status */, + ); + path = Diffable; + sourceTree = ""; + }; + DB1B80212A4030AA00C90A7D /* Misc */ = { + isa = PBXGroup; + children = ( + DB1B80222A4030AA00C90A7D /* Sidebar */, + DB1B80252A4030AA00C90A7D /* Hashtag */, + DB1B80292A4030AA00C90A7D /* Notification */, + DB1B802C2A4030AA00C90A7D /* TabBar */, + DB1B802E2A4030AA00C90A7D /* CoverFlowStack */, + DB1B80312A4030AA00C90A7D /* Search */, + DB1B80342A4030AA00C90A7D /* List */, + DB1B80372A4030AA00C90A7D /* History */, + DB1B803A2A4030AA00C90A7D /* Media */, + ); + path = Misc; + sourceTree = ""; + }; + DB1B80222A4030AA00C90A7D /* Sidebar */ = { + isa = PBXGroup; + children = ( + DB1B80232A4030AA00C90A7D /* SidebarSection.swift */, + DB1B80242A4030AA00C90A7D /* SidebarItem.swift */, + ); + path = Sidebar; + sourceTree = ""; + }; + DB1B80252A4030AA00C90A7D /* Hashtag */ = { + isa = PBXGroup; + children = ( + DB1B80272A4030AA00C90A7D /* HashtagSection.swift */, + DB1B80282A4030AA00C90A7D /* HashtagItem.swift */, + ); + path = Hashtag; + sourceTree = ""; + }; + DB1B80292A4030AA00C90A7D /* Notification */ = { + isa = PBXGroup; + children = ( + DB1B802A2A4030AA00C90A7D /* NotificationSection.swift */, + DB1B802B2A4030AA00C90A7D /* NotificationItem.swift */, + ); + path = Notification; + sourceTree = ""; + }; + DB1B802C2A4030AA00C90A7D /* TabBar */ = { + isa = PBXGroup; + children = ( + DB1B802D2A4030AA00C90A7D /* TabBarItem.swift */, + ); + path = TabBar; + sourceTree = ""; + }; + DB1B802E2A4030AA00C90A7D /* CoverFlowStack */ = { + isa = PBXGroup; + children = ( + DB1B802F2A4030AA00C90A7D /* CoverFlowStackSection.swift */, + DB1B80302A4030AA00C90A7D /* CoverFlowStackItem.swift */, + ); + path = CoverFlowStack; + sourceTree = ""; + }; + DB1B80312A4030AA00C90A7D /* Search */ = { + isa = PBXGroup; + children = ( + DB1B80322A4030AA00C90A7D /* SearchSection.swift */, + DB1B80332A4030AA00C90A7D /* SearchItem.swift */, + ); + path = Search; + sourceTree = ""; + }; + DB1B80342A4030AA00C90A7D /* List */ = { + isa = PBXGroup; + children = ( + DB1B80352A4030AA00C90A7D /* ListItem.swift */, + DB1B80362A4030AA00C90A7D /* ListSection.swift */, + ); + path = List; + sourceTree = ""; + }; + DB1B80372A4030AA00C90A7D /* History */ = { + isa = PBXGroup; + children = ( + DB1B80382A4030AA00C90A7D /* HistorySection.swift */, + DB1B80392A4030AA00C90A7D /* HistoryItem.swift */, + ); + path = History; + sourceTree = ""; + }; + DB1B803A2A4030AA00C90A7D /* Media */ = { + isa = PBXGroup; + children = ( + DB1B803B2A4030AA00C90A7D /* MediaSection.swift */, + ); + path = Media; + sourceTree = ""; + }; + DB1B803F2A4030AA00C90A7D /* Status */ = { + isa = PBXGroup; + children = ( + DB1B80402A4030AA00C90A7D /* StatusMediaGallerySection.swift */, + DB1B80412A4030AA00C90A7D /* StatusItem.swift */, + DB1B80422A4030AA00C90A7D /* StatusSection.swift */, + ); + path = Status; + sourceTree = ""; + }; + DB1B80432A403B0A00C90A7D /* User */ = { + isa = PBXGroup; + children = ( + DB1B80442A403B0A00C90A7D /* UserItem.swift */, + DB1B80452A403B0A00C90A7D /* UserSection.swift */, + ); + path = User; + sourceTree = ""; + }; DB1D3DEB289388CF008F0BD0 /* History */ = { isa = PBXGroup; children = ( @@ -1128,15 +1226,6 @@ path = Option; sourceTree = ""; }; - DB1E48172772CEC40074F6A0 /* Search */ = { - isa = PBXGroup; - children = ( - DB1E48152772CEC20074F6A0 /* SearchSection.swift */, - DB1E48182772CECC0074F6A0 /* SearchItem.swift */, - ); - path = Search; - sourceTree = ""; - }; DB25C4C62779949F00EC1435 /* Cell */ = { isa = PBXGroup; children = ( @@ -1368,15 +1457,6 @@ path = List; sourceTree = ""; }; - DB47AB1E27CCC18500CD73C7 /* List */ = { - isa = PBXGroup; - children = ( - DB47AB2027CCC18500CD73C7 /* ListSection.swift */, - DB47AB1F27CCC18500CD73C7 /* ListItem.swift */, - ); - path = List; - sourceTree = ""; - }; DB47AB2327CCDCA800CD73C7 /* List */ = { isa = PBXGroup; children = ( @@ -1499,14 +1579,6 @@ path = SecondaryTab; sourceTree = ""; }; - DB66DB912823AC400071F5F3 /* TabBar */ = { - isa = PBXGroup; - children = ( - DB66DB8F2823AC3C0071F5F3 /* TabBarItem.swift */, - ); - path = TabBar; - sourceTree = ""; - }; DB676105254AD4B1006C6798 /* CollectionViewCell */ = { isa = PBXGroup; children = ( @@ -1668,87 +1740,6 @@ path = NotificationTimeline; sourceTree = ""; }; - DB8761A22745515F00BA7EE2 /* Diffable */ = { - isa = PBXGroup; - children = ( - DB8761A32745515F00BA7EE2 /* User */, - DB8761A92745515F00BA7EE2 /* Status */, - DB8761AF2745515F00BA7EE2 /* Misc */, - ); - path = Diffable; - sourceTree = ""; - }; - DB8761A32745515F00BA7EE2 /* User */ = { - isa = PBXGroup; - children = ( - DB8761A42745515F00BA7EE2 /* UserItem.swift */, - DB8761A82745515F00BA7EE2 /* UserSection.swift */, - ); - path = User; - sourceTree = ""; - }; - DB8761A92745515F00BA7EE2 /* Status */ = { - isa = PBXGroup; - children = ( - DB8761AA2745515F00BA7EE2 /* StatusMediaGallerySection.swift */, - DB8761AD2745515F00BA7EE2 /* StatusItem.swift */, - DB8761AE2745515F00BA7EE2 /* StatusSection.swift */, - ); - path = Status; - sourceTree = ""; - }; - DB8761AF2745515F00BA7EE2 /* Misc */ = { - isa = PBXGroup; - children = ( - DB8761B02745515F00BA7EE2 /* Hashtag */, - DB8761B42745515F00BA7EE2 /* Notification */, - DB8761BA2745515F00BA7EE2 /* CoverFlowStack */, - DB8761D12745553A00BA7EE2 /* Media */, - DBD0B49E2758B5F70015A388 /* Sidebar */, - DB1E48172772CEC40074F6A0 /* Search */, - DB47AB1E27CCC18500CD73C7 /* List */, - DB66DB912823AC400071F5F3 /* TabBar */, - DBFCEF1C2893D5C800EEBFB1 /* History */, - ); - path = Misc; - sourceTree = ""; - }; - DB8761B02745515F00BA7EE2 /* Hashtag */ = { - isa = PBXGroup; - children = ( - DB8761B12745515F00BA7EE2 /* HashtagData.swift */, - DB8761B22745515F00BA7EE2 /* HashtagSection.swift */, - DB8761B32745515F00BA7EE2 /* HashtagItem.swift */, - ); - path = Hashtag; - sourceTree = ""; - }; - DB8761B42745515F00BA7EE2 /* Notification */ = { - isa = PBXGroup; - children = ( - DB8761B52745515F00BA7EE2 /* NotificationSection.swift */, - DB8761B72745515F00BA7EE2 /* NotificationItem.swift */, - ); - path = Notification; - sourceTree = ""; - }; - DB8761BA2745515F00BA7EE2 /* CoverFlowStack */ = { - isa = PBXGroup; - children = ( - DB8761BB2745515F00BA7EE2 /* CoverFlowStackSection.swift */, - DB8761BC2745515F00BA7EE2 /* CoverFlowStackItem.swift */, - ); - path = CoverFlowStack; - sourceTree = ""; - }; - DB8761D12745553A00BA7EE2 /* Media */ = { - isa = PBXGroup; - children = ( - DB8761CF2745553600BA7EE2 /* MediaSection.swift */, - ); - path = Media; - sourceTree = ""; - }; DB88AC34250B25170009E562 /* Vender */ = { isa = PBXGroup; children = ( @@ -2021,15 +2012,6 @@ path = FollowingList; sourceTree = ""; }; - DBD0B49E2758B5F70015A388 /* Sidebar */ = { - isa = PBXGroup; - children = ( - DBD0B49C2758B5F50015A388 /* SidebarSection.swift */, - DBD0B49F2758B6010015A388 /* SidebarItem.swift */, - ); - path = Sidebar; - sourceTree = ""; - }; DBD40BF62599CB93006E4ABC /* FollowerList */ = { isa = PBXGroup; children = ( @@ -2091,7 +2073,7 @@ DBA122CA256C13B000928671 /* GoogleService-Info.plist */, DBCB408E255D8C2E00DD8D8F /* Activity */, DB2C8710274F4B1C00CE0398 /* Coordinator */, - DB8761A22745515F00BA7EE2 /* Diffable */, + DB1B80202A4030AA00C90A7D /* Diffable */, DB88AC34250B25170009E562 /* Vender */, DBDA8E5424FDF3E2006750DC /* Scene */, DBDA8E9924FE0DC8006750DC /* Resources */, @@ -2250,7 +2232,6 @@ DBF63A08259B4818009E12C8 /* CollectionView */, DB676105254AD4B1006C6798 /* CollectionViewCell */, DB04920A25904D4900CCD50E /* Content */, - DB04920325904CFA00CCD50E /* Container */, DB0491FF25904CE400CCD50E /* Toolbar */, DB97D1EB256CBA6F0056F8C2 /* Button */, ); @@ -2410,15 +2391,6 @@ path = User; sourceTree = ""; }; - DBFCEF1C2893D5C800EEBFB1 /* History */ = { - isa = PBXGroup; - children = ( - DBFCEF1A2893D5C500EEBFB1 /* HistorySection.swift */, - DBFCEF1D2893D5D400EEBFB1 /* HistoryItem.swift */, - ); - path = History; - sourceTree = ""; - }; DBFDCE4227F450FC00BE99E3 /* TwidereXIntent */ = { isa = PBXGroup; children = ( @@ -2908,10 +2880,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DBBA21252A403BE100CEBCF8 /* StatusSection.swift in Sources */, DBF87ADA2892A67D0029A7C7 /* TranslationServicePreferenceView.swift in Sources */, DB2D36F627D5E73A00C1FBE0 /* CompositeListViewController.swift in Sources */, DB5FD9B726D8EFF700CF5439 /* UserBriefInfoView+ViewModel.swift in Sources */, - DB8761BE274552F800BA7EE2 /* StatusMediaGallerySection.swift in Sources */, DB3B906926E8D1D80010F64C /* LocalProfileViewModel.swift in Sources */, DB8E4FD92563CD6900459DA2 /* TwitterPinBasedAuthenticationViewController.swift in Sources */, DB7274F2273BB29600577D95 /* NotificationViewModel.swift in Sources */, @@ -2934,8 +2906,8 @@ DB36F35E257F74C10028F81E /* ScrollViewContainer.swift in Sources */, DBEA97662A1B58E200C8B75B /* SecondaryContainerViewModel.swift in Sources */, DB76A65D275F52D500A50673 /* MediaInfoDescriptionView+ViewModel.swift in Sources */, - DB66DB902823AC3C0071F5F3 /* TabBarItem.swift in Sources */, DB697E0927904BFB004EF2F7 /* FederatedTimelineViewController.swift in Sources */, + DB1B80472A403B0A00C90A7D /* UserSection.swift in Sources */, DB76A664275F688D00A50673 /* DataSourceFacade+Share.swift in Sources */, DB761E4E255935380050DC01 /* DrawerSidebarHeaderView.swift in Sources */, DBCB402E255B670C00DD8D8F /* AccountListViewController.swift in Sources */, @@ -2946,7 +2918,6 @@ DB02C76D27350D71007EA0BF /* SearchHashtagViewController.swift in Sources */, DB2C873E274F4B7D00CE0398 /* DeveloperViewController.swift in Sources */, DBFA4A2025A5924C00D51703 /* ListTimelineViewModel+Diffable.swift in Sources */, - DBFCEF1E2893D5D400EEBFB1 /* HistoryItem.swift in Sources */, DB925749251C9D48004FEFB5 /* ProfilePagingViewModel.swift in Sources */, DB932E5227FEC7390036A824 /* Intents.intentdefinition in Sources */, DBA210362759D7B1000B7CB2 /* FollowingListViewModel+State.swift in Sources */, @@ -2968,32 +2939,32 @@ DB25C4D32779ADD800EC1435 /* SearchResultContainerViewController.swift in Sources */, DB02C777273520B8007EA0BF /* SearchHashtagViewModel+State.swift in Sources */, DB580EB6288187BD00BC4A0F /* MastodonNotificationSectionView.swift in Sources */, + DBBA21242A403BCF00CEBCF8 /* MediaSection.swift in Sources */, DBDAF243274F530B00050319 /* SceneCoordinator.swift in Sources */, DB581A0127D89A3700C35B91 /* DataSourceFacade+LIst.swift in Sources */, + DBBA21262A403BE100CEBCF8 /* StatusItem.swift in Sources */, DB25C4CC27799FE600EC1435 /* SavedSearchViewController.swift in Sources */, DB0AD4F0285893FA0002ABDB /* TimelineViewModelDriver.swift in Sources */, DB830200273D04D000BF5224 /* NotificationTimelineViewModel+Diffable.swift in Sources */, DBCB4060255CAC0300DD8D8F /* TwitterAuthenticationController.swift in Sources */, DB5632B426DDE3F300FC893F /* StatusThreadViewModel+LoadThreadState.swift in Sources */, - DBD0B49D2758B5F50015A388 /* SidebarSection.swift in Sources */, DB0AD4EB28587B520002ABDB /* FederatedTimelineViewModel+Diffable.swift in Sources */, DBF739CF275C247F00BF6AB5 /* DataSourceFacade+Mute.swift in Sources */, DB578CCD254C0BDB00745336 /* UserBriefInfoView.swift in Sources */, DBA21039275A0E77000B7CB2 /* FollowerListViewController.swift in Sources */, DB5632C226DF8DE100FC893F /* TimelineViewModel+LoadOldestState.swift in Sources */, DB3B906326E8BBD70010F64C /* ProfileHeaderView.swift in Sources */, - DB02C77227351B7D007EA0BF /* HashtagTableViewCell.swift in Sources */, DB3B906726E8CD6D0010F64C /* ProfileHeaderViewModel.swift in Sources */, DB51DC432718117900A0D8FB /* StatusMediaGalleryCollectionCell+ViewModel.swift in Sources */, DB44245F285A49CD0095AECF /* HashtagTimelineViewModel+Diffable.swift in Sources */, DB51DC412717FCAA00A0D8FB /* StatusMediaGalleryCollectionCell.swift in Sources */, DB9B3251285735F200AC818D /* GridTimelineViewController.swift in Sources */, DB46D11A27DB26FF003B8BA1 /* ListUserViewController.swift in Sources */, + DBBA21182A403BB600CEBCF8 /* HashtagItem.swift in Sources */, DB1E48122772CE380074F6A0 /* SearchViewModel.swift in Sources */, DB76A662275F65FF00A50673 /* DataSourceFacade+Model.swift in Sources */, DB2EBBF0255D368200956CAA /* TableViewEntryRow.swift in Sources */, DB580EB7288187BD00BC4A0F /* MastodonNotificationSectionViewModel.swift in Sources */, - DB8761CB2745530200BA7EE2 /* CoverFlowStackSection.swift in Sources */, DB8AC0F825401BA200E636BE /* UIViewController.swift in Sources */, DB434888251DFE2D005B599F /* ProfileBannerStatusView.swift in Sources */, DB442475285B17830095AECF /* SearchMediaTimelineViewModel+Diffable.swift in Sources */, @@ -3001,7 +2972,8 @@ DB761E62255942050050DC01 /* DrawerSidebarEntryView.swift in Sources */, DB92570A251C8FE0004FEFB5 /* ProfileHeaderViewController.swift in Sources */, DB47AB1D27CCB88000CD73C7 /* ListViewModel.swift in Sources */, - DB8761BF274552F800BA7EE2 /* StatusItem.swift in Sources */, + DBBA21212A403BC900CEBCF8 /* ListSection.swift in Sources */, + DBBA211E2A403BC500CEBCF8 /* SearchSection.swift in Sources */, DB747FF9251C496A000C4BD7 /* ProfileViewController.swift in Sources */, DB697E0C27904C56004EF2F7 /* FederatedTimelineViewModel.swift in Sources */, DB01A9A427637FE60055FABC /* DataSourceFacade+Meta.swift in Sources */, @@ -3010,7 +2982,6 @@ DBA5FA192553DCBC00D2E98E /* TransitioningMath.swift in Sources */, DB56329C26DCC23700FC893F /* DataSourceProvider.swift in Sources */, DB44A56226C4FEAB004C8B78 /* WelcomeViewModel.swift in Sources */, - DBFCEF1B2893D5C500EEBFB1 /* HistorySection.swift in Sources */, DB1D3DED28938ACD008F0BD0 /* HistoryViewModel.swift in Sources */, DB522F172886A08F0088017C /* UIStatusBarManager+HandleTapAction.m in Sources */, DBC8E04B2576337F00401E20 /* DisposeBagCollectable.swift in Sources */, @@ -3025,17 +2996,17 @@ DB55496029E01330004AF42A /* DataSourceProvider+UITableViewDataSourcePrefetching.swift in Sources */, DBA6B30E27E9CB6F004D052D /* AddListMemberViewController.swift in Sources */, DBF63A38259D7CCB009E12C8 /* CornerSmoothPreviewViewController.swift in Sources */, + DBBA21202A403BC900CEBCF8 /* ListItem.swift in Sources */, DB0CC4BA27D5F7BA00A051B4 /* CompositeListViewModel+Diffable.swift in Sources */, DBFCEF172893B92B00EEBFB1 /* StatusHistoryViewModel+Diffable.swift in Sources */, - DB02C77527351DA2007EA0BF /* HashtagTableViewCell+ViewModel.swift in Sources */, DB0AD4E22858734A0002ABDB /* UserMediaTimelineViewModel.swift in Sources */, DB522F1628869DAE0088017C /* Notification+Name+HandleTapAction.swift in Sources */, DB86433C26E898C5000C9879 /* DataSourceFacade+Like.swift in Sources */, DB6BCD70277AEAC700847054 /* TrendTableViewCell.swift in Sources */, DB442461285AD8530095AECF /* ListStatusTimelineViewController.swift in Sources */, + DBBA21232A403BCC00CEBCF8 /* HistorySection.swift in Sources */, DB914C4A26C3AC7C00E85F1A /* ContentOffsetFixedCollectionView.swift in Sources */, DB1D3DEF28938CD1008F0BD0 /* StatusHistoryViewController.swift in Sources */, - DBD0B4A02758B6010015A388 /* SidebarItem.swift in Sources */, DBFCEF1228939C9600EEBFB1 /* UserHistoryViewController.swift in Sources */, DB76A67127609A8700A50673 /* RemoteProfileViewModel.swift in Sources */, DB6BCD6B277ADBF300847054 /* TrendViewController.swift in Sources */, @@ -3044,6 +3015,7 @@ DB580EB9288187BD00BC4A0F /* AccountPreferenceViewModel.swift in Sources */, DB33A4A925A319A0003CED7D /* ActionToolbarContainer.swift in Sources */, DB2C873C274F4B7D00CE0398 /* DeveloperView.swift in Sources */, + DBBA21192A403BB600CEBCF8 /* HashtagSection.swift in Sources */, DB2C8742274F4B7D00CE0398 /* AboutViewController.swift in Sources */, DB442465285AD8660095AECF /* ListStatusTimelineViewModel+Diffable.swift in Sources */, DB2C0BDA27DF14950033FC94 /* EditListViewController.swift in Sources */, @@ -3053,6 +3025,7 @@ DB5632A726DCC84C00FC893F /* DataSourceProvider+UITableViewDelegate.swift in Sources */, DB9B324D285732A400AC818D /* UserTimelineViewController.swift in Sources */, DBB47DAD26EB440C001590F7 /* ProfileFieldCollectionViewCell.swift in Sources */, + DBBA21222A403BCC00CEBCF8 /* HistoryItem.swift in Sources */, DBB04A322861AE24003799CA /* TrendPlaceViewController.swift in Sources */, DB3B905F26E8A9650010F64C /* StatusThreadViewController+DataSourceProvider.swift in Sources */, DBF739D1275C3EF300BF6AB5 /* DataSourceFacade+Report.swift in Sources */, @@ -3060,13 +3033,12 @@ DB262A3D27228B4800D18EF3 /* SearchResultViewController.swift in Sources */, DB5FB0122727FD4A006520FA /* SearchUserViewModel+State.swift in Sources */, DB46D12327DB6223003B8BA1 /* ListUserViewController+DataSourceProvider.swift in Sources */, - DB1E48192772CECC0074F6A0 /* SearchItem.swift in Sources */, DB43488D251DFF0F005B599F /* ProfileBannerStatusItemView.swift in Sources */, DBDA7F1E27D2256400BA6BE1 /* ListViewModel+State.swift in Sources */, DBE76D1E2500E65D00DEB0FC /* HomeTimelineViewModel.swift in Sources */, DBD0B4982758B58F0015A388 /* DrawerSidebarPresentationController.swift in Sources */, - DB47AB2F27CE097C00CD73C7 /* ListSection.swift in Sources */, DB5A2288255B9155006CA5B2 /* AccountListViewModel+Diffable.swift in Sources */, + DBBA21172A403BB100CEBCF8 /* SidebarItem.swift in Sources */, DB12BEE727329F55002AA635 /* SearchUserViewController+DataSourceProvider.swift in Sources */, DB42411426C3E55200B6C5F8 /* WelcomeView.swift in Sources */, DB1D7B5525B5A87200397DCD /* TwitterAuthenticationOptionViewModel.swift in Sources */, @@ -3074,16 +3046,17 @@ DB01A9A2276347B60055FABC /* DataSourceFacade+Poll.swift in Sources */, DB2FFF3E258B78B0003DBC19 /* AVPlayer.swift in Sources */, DBCB4047255B683B00DD8D8F /* AccountListTableViewCell.swift in Sources */, - DBF639FC259B333A009E12C8 /* EmptyStateView.swift in Sources */, DBFCA3F429F9185100B9DCA3 /* HomeListStatusTimelineViewController.swift in Sources */, DBA6B30C27E9B4CB004D052D /* DataSourceFacade+Banner.swift in Sources */, DB25C4C527798E1A00EC1435 /* DataSourceFacade+SavedSearch.swift in Sources */, DBA210332759D79C000B7CB2 /* FollowingListViewController.swift in Sources */, + DBBA211A2A403BBA00CEBCF8 /* NotificationItem.swift in Sources */, DB44246C285AFE770095AECF /* SearchTimelineViewModel+Diffable.swift in Sources */, DBFA471F2859C4AE00C9FF7F /* UserLikeTimelineViewController.swift in Sources */, DBFCC44725667C620016698E /* UILabel.swift in Sources */, DB76A653275F1C4F00A50673 /* MediaPreviewViewController.swift in Sources */, DB76A660275F58CD00A50673 /* MediaPreviewViewController+DataSourceProvider.swift in Sources */, + DBBA21152A403BAA00CEBCF8 /* TabBarItem.swift in Sources */, DBAA89902758DFC9001C273B /* AvatarBarButtonItem+ViewModel.swift in Sources */, DBA6B31127E9CBCC004D052D /* AddListMemberViewModel.swift in Sources */, DB44245D285A48950095AECF /* HashtagTimelineViewModel.swift in Sources */, @@ -3092,11 +3065,11 @@ DB71C7D1271EB09A00BE3819 /* DataSourceFacade+Friendship.swift in Sources */, DBFCA3F729F9191F00B9DCA3 /* HostListStatusTimelineViewModel.swift in Sources */, DB442473285B177B0095AECF /* SearchMediaTimelineViewModel.swift in Sources */, + DBBA21162A403BAF00CEBCF8 /* SidebarSection.swift in Sources */, DB1D3DEA289388CD008F0BD0 /* HistoryViewController.swift in Sources */, DB92DB3F2898026B0011B564 /* UserHistoryViewController+DataSourceProvider.swift in Sources */, DBCE2E912591A06300926D09 /* UINavigationController.swift in Sources */, DB915406254FC8CE00613473 /* FollowActionButton.swift in Sources */, - DB8761C0274552F800BA7EE2 /* StatusSection.swift in Sources */, DB01092426E6199C005F67D7 /* DataSourceProvider+StatusViewTableViewCellDelegate.swift in Sources */, DB46D11D27DB2807003B8BA1 /* ListUserViewModel.swift in Sources */, DB2C0BDD27DF151D0033FC94 /* EditListView.swift in Sources */, @@ -3109,10 +3082,8 @@ DB02C77027350D8A007EA0BF /* SearchHashtagViewModel.swift in Sources */, DB5632B226DCF1CD00FC893F /* StatusThreadRootTableViewCell+ViewModel.swift in Sources */, DB0618102786EC870030EE79 /* LineChartView.swift in Sources */, - DB8761CC2745530200BA7EE2 /* CoverFlowStackItem.swift in Sources */, DB2C8741274F4B7D00CE0398 /* AboutView.swift in Sources */, DB442468285AFE680095AECF /* SearchTimelineViewController.swift in Sources */, - DB8761C2274552F800BA7EE2 /* UserSection.swift in Sources */, DB76A65A275F49AE00A50673 /* ProgressBarView.swift in Sources */, DB76A655275F30E800A50673 /* DataSourceFacade+Media.swift in Sources */, DB47AB1A27CCB7EC00CD73C7 /* ListViewController.swift in Sources */, @@ -3131,10 +3102,8 @@ DB2C873D274F4B7D00CE0398 /* DeveloperViewModel.swift in Sources */, DB9B32542857369800AC818D /* GridTimelineViewController+DataSourceProvider.swift in Sources */, DBD0B4972758B57F0015A388 /* DrawerSidebarTransitionController.swift in Sources */, - DB8761C7274552FF00BA7EE2 /* NotificationSection.swift in Sources */, DB76A657275F498F00A50673 /* MediaPreviewImageViewController.swift in Sources */, DB5632B026DCED1300FC893F /* StatusThreadRootTableViewCell.swift in Sources */, - DB8761C4274552FB00BA7EE2 /* HashtagData.swift in Sources */, DBFCEF1528939CAC00EEBFB1 /* UserHistoryViewModel.swift in Sources */, DBDA8E2224FCF8A3006750DC /* AppDelegate.swift in Sources */, DB46D11F27DB2B50003B8BA1 /* ListUserViewModel+Diffable.swift in Sources */, @@ -3153,7 +3122,6 @@ DB01092026E60756005F67D7 /* DataSourceFacade+StatusThread.swift in Sources */, DBFCEF202893E18400EEBFB1 /* DataSourceFacade+History.swift in Sources */, DBDAF244274F530B00050319 /* NeedsDependency.swift in Sources */, - DB8761C3274552FB00BA7EE2 /* HashtagSection.swift in Sources */, DB9B325C2857493D00AC818D /* UserTimelineViewModel+Diffable.swift in Sources */, DBE6357A28855302001C114B /* PushNotificationScratchViewController.swift in Sources */, DBF81C8E27F843D700004A56 /* AppIconAssets.swift in Sources */, @@ -3161,17 +3129,19 @@ DBF3309125B96E0B00A678FB /* WKNavigationDelegateShim.swift in Sources */, DB580EB5288187BD00BC4A0F /* AccountPreferenceView.swift in Sources */, DB2D36F927D5E74D00C1FBE0 /* CompositeListViewModel.swift in Sources */, + DBBA211B2A403BBA00CEBCF8 /* NotificationSection.swift in Sources */, + DB1B80462A403B0A00C90A7D /* UserItem.swift in Sources */, DBB47DAB26EB3BF8001590F7 /* ProfileFieldContentView.swift in Sources */, DBBF70F726D7B62F00971656 /* AccountListTableViewCell+ViewModel.swift in Sources */, DB92DB41289804440011B564 /* UserHistoryViewModel+Diffable.swift in Sources */, DBB47DA926EB3AA2001590F7 /* ProfileFieldListView.swift in Sources */, DBF69EE12549705A00E2A915 /* ViewControllerAnimatedTransitioningDelegate.swift in Sources */, DB76A658275F498F00A50673 /* MediaPreviewImageViewModel.swift in Sources */, - DB47AB2E27CE097C00CD73C7 /* ListItem.swift in Sources */, DBB04A372861AF2D003799CA /* TrendPlaceView.swift in Sources */, DB6BCD6E277ADC0900847054 /* TrendViewModel.swift in Sources */, DB51DC4E27181D7500A0D8FB /* CoverFlowStackMediaCollectionCell.swift in Sources */, DBE6357D2885557C001C114B /* PushNotificationScratchViewModel.swift in Sources */, + DBBA211D2A403BBE00CEBCF8 /* CoverFlowStackItem.swift in Sources */, DBED96D8253F5D7800C5383A /* NamingState.swift in Sources */, DB6DF3E0252060AA00E8A273 /* ProfileViewModel.swift in Sources */, DB262A332721377800D18EF3 /* DataSourceFacade+Block.swift in Sources */, @@ -3181,7 +3151,6 @@ DB67610D254AD681006C6798 /* ActivityIndicatorCollectionViewCell.swift in Sources */, DB94B6B426C65BE100A2E8A1 /* MastodonAuthenticationController.swift in Sources */, DB76A668275F896E00A50673 /* ShareActivityProvider.swift in Sources */, - DB1E48162772CEC20074F6A0 /* SearchSection.swift in Sources */, DB51DC5027181DE500A0D8FB /* CoverFlowStackMediaCollectionCell+ViewModel.swift in Sources */, DB9B325A28573E0000AC818D /* UserTimelineViewModel.swift in Sources */, DB148B03281A7AB300B596C7 /* ContentSplitViewController.swift in Sources */, @@ -3193,11 +3162,12 @@ DB76A66D2760721400A50673 /* MediaPreviewVideoViewModel.swift in Sources */, DB47AB2D27CE085900CD73C7 /* ListViewModel+Diffable.swift in Sources */, DB148B00281A729200B596C7 /* SidebarViewController.swift in Sources */, - DB8761C6274552FF00BA7EE2 /* NotificationItem.swift in Sources */, DBF69EE02549705A00E2A915 /* ViewControllerAnimatedTransitioning.swift in Sources */, DB8E4FEC2563D42800459DA2 /* TwitterPinBasedAuthenticationViewModel.swift in Sources */, + DBBA21272A403BE100CEBCF8 /* StatusMediaGallerySection.swift in Sources */, DB8301F7273CED0400BF5224 /* NotificationTimelineViewController.swift in Sources */, DBAA89922758EF12001C273B /* DrawerSidebarViewModel+Diffable.swift in Sources */, + DBBA211F2A403BC500CEBCF8 /* SearchItem.swift in Sources */, DB5632AD26DCE93900FC893F /* StatusThreadViewModel+Diffable.swift in Sources */, DBF639F7259B1728009E12C8 /* TimelineHeaderCollectionViewCell.swift in Sources */, DBD0B49B2758B5A60015A388 /* DrawerSidebarViewController.swift in Sources */, @@ -3209,14 +3179,12 @@ DB3B906B26E8D3AB0010F64C /* DataSourceFacade+Status.swift in Sources */, DB5FB00F2727FCB7006520FA /* SearchUserViewController.swift in Sources */, DB5632AB26DCCD3900FC893F /* DataSourceFacade.swift in Sources */, - DB8761D02745553700BA7EE2 /* MediaSection.swift in Sources */, DB51DC1C2715588E00A0D8FB /* ProfileDashboardMeterView.swift in Sources */, DBF87AD52892A6740029A7C7 /* AppIconPreferenceView.swift in Sources */, DBB04A352861AE4D003799CA /* TrendPlaceViewModel.swift in Sources */, DB2C8743274F4B7D00CE0398 /* SettingListView.swift in Sources */, DB76A650275F1C3200A50673 /* MediaPreviewTransitionController.swift in Sources */, DB88E62E2890DF7E009A01F5 /* BehaviorsPreferenceView.swift in Sources */, - DB8761C5274552FB00BA7EE2 /* HashtagItem.swift in Sources */, DB76A65B275F49AE00A50673 /* MediaInfoDescriptionView.swift in Sources */, DB76A64D275DFA0600A50673 /* TwitterStatusThreadReplyViewModel+State.swift in Sources */, DBF87AD92892A67D0029A7C7 /* TranslateButtonPreferenceView.swift in Sources */, @@ -3227,8 +3195,8 @@ DBC8E050257653E100401E20 /* SavePhotoActivity.swift in Sources */, DB1D7B4325B5938400397DCD /* TwitterAuthenticationOptionViewController.swift in Sources */, DB830209273D12E600BF5224 /* NotificationTimelineViewController+DataSourceProvider.swift in Sources */, - DB8761C1274552F800BA7EE2 /* UserItem.swift in Sources */, DB5FB0102727FCC5006520FA /* SearchUserViewModel.swift in Sources */, + DBBA211C2A403BBE00CEBCF8 /* CoverFlowStackSection.swift in Sources */, DB88E62C2890DF79009A01F5 /* BehaviorsPreferenceViewModel.swift in Sources */, DB5632A926DCC96C00FC893F /* ListTimelineViewController+DataSourceProvider.swift in Sources */, DB56329A26DCBE7300FC893F /* StatusThreadViewModel.swift in Sources */, diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index e45523af..a6808d66 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -258,8 +258,8 @@ "repositoryURL": "https://github.com/TwidereProject/TwitterSDK.git", "state": { "branch": null, - "revision": "e6e0d3f1806f3eb04b8592f469c66d3dd0e1e969", - "version": "0.10.0" + "revision": "fdfc56029c530b76e93dc6ae052dcc1a64360c26", + "version": "0.11.0" } }, { diff --git a/TwidereX/Scene/Profile/FriendshipList/FollowerList/FollowerListViewController.swift b/TwidereX/Scene/Profile/FriendshipList/FollowerList/FollowerListViewController.swift index e027ba44..31bed0a5 100644 --- a/TwidereX/Scene/Profile/FriendshipList/FollowerList/FollowerListViewController.swift +++ b/TwidereX/Scene/Profile/FriendshipList/FollowerList/FollowerListViewController.swift @@ -8,10 +8,12 @@ import os.log import UIKit +import SwiftUI import Combine import GameplayKit import TwidereAsset import TwidereLocalization +import TwidereUI final class FollowerListViewController: UIViewController, NeedsDependency { @@ -22,8 +24,8 @@ final class FollowerListViewController: UIViewController, NeedsDependency { var disposeBag = Set() var viewModel: FriendshipListViewModel! - - let emptyStateView = EmptyStateView() + let emptyStateViewModel = EmptyStateView.ViewModel() + let tableView: UITableView = { let tableView = ControlContainableTableView() tableView.backgroundColor = .clear @@ -44,16 +46,6 @@ extension FollowerListViewController { title = L10n.Scene.Followers.title view.backgroundColor = .systemBackground - - emptyStateView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(emptyStateView) - NSLayoutConstraint.activate([ - emptyStateView.topAnchor.constraint(equalTo: view.topAnchor), - emptyStateView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - emptyStateView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - emptyStateView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - emptyStateView.isHidden = true tableView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tableView) @@ -76,14 +68,24 @@ extension FollowerListViewController { } .store(in: &disposeBag) + let emptyStateViewHostingController = UIHostingController(rootView: EmptyStateView(viewModel: emptyStateViewModel)) + addChild(emptyStateViewHostingController) + emptyStateViewHostingController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(emptyStateViewHostingController.view) + NSLayoutConstraint.activate([ + emptyStateViewHostingController.view.topAnchor.constraint(equalTo: view.topAnchor), + emptyStateViewHostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + emptyStateViewHostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + emptyStateViewHostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + emptyStateViewHostingController.view.isHidden = true + viewModel.$isPermissionDenied .receive(on: DispatchQueue.main) .sink { [weak self] isPermissionDenied in guard let self = self else { return } - self.emptyStateView.iconImageView.image = Asset.Human.eyeSlashLarge.image.withRenderingMode(.alwaysTemplate) - self.emptyStateView.titleLabel.text = L10n.Common.Alerts.PermissionDeniedNotAuthorized.title - self.emptyStateView.messageLabel.text = L10n.Common.Alerts.PermissionDeniedNotAuthorized.message - self.emptyStateView.isHidden = !isPermissionDenied + self.emptyStateViewModel.emptyState = isPermissionDenied ? .unableToAccess : nil + emptyStateViewHostingController.view.isHidden = !isPermissionDenied } .store(in: &disposeBag) } diff --git a/TwidereX/Scene/Profile/FriendshipList/FollowingList/FollowingListViewController.swift b/TwidereX/Scene/Profile/FriendshipList/FollowingList/FollowingListViewController.swift index 42bc2756..3fe9c7eb 100644 --- a/TwidereX/Scene/Profile/FriendshipList/FollowingList/FollowingListViewController.swift +++ b/TwidereX/Scene/Profile/FriendshipList/FollowingList/FollowingListViewController.swift @@ -8,11 +8,13 @@ import os.log import UIKit +import SwiftUI import Combine import GameplayKit import TwidereCore import TwidereAsset import TwidereLocalization +import TwidereUI final class FollowingListViewController: UIViewController, NeedsDependency { @@ -23,8 +25,7 @@ final class FollowingListViewController: UIViewController, NeedsDependency { var disposeBag = Set() var viewModel: FriendshipListViewModel! - - let emptyStateView = EmptyStateView() + let emptyStateViewModel = EmptyStateView.ViewModel() let tableView: UITableView = { let tableView = ControlContainableTableView() @@ -48,16 +49,6 @@ extension FollowingListViewController { title = L10n.Scene.Following.title view.backgroundColor = .systemBackground - emptyStateView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(emptyStateView) - NSLayoutConstraint.activate([ - emptyStateView.topAnchor.constraint(equalTo: view.topAnchor), - emptyStateView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - emptyStateView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - emptyStateView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - emptyStateView.isHidden = true - tableView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tableView) NSLayoutConstraint.activate([ @@ -79,14 +70,24 @@ extension FollowingListViewController { } .store(in: &disposeBag) + let emptyStateViewHostingController = UIHostingController(rootView: EmptyStateView(viewModel: emptyStateViewModel)) + addChild(emptyStateViewHostingController) + emptyStateViewHostingController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(emptyStateViewHostingController.view) + NSLayoutConstraint.activate([ + emptyStateViewHostingController.view.topAnchor.constraint(equalTo: view.topAnchor), + emptyStateViewHostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + emptyStateViewHostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + emptyStateViewHostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + emptyStateViewHostingController.view.isHidden = true + viewModel.$isPermissionDenied .receive(on: DispatchQueue.main) .sink { [weak self] isPermissionDenied in guard let self = self else { return } - self.emptyStateView.iconImageView.image = Asset.Human.eyeSlashLarge.image.withRenderingMode(.alwaysTemplate) - self.emptyStateView.titleLabel.text = L10n.Common.Alerts.PermissionDeniedNotAuthorized.title - self.emptyStateView.messageLabel.text = L10n.Common.Alerts.PermissionDeniedNotAuthorized.message - self.emptyStateView.isHidden = !isPermissionDenied + self.emptyStateViewModel.emptyState = isPermissionDenied ? .unableToAccess : nil + emptyStateViewHostingController.view.isHidden = !isPermissionDenied } .store(in: &disposeBag) } diff --git a/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewModel.swift b/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewModel.swift index df4b05c7..8d52a1a5 100644 --- a/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewModel.swift +++ b/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewModel.swift @@ -119,7 +119,7 @@ extension SecondaryContainerViewModel { var menuElements: [UIMenuElement] = [] - let closeColumnAction = UIAction(title: "Close column", image: UIImage(systemName: "xmark.square")) { [weak self, weak stack, weak navigationController] _ in + let closeColumnAction = UIAction(title: "Close column", image: UIImage(systemName: "xmark.square"), attributes: .destructive) { [weak self, weak stack, weak navigationController] _ in guard let self = self else { return } guard let stack = stack else { return } guard let navigationController = navigationController else { return } @@ -128,14 +128,12 @@ extension SecondaryContainerViewModel { navigationController.view.isHidden = true self.viewControllers.removeAll(where: { $0 === navigationController }) } - let closeMenu = UIMenu(title: "", options: .displayInline, children: [closeColumnAction]) - menuElements.append(closeMenu) + menuElements.append(closeColumnAction) let _index: Int? = stack.arrangedSubviews.firstIndex(where: { view in return navigationController.view === view }) if let index = _index { - var moveMenuElements: [UIMenuElement] = [] if index > 0 { let moveLeftMenuAction = UIAction(title: "Move left", image: UIImage(systemName: "arrow.left.square")) { [weak self, weak stack, weak navigationController] _ in guard let self = self else { return } @@ -144,7 +142,7 @@ extension SecondaryContainerViewModel { stack.removeArrangedSubview(navigationController.view) stack.insertArrangedSubview(navigationController.view, at: index - 1) } - moveMenuElements.append(moveLeftMenuAction) + menuElements.append(moveLeftMenuAction) } if index < stack.arrangedSubviews.count - 2 { let moveRightMenuAction = UIAction(title: "Move Right", image: UIImage(systemName: "arrow.right.square")) { [weak self, weak stack, weak navigationController] _ in @@ -154,15 +152,16 @@ extension SecondaryContainerViewModel { stack.removeArrangedSubview(navigationController.view) stack.insertArrangedSubview(navigationController.view, at: index + 1) } - moveMenuElements.append(moveRightMenuAction) - } - if !moveMenuElements.isEmpty { - let moveMenu = UIMenu(title: "", options: .displayInline, children: moveMenuElements) - menuElements.append(moveMenu) + menuElements.append(moveRightMenuAction) } } - let menu = UIMenu(title: "", options: .displayInline, children: menuElements) + let menu = UIMenu( + title: "", + options: .displayInline, + preferredElementSize: menuElements.count > 1 ? .small : .large, + children: menuElements + ) handler([menu]) } diff --git a/TwidereX/Scene/Share/View/Container/EmptyStateView.swift b/TwidereX/Scene/Share/View/Container/EmptyStateView.swift deleted file mode 100644 index 28b0fc1d..00000000 --- a/TwidereX/Scene/Share/View/Container/EmptyStateView.swift +++ /dev/null @@ -1,133 +0,0 @@ -// -// EmptyStateView.swift -// TwidereX -// -// Created by Cirno MainasuK on 2020-12-29. -// Copyright © 2020 Twidere. All rights reserved. -// - -import UIKit - -final class EmptyStateView: UIView { - - let iconImageView: UIImageView = { - let imageView = UIImageView() - imageView.tintColor = UIColor.secondaryLabel.withAlphaComponent(0.5) - return imageView - }() - - let titleLabel: UILabel = { - let label = UILabel() - label.font = .preferredFont(forTextStyle: .headline) - label.textAlignment = .center - label.textColor = .secondaryLabel - label.text = " " - return label - }() - - let messageLabel: UILabel = { - let label = UILabel() - label.font = .preferredFont(forTextStyle: .footnote) - label.textAlignment = .center - label.textColor = .secondaryLabel - label.text = " " - return label - }() - - override init(frame: CGRect) { - super.init(frame: frame) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension EmptyStateView { - private func _init() { - preservesSuperviewLayoutMargins = true - - let topPaddingView = UIView() - let centerPaddingView = UIView() - let bottomPaddingView = UIView() - - topPaddingView.translatesAutoresizingMaskIntoConstraints = false - addSubview(topPaddingView) - NSLayoutConstraint.activate([ - topPaddingView.topAnchor.constraint(equalTo: topAnchor), - topPaddingView.leadingAnchor.constraint(equalTo: leadingAnchor), - topPaddingView.trailingAnchor.constraint(equalTo: trailingAnchor), - ]) - - iconImageView.translatesAutoresizingMaskIntoConstraints = false - addSubview(iconImageView) - NSLayoutConstraint.activate([ - iconImageView.topAnchor.constraint(equalTo: topPaddingView.bottomAnchor), - iconImageView.centerXAnchor.constraint(equalTo: centerXAnchor), - iconImageView.widthAnchor.constraint(equalToConstant: 120).priority(.defaultHigh), - iconImageView.heightAnchor.constraint(equalToConstant: 120).priority(.defaultHigh), - ]) - - centerPaddingView.translatesAutoresizingMaskIntoConstraints = false - addSubview(centerPaddingView) - NSLayoutConstraint.activate([ - centerPaddingView.topAnchor.constraint(equalTo: iconImageView.bottomAnchor), - centerPaddingView.leadingAnchor.constraint(equalTo: leadingAnchor), - centerPaddingView.trailingAnchor.constraint(equalTo: trailingAnchor), - ]) - - titleLabel.translatesAutoresizingMaskIntoConstraints = false - addSubview(titleLabel) - NSLayoutConstraint.activate([ - titleLabel.topAnchor.constraint(equalTo: iconImageView.bottomAnchor), - titleLabel.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), - titleLabel.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), - ]) - - messageLabel.translatesAutoresizingMaskIntoConstraints = false - addSubview(messageLabel) - NSLayoutConstraint.activate([ - messageLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor), - messageLabel.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), - messageLabel.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), - ]) - - bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false - addSubview(bottomPaddingView) - NSLayoutConstraint.activate([ - bottomPaddingView.topAnchor.constraint(equalTo: messageLabel.bottomAnchor), - bottomPaddingView.leadingAnchor.constraint(equalTo: leadingAnchor), - bottomPaddingView.trailingAnchor.constraint(equalTo: trailingAnchor), - bottomAnchor.constraint(equalTo: bottomPaddingView.bottomAnchor) - ]) - - NSLayoutConstraint.activate([ - centerPaddingView.heightAnchor.constraint(equalTo: topPaddingView.heightAnchor, multiplier: 0.5), - bottomPaddingView.heightAnchor.constraint(equalTo: topPaddingView.heightAnchor, multiplier: 1.0), - ]) - } -} - - -#if canImport(SwiftUI) && DEBUG -import SwiftUI - -struct EmptyStateView_Previews: PreviewProvider { - - static var previews: some View { - UIViewPreview { - let emptyStateView = EmptyStateView() - emptyStateView.iconImageView.image = Asset.Human.eyeSlashLarge.image.withRenderingMode(.alwaysTemplate) - emptyStateView.titleLabel.text = "Permission Denied" - - return emptyStateView - } - } - -} - -#endif - diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift index 546e56b7..c903f944 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift @@ -245,7 +245,6 @@ extension TimelineViewController { case .hashtag(let hashtag): return .hashtag(hashtag: hashtag) case .list: - assertionFailure("do not support post on list status") return .post case .search: assertionFailure("do not support post on search status") diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift index f7378bff..22a8b3b0 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel.swift @@ -46,6 +46,7 @@ class TimelineViewModel: TimelineViewModelDriver { // output let didLoadLatest = PassthroughSubject() + @Published var emptyState: EmptyState? // auto fetch private var autoFetchLatestActionTime = CACurrentMediaTime() @@ -191,6 +192,10 @@ extension TimelineViewModel { self.didLoadLatest.send() logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(error.localizedDescription)") context.apiService.error.send(.explicit(.twitterResponseError(error))) + } catch let error as EmptyState { + self.didLoadLatest.send() + self.emptyState = error + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(error.localizedDescription)") } catch { self.didLoadLatest.send() logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(error.localizedDescription)") diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController.swift index 51347bef..af7d1dfe 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController.swift @@ -15,7 +15,8 @@ final class HomeTimelineViewController: ListTimelineViewController { static var unreadIndicatorViewTopMargin: CGFloat { 16 } let unreadIndicatorView = UnreadIndicatorView() - + var unreadIndicatorViewTrailingLayoutConstraint: NSLayoutConstraint! + // ref: https://medium.com/@Mos6yCanSwift/swift-ios-determine-scroll-direction-d48a2327a004 var lastVelocityYSign = 0 var lastContentOffset: CGPoint? @@ -36,12 +37,24 @@ extension HomeTimelineViewController { // setup unreadIndicatorView unreadIndicatorView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(unreadIndicatorView) + unreadIndicatorViewTrailingLayoutConstraint = view.layoutMarginsGuide.trailingAnchor.constraint(equalTo: unreadIndicatorView.trailingAnchor) NSLayoutConstraint.activate([ unreadIndicatorView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 16), - view.layoutMarginsGuide.trailingAnchor.constraint(equalTo: unreadIndicatorView.trailingAnchor), + unreadIndicatorViewTrailingLayoutConstraint, unreadIndicatorView.widthAnchor.constraint(greaterThanOrEqualToConstant: 36).priority(.required - 1), unreadIndicatorView.heightAnchor.constraint(greaterThanOrEqualToConstant: 36).priority(.required - 1), ]) + viewModel.$viewLayoutFrame + .receive(on: DispatchQueue.main) + .sink { [weak self] viewLayoutFrame in + guard let self = self else { return } + if viewLayoutFrame.layoutFrame.width == viewLayoutFrame.readableContentLayoutFrame.width { + self.unreadIndicatorViewTrailingLayoutConstraint.constant = 16 + } else { + self.unreadIndicatorViewTrailingLayoutConstraint.constant = 0 + } + } + .store(in: &disposeBag) unreadIndicatorView.alpha = 0 viewModel.didLoadLatest .receive(on: DispatchQueue.main) diff --git a/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel.swift b/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel.swift index 0ca81231..473ba47c 100644 --- a/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel.swift @@ -34,7 +34,7 @@ final class ListStatusTimelineViewModel: ListTimelineViewModel { ) // end init - isFloatyButtonDisplay = false + isFloatyButtonDisplay = true statusRecordFetchedResultController.userIdentifier = authContext.authenticationContext.userIdentifier // bind titile From b2332c387beb510a01082d3ed520a5ca7d83f5cc Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 4 Jul 2023 18:29:42 +0800 Subject: [PATCH 113/128] fix: update for new Basic API --- TwidereSDK/Package.swift | 2 +- .../StatusRecordFetchedResultController.swift | 4 +++ .../Twitter/Persistence+Twitter.swift | 23 ++++++++-------- .../Twitter/Persistence+TwitterUser+V2.swift | 5 ++++ .../APIService+FriendshipList.swift | 1 + .../APIService/APIService+Status+List.swift | 11 ++++---- .../StatusFetchViewModel+Timeline+User.swift | 23 +++------------- .../User/UserFetchViewModel+Friendship.swift | 2 +- TwidereX.xcodeproj/project.pbxproj | 18 ++++++++++--- .../xcshareddata/swiftpm/Package.resolved | 4 +-- .../Scene/List/List/ListViewModel+State.swift | 26 ++++++++++++------- TwidereX/Scene/List/List/ListViewModel.swift | 1 + .../TimelineViewModel+LoadOldestState.swift | 18 ++++++++----- 13 files changed, 80 insertions(+), 58 deletions(-) diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index 6c48eb1b..b896a523 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -50,7 +50,7 @@ let package = Package( .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.6.0"), .package(url: "https://github.com/tid-kijyun/Kanna.git", from: "5.2.7"), .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"), - .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.11.0"), + .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.13.0"), .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"), .package(name: "CoverFlowStackLayout", path: "../CoverFlowStackLayout"), ], diff --git a/TwidereSDK/Sources/TwidereCore/FetchedResultsController/Status/StatusRecordFetchedResultController.swift b/TwidereSDK/Sources/TwidereCore/FetchedResultsController/Status/StatusRecordFetchedResultController.swift index d3f0d0ba..11190c00 100644 --- a/TwidereSDK/Sources/TwidereCore/FetchedResultsController/Status/StatusRecordFetchedResultController.swift +++ b/TwidereSDK/Sources/TwidereCore/FetchedResultsController/Status/StatusRecordFetchedResultController.swift @@ -68,4 +68,8 @@ extension StatusRecordFetchedResultController { twitterStatusFetchedResultController.statusIDs.value = [] mastodonStatusFetchedResultController.statusIDs.value = [] } + + public func reload() { + self.records = self.records + } } diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+Twitter.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+Twitter.swift index 0d15f64d..46439a19 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+Twitter.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+Twitter.swift @@ -93,19 +93,20 @@ extension Persistence.Twitter { } // end switch } // end for + let contextV2 = Persistence.TwitterStatus.PersistContextV2( + entity: .init(status: status, author: author), + repost: repost, + quote: quote, + replyTo: replyTo, + dictionary: context.dictionary, + me: context.me, + statusCache: statusCache, + userCache: userCache, + networkDate: context.networkDate + ) let result = Persistence.TwitterStatus.createOrMerge( in: managedObjectContext, - context: Persistence.TwitterStatus.PersistContextV2( - entity: .init(status: status, author: author), - repost: repost, - quote: quote, - replyTo: replyTo, - dictionary: context.dictionary, - me: context.me, - statusCache: statusCache, - userCache: userCache, - networkDate: context.networkDate - ) + context: contextV2 ) // end .createOrMerge(…) #if DEBUG diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser+V2.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser+V2.swift index 4ace8978..957a5847 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser+V2.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser+V2.swift @@ -105,6 +105,11 @@ extension Persistence.TwitterUser { user.update(bioEntitiesTransient: TwitterEntity(entity: context.entity.entities?.description)) user.update(urlEntitiesTransient: TwitterEntity(entity: context.entity.entities?.url)) + // convertible properties + if let profileBannerURL = context.entity.profileBannerURL { + user.update(profileBannerURL: profileBannerURL) + } + // V2 entity not contains relationship flags } diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+FriendshipList.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+FriendshipList.swift index 1120143e..1ddf1b75 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+FriendshipList.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+FriendshipList.swift @@ -13,6 +13,7 @@ import CoreDataStack import TwitterSDK import MastodonSDK +// Twitter V2 extension APIService { public func twitterUserFollowingList( diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+List.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+List.swift index 5da1df49..9057ee1c 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+List.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+List.swift @@ -61,13 +61,14 @@ extension APIService { try await managedObjectContext.performChanges { let me = authenticationContext.authenticationRecord.object(in: managedObjectContext)?.user + let contextV2 = Persistence.Twitter.PersistContextV2( + dictionary: dictionary, + me: me, + networkDate: response.networkDate + ) _ = Persistence.Twitter.persist( in: managedObjectContext, - context: Persistence.Twitter.PersistContextV2( - dictionary: dictionary, - me: me, - networkDate: response.networkDate - ) + context: contextV2 ) } // end .performChanges { … } diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift index cd30f904..f9d06f8b 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift @@ -146,6 +146,7 @@ extension StatusFetchViewModel.Timeline.User { func nextInput(fetchContext: TwitterFetchContext) -> Input? { switch self { case .v2(let response): + guard response.value.meta.resultCount > 0 else { return nil } guard let nextToken = response.value.meta.nextToken else { return nil } guard nextToken != fetchContext.paginationToken else { return nil } let fetchContext = fetchContext.map(paginationToken: nextToken) @@ -170,11 +171,9 @@ extension StatusFetchViewModel.Timeline.User { case .status, .media: do { guard !fetchContext.protected else { - throw Twitter.API.Error.ResponseError(httpResponseStatus: .ok, twitterAPIError: .rateLimitExceeded) - } - guard !fetchContext.needsAPIFallback else { - throw Twitter.API.Error.ResponseError(httpResponseStatus: .ok, twitterAPIError: .rateLimitExceeded) + throw EmptyState.unableToAccess } + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [UserTimeline] fetch user timeline: userID[\(fetchContext.userID)] cursor[\(fetchContext.paginationToken ?? "")]") let response = try await api.twitterUserTimeline( userID: fetchContext.userID, query: .init( @@ -187,22 +186,8 @@ extension StatusFetchViewModel.Timeline.User { authenticationContext: fetchContext.authenticationContext ) return .v2(response) - } catch let error as Twitter.API.Error.ResponseError where error.twitterAPIError == .rateLimitExceeded { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Rate Limit] fallback to v1") - let response = try await api.twitterUserTimelineV1( - query: .init( - count: fetchContext.maxResults ?? 20, - userID: fetchContext.userID, - maxID: fetchContext.maxID, - sinceID: nil, - excludeReplies: false, - query: nil - ), - authenticationContext: fetchContext.authenticationContext - ) - return .v1(response) } catch { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch failure: \(error.localizedDescription)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [UserTimeline] fetch failure: \(error.localizedDescription)") throw error } case .like: diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/User/UserFetchViewModel+Friendship.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/User/UserFetchViewModel+Friendship.swift index 91f20bfa..be21f120 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/User/UserFetchViewModel+Friendship.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/User/UserFetchViewModel+Friendship.swift @@ -101,7 +101,7 @@ extension UserFetchViewModel.Friendship { case .twitter(let fetchContext): let query = Twitter.API.V2.User.Follow.FriendshipListQuery( userID: fetchContext.userID, - maxResults: fetchContext.maxResults ?? (fetchContext.paginationToken == nil ? 200 : 1000), + maxResults: fetchContext.maxResults ?? (fetchContext.paginationToken == nil ? 20 : 200), paginationToken: fetchContext.paginationToken ) let response = try await { () -> Twitter.Response.Content in diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 3c2ba9fc..9a0809d7 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3448,9 +3448,13 @@ PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_OBJC_BRIDGING_HEADER = "TwidereX/Supporting Files/TwidereX-Bridging-Header.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,7"; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; @@ -3842,10 +3846,14 @@ PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_OBJC_BRIDGING_HEADER = "TwidereX/Supporting Files/TwidereX-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,7"; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -3874,9 +3882,13 @@ PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_OBJC_BRIDGING_HEADER = "TwidereX/Supporting Files/TwidereX-Bridging-Header.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,7"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index a6808d66..05128940 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -258,8 +258,8 @@ "repositoryURL": "https://github.com/TwidereProject/TwitterSDK.git", "state": { "branch": null, - "revision": "fdfc56029c530b76e93dc6ae052dcc1a64360c26", - "version": "0.11.0" + "revision": "e0e93420c9f6728f5c9a39e716100e6bd5a5d6b9", + "version": "0.13.0" } }, { diff --git a/TwidereX/Scene/List/List/ListViewModel+State.swift b/TwidereX/Scene/List/List/ListViewModel+State.swift index 1c90a67f..aefdf5e2 100644 --- a/TwidereX/Scene/List/List/ListViewModel+State.swift +++ b/TwidereX/Scene/List/List/ListViewModel+State.swift @@ -146,16 +146,16 @@ extension ListViewModel.State { } // The state machine needs guard the Task is re-entry issue-free - Task { + Task { @MainActor in do { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch…") let output = try await ListFetchViewModel.List.list(api: viewModel.context.apiService, input: input) nextInput = output.nextInput if output.hasMore { - await enter(state: Idle.self) + enter(state: Idle.self) } else { - await enter(state: NoMore.self) + enter(state: NoMore.self) } switch output.result { @@ -168,10 +168,12 @@ extension ListViewModel.State { } logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch success") + + viewModel.retryCount = 0 } catch { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch user timeline fail: \(error.localizedDescription)") - await enter(state: Fail.self) + enter(state: Fail.self) } } // end Task } // end func @@ -190,13 +192,17 @@ extension ListViewModel.State { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - guard let _ = viewModel, let stateMachine = stateMachine else { return } + guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: retry loading 3s later…", ((#file as NSString).lastPathComponent), #line, #function) - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: retry loading", ((#file as NSString).lastPathComponent), #line, #function) - stateMachine.enter(Loading.self) - } + Task { @MainActor in + let delay = min(64.0, pow(2.0, Double(viewModel.retryCount))) + viewModel.retryCount += 1 + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: retry loading %.2fs later…", ((#file as NSString).lastPathComponent), #line, #function, delay) + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: retry loading", ((#file as NSString).lastPathComponent), #line, #function) + stateMachine.enter(Loading.self) + } + } // end Task } } diff --git a/TwidereX/Scene/List/List/ListViewModel.swift b/TwidereX/Scene/List/List/ListViewModel.swift index dbd2ef3f..c54d4dd0 100644 --- a/TwidereX/Scene/List/List/ListViewModel.swift +++ b/TwidereX/Scene/List/List/ListViewModel.swift @@ -39,6 +39,7 @@ class ListViewModel { stateMachine.enter(State.Initial.self) return stateMachine }() + @MainActor var retryCount = 0 init( context: AppContext, diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift index 83097e58..b53d15f4 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift @@ -71,7 +71,7 @@ extension TimelineViewModel.LoadOldestState { class Loading: TimelineViewModel.LoadOldestState { var nextInput: StatusFetchViewModel.Timeline.Input? - var failCount = 0 + var retryCount = 0 var nonce = UUID() override func isValidNextState(_ stateClass: AnyClass) -> Bool { @@ -103,9 +103,9 @@ extension TimelineViewModel.LoadOldestState { // reset fail count if needs switch previousState { case is Fail: - failCount += 1 + retryCount += 1 default: - failCount = 0 + retryCount = 0 } guard let viewModel = viewModel, let _ = stateMachine else { return } @@ -126,9 +126,10 @@ extension TimelineViewModel.LoadOldestState { } } - let failCount = UInt64(min(failCount, 60)) - if failCount > 0 { - try? await Task.sleep(nanoseconds: failCount * .second) + if retryCount > 0 { + let delay = min(64.0, pow(2.0, Double(retryCount))) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Loading] restore loading from fail case with delay: \(delay, format: .fixed(precision: 2))s") + try? await Task.sleep(nanoseconds: UInt64(delay) * .second) } guard nonce == self.nonce else { return } await fetch(anchor: _anchorRecord) @@ -202,6 +203,11 @@ extension TimelineViewModel.LoadOldestState { viewModel.statusRecordFetchedResultController.mastodonStatusFetchedResultController.append(statusIDs: statusIDs) } } + + } catch let error as EmptyState { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch failure: \(error.localizedDescription)") + enter(state: NoMore.self) + viewModel.statusRecordFetchedResultController.reload() } catch { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch failure: \(error.localizedDescription)") enter(state: Fail.self) From 369eb7ce694e387679d997409991e6859576f06a Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 4 Jul 2023 18:30:23 +0800 Subject: [PATCH 114/128] chore: update version to v2.0.0 (137) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 36 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 63c72766..0bd836d6 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 136 + 137 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 78cf58d7..80551e3f 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 136 + 137 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 9a0809d7..86d93146 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3435,7 +3435,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 136; + CURRENT_PROJECT_VERSION = 137; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3466,7 +3466,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 136; + CURRENT_PROJECT_VERSION = 137; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3489,7 +3489,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 136; + CURRENT_PROJECT_VERSION = 137; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3513,7 +3513,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 136; + CURRENT_PROJECT_VERSION = 137; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3540,7 +3540,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 136; + CURRENT_PROJECT_VERSION = 137; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3569,7 +3569,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 136; + CURRENT_PROJECT_VERSION = 137; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3598,7 +3598,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 136; + CURRENT_PROJECT_VERSION = 137; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3626,7 +3626,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 136; + CURRENT_PROJECT_VERSION = 137; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3652,7 +3652,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 136; + CURRENT_PROJECT_VERSION = 137; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3679,7 +3679,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 136; + CURRENT_PROJECT_VERSION = 137; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3833,7 +3833,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 136; + CURRENT_PROJECT_VERSION = 137; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3869,7 +3869,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 136; + CURRENT_PROJECT_VERSION = 137; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3900,7 +3900,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 136; + CURRENT_PROJECT_VERSION = 137; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3925,7 +3925,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 136; + CURRENT_PROJECT_VERSION = 137; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3948,7 +3948,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 136; + CURRENT_PROJECT_VERSION = 137; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3971,7 +3971,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 136; + CURRENT_PROJECT_VERSION = 137; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3995,7 +3995,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 136; + CURRENT_PROJECT_VERSION = 137; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -4022,7 +4022,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 136; + CURRENT_PROJECT_VERSION = 137; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index ab71b5e2..3e754b1d 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 136 + 137 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index e4be7a0d..ac78ee76 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 136 + 137 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index 76ab8f26..bb7841a0 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 136 + 137 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index 76ab8f26..bb7841a0 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 136 + 137 From f83666df26fe306da65361fd83935f4fb6c61492 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 5 Jul 2023 14:33:39 +0800 Subject: [PATCH 115/128] chore: remove StubMixer --- StubMixer/AppDelegate.swift | 35 ------- .../AccentColor.colorset/Contents.json | 11 --- .../AppIcon.appiconset/Contents.json | 98 ------------------- StubMixer/Assets.xcassets/Contents.json | 6 -- StubMixer/Base.lproj/LaunchScreen.storyboard | 25 ----- StubMixer/Base.lproj/Main.storyboard | 32 ------ StubMixer/Info.plist | 66 ------------- StubMixer/SceneDelegate.swift | 65 ------------ StubMixer/StubMixer.swift | 34 ------- StubMixer/ViewController.swift | 19 ---- TwidereX.xcodeproj/project.pbxproj | 40 -------- 11 files changed, 431 deletions(-) delete mode 100644 StubMixer/AppDelegate.swift delete mode 100644 StubMixer/Assets.xcassets/AccentColor.colorset/Contents.json delete mode 100644 StubMixer/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 StubMixer/Assets.xcassets/Contents.json delete mode 100644 StubMixer/Base.lproj/LaunchScreen.storyboard delete mode 100644 StubMixer/Base.lproj/Main.storyboard delete mode 100644 StubMixer/Info.plist delete mode 100644 StubMixer/SceneDelegate.swift delete mode 100644 StubMixer/StubMixer.swift delete mode 100644 StubMixer/ViewController.swift diff --git a/StubMixer/AppDelegate.swift b/StubMixer/AppDelegate.swift deleted file mode 100644 index 1f5fa4f8..00000000 --- a/StubMixer/AppDelegate.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// AppDelegate.swift -// StubMixer -// -// Created by Cirno MainasuK on 2020-9-3. -// - -import UIKit - -@main -class AppDelegate: UIResponder, UIApplicationDelegate { - - - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - return true - } - - // MARK: UISceneSession Lifecycle - - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } - - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. - // Use this method to release any resources that were specific to the discarded scenes, as they will not return. - } - - -} - diff --git a/StubMixer/Assets.xcassets/AccentColor.colorset/Contents.json b/StubMixer/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb878970..00000000 --- a/StubMixer/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/StubMixer/Assets.xcassets/AppIcon.appiconset/Contents.json b/StubMixer/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 9221b9bb..00000000 --- a/StubMixer/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" - }, - { - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" - }, - { - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "20x20" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "20x20" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "29x29" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "29x29" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "76x76" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "76x76" - }, - { - "idiom" : "ipad", - "scale" : "2x", - "size" : "83.5x83.5" - }, - { - "idiom" : "ios-marketing", - "scale" : "1x", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/StubMixer/Assets.xcassets/Contents.json b/StubMixer/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596..00000000 --- a/StubMixer/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/StubMixer/Base.lproj/LaunchScreen.storyboard b/StubMixer/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index 865e9329..00000000 --- a/StubMixer/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/StubMixer/Base.lproj/Main.storyboard b/StubMixer/Base.lproj/Main.storyboard deleted file mode 100644 index e95b2d40..00000000 --- a/StubMixer/Base.lproj/Main.storyboard +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/StubMixer/Info.plist b/StubMixer/Info.plist deleted file mode 100644 index 6f183d94..00000000 --- a/StubMixer/Info.plist +++ /dev/null @@ -1,66 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.2.0 - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - LSRequiresIPhoneOS - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main - - - - - UIApplicationSupportsIndirectInputEvents - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/StubMixer/SceneDelegate.swift b/StubMixer/SceneDelegate.swift deleted file mode 100644 index 313d6c9d..00000000 --- a/StubMixer/SceneDelegate.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// SceneDelegate.swift -// StubMixer -// -// Created by Cirno MainasuK on 2020-9-3. -// - -import UIKit - -class SceneDelegate: UIResponder, UIWindowSceneDelegate { - - var window: UIWindow? - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } - } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - } - - -} - -#if DEBUG -class TestWindow: UIWindow { - - override func sendEvent(_ event: UIEvent) { - event.allTouches?.forEach({ (touch) in - let location = touch.location(in: self) - let view = hitTest(location, with: event) - print(view) - }) - - super.sendEvent(event) - } -} -#endif diff --git a/StubMixer/StubMixer.swift b/StubMixer/StubMixer.swift deleted file mode 100644 index d4f2a9f4..00000000 --- a/StubMixer/StubMixer.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// StubMixer.swift -// StubMixer -// -// Created by Cirno MainasuK on 2020-9-3. -// - -import Foundation -import CryptoKit - -public enum StubMixer { - public static func mix(message: Data, use key: String, nonce: String) -> Data { - var sha256 = SHA256() - sha256.update(data: Data(key.utf8)) - sha256.update(data: Data(nonce.utf8)) - let keyDigest = sha256.finalize() - let symmetricKey = SymmetricKey(data: keyDigest) - - let sealedBox = try! AES.GCM.seal(message, using: symmetricKey) - return sealedBox.combined! - } - - public static func restore(combined: Data, use key: String, nonce: String) -> Data { - var sha256 = SHA256() - sha256.update(data: Data(key.utf8)) - sha256.update(data: Data(nonce.utf8)) - let keyDigest = sha256.finalize() - let symmetricKey = SymmetricKey(data: keyDigest) - - let sealedBox = try! AES.GCM.SealedBox(combined: combined) - let message = try! AES.GCM.open(sealedBox, using: symmetricKey) - return message - } -} diff --git a/StubMixer/ViewController.swift b/StubMixer/ViewController.swift deleted file mode 100644 index 7178a025..00000000 --- a/StubMixer/ViewController.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// ViewController.swift -// StubMixer -// -// Created by Cirno MainasuK on 2020-9-3. -// - -import UIKit - -class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view. - } - - -} - diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 86d93146..dcda48f5 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -821,14 +821,6 @@ DBE635832886940B001C114B /* UIStatusBarManager+HandleTapAction.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIStatusBarManager+HandleTapAction.m"; sourceTree = ""; }; DBE6358528869441001C114B /* Notification+Name+HandleTapAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+Name+HandleTapAction.swift"; sourceTree = ""; }; DBE76CD2250095D900DEB0FC /* TwidereX.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TwidereX.entitlements; sourceTree = ""; }; - DBE76CEA2500B29300DEB0FC /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - DBE76CEC2500B29300DEB0FC /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - DBE76CEE2500B29300DEB0FC /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - DBE76CF12500B29300DEB0FC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - DBE76CF32500B29500DEB0FC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - DBE76CF62500B29500DEB0FC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - DBE76CF82500B29500DEB0FC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - DBE76CFE2500B43300DEB0FC /* StubMixer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubMixer.swift; sourceTree = ""; }; DBE76D1D2500E65D00DEB0FC /* HomeTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineViewModel.swift; sourceTree = ""; }; DBEA97622A1B556C00C8B75B /* SecondaryContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryContainerViewController.swift; sourceTree = ""; }; DBEA97652A1B58E200C8B75B /* SecondaryContainerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryContainerViewModel.swift; sourceTree = ""; }; @@ -2042,7 +2034,6 @@ DBDA8E2024FCF8A3006750DC /* TwidereX */, DBDA8E3724FCF8A7006750DC /* TwidereXTests */, DBDA8E4224FCF8A7006750DC /* TwidereXUITests */, - DBE76CE92500B29300DEB0FC /* StubMixer */, DBBBBE5F2744E8CC007ACB4B /* ShareExtension */, DBFDCE4227F450FC00BE99E3 /* TwidereXIntent */, DB7FF05F28853A7F00BFD55E /* NotificationService */, @@ -2182,21 +2173,6 @@ path = PushNotificationScratch; sourceTree = ""; }; - DBE76CE92500B29300DEB0FC /* StubMixer */ = { - isa = PBXGroup; - children = ( - DBE76CEA2500B29300DEB0FC /* AppDelegate.swift */, - DBE76CEC2500B29300DEB0FC /* SceneDelegate.swift */, - DBE76CEE2500B29300DEB0FC /* ViewController.swift */, - DBE76CFE2500B43300DEB0FC /* StubMixer.swift */, - DBE76CF02500B29300DEB0FC /* Main.storyboard */, - DBE76CF32500B29500DEB0FC /* Assets.xcassets */, - DBE76CF52500B29500DEB0FC /* LaunchScreen.storyboard */, - DBE76CF82500B29500DEB0FC /* Info.plist */, - ); - path = StubMixer; - sourceTree = ""; - }; DBE76D312502147200DEB0FC /* Extension */ = { isa = PBXGroup; children = ( @@ -3342,22 +3318,6 @@ name = LaunchScreen.storyboard; sourceTree = ""; }; - DBE76CF02500B29300DEB0FC /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - DBE76CF12500B29300DEB0FC /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - DBE76CF52500B29500DEB0FC /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - DBE76CF62500B29500DEB0FC /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ From 91725da0acf91f719c60869fc3811cc0f3bb6db7 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 5 Jul 2023 16:20:27 +0800 Subject: [PATCH 116/128] fix: notification badge not update issue --- .../Service/AuthenticationService.swift | 64 +++--- .../Service/NotificationService.swift | 202 +++++++++--------- .../TwidereCore/State/AppContext.swift | 40 +++- .../Facade/DataSourceFacade+User.swift | 4 +- ...ovider+UserViewTableViewCellDelegate.swift | 2 +- .../NotificationTimelineViewController.swift | 2 +- ...tificationTimelineViewModel+Diffable.swift | 1 + .../NotificationViewController.swift | 2 +- .../AccountPreferenceViewController.swift | 7 + 9 files changed, 176 insertions(+), 148 deletions(-) diff --git a/TwidereSDK/Sources/TwidereCore/Service/AuthenticationService.swift b/TwidereSDK/Sources/TwidereCore/Service/AuthenticationService.swift index 35938357..1f94e18b 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/AuthenticationService.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/AuthenticationService.swift @@ -23,12 +23,10 @@ public class AuthenticationService: NSObject { let appSecret: AppSecret let managedObjectContext: NSManagedObjectContext // read-only let backgroundManagedObjectContext: NSManagedObjectContext - // let authenticationIndexFetchedResultsController: NSFetchedResultsController + let authenticationIndexFetchedResultsController: NSFetchedResultsController // output - // @Published public var authenticationIndexes: [AuthenticationIndex] = [] - // public let activeAuthenticationIndex = CurrentValueSubject(nil) - // @Published public var activeAuthenticationContext: AuthenticationContext? = nil + @Published public var authenticationIndexes: [AuthenticationIndex] = [] public init( managedObjectContext: NSManagedObjectContext, @@ -40,21 +38,21 @@ public class AuthenticationService: NSObject { self.backgroundManagedObjectContext = backgroundManagedObjectContext self.apiService = apiService self.appSecret = appSecret - // self.authenticationIndexFetchedResultsController = { - // let fetchRequest = AuthenticationIndex.sortedFetchRequest - // fetchRequest.returnsObjectsAsFaults = false - // fetchRequest.fetchBatchSize = 20 - // let controller = NSFetchedResultsController( - // fetchRequest: fetchRequest, - // managedObjectContext: managedObjectContext, - // sectionNameKeyPath: nil, - // cacheName: nil - // ) - // return controller - // }() + self.authenticationIndexFetchedResultsController = { + let fetchRequest = AuthenticationIndex.sortedFetchRequest + fetchRequest.returnsObjectsAsFaults = false + fetchRequest.fetchBatchSize = 20 + let controller = NSFetchedResultsController( + fetchRequest: fetchRequest, + managedObjectContext: managedObjectContext, + sectionNameKeyPath: nil, + cacheName: nil + ) + return controller + }() super.init() - //authenticationIndexFetchedResultsController.delegate = self + authenticationIndexFetchedResultsController.delegate = self // verify credentials for active authentication // FIXME: @@ -184,19 +182,19 @@ extension AuthenticationService { } // MARK: - NSFetchedResultsControllerDelegate -//extension AuthenticationService: NSFetchedResultsControllerDelegate { -// -// public func controllerWillChangeContent(_ controller: NSFetchedResultsController) { -// os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) -// } -// -// public func controllerDidChangeContent(_ controller: NSFetchedResultsController) { -// switch controller { -// case authenticationIndexFetchedResultsController: -// authenticationIndexes = authenticationIndexFetchedResultsController.fetchedObjects ?? [] -// default: -// assertionFailure() -// } -// } -// -//} +extension AuthenticationService: NSFetchedResultsControllerDelegate { + + public func controllerWillChangeContent(_ controller: NSFetchedResultsController) { + os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + + public func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + switch controller { + case authenticationIndexFetchedResultsController: + authenticationIndexes = authenticationIndexFetchedResultsController.fetchedObjects ?? [] + default: + assertionFailure() + } + } + +} diff --git a/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift b/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift index 3db6be7f..71c522d1 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift @@ -51,126 +51,116 @@ final public actor NotificationService { self.api = apiService self.authenticationService = authenticationService self.appSecret = appSecret + // end init + } + +} + +extension NotificationService { + func registerNotification() { + guard let authenticationService = authenticationService else { return } // request notification permission if needs - // register notification subscriber - // FIXME: add logic for refresh all accounts -// authenticationService.$authenticationIndexes -// .sink { [weak self] authenticationIndexes in -// guard let self = self else { return } -// -// let managedObjectContext = authenticationService.managedObjectContext -// // request permission when sign-in account -// Task { -// let isEmpty = await managedObjectContext.perform { -// return authenticationIndexes.isEmpty -// } -// if !isEmpty { -// await self.requestNotificationPermission() -// } -// } // end Task -// -// Task { -// let authenticationContexts: [AuthenticationContext] = await managedObjectContext.perform { -// let authenticationContexts = authenticationIndexes.compactMap { authenticationIndex in -// AuthenticationContext(authenticationIndex: authenticationIndex, secret: self.appSecret.secret) -// } -// return authenticationContexts -// } -// await self.updateSubscribers(authenticationContexts) -// } // end Task -// } -// .store(in: &disposeBag) // FIXME: how to use disposeBag in actor under Swift 6 ?? + Task { + let isEmpty = authenticationService.authenticationIndexes.isEmpty + if !isEmpty { + await self.requestNotificationPermission() + } + } // end Task -// Publishers.CombineLatest( -// authenticationService.$authenticationIndexes, -// applicationIconBadgeNeedsUpdate -// ) -// .receive(on: DispatchQueue.main) -// .sink { [weak self] authenticationIndexes, _ in -// guard let self = self else { return } -// -// let authenticationContexts = authenticationIndexes.compactMap { authenticationIndex in -// AuthenticationContext(authenticationIndex: authenticationIndex, secret: self.appSecret.secret) -// } -// -// var count = 0 -// for authenticationContext in authenticationContexts { -// switch authenticationContext { -// case .twitter: -// continue -// case .mastodon(let authenticationContext): -// let accessToken = authenticationContext.authorization.accessToken -// let _count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) -// count += _count -// } -// } -// -// UserDefaults.shared.notificationBadgeCount = count -// let _count = count -// Task { -// await self.updateApplicationIconBadge(count: _count) -// } -// self.unreadNotificationCountDidUpdate.send() -// } -// .store(in: &disposeBag) + // register notification subscriber + Task { + let managedObjectContext = authenticationService.managedObjectContext + let authenticationContexts: [AuthenticationContext] = await managedObjectContext.perform { + let authenticationContexts = authenticationService.authenticationIndexes.compactMap { authenticationIndex in + AuthenticationContext(authenticationIndex: authenticationIndex, secret: self.appSecret.secret) + } + return authenticationContexts + } + await self.updateSubscribers(authenticationContexts) + } // end Task } + func updateApplicationIconBadge() async { + guard let authenticationService = authenticationService else { return } + + let managedObjectContext = authenticationService.managedObjectContext + let authenticationContexts: [AuthenticationContext] = await managedObjectContext.perform { + let authenticationContexts = authenticationService.authenticationIndexes.compactMap { authenticationIndex in + AuthenticationContext(authenticationIndex: authenticationIndex, secret: self.appSecret.secret) + } + return authenticationContexts + } + + var count = 0 + for authenticationContext in authenticationContexts { + switch authenticationContext { + case .twitter: + continue + case .mastodon(let authenticationContext): + let accessToken = authenticationContext.authorization.accessToken + let _count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) + count += _count + } + } + + UserDefaults.shared.notificationBadgeCount = count + let _count = count + Task { + await self.updateApplicationIconBadge(count: _count) + } + self.unreadNotificationCountDidUpdate.send() + } } extension NotificationService { - public nonisolated func unreadApplicationShortcutItems() async -> [UIApplicationShortcutItem] { - return [] -// guard let authenticationService = await self.authenticationService else { return [] } -// let managedObjectContext = authenticationService.managedObjectContext -// return await managedObjectContext.perform { -// var items: [UIApplicationShortcutItem] = [] -// for object in authenticationService.authenticationIndexes { -// guard let authenticationIndex = managedObjectContext.object(with: object.objectID) as? AuthenticationIndex else { continue } -// let _accessToken: String? = { -// return authenticationIndex.mastodonAuthentication?.userAccessToken -// }() -// guard let accessToken = _accessToken else { continue} -// -// let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) -// guard count > 0 else { continue } -// -// guard let user = authenticationIndex.user else { continue} -// let title = "@\(user.username)" -// let subtitle = L10n.Count.notification(count) -// -// let item = UIApplicationShortcutItem( -// type: NotificationService.unreadShortcutItemIdentifier, -// localizedTitle: title, -// localizedSubtitle: subtitle, -// icon: nil, -// userInfo: [ -// "accessToken": accessToken as NSSecureCoding -// ] -// ) -// items.append(item) -// } -// return items -// } + guard let authenticationService = await self.authenticationService else { return [] } + let managedObjectContext = authenticationService.managedObjectContext + return await managedObjectContext.perform { + var items: [UIApplicationShortcutItem] = [] + for object in authenticationService.authenticationIndexes { + guard let authenticationIndex = managedObjectContext.object(with: object.objectID) as? AuthenticationIndex else { continue } + let _accessToken: String? = { + return authenticationIndex.mastodonAuthentication?.userAccessToken + }() + guard let accessToken = _accessToken else { continue} + + let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) + guard count > 0 else { continue } + + guard let user = authenticationIndex.user else { continue} + let title = "@\(user.username)" + let subtitle = L10n.Count.notification(count) + + let item = UIApplicationShortcutItem( + type: NotificationService.unreadShortcutItemIdentifier, + localizedTitle: title, + localizedSubtitle: subtitle, + icon: nil, + userInfo: [ + "accessToken": accessToken as NSSecureCoding + ] + ) + items.append(item) + } + return items + } } - } extension NotificationService { - public func clearNotificationCountForActiveUser() { -// guard let authenticationService = self.authenticationService else { return } -// guard let authenticationContext = authenticationService.activeAuthenticationContext else { return } -// switch authenticationContext { -// case .twitter: -// return -// case .mastodon(let authenticationContext): -// let accessToken = authenticationContext.authorization.accessToken -// UserDefaults.shared.setNotificationCountWithAccessToken(accessToken: accessToken, value: 0) -// } -// -// applicationIconBadgeNeedsUpdate.send() + public func clearNotificationCountForUser(authContext: AuthContext) { + switch authContext.authenticationContext { + case .twitter: + return + case .mastodon(let authenticationContext): + let accessToken = authenticationContext.authorization.accessToken + UserDefaults.shared.setNotificationCountWithAccessToken(accessToken: accessToken, value: 0) + } + + applicationIconBadgeNeedsUpdate.send() } public func updateToken(_ token: String?) { diff --git a/TwidereSDK/Sources/TwidereCore/State/AppContext.swift b/TwidereSDK/Sources/TwidereCore/State/AppContext.swift index 2cfce99b..e00f3e62 100644 --- a/TwidereSDK/Sources/TwidereCore/State/AppContext.swift +++ b/TwidereSDK/Sources/TwidereCore/State/AppContext.swift @@ -67,6 +67,42 @@ public class AppContext: ObservableObject { authenticationService: _authenticationService, appSecret: appSecret ) + + setupNotification() + setupStoreReview() + } + + + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + +} + +extension AppContext { + private func setupNotification() { + authenticationService.$authenticationIndexes + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + Task { + await self.notificationService.registerNotification() + } // end Task + } + .store(in: &disposeBag) + + Publishers.CombineLatest( + authenticationService.$authenticationIndexes, + notificationService.applicationIconBadgeNeedsUpdate + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] _, _ in + guard let self = self else { return } + Task { + await self.notificationService.updateApplicationIconBadge() + } // end Task + } + .store(in: &disposeBag) } private func setupStoreReview() { @@ -103,8 +139,4 @@ public class AppContext: ObservableObject { } - deinit { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - } - } diff --git a/TwidereX/Protocol/Facade/DataSourceFacade+User.swift b/TwidereX/Protocol/Facade/DataSourceFacade+User.swift index f5eaf808..312f1044 100644 --- a/TwidereX/Protocol/Facade/DataSourceFacade+User.swift +++ b/TwidereX/Protocol/Facade/DataSourceFacade+User.swift @@ -315,7 +315,7 @@ extension DataSourceFacade { extension DataSourceFacade { @MainActor static func responseToUserSignOut( - dependency: NeedsDependency & UIViewController, + dependency: NeedsDependency & AuthContextProvider & UIViewController, user: UserRecord ) async throws { let alertController = UIAlertController( @@ -332,7 +332,7 @@ extension DataSourceFacade { var isSignOut = false // clear badge before sign-out - await dependency.context.notificationService.clearNotificationCountForActiveUser() + await dependency.context.notificationService.clearNotificationCountForUser(authContext: dependency.authContext) // cancel push notification subscription do { diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift index 6bafb393..d16ad04f 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift @@ -29,7 +29,7 @@ extension UserViewTableViewCellDelegate where Self: DataSourceProvider & AuthCon // MARK: - menu button -extension UserViewTableViewCellDelegate where Self: DataSourceProvider { +extension UserViewTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { func tableViewCell( _ cell: UITableViewCell, viewModel: UserView.ViewModel, diff --git a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift index 5c196eba..4b58dc47 100644 --- a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift +++ b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift @@ -187,7 +187,7 @@ extension NotificationTimelineViewController: UITableViewDelegate, AutoGenerateT // check item type inside `loadMore` Task { await viewModel.loadMore(item: item) - } + } // end Task } } diff --git a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift index fbdb7158..18360721 100644 --- a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift @@ -156,6 +156,7 @@ extension NotificationTimelineViewModel { } // load timeline gap + @MainActor func loadMore(item: NotificationItem) async { guard case let .feedLoader(record) = item else { return } diff --git a/TwidereX/Scene/Notification/NotificationViewController.swift b/TwidereX/Scene/Notification/NotificationViewController.swift index 45716a4d..9b96b6cd 100644 --- a/TwidereX/Scene/Notification/NotificationViewController.swift +++ b/TwidereX/Scene/Notification/NotificationViewController.swift @@ -128,7 +128,7 @@ extension NotificationViewController { // reset notification count Task { - await self.context.notificationService.clearNotificationCountForActiveUser() + await self.context.notificationService.clearNotificationCountForUser(authContext: viewModel.authContext) } // end Task } diff --git a/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceViewController.swift b/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceViewController.swift index 2e810c5a..c1559e27 100644 --- a/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceViewController.swift +++ b/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceViewController.swift @@ -69,3 +69,10 @@ extension AccountPreferenceViewController { } } + +// MARK: - AuthContextProvider +extension AccountPreferenceViewController: AuthContextProvider { + var authContext: AuthContext { + return viewModel.authContext + } +} From 7ff8a85083f9b30bd2400ad40653903ed3d5beaa Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 5 Jul 2023 16:20:59 +0800 Subject: [PATCH 117/128] fix: host list title menu not fetch latest data issue --- .../HomeList/HomeListStatusTimelineViewController.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/TwidereX/Scene/List/HomeList/HomeListStatusTimelineViewController.swift b/TwidereX/Scene/List/HomeList/HomeListStatusTimelineViewController.swift index 7503f3ce..b10f80a7 100644 --- a/TwidereX/Scene/List/HomeList/HomeListStatusTimelineViewController.swift +++ b/TwidereX/Scene/List/HomeList/HomeListStatusTimelineViewController.swift @@ -73,6 +73,10 @@ extension HomeListStatusTimelineViewController { navigationItem.titleMenuProvider = { [weak self] _ -> UIMenu? in guard let self = self else { return nil } + defer { + self.reloadList() + } + let menuContext = self.viewModel.createHomeListMenuContext() var children: [UIMenuElement] = [ @@ -204,6 +208,11 @@ extension HomeListStatusTimelineViewController { title = L10n.Scene.Timeline.title } + private func reloadList() { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): reload owned list") + viewModel.ownedListViewModel.stateMachine.enter(ListViewModel.State.Reloading.self) + } + } // MARK: - AuthContextProvider From fcb1db3d49c37bc4310b959a18275f773472bd9d Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 5 Jul 2023 17:00:27 +0800 Subject: [PATCH 118/128] feat: add list private error prompt --- .../TwidereCore/Error/EmptyState.swift | 6 ++-- ...donListRecordFetchedResultController.swift | 4 +++ ...terListRecordFetchedResultController.swift | 4 +++ .../StatusFetchViewModel+Timeline+List.swift | 2 +- .../StatusFetchViewModel+Timeline+User.swift | 2 +- .../TwidereUI/Content/EmptyStateView.swift | 2 +- .../HostListStatusTimelineViewModel.swift | 12 ++++++-- .../Scene/List/List/ListViewModel+State.swift | 29 ++++++++++++++++--- TwidereX/Scene/List/List/ListViewModel.swift | 2 ++ .../FollowerListViewController.swift | 2 +- .../FollowingListViewController.swift | 2 +- .../TimelineViewModel+LoadOldestState.swift | 1 + .../List/ListTimelineViewController.swift | 24 +++++++++++++++ 13 files changed, 78 insertions(+), 14 deletions(-) diff --git a/TwidereSDK/Sources/TwidereCore/Error/EmptyState.swift b/TwidereSDK/Sources/TwidereCore/Error/EmptyState.swift index ac0e263f..ab8855c2 100644 --- a/TwidereSDK/Sources/TwidereCore/Error/EmptyState.swift +++ b/TwidereSDK/Sources/TwidereCore/Error/EmptyState.swift @@ -10,7 +10,7 @@ import TwidereLocalization public enum EmptyState: Swift.Error { case noResults - case unableToAccess + case unableToAccess(reason: String? = nil) } extension EmptyState { @@ -36,8 +36,8 @@ extension EmptyState { switch self { case .noResults: return nil - case .unableToAccess: - return nil + case .unableToAccess(let reason): + return reason } } } diff --git a/TwidereSDK/Sources/TwidereCore/FetchedResultsController/List/MastodonListRecordFetchedResultController.swift b/TwidereSDK/Sources/TwidereCore/FetchedResultsController/List/MastodonListRecordFetchedResultController.swift index 6fc0330d..3db99d84 100644 --- a/TwidereSDK/Sources/TwidereCore/FetchedResultsController/List/MastodonListRecordFetchedResultController.swift +++ b/TwidereSDK/Sources/TwidereCore/FetchedResultsController/List/MastodonListRecordFetchedResultController.swift @@ -92,6 +92,10 @@ extension MastodonListRecordFetchedResultController { ids = [] } + public func update(ids: [TwitterList.ID]) { + self.ids = ids + } + public func prepend(ids: [TwitterList.ID]) { var result = self.ids let ids = ids.filter { !result.contains($0) } diff --git a/TwidereSDK/Sources/TwidereCore/FetchedResultsController/List/TwitterListRecordFetchedResultController.swift b/TwidereSDK/Sources/TwidereCore/FetchedResultsController/List/TwitterListRecordFetchedResultController.swift index eb20db53..3ee32729 100644 --- a/TwidereSDK/Sources/TwidereCore/FetchedResultsController/List/TwitterListRecordFetchedResultController.swift +++ b/TwidereSDK/Sources/TwidereCore/FetchedResultsController/List/TwitterListRecordFetchedResultController.swift @@ -89,6 +89,10 @@ extension TwitterListRecordFetchedResultController { ids = [] } + public func update(ids: [TwitterList.ID]) { + self.ids = ids + } + public func prepend(ids: [TwitterList.ID]) { var result = self.ids let ids = ids.filter { !result.contains($0) } diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+List.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+List.swift index 4aa5f6bc..9b0f4c26 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+List.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+List.swift @@ -196,7 +196,7 @@ extension StatusFetchViewModel.Timeline.List { try await fetchContext.managedObjectContext.perform { guard let list = fetchContext.list.object(in: fetchContext.managedObjectContext) else { return } if list.private { - throw EmptyState.unableToAccess + throw EmptyState.unableToAccess(reason: "The list is private") } } } diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift index f9d06f8b..d4a47f57 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift @@ -171,7 +171,7 @@ extension StatusFetchViewModel.Timeline.User { case .status, .media: do { guard !fetchContext.protected else { - throw EmptyState.unableToAccess + throw EmptyState.unableToAccess() } logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [UserTimeline] fetch user timeline: userID[\(fetchContext.userID)] cursor[\(fetchContext.paginationToken ?? "")]") let response = try await api.twitterUserTimeline( diff --git a/TwidereSDK/Sources/TwidereUI/Content/EmptyStateView.swift b/TwidereSDK/Sources/TwidereUI/Content/EmptyStateView.swift index 78a39873..d5916de1 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/EmptyStateView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/EmptyStateView.swift @@ -76,7 +76,7 @@ extension EmptyStateView { struct EmptyStateView_Previews: PreviewProvider { static var previews: some View { EmptyStateView(viewModel: .init(emptyState: .noResults)) - EmptyStateView(viewModel: .init(emptyState: .unableToAccess)) + EmptyStateView(viewModel: .init(emptyState: .unableToAccess())) } } diff --git a/TwidereX/Scene/List/HomeList/HostListStatusTimelineViewModel.swift b/TwidereX/Scene/List/HomeList/HostListStatusTimelineViewModel.swift index 02624645..bf31b429 100644 --- a/TwidereX/Scene/List/HomeList/HostListStatusTimelineViewModel.swift +++ b/TwidereX/Scene/List/HomeList/HostListStatusTimelineViewModel.swift @@ -42,8 +42,16 @@ final class HomeListStatusTimelineViewModel: ObservableObject { self.context = context self.authContext = authContext if let me = authContext.authenticationContext.user(in: context.managedObjectContext)?.asRecord { - self.ownedListViewModel = ListViewModel(context: context, authContext: authContext, kind: .owned(user: me)) - self.subscribedListViewModel = ListViewModel(context: context, authContext: authContext, kind: .subscribed(user: me)) + self.ownedListViewModel = { + let viewModel = ListViewModel(context: context, authContext: authContext, kind: .owned(user: me)) + viewModel.needsResetBeforeReloading = false + return viewModel + }() + self.subscribedListViewModel = { + let viewModel = ListViewModel(context: context, authContext: authContext, kind: .subscribed(user: me)) + viewModel.needsResetBeforeReloading = false + return viewModel + }() } else { self.ownedListViewModel = ListViewModel(context: context, authContext: authContext, kind: .none) self.subscribedListViewModel = ListViewModel(context: context, authContext: authContext, kind: .none) diff --git a/TwidereX/Scene/List/List/ListViewModel+State.swift b/TwidereX/Scene/List/List/ListViewModel+State.swift index aefdf5e2..cd15775c 100644 --- a/TwidereX/Scene/List/List/ListViewModel+State.swift +++ b/TwidereX/Scene/List/List/ListViewModel+State.swift @@ -71,7 +71,9 @@ extension ListViewModel.State { guard let viewModel = viewModel, let stateMachine = stateMachine else { return } // reset - viewModel.fetchedResultController.reset() + if viewModel.needsResetBeforeReloading { + viewModel.fetchedResultController.reset() + } stateMachine.enter(Loading.self) } @@ -80,6 +82,7 @@ extension ListViewModel.State { class Loading: ListViewModel.State { var nextInput: ListFetchViewModel.List.Input? + var nonce = UUID() override func isValidNextState(_ stateClass: AnyClass) -> Bool { switch stateClass { @@ -97,12 +100,16 @@ extension ListViewModel.State { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) guard let viewModel = viewModel, let stateMachine = stateMachine else { return } + nonce = UUID() + let nonce = self.nonce + let isReloading: Bool switch previousState { case is Reloading: nextInput = nil + isReloading = true default: - break + isReloading = false } guard let user = viewModel.kind.user else { @@ -151,6 +158,12 @@ extension ListViewModel.State { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch…") let output = try await ListFetchViewModel.List.list(api: viewModel.context.apiService, input: input) + + // check nonce + guard nonce == self.nonce else { + return + } + nextInput = output.nextInput if output.hasMore { enter(state: Idle.self) @@ -161,10 +174,18 @@ extension ListViewModel.State { switch output.result { case .twitter(let lists): let ids = lists.map { $0.id } - viewModel.fetchedResultController.twitterListRecordFetchedResultController.append(ids: ids) + if isReloading { + viewModel.fetchedResultController.twitterListRecordFetchedResultController.update(ids: ids) + } else { + viewModel.fetchedResultController.twitterListRecordFetchedResultController.append(ids: ids) + } case .mastodon(let lists): let ids = lists.map { $0.id } - viewModel.fetchedResultController.mastodonListRecordFetchedResultController.append(ids: ids) + if isReloading { + viewModel.fetchedResultController.mastodonListRecordFetchedResultController.update(ids: ids) + } else { + viewModel.fetchedResultController.mastodonListRecordFetchedResultController.append(ids: ids) + } } logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch success") diff --git a/TwidereX/Scene/List/List/ListViewModel.swift b/TwidereX/Scene/List/List/ListViewModel.swift index c54d4dd0..16aef448 100644 --- a/TwidereX/Scene/List/List/ListViewModel.swift +++ b/TwidereX/Scene/List/List/ListViewModel.swift @@ -23,6 +23,8 @@ class ListViewModel { let fetchedResultController: ListRecordFetchedResultController let listBatchFetchViewModel = ListBatchFetchViewModel() + var needsResetBeforeReloading = true + // output var diffableDataSource: UITableViewDiffableDataSource? diff --git a/TwidereX/Scene/Profile/FriendshipList/FollowerList/FollowerListViewController.swift b/TwidereX/Scene/Profile/FriendshipList/FollowerList/FollowerListViewController.swift index 31bed0a5..f15fa94e 100644 --- a/TwidereX/Scene/Profile/FriendshipList/FollowerList/FollowerListViewController.swift +++ b/TwidereX/Scene/Profile/FriendshipList/FollowerList/FollowerListViewController.swift @@ -84,7 +84,7 @@ extension FollowerListViewController { .receive(on: DispatchQueue.main) .sink { [weak self] isPermissionDenied in guard let self = self else { return } - self.emptyStateViewModel.emptyState = isPermissionDenied ? .unableToAccess : nil + self.emptyStateViewModel.emptyState = isPermissionDenied ? .unableToAccess() : nil emptyStateViewHostingController.view.isHidden = !isPermissionDenied } .store(in: &disposeBag) diff --git a/TwidereX/Scene/Profile/FriendshipList/FollowingList/FollowingListViewController.swift b/TwidereX/Scene/Profile/FriendshipList/FollowingList/FollowingListViewController.swift index 3fe9c7eb..dc9925b3 100644 --- a/TwidereX/Scene/Profile/FriendshipList/FollowingList/FollowingListViewController.swift +++ b/TwidereX/Scene/Profile/FriendshipList/FollowingList/FollowingListViewController.swift @@ -86,7 +86,7 @@ extension FollowingListViewController { .receive(on: DispatchQueue.main) .sink { [weak self] isPermissionDenied in guard let self = self else { return } - self.emptyStateViewModel.emptyState = isPermissionDenied ? .unableToAccess : nil + self.emptyStateViewModel.emptyState = isPermissionDenied ? .unableToAccess() : nil emptyStateViewHostingController.view.isHidden = !isPermissionDenied } .store(in: &disposeBag) diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift index b53d15f4..9eeaeb50 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift @@ -207,6 +207,7 @@ extension TimelineViewModel.LoadOldestState { } catch let error as EmptyState { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch failure: \(error.localizedDescription)") enter(state: NoMore.self) + viewModel.emptyState = error viewModel.statusRecordFetchedResultController.reload() } catch { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch failure: \(error.localizedDescription)") diff --git a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift index 9bc29d48..248ce72e 100644 --- a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift @@ -8,6 +8,7 @@ import os.log import UIKit +import SwiftUI import Combine import Floaty import TabBarPager @@ -22,6 +23,8 @@ class ListTimelineViewController: TimelineViewController { _viewModel = newValue } } + let emptyStateViewModel = EmptyStateView.ViewModel() + private(set) lazy var tableView: UITableView = { let tableView = UITableView() tableView.backgroundColor = .systemBackground @@ -98,6 +101,27 @@ extension ListTimelineViewController { } .store(in: &disposeBag) + viewModel.$emptyState + .assign(to: \.emptyState, on: emptyStateViewModel) + .store(in: &disposeBag) + + let emptyStateViewHostingController = UIHostingController(rootView: EmptyStateView(viewModel: emptyStateViewModel)) + addChild(emptyStateViewHostingController) + emptyStateViewHostingController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(emptyStateViewHostingController.view) + NSLayoutConstraint.activate([ + emptyStateViewHostingController.view.topAnchor.constraint(equalTo: view.topAnchor), + emptyStateViewHostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + emptyStateViewHostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + emptyStateViewHostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + emptyStateViewHostingController.view.isHidden = true + emptyStateViewModel.$emptyState + .map { $0 == nil } + .receive(on: DispatchQueue.main) + .assign(to: \.isHidden, on: emptyStateViewHostingController.view) + .store(in: &disposeBag) + NotificationCenter.default .publisher(for: .statusBarTapped, object: nil) .throttle(for: 0.5, scheduler: DispatchQueue.main, latest: false) From c61670c2cf0dcd87ef3d5aa42f6089fbb4bd7714 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 5 Jul 2023 18:02:28 +0800 Subject: [PATCH 119/128] feat: add Twitter list not select prompt --- .../TwidereCore/Error/EmptyState.swift | 7 +++++ .../TwidereUI/Content/EmptyStateView.swift | 28 +++++++++++-------- ...HomeListStatusTimelineViewController.swift | 28 ++++++++++++++++++- 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/TwidereSDK/Sources/TwidereCore/Error/EmptyState.swift b/TwidereSDK/Sources/TwidereCore/Error/EmptyState.swift index ab8855c2..df922252 100644 --- a/TwidereSDK/Sources/TwidereCore/Error/EmptyState.swift +++ b/TwidereSDK/Sources/TwidereCore/Error/EmptyState.swift @@ -11,6 +11,7 @@ import TwidereLocalization public enum EmptyState: Swift.Error { case noResults case unableToAccess(reason: String? = nil) + case homeListNotSelected } extension EmptyState { @@ -20,6 +21,8 @@ extension EmptyState { return "eye.slash" case .unableToAccess: return "exclamationmark.triangle" + case .homeListNotSelected: + return "list.bullet" } } @@ -29,6 +32,8 @@ extension EmptyState { return L10n.Common.Controls.List.noResults case .unableToAccess: return "Unable to access" + case .homeListNotSelected: + return "No list selected" } } @@ -38,6 +43,8 @@ extension EmptyState { return nil case .unableToAccess(let reason): return reason + case .homeListNotSelected: + return "Please select a list to continue browsing. The home timeline is no longer available due to API changes." } } } diff --git a/TwidereSDK/Sources/TwidereUI/Content/EmptyStateView.swift b/TwidereSDK/Sources/TwidereUI/Content/EmptyStateView.swift index d5916de1..4c3b4015 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/EmptyStateView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/EmptyStateView.swift @@ -31,22 +31,26 @@ public struct EmptyStateView: View { .font(.title) .opacity(0.5) } - if let title = viewModel.title { - Text(verbatim: title) - .foregroundColor(.secondary) - .font(.headline) - } - if let subtitle = viewModel.subtitle { - Text(verbatim: subtitle) - .foregroundColor(.secondary) - .font(.subheadline) - } - } + VStack(spacing: 8) { + if let title = viewModel.title { + Text(verbatim: title) + .foregroundColor(.secondary) + .font(.headline) + } + if let subtitle = viewModel.subtitle { + Text(verbatim: subtitle) + .foregroundColor(.secondary) + .font(.subheadline) + .frame(maxWidth: 300) + .multilineTextAlignment(.center) + } + } // end VStack + } // end VStack Spacer() Spacer() Spacer() Spacer() - } + } // end VStack } } diff --git a/TwidereX/Scene/List/HomeList/HomeListStatusTimelineViewController.swift b/TwidereX/Scene/List/HomeList/HomeListStatusTimelineViewController.swift index b10f80a7..6d6a53ec 100644 --- a/TwidereX/Scene/List/HomeList/HomeListStatusTimelineViewController.swift +++ b/TwidereX/Scene/List/HomeList/HomeListStatusTimelineViewController.swift @@ -8,6 +8,7 @@ import os.log import UIKit +import SwiftUI import Combine import TwidereLocalization @@ -29,7 +30,9 @@ final class HomeListStatusTimelineViewController: UIViewController, NeedsDepende public var viewModel: HomeListStatusTimelineViewModel! var disposeBag = Set() - var listStatusTimelineViewController: ListTimelineViewController? + let emptyStateViewModel = EmptyStateView.ViewModel() + + @Published var listStatusTimelineViewController: ListTimelineViewController? } extension HomeListStatusTimelineViewController { @@ -114,6 +117,29 @@ extension HomeListStatusTimelineViewController { self.attachTimelineViewController(menuContext: menuContext) } .store(in: &disposeBag) + + let emptyStateViewHostingController = UIHostingController(rootView: EmptyStateView(viewModel: emptyStateViewModel)) + addChild(emptyStateViewHostingController) + emptyStateViewHostingController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(emptyStateViewHostingController.view) + NSLayoutConstraint.activate([ + emptyStateViewHostingController.view.topAnchor.constraint(equalTo: view.topAnchor), + emptyStateViewHostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + emptyStateViewHostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + emptyStateViewHostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + emptyStateViewHostingController.view.isHidden = true + emptyStateViewModel.$emptyState + .map { $0 == nil } + .receive(on: DispatchQueue.main) + .assign(to: \.isHidden, on: emptyStateViewHostingController.view) + .store(in: &disposeBag) + + $listStatusTimelineViewController + .map { $0 == nil ? EmptyState.homeListNotSelected : nil } + .receive(on: DispatchQueue.main) + .assign(to: \.emptyState, on: emptyStateViewModel) + .store(in: &disposeBag) } override func viewDidAppear(_ animated: Bool) { From 0b1586d810853d306e30d0363b4104f70d086403 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 5 Jul 2023 18:03:30 +0800 Subject: [PATCH 120/128] fix: welcome mastodon domain text field entry keyboard avoid broken in iOS 16.2 issue --- TwidereX/Scene/Onboarding/Welcome/WelcomeView.swift | 4 ++++ .../Onboarding/Welcome/WelcomeViewController.swift | 12 +++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/TwidereX/Scene/Onboarding/Welcome/WelcomeView.swift b/TwidereX/Scene/Onboarding/Welcome/WelcomeView.swift index f14e738e..d68afab5 100644 --- a/TwidereX/Scene/Onboarding/Welcome/WelcomeView.swift +++ b/TwidereX/Scene/Onboarding/Welcome/WelcomeView.swift @@ -105,6 +105,10 @@ struct WelcomeView: View { } ) .disabled(viewModel.isBusy) + .transaction { transaction in + // disable titile slide in animation + transaction.disablesAnimations = true + } } .padding(.bottom, 20) } diff --git a/TwidereX/Scene/Onboarding/Welcome/WelcomeViewController.swift b/TwidereX/Scene/Onboarding/Welcome/WelcomeViewController.swift index 31effd3c..80d16dc7 100644 --- a/TwidereX/Scene/Onboarding/Welcome/WelcomeViewController.swift +++ b/TwidereX/Scene/Onboarding/Welcome/WelcomeViewController.swift @@ -63,10 +63,16 @@ extension WelcomeViewController { navigationItem.scrollEdgeAppearance = navigationBarAppearance let hostingController = UIHostingController(rootView: WelcomeView().environmentObject(viewModel)) - hostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] - hostingController.view.frame = view.bounds + addChild(hostingController) + hostingController.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(hostingController.view) - + NSLayoutConstraint.activate([ + hostingController.view.topAnchor.constraint(equalTo: view.topAnchor), + hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + viewModel.delegate = self viewModel.error From bad17c7bf064379763589799b4604da11654e289 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 5 Jul 2023 18:04:07 +0800 Subject: [PATCH 121/128] fix: hide invalid subscribed list section for Twitter due to API changes --- .../List/CompositeList/CompositeListViewModel+Diffable.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/TwidereX/Scene/List/CompositeList/CompositeListViewModel+Diffable.swift b/TwidereX/Scene/List/CompositeList/CompositeListViewModel+Diffable.swift index ee4cab03..438bcc9a 100644 --- a/TwidereX/Scene/List/CompositeList/CompositeListViewModel+Diffable.swift +++ b/TwidereX/Scene/List/CompositeList/CompositeListViewModel+Diffable.swift @@ -64,7 +64,9 @@ extension CompositeListViewModel { let _subscribedListSection: ListSection? = { switch user { - case .twitter: return ListSection.twitter(kind: .subscribed) + // Deprecated: hide due to API invalid + // case .twitter: return ListSection.twitter(kind: .subscribed) + case .twitter: return nil case .mastodon: return nil } }() From 65df486f43c59f41974b007f852250951fa357e8 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 5 Jul 2023 18:06:02 +0800 Subject: [PATCH 122/128] chore: update to version v2.0.0 (138) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 42 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 27 insertions(+), 27 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 0bd836d6..a45294cc 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 137 + 138 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 80551e3f..b54ac954 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 137 + 138 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index dcda48f5..dde32ef5 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3395,7 +3395,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 137; + CURRENT_PROJECT_VERSION = 138; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3410,7 +3410,7 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_OBJC_BRIDGING_HEADER = "TwidereX/Supporting Files/TwidereX-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -3426,7 +3426,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 137; + CURRENT_PROJECT_VERSION = 138; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3449,7 +3449,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 137; + CURRENT_PROJECT_VERSION = 138; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3473,7 +3473,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 137; + CURRENT_PROJECT_VERSION = 138; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3500,7 +3500,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 137; + CURRENT_PROJECT_VERSION = 138; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3529,7 +3529,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 137; + CURRENT_PROJECT_VERSION = 138; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3558,7 +3558,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 137; + CURRENT_PROJECT_VERSION = 138; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3586,7 +3586,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 137; + CURRENT_PROJECT_VERSION = 138; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3612,7 +3612,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 137; + CURRENT_PROJECT_VERSION = 138; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3639,7 +3639,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 137; + CURRENT_PROJECT_VERSION = 138; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3793,7 +3793,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 137; + CURRENT_PROJECT_VERSION = 138; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3808,7 +3808,7 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_OBJC_BRIDGING_HEADER = "TwidereX/Supporting Files/TwidereX-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -3829,7 +3829,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 137; + CURRENT_PROJECT_VERSION = 138; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3844,7 +3844,7 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_OBJC_BRIDGING_HEADER = "TwidereX/Supporting Files/TwidereX-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -3860,7 +3860,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 137; + CURRENT_PROJECT_VERSION = 138; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3885,7 +3885,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 137; + CURRENT_PROJECT_VERSION = 138; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3908,7 +3908,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 137; + CURRENT_PROJECT_VERSION = 138; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3931,7 +3931,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 137; + CURRENT_PROJECT_VERSION = 138; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3955,7 +3955,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 137; + CURRENT_PROJECT_VERSION = 138; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3982,7 +3982,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 137; + CURRENT_PROJECT_VERSION = 138; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index 3e754b1d..d9ce69da 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 137 + 138 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index ac78ee76..ce98304d 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 137 + 138 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index bb7841a0..aea8a838 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 137 + 138 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index bb7841a0..aea8a838 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 137 + 138 From 6956f809de072500620f2de87e59eff694ee8973 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 5 Jul 2023 18:20:55 +0800 Subject: [PATCH 123/128] chore: update i18n resources --- .../Generated/Strings.swift | 22 +++++++++++++++++++ .../Resources/ar.lproj/Localizable.strings | 8 +++++++ .../Resources/ca.lproj/Localizable.strings | 8 +++++++ .../Resources/de.lproj/Localizable.strings | 8 +++++++ .../Resources/en.lproj/Localizable.strings | 8 +++++++ .../Resources/es.lproj/Localizable.strings | 8 +++++++ .../Resources/eu.lproj/Localizable.strings | 8 +++++++ .../Resources/gl.lproj/Localizable.strings | 8 +++++++ .../Resources/ja.lproj/Localizable.strings | 8 +++++++ .../Resources/ko.lproj/Localizable.strings | 8 +++++++ .../Resources/pt-BR.lproj/Localizable.strings | 8 +++++++ .../Resources/tr.lproj/Localizable.strings | 10 ++++++++- .../zh-Hans.lproj/Localizable.strings | 18 ++++++++++----- .../Scene/Root/NewColumn/NewColumnView.swift | 2 +- 14 files changed, 125 insertions(+), 7 deletions(-) diff --git a/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift b/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift index 86da00ae..b0f79826 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift +++ b/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift @@ -594,6 +594,12 @@ public enum L10n { public static let media = L10n.tr("Localizable", "Common.Controls.Actions.ShareMediaMenu.Media", fallback: "Media") } } + public enum EmptyState { + /// No results + public static let noResults = L10n.tr("Localizable", "Common.Controls.EmptyState.NoResults", fallback: "No results") + /// Unable to access + public static let unableToAccess = L10n.tr("Localizable", "Common.Controls.EmptyState.UnableToAccess", fallback: "Unable to access") + } public enum Friendship { /// Block %@ public static func blockUser(_ p1: Any) -> String { @@ -937,6 +943,20 @@ public enum L10n { /// Bookmark public static let title = L10n.tr("Localizable", "Scene.Bookmark.Title", fallback: "Bookmark") } + public enum Column { + /// New Column + public static let title = L10n.tr("Localizable", "Scene.Column.Title", fallback: "New Column") + public enum Actions { + /// Close column + public static let closeColumn = L10n.tr("Localizable", "Scene.Column.Actions.CloseColumn", fallback: "Close column") + /// Move left + public static let moveLeft = L10n.tr("Localizable", "Scene.Column.Actions.MoveLeft", fallback: "Move left") + /// Move right + public static let moveRight = L10n.tr("Localizable", "Scene.Column.Actions.MoveRight", fallback: "Move right") + /// Open in new column + public static let openInNewColumn = L10n.tr("Localizable", "Scene.Column.Actions.OpenInNewColumn", fallback: "Open in new column") + } + } public enum Compose { /// , public static let and = L10n.tr("Localizable", "Scene.Compose.And", fallback: ", ") @@ -1206,6 +1226,8 @@ public enum L10n { public enum ManageAccounts { /// Delete account public static let deleteAccount = L10n.tr("Localizable", "Scene.ManageAccounts.DeleteAccount", fallback: "Delete account") + /// Open in new window + public static let openInNewWindow = L10n.tr("Localizable", "Scene.ManageAccounts.OpenInNewWindow", fallback: "Open in new window") /// Accounts public static let title = L10n.tr("Localizable", "Scene.ManageAccounts.Title", fallback: "Accounts") } diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings index f0b17c45..0f37541b 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings @@ -181,6 +181,8 @@ "Common.Controls.Actions.SignOut" = "خروج"; "Common.Controls.Actions.TakePhoto" = "التقط صورة"; "Common.Controls.Actions.Yes" = "نعم"; +"Common.Controls.EmptyState.NoResults" = "لا نتائج"; +"Common.Controls.EmptyState.UnableToAccess" = "Unable to access"; "Common.Controls.Friendship.Actions.Block" = "احجب"; "Common.Controls.Friendship.Actions.Blocked" = "محجوب"; "Common.Controls.Friendship.Actions.Follow" = "تابعه"; @@ -282,6 +284,11 @@ "Common.NotificationChannel.ContentMessages.Name" = "الرسائل"; "Scene.Authentication.Title" = "المصادقة"; "Scene.Bookmark.Title" = "العلامات"; +"Scene.Column.Actions.CloseColumn" = "Close column"; +"Scene.Column.Actions.MoveLeft" = "Move left"; +"Scene.Column.Actions.MoveRight" = "Move right"; +"Scene.Column.Actions.OpenInNewColumn" = "Open in new column"; +"Scene.Column.Title" = "New Column"; "Scene.Compose.And" = "، "; "Scene.Compose.CwPlaceholder" = "اكتب التحذير"; "Scene.Compose.LastEnd" = " و "; @@ -374,6 +381,7 @@ "Scene.ListsUsers.MenuActions.Remove" = "أزِل"; "Scene.Local.Title" = "محلي"; "Scene.ManageAccounts.DeleteAccount" = "احذف الحساب"; +"Scene.ManageAccounts.OpenInNewWindow" = "Open in new window"; "Scene.ManageAccounts.Title" = "الحسابات"; "Scene.Mentions.Title" = "الذِكر"; "Scene.Messages.Action.CopyText" = "انسخ نص الرسالة"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings index c794d577..3076b00c 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings @@ -181,6 +181,8 @@ "Common.Controls.Actions.SignOut" = "Tanca sessió"; "Common.Controls.Actions.TakePhoto" = "Fes una foto"; "Common.Controls.Actions.Yes" = "Sí"; +"Common.Controls.EmptyState.NoResults" = "Cap resultat"; +"Common.Controls.EmptyState.UnableToAccess" = "Unable to access"; "Common.Controls.Friendship.Actions.Block" = "Bloca"; "Common.Controls.Friendship.Actions.Blocked" = "Blocat"; "Common.Controls.Friendship.Actions.Follow" = "Segueix"; @@ -282,6 +284,11 @@ "Common.NotificationChannel.ContentMessages.Name" = "Missatges"; "Scene.Authentication.Title" = "Autenticació"; "Scene.Bookmark.Title" = "Marcador"; +"Scene.Column.Actions.CloseColumn" = "Close column"; +"Scene.Column.Actions.MoveLeft" = "Move left"; +"Scene.Column.Actions.MoveRight" = "Move right"; +"Scene.Column.Actions.OpenInNewColumn" = "Open in new column"; +"Scene.Column.Title" = "New Column"; "Scene.Compose.And" = ", "; "Scene.Compose.CwPlaceholder" = "Escriviu la vostra advertència aquí"; "Scene.Compose.LastEnd" = " i "; @@ -374,6 +381,7 @@ "Scene.ListsUsers.MenuActions.Remove" = "Elimina"; "Scene.Local.Title" = "Local"; "Scene.ManageAccounts.DeleteAccount" = "Esborra el compte"; +"Scene.ManageAccounts.OpenInNewWindow" = "Open in new window"; "Scene.ManageAccounts.Title" = "Comptes"; "Scene.Mentions.Title" = "Mencions"; "Scene.Messages.Action.CopyText" = "Copia el text del missatge"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings index 9ab45fe7..712a7a96 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings @@ -181,6 +181,8 @@ "Common.Controls.Actions.SignOut" = "Abmelden"; "Common.Controls.Actions.TakePhoto" = "Foto aufnehmen"; "Common.Controls.Actions.Yes" = "Ja"; +"Common.Controls.EmptyState.NoResults" = "Keine Ergebnisse"; +"Common.Controls.EmptyState.UnableToAccess" = "Unable to access"; "Common.Controls.Friendship.Actions.Block" = "Blockieren"; "Common.Controls.Friendship.Actions.Blocked" = "Blockiert"; "Common.Controls.Friendship.Actions.Follow" = "Folgen"; @@ -282,6 +284,11 @@ "Common.NotificationChannel.ContentMessages.Name" = "Nachrichten"; "Scene.Authentication.Title" = "Authentifizierung"; "Scene.Bookmark.Title" = "Lesezeichen"; +"Scene.Column.Actions.CloseColumn" = "Close column"; +"Scene.Column.Actions.MoveLeft" = "Move left"; +"Scene.Column.Actions.MoveRight" = "Move right"; +"Scene.Column.Actions.OpenInNewColumn" = "Open in new column"; +"Scene.Column.Title" = "New Column"; "Scene.Compose.And" = ", "; "Scene.Compose.CwPlaceholder" = "Write your warning here"; "Scene.Compose.LastEnd" = " und "; @@ -374,6 +381,7 @@ "Scene.ListsUsers.MenuActions.Remove" = "Entfernen"; "Scene.Local.Title" = "Lokal"; "Scene.ManageAccounts.DeleteAccount" = "Konto löschen"; +"Scene.ManageAccounts.OpenInNewWindow" = "Open in new window"; "Scene.ManageAccounts.Title" = "Konten"; "Scene.Mentions.Title" = "Erwähnungen"; "Scene.Messages.Action.CopyText" = "Nachrichtentext kopieren"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings index a921e13e..50502515 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings @@ -181,6 +181,8 @@ "Common.Controls.Actions.SignOut" = "Sign out"; "Common.Controls.Actions.TakePhoto" = "Take photo"; "Common.Controls.Actions.Yes" = "Yes"; +"Common.Controls.EmptyState.NoResults" = "No results"; +"Common.Controls.EmptyState.UnableToAccess" = "Unable to access"; "Common.Controls.Friendship.Actions.Block" = "Block"; "Common.Controls.Friendship.Actions.Blocked" = "Blocked"; "Common.Controls.Friendship.Actions.Follow" = "Follow"; @@ -282,6 +284,11 @@ "Common.NotificationChannel.ContentMessages.Name" = "Messages"; "Scene.Authentication.Title" = "Authentication"; "Scene.Bookmark.Title" = "Bookmark"; +"Scene.Column.Actions.CloseColumn" = "Close column"; +"Scene.Column.Actions.MoveLeft" = "Move left"; +"Scene.Column.Actions.MoveRight" = "Move right"; +"Scene.Column.Actions.OpenInNewColumn" = "Open in new column"; +"Scene.Column.Title" = "New Column"; "Scene.Compose.And" = ", "; "Scene.Compose.CwPlaceholder" = "Write your warning here"; "Scene.Compose.LastEnd" = " and "; @@ -374,6 +381,7 @@ "Scene.ListsUsers.MenuActions.Remove" = "Remove"; "Scene.Local.Title" = "Local"; "Scene.ManageAccounts.DeleteAccount" = "Delete account"; +"Scene.ManageAccounts.OpenInNewWindow" = "Open in new window"; "Scene.ManageAccounts.Title" = "Accounts"; "Scene.Mentions.Title" = "Mentions"; "Scene.Messages.Action.CopyText" = "Copy message text"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings index 6074affc..b46f04b5 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings @@ -181,6 +181,8 @@ "Common.Controls.Actions.SignOut" = "Cerrar sesión"; "Common.Controls.Actions.TakePhoto" = "Hacer una foto"; "Common.Controls.Actions.Yes" = "Sí"; +"Common.Controls.EmptyState.NoResults" = "Sin resultados"; +"Common.Controls.EmptyState.UnableToAccess" = "Unable to access"; "Common.Controls.Friendship.Actions.Block" = "Bloquear"; "Common.Controls.Friendship.Actions.Blocked" = "Bloqueado"; "Common.Controls.Friendship.Actions.Follow" = "Seguir"; @@ -282,6 +284,11 @@ "Common.NotificationChannel.ContentMessages.Name" = "Mensajes"; "Scene.Authentication.Title" = "Autentificación"; "Scene.Bookmark.Title" = "Marcador"; +"Scene.Column.Actions.CloseColumn" = "Close column"; +"Scene.Column.Actions.MoveLeft" = "Move left"; +"Scene.Column.Actions.MoveRight" = "Move right"; +"Scene.Column.Actions.OpenInNewColumn" = "Open in new column"; +"Scene.Column.Title" = "New Column"; "Scene.Compose.And" = ", "; "Scene.Compose.CwPlaceholder" = "Escribe tu advertencia aquí"; "Scene.Compose.LastEnd" = " y "; @@ -374,6 +381,7 @@ "Scene.ListsUsers.MenuActions.Remove" = "Eliminar"; "Scene.Local.Title" = "Local"; "Scene.ManageAccounts.DeleteAccount" = "Eliminar cuenta"; +"Scene.ManageAccounts.OpenInNewWindow" = "Open in new window"; "Scene.ManageAccounts.Title" = "Cuentas"; "Scene.Mentions.Title" = "Menciones"; "Scene.Messages.Action.CopyText" = "Copiar texto del mensaje"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings index 4fd104dd..88e03354 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings @@ -181,6 +181,8 @@ "Common.Controls.Actions.SignOut" = "Amaitu saioa"; "Common.Controls.Actions.TakePhoto" = "Atera argazkia"; "Common.Controls.Actions.Yes" = "Bai"; +"Common.Controls.EmptyState.NoResults" = "Emaitzarik ez"; +"Common.Controls.EmptyState.UnableToAccess" = "Unable to access"; "Common.Controls.Friendship.Actions.Block" = "Blokeatu"; "Common.Controls.Friendship.Actions.Blocked" = "Blokeatuta"; "Common.Controls.Friendship.Actions.Follow" = "Jarraitu"; @@ -282,6 +284,11 @@ "Common.NotificationChannel.ContentMessages.Name" = "Mezuak"; "Scene.Authentication.Title" = "Autentifikazioa"; "Scene.Bookmark.Title" = "Laster-markak"; +"Scene.Column.Actions.CloseColumn" = "Close column"; +"Scene.Column.Actions.MoveLeft" = "Move left"; +"Scene.Column.Actions.MoveRight" = "Move right"; +"Scene.Column.Actions.OpenInNewColumn" = "Open in new column"; +"Scene.Column.Title" = "New Column"; "Scene.Compose.And" = ", "; "Scene.Compose.CwPlaceholder" = "Idatz ezazu hemen zure ohartarazpena"; "Scene.Compose.LastEnd" = " eta "; @@ -374,6 +381,7 @@ "Scene.ListsUsers.MenuActions.Remove" = "Ezabatu"; "Scene.Local.Title" = "Lokala"; "Scene.ManageAccounts.DeleteAccount" = "Ezabatu kontua"; +"Scene.ManageAccounts.OpenInNewWindow" = "Open in new window"; "Scene.ManageAccounts.Title" = "Kontuak"; "Scene.Mentions.Title" = "Aipamenak"; "Scene.Messages.Action.CopyText" = "Mezuaren testua kopiatu"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings index 70addc89..c581b850 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings @@ -181,6 +181,8 @@ "Common.Controls.Actions.SignOut" = "Pechar sesión"; "Common.Controls.Actions.TakePhoto" = "Tirar foto"; "Common.Controls.Actions.Yes" = "Si"; +"Common.Controls.EmptyState.NoResults" = "Sen resultados"; +"Common.Controls.EmptyState.UnableToAccess" = "Unable to access"; "Common.Controls.Friendship.Actions.Block" = "Bloquear"; "Common.Controls.Friendship.Actions.Blocked" = "Bloqueada"; "Common.Controls.Friendship.Actions.Follow" = "Seguir"; @@ -282,6 +284,11 @@ "Common.NotificationChannel.ContentMessages.Name" = "Mensaxes"; "Scene.Authentication.Title" = "Autenticación"; "Scene.Bookmark.Title" = "Marcador"; +"Scene.Column.Actions.CloseColumn" = "Close column"; +"Scene.Column.Actions.MoveLeft" = "Move left"; +"Scene.Column.Actions.MoveRight" = "Move right"; +"Scene.Column.Actions.OpenInNewColumn" = "Open in new column"; +"Scene.Column.Title" = "New Column"; "Scene.Compose.And" = ", "; "Scene.Compose.CwPlaceholder" = "Escribe aquí o teu aviso"; "Scene.Compose.LastEnd" = " e "; @@ -374,6 +381,7 @@ "Scene.ListsUsers.MenuActions.Remove" = "Eliminar"; "Scene.Local.Title" = "Local"; "Scene.ManageAccounts.DeleteAccount" = "Eliminar conta"; +"Scene.ManageAccounts.OpenInNewWindow" = "Open in new window"; "Scene.ManageAccounts.Title" = "Contas"; "Scene.Mentions.Title" = "Mencións"; "Scene.Messages.Action.CopyText" = "Copiar texto da mensaxe"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings index 0002ce49..e369bf54 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings @@ -181,6 +181,8 @@ "Common.Controls.Actions.SignOut" = "サインアウト"; "Common.Controls.Actions.TakePhoto" = "写真を撮影"; "Common.Controls.Actions.Yes" = "はい"; +"Common.Controls.EmptyState.NoResults" = "該当なし"; +"Common.Controls.EmptyState.UnableToAccess" = "Unable to access"; "Common.Controls.Friendship.Actions.Block" = "ブロック"; "Common.Controls.Friendship.Actions.Blocked" = "ブロックしました"; "Common.Controls.Friendship.Actions.Follow" = "フォロー"; @@ -282,6 +284,11 @@ "Common.NotificationChannel.ContentMessages.Name" = "メッセージ"; "Scene.Authentication.Title" = "認証"; "Scene.Bookmark.Title" = "ブックマーク"; +"Scene.Column.Actions.CloseColumn" = "Close column"; +"Scene.Column.Actions.MoveLeft" = "Move left"; +"Scene.Column.Actions.MoveRight" = "Move right"; +"Scene.Column.Actions.OpenInNewColumn" = "Open in new column"; +"Scene.Column.Title" = "New Column"; "Scene.Compose.And" = ", "; "Scene.Compose.CwPlaceholder" = "ここに警告を書いてください"; "Scene.Compose.LastEnd" = " と "; @@ -374,6 +381,7 @@ "Scene.ListsUsers.MenuActions.Remove" = "削除"; "Scene.Local.Title" = "ローカル"; "Scene.ManageAccounts.DeleteAccount" = "アカウントの削除"; +"Scene.ManageAccounts.OpenInNewWindow" = "Open in new window"; "Scene.ManageAccounts.Title" = "アカウント"; "Scene.Mentions.Title" = "メンション"; "Scene.Messages.Action.CopyText" = "メッセージをコピー"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings index 97dcecc0..e7b96b99 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings @@ -181,6 +181,8 @@ "Common.Controls.Actions.SignOut" = "로그아웃 됐습니다."; "Common.Controls.Actions.TakePhoto" = "사진 찍기"; "Common.Controls.Actions.Yes" = "네"; +"Common.Controls.EmptyState.NoResults" = "결과 없음"; +"Common.Controls.EmptyState.UnableToAccess" = "Unable to access"; "Common.Controls.Friendship.Actions.Block" = "차단하기"; "Common.Controls.Friendship.Actions.Blocked" = "차단됨"; "Common.Controls.Friendship.Actions.Follow" = "팔로우하기"; @@ -282,6 +284,11 @@ "Common.NotificationChannel.ContentMessages.Name" = "메시지"; "Scene.Authentication.Title" = "인증"; "Scene.Bookmark.Title" = "즐겨찾기"; +"Scene.Column.Actions.CloseColumn" = "Close column"; +"Scene.Column.Actions.MoveLeft" = "Move left"; +"Scene.Column.Actions.MoveRight" = "Move right"; +"Scene.Column.Actions.OpenInNewColumn" = "Open in new column"; +"Scene.Column.Title" = "New Column"; "Scene.Compose.And" = ", "; "Scene.Compose.CwPlaceholder" = "여기에 주의사항을 적어주세요"; "Scene.Compose.LastEnd" = " 그리고 "; @@ -374,6 +381,7 @@ "Scene.ListsUsers.MenuActions.Remove" = "지우기"; "Scene.Local.Title" = "로컬"; "Scene.ManageAccounts.DeleteAccount" = "계정 지우기"; +"Scene.ManageAccounts.OpenInNewWindow" = "Open in new window"; "Scene.ManageAccounts.Title" = "계정"; "Scene.Mentions.Title" = "답글"; "Scene.Messages.Action.CopyText" = "메시지 글자 복사"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings index cb2f1043..a00239ad 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings @@ -181,6 +181,8 @@ "Common.Controls.Actions.SignOut" = "Encerrar sessão"; "Common.Controls.Actions.TakePhoto" = "Tirar foto"; "Common.Controls.Actions.Yes" = "Sim"; +"Common.Controls.EmptyState.NoResults" = "Nenhum resultado"; +"Common.Controls.EmptyState.UnableToAccess" = "Unable to access"; "Common.Controls.Friendship.Actions.Block" = "Bloquear"; "Common.Controls.Friendship.Actions.Blocked" = "Bloqueado"; "Common.Controls.Friendship.Actions.Follow" = "Seguir"; @@ -282,6 +284,11 @@ "Common.NotificationChannel.ContentMessages.Name" = "Mensagens"; "Scene.Authentication.Title" = "Autenticação"; "Scene.Bookmark.Title" = "Favorito"; +"Scene.Column.Actions.CloseColumn" = "Close column"; +"Scene.Column.Actions.MoveLeft" = "Move left"; +"Scene.Column.Actions.MoveRight" = "Move right"; +"Scene.Column.Actions.OpenInNewColumn" = "Open in new column"; +"Scene.Column.Title" = "New Column"; "Scene.Compose.And" = ", "; "Scene.Compose.CwPlaceholder" = "Escreva seu aviso aqui"; "Scene.Compose.LastEnd" = " e "; @@ -374,6 +381,7 @@ "Scene.ListsUsers.MenuActions.Remove" = "Remover"; "Scene.Local.Title" = "Local"; "Scene.ManageAccounts.DeleteAccount" = "Excluir conta"; +"Scene.ManageAccounts.OpenInNewWindow" = "Open in new window"; "Scene.ManageAccounts.Title" = "Contas"; "Scene.Mentions.Title" = "Menções"; "Scene.Messages.Action.CopyText" = "Copiar texto da mensagem"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings index 6b12331f..c95340cc 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings @@ -181,6 +181,8 @@ "Common.Controls.Actions.SignOut" = "Çıkış Yap"; "Common.Controls.Actions.TakePhoto" = "Fotoğraf çek"; "Common.Controls.Actions.Yes" = "Evet"; +"Common.Controls.EmptyState.NoResults" = "Sonuç bulunamadı"; +"Common.Controls.EmptyState.UnableToAccess" = "Unable to access"; "Common.Controls.Friendship.Actions.Block" = "Engelle"; "Common.Controls.Friendship.Actions.Blocked" = "Engellenmiş"; "Common.Controls.Friendship.Actions.Follow" = "Takip et"; @@ -282,6 +284,11 @@ "Common.NotificationChannel.ContentMessages.Name" = "Mesajlar"; "Scene.Authentication.Title" = "Yetkilendirme"; "Scene.Bookmark.Title" = "Yer işareti"; +"Scene.Column.Actions.CloseColumn" = "Sütunu kapat"; +"Scene.Column.Actions.MoveLeft" = "Sola taşı"; +"Scene.Column.Actions.MoveRight" = "Sağa taşı"; +"Scene.Column.Actions.OpenInNewColumn" = "Yeni sütunda"; +"Scene.Column.Title" = "Yeni sütun"; "Scene.Compose.And" = ", "; "Scene.Compose.CwPlaceholder" = "Uyarınızı buraya yazın"; "Scene.Compose.LastEnd" = " ve "; @@ -322,7 +329,7 @@ "Scene.Compose.Vote.PlaceholderIndex" = "Seçim %d"; "Scene.ComposeHashtagSearch.SearchPlaceholder" = "Hashtag ara"; "Scene.ComposeUserSearch.SearchPlaceholder" = "Kullanıcıları ara"; -"Scene.Detail.Title" = "Detail"; +"Scene.Detail.Title" = "Ayrıntılar"; "Scene.Drafts.Actions.DeleteDraft" = "Taslağı sil"; "Scene.Drafts.Actions.EditDraft" = "Taslağı düzenle"; "Scene.Drafts.Title" = "Taslaklar"; @@ -374,6 +381,7 @@ "Scene.ListsUsers.MenuActions.Remove" = "Kaldır"; "Scene.Local.Title" = "Yerel"; "Scene.ManageAccounts.DeleteAccount" = "Hesabı sil"; +"Scene.ManageAccounts.OpenInNewWindow" = "Yeni pencerede aç"; "Scene.ManageAccounts.Title" = "Hesaplar"; "Scene.Mentions.Title" = "Etiketler"; "Scene.Messages.Action.CopyText" = "Mesaj metnini kopyala"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings index 630827dd..d1de15bc 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings @@ -181,6 +181,8 @@ "Common.Controls.Actions.SignOut" = "登出"; "Common.Controls.Actions.TakePhoto" = "拍摄照片"; "Common.Controls.Actions.Yes" = "确定"; +"Common.Controls.EmptyState.NoResults" = "无结果"; +"Common.Controls.EmptyState.UnableToAccess" = "无法访问"; "Common.Controls.Friendship.Actions.Block" = "屏蔽"; "Common.Controls.Friendship.Actions.Blocked" = "已屏蔽"; "Common.Controls.Friendship.Actions.Follow" = "关注"; @@ -216,16 +218,16 @@ "Common.Controls.Status.Actions.PinOnProfile" = "在个人资料页面置顶"; "Common.Controls.Status.Actions.Quote" = "引用"; "Common.Controls.Status.Actions.Reply" = "回复"; -"Common.Controls.Status.Actions.Repost" = "Repost"; +"Common.Controls.Status.Actions.Repost" = "转发"; "Common.Controls.Status.Actions.Retweet" = "转推"; "Common.Controls.Status.Actions.SaveMedia" = "保存媒体"; "Common.Controls.Status.Actions.Share" = "分享"; "Common.Controls.Status.Actions.ShareContent" = "分享内容"; "Common.Controls.Status.Actions.ShareLink" = "分享链接"; "Common.Controls.Status.Actions.Translate" = "翻译"; -"Common.Controls.Status.Actions.UndoBoost" = "Undo Boost"; -"Common.Controls.Status.Actions.UndoRepost" = "Undo Repost"; -"Common.Controls.Status.Actions.UndoRetweet" = "Undo Retweet"; +"Common.Controls.Status.Actions.UndoBoost" = "撤消转嘟"; +"Common.Controls.Status.Actions.UndoRepost" = "撤消转发"; +"Common.Controls.Status.Actions.UndoRetweet" = "撤消转推"; "Common.Controls.Status.Actions.UnpinFromProfile" = "在个人资料页面取消置顶"; "Common.Controls.Status.Actions.Vote" = "投票"; "Common.Controls.Status.Media" = "媒体"; @@ -282,6 +284,11 @@ "Common.NotificationChannel.ContentMessages.Name" = "私信"; "Scene.Authentication.Title" = "身份认证"; "Scene.Bookmark.Title" = "书签"; +"Scene.Column.Actions.CloseColumn" = "关闭分栏"; +"Scene.Column.Actions.MoveLeft" = "左移"; +"Scene.Column.Actions.MoveRight" = "右移"; +"Scene.Column.Actions.OpenInNewColumn" = "在新分栏中打开"; +"Scene.Column.Title" = "创建分栏"; "Scene.Compose.And" = ","; "Scene.Compose.CwPlaceholder" = "折叠部分的警告消息"; "Scene.Compose.LastEnd" = " 和 "; @@ -322,7 +329,7 @@ "Scene.Compose.Vote.PlaceholderIndex" = "选项 %d"; "Scene.ComposeHashtagSearch.SearchPlaceholder" = "搜索标签"; "Scene.ComposeUserSearch.SearchPlaceholder" = "搜索用户"; -"Scene.Detail.Title" = "Detail"; +"Scene.Detail.Title" = "详情"; "Scene.Drafts.Actions.DeleteDraft" = "删除草稿"; "Scene.Drafts.Actions.EditDraft" = "编辑草稿"; "Scene.Drafts.Title" = "草稿"; @@ -374,6 +381,7 @@ "Scene.ListsUsers.MenuActions.Remove" = "删除"; "Scene.Local.Title" = "本站"; "Scene.ManageAccounts.DeleteAccount" = "删除帐号"; +"Scene.ManageAccounts.OpenInNewWindow" = "在新窗口中打开"; "Scene.ManageAccounts.Title" = "帐号"; "Scene.Mentions.Title" = "提及"; "Scene.Messages.Action.CopyText" = "复制消息文本"; diff --git a/TwidereX/Scene/Root/NewColumn/NewColumnView.swift b/TwidereX/Scene/Root/NewColumn/NewColumnView.swift index a6504df9..04e4f119 100644 --- a/TwidereX/Scene/Root/NewColumn/NewColumnView.swift +++ b/TwidereX/Scene/Root/NewColumn/NewColumnView.swift @@ -53,6 +53,6 @@ extension NewColumnView { viewModel.delegate?.newColumnView(viewModel, source: source, openTabMenuAction: tab) } } - return UIMenu(options: .displayInline, children: actions) + return UIMenu(title: "New Column", options: .displayInline, children: actions) } } From b5be332d7487ab0cdda8e31c7530944b17a62606 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 5 Jul 2023 18:25:41 +0800 Subject: [PATCH 124/128] chore: update i18n label --- TwidereX/Scene/Root/NewColumn/NewColumnView.swift | 2 +- TwidereX/Scene/Root/NewColumn/NewColumnViewController.swift | 3 ++- .../SecondaryContainer/SecondaryContainerViewModel.swift | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/TwidereX/Scene/Root/NewColumn/NewColumnView.swift b/TwidereX/Scene/Root/NewColumn/NewColumnView.swift index 04e4f119..e7195bda 100644 --- a/TwidereX/Scene/Root/NewColumn/NewColumnView.swift +++ b/TwidereX/Scene/Root/NewColumn/NewColumnView.swift @@ -53,6 +53,6 @@ extension NewColumnView { viewModel.delegate?.newColumnView(viewModel, source: source, openTabMenuAction: tab) } } - return UIMenu(title: "New Column", options: .displayInline, children: actions) + return UIMenu(title: "Open Column", options: .displayInline, children: actions) } } diff --git a/TwidereX/Scene/Root/NewColumn/NewColumnViewController.swift b/TwidereX/Scene/Root/NewColumn/NewColumnViewController.swift index 779d9049..7f9074a9 100644 --- a/TwidereX/Scene/Root/NewColumn/NewColumnViewController.swift +++ b/TwidereX/Scene/Root/NewColumn/NewColumnViewController.swift @@ -8,6 +8,7 @@ import UIKit import SwiftUI +import TwidereLocalization final class NewColumnViewController: UIViewController { weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } @@ -38,7 +39,7 @@ extension NewColumnViewController { override func viewDidLoad() { super.viewDidLoad() - title = "New Column" + title = L10n.Scene.Column.title let hostingViewController = UIHostingController(rootView: contentView) hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false diff --git a/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewModel.swift b/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewModel.swift index 8d52a1a5..4ea978ee 100644 --- a/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewModel.swift +++ b/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewModel.swift @@ -119,7 +119,7 @@ extension SecondaryContainerViewModel { var menuElements: [UIMenuElement] = [] - let closeColumnAction = UIAction(title: "Close column", image: UIImage(systemName: "xmark.square"), attributes: .destructive) { [weak self, weak stack, weak navigationController] _ in + let closeColumnAction = UIAction(title: L10n.Scene.Column.Actions.closeColumn, image: UIImage(systemName: "xmark.square"), attributes: .destructive) { [weak self, weak stack, weak navigationController] _ in guard let self = self else { return } guard let stack = stack else { return } guard let navigationController = navigationController else { return } @@ -135,7 +135,7 @@ extension SecondaryContainerViewModel { }) if let index = _index { if index > 0 { - let moveLeftMenuAction = UIAction(title: "Move left", image: UIImage(systemName: "arrow.left.square")) { [weak self, weak stack, weak navigationController] _ in + let moveLeftMenuAction = UIAction(title: L10n.Scene.Column.Actions.moveLeft, image: UIImage(systemName: "arrow.left.square")) { [weak self, weak stack, weak navigationController] _ in guard let self = self else { return } guard let stack = stack else { return } guard let navigationController = navigationController else { return } @@ -145,7 +145,7 @@ extension SecondaryContainerViewModel { menuElements.append(moveLeftMenuAction) } if index < stack.arrangedSubviews.count - 2 { - let moveRightMenuAction = UIAction(title: "Move Right", image: UIImage(systemName: "arrow.right.square")) { [weak self, weak stack, weak navigationController] _ in + let moveRightMenuAction = UIAction(title: L10n.Scene.Column.Actions.moveRight, image: UIImage(systemName: "arrow.right.square")) { [weak self, weak stack, weak navigationController] _ in guard let self = self else { return } guard let stack = stack else { return } guard let navigationController = navigationController else { return } From 835b5f5f67c7ba5b636f23ec48ac2e53831242f5 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 5 Jul 2023 18:26:14 +0800 Subject: [PATCH 125/128] chore: update version to v2.0.0 (139) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 36 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index a45294cc..0f95f005 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 138 + 139 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index b54ac954..f31f14ca 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 138 + 139 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index dde32ef5..6b5d6a43 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3395,7 +3395,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 138; + CURRENT_PROJECT_VERSION = 139; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3426,7 +3426,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 138; + CURRENT_PROJECT_VERSION = 139; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3449,7 +3449,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 138; + CURRENT_PROJECT_VERSION = 139; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3473,7 +3473,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 138; + CURRENT_PROJECT_VERSION = 139; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3500,7 +3500,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 138; + CURRENT_PROJECT_VERSION = 139; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3529,7 +3529,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 138; + CURRENT_PROJECT_VERSION = 139; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3558,7 +3558,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 138; + CURRENT_PROJECT_VERSION = 139; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3586,7 +3586,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 138; + CURRENT_PROJECT_VERSION = 139; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3612,7 +3612,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 138; + CURRENT_PROJECT_VERSION = 139; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3639,7 +3639,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 138; + CURRENT_PROJECT_VERSION = 139; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3793,7 +3793,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 138; + CURRENT_PROJECT_VERSION = 139; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3829,7 +3829,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 138; + CURRENT_PROJECT_VERSION = 139; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3860,7 +3860,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 138; + CURRENT_PROJECT_VERSION = 139; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3885,7 +3885,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 138; + CURRENT_PROJECT_VERSION = 139; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3908,7 +3908,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 138; + CURRENT_PROJECT_VERSION = 139; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3931,7 +3931,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 138; + CURRENT_PROJECT_VERSION = 139; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3955,7 +3955,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 138; + CURRENT_PROJECT_VERSION = 139; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3982,7 +3982,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 138; + CURRENT_PROJECT_VERSION = 139; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index d9ce69da..7acf3ffb 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 138 + 139 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index ce98304d..90010757 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 138 + 139 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index aea8a838..5a97ea25 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 138 + 139 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index aea8a838..5a97ea25 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 138 + 139 From f1bbe1aa184b72838ffe752bf44e0b6c9354f440 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 1 Aug 2023 18:44:44 +0800 Subject: [PATCH 126/128] feat: update for the new Twitter API --- TwidereSDK/Package.swift | 2 +- .../Entity/Twitter/TwitterStatus.swift | 16 +++++++++++++--- .../Twitter/Persistence+TwitterStatus+V2.swift | 3 +++ .../Twitter/Persistence+TwitterStatus.swift | 3 +++ .../xcshareddata/swiftpm/Package.resolved | 4 ++-- .../StatusThread/StatusThreadViewModel.swift | 3 +++ 6 files changed, 25 insertions(+), 6 deletions(-) diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index b896a523..f5cb138c 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -50,7 +50,7 @@ let package = Package( .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.6.0"), .package(url: "https://github.com/tid-kijyun/Kanna.git", from: "5.2.7"), .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"), - .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.13.0"), + .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.14.0"), .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"), .package(name: "CoverFlowStackLayout", path: "../CoverFlowStackLayout"), ], diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift index e83d7811..938c8a56 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift @@ -64,11 +64,11 @@ final public class TwitterStatus: NSManagedObject { // many-to-one relationship // sourcery: autoGenerateRelationship @NSManaged public private(set) var author: TwitterUser - // sourcery: autoGenerateRelationship + // sourcery: autoUpdatableObject, autoGenerateRelationship @NSManaged public private(set) var repost: TwitterStatus? - // sourcery: autoGenerateRelationship + // sourcery: autoUpdatableObject, autoGenerateRelationship @NSManaged public private(set) var quote: TwitterStatus? - // sourcery: autoGenerateRelationship, autoUpdatableObject + // sourcery: autoUpdatableObject, autoGenerateRelationship @NSManaged public private(set) var replyTo: TwitterStatus? // one-to-many relationship @@ -471,6 +471,16 @@ extension TwitterStatus: AutoUpdatableObject { self.updatedAt = updatedAt } } + public func update(repost: TwitterStatus?) { + if self.repost != repost { + self.repost = repost + } + } + public func update(quote: TwitterStatus?) { + if self.quote != quote { + self.quote = quote + } + } public func update(replyTo: TwitterStatus?) { if self.replyTo != replyTo { self.replyTo = replyTo diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterStatus+V2.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterStatus+V2.swift index b46b6e84..50d9e1ea 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterStatus+V2.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterStatus+V2.swift @@ -141,6 +141,9 @@ extension Persistence.TwitterStatus { if let old = fetch(in: managedObjectContext, context: context) { merge(twitterStatus: old, context: context) + if let repost = repost, old.repost == nil { + old.update(repost: repost) + } return .init(status: old, isNewInsertion: false, isNewInsertionAuthor: false) } else { let poll: TwitterPoll? = { diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterStatus.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterStatus.swift index 4761af5c..440f6eec 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterStatus.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterStatus.swift @@ -98,6 +98,9 @@ extension Persistence.TwitterStatus { if let oldStatus = fetch(in: managedObjectContext, context: context) { merge(twitterStatus: oldStatus, context: context) + if let repost = repost, oldStatus.repost == nil { + oldStatus.update(repost: repost) + } return PersistResult( status: oldStatus, isNewInsertion: false, diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index 05128940..699d1aa1 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -258,8 +258,8 @@ "repositoryURL": "https://github.com/TwidereProject/TwitterSDK.git", "state": { "branch": null, - "revision": "e0e93420c9f6728f5c9a39e716100e6bd5a5d6b9", - "version": "0.13.0" + "revision": "ebf8f5195922f00bba3d1cb5764583c7095a265d", + "version": "0.14.0" } }, { diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift index 9e715f24..4139dc6c 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel.swift @@ -412,6 +412,9 @@ extension StatusThreadViewModel { guard let status = statusDict[statusID] else { continue } + guard statusID != conversationRootStatusID else { + continue + } threads.append(.selfThread(status: .twitter(record: status))) } return threads From fc1521f58f33a5ebde854375b571704fa6949564 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 1 Aug 2023 20:10:10 +0800 Subject: [PATCH 127/128] fix: media timeline decoding issue --- TwidereSDK/Package.swift | 2 +- .../Entity/Twitter/TwitterUser.swift | 53 +++++++++---------- .../Twitter/Persistence+TwitterUser+V2.swift | 15 ++++++ .../Twitter/Persistence+TwitterUser.swift | 5 ++ .../xcshareddata/xcschemes/TwidereX.xcscheme | 2 + .../xcshareddata/swiftpm/Package.resolved | 4 +- 6 files changed, 50 insertions(+), 31 deletions(-) diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index f5cb138c..2d6228b1 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -50,7 +50,7 @@ let package = Package( .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.6.0"), .package(url: "https://github.com/tid-kijyun/Kanna.git", from: "5.2.7"), .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"), - .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.14.0"), + .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.17.0"), .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"), .package(name: "CoverFlowStackLayout", path: "../CoverFlowStackLayout"), ], diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift index c5a5fb13..fae31bef 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift @@ -36,13 +36,13 @@ public final class TwitterUser: NSManagedObject { // sourcery: autoUpdatableObject, autoGenerateProperty @NSManaged public private(set) var verified: Bool - // sourcery: autoUpdatableObject, autoGenerateProperty + // sourcery: autoGenerateProperty @NSManaged public private(set) var statusesCount: Int64 - // sourcery: autoUpdatableObject, autoGenerateProperty + // sourcery: autoGenerateProperty @NSManaged public private(set) var followingCount: Int64 - // sourcery: autoUpdatableObject, autoGenerateProperty + // sourcery: autoGenerateProperty @NSManaged public private(set) var followersCount: Int64 - // sourcery: autoUpdatableObject, autoGenerateProperty + // sourcery: autoGenerateProperty @NSManaged public private(set) var listedCount: Int64 // sourcery: autoUpdatableObject, autoGenerateProperty @@ -293,10 +293,6 @@ extension TwitterUser: AutoGenerateProperty { update(protected: property.protected) update(url: property.url) update(verified: property.verified) - update(statusesCount: property.statusesCount) - update(followingCount: property.followingCount) - update(followersCount: property.followersCount) - update(listedCount: property.listedCount) update(updatedAt: property.updatedAt) } // sourcery:end @@ -364,26 +360,6 @@ extension TwitterUser: AutoUpdatableObject { self.verified = verified } } - public func update(statusesCount: Int64) { - if self.statusesCount != statusesCount { - self.statusesCount = statusesCount - } - } - public func update(followingCount: Int64) { - if self.followingCount != followingCount { - self.followingCount = followingCount - } - } - public func update(followersCount: Int64) { - if self.followersCount != followersCount { - self.followersCount = followersCount - } - } - public func update(listedCount: Int64) { - if self.listedCount != listedCount { - self.listedCount = listedCount - } - } public func update(updatedAt: Date) { if self.updatedAt != updatedAt { self.updatedAt = updatedAt @@ -401,6 +377,27 @@ extension TwitterUser: AutoUpdatableObject { } // sourcery:end + public func update(statusesCount: Int64) { + if self.statusesCount != statusesCount, statusesCount >= 0 { + self.statusesCount = statusesCount + } + } + public func update(followingCount: Int64) { + if self.followingCount != followingCount, followingCount >= 0 { + self.followingCount = followingCount + } + } + public func update(followersCount: Int64) { + if self.followersCount != followersCount, followersCount >= 0 { + self.followersCount = followersCount + } + } + public func update(listedCount: Int64) { + if self.listedCount != listedCount, listedCount >= 0 { + self.listedCount = listedCount + } + } + public func update(isFollow: Bool, by user: TwitterUser) { if isFollow { if !followingBy.contains(user) { diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser+V2.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser+V2.swift index 957a5847..01ec5dbd 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser+V2.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser+V2.swift @@ -105,6 +105,21 @@ extension Persistence.TwitterUser { user.update(bioEntitiesTransient: TwitterEntity(entity: context.entity.entities?.description)) user.update(urlEntitiesTransient: TwitterEntity(entity: context.entity.entities?.url)) + if let publicMetrics = context.entity.publicMetrics { + if let tweetCount = publicMetrics.tweetCount { + user.update(statusesCount: Int64(tweetCount)) + } + if let followingCount = publicMetrics.followingCount { + user.update(followingCount: Int64(followingCount)) + } + if let followersCount = publicMetrics.followersCount { + user.update(followersCount: Int64(followersCount)) + } + if let listedCount = publicMetrics.listedCount { + user.update(listedCount: Int64(listedCount)) + } + } + // convertible properties if let profileBannerURL = context.entity.profileBannerURL { user.update(profileBannerURL: profileBannerURL) diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser.swift index 0c01df9c..d47b3db4 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser.swift @@ -117,6 +117,11 @@ extension Persistence.TwitterUser { user.update(profileBannerURL: context.entity.profileBannerURL) user.update(bioEntitiesTransient: TwitterEntity(entity: context.entity.entities?.description)) user.update(urlEntitiesTransient: TwitterEntity(entity: context.entity.entities?.url)) + + context.entity.statusesCount.flatMap { user.update(statusesCount: Int64($0)) } + context.entity.friendsCount.flatMap { user.update(followingCount: Int64($0)) } + context.entity.followersCount.flatMap { user.update(followersCount: Int64($0)) } + context.entity.listedCount.flatMap { user.update(listedCount: Int64($0)) } // relationship if let me = context.me { diff --git a/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX.xcscheme b/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX.xcscheme index a1237242..0f717735 100644 --- a/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX.xcscheme +++ b/TwidereX.xcodeproj/xcshareddata/xcschemes/TwidereX.xcscheme @@ -73,6 +73,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + enableThreadSanitizer = "YES" + enableUBSanitizer = "YES" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index 699d1aa1..706aec8a 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -258,8 +258,8 @@ "repositoryURL": "https://github.com/TwidereProject/TwitterSDK.git", "state": { "branch": null, - "revision": "ebf8f5195922f00bba3d1cb5764583c7095a265d", - "version": "0.14.0" + "revision": "c2f03e475d48bc832054a121ede6a660f39af501", + "version": "0.17.0" } }, { From 3f253414ee75b7224e268b2278360e1b2d5aa2ad Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 4 Aug 2023 16:01:12 +0800 Subject: [PATCH 128/128] chore: update version to 2.0.1 (140) --- NotificationService/Info.plist | 4 ++-- ShareExtension/Info.plist | 4 ++-- TwidereX.xcodeproj/project.pbxproj | 36 +++++++++++++++--------------- TwidereX/Info.plist | 4 ++-- TwidereXIntent/Info.plist | 4 ++-- TwidereXTests/Info.plist | 4 ++-- TwidereXUITests/Info.plist | 4 ++-- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 0f95f005..2d70cada 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -3,7 +3,7 @@ CFBundleShortVersionString - 2.0.0 + 2.0.1 CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 139 + 140 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index f31f14ca..cfca8ab8 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 2.0.0 + 2.0.1 CFBundleVersion - 139 + 140 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 6b5d6a43..e95282c9 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3395,7 +3395,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 139; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3426,7 +3426,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 139; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3449,7 +3449,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 139; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3473,7 +3473,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 139; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3500,7 +3500,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 139; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3529,7 +3529,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 139; + CURRENT_PROJECT_VERSION = 140; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3558,7 +3558,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 139; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3586,7 +3586,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 139; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3612,7 +3612,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 139; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3639,7 +3639,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 139; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3793,7 +3793,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 139; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3829,7 +3829,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 139; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3860,7 +3860,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 139; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3885,7 +3885,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 139; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3908,7 +3908,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 139; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3931,7 +3931,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 139; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3955,7 +3955,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 139; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3982,7 +3982,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 139; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index 7acf3ffb..56e9fbd7 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -38,9 +38,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 2.0.0 + 2.0.1 CFBundleVersion - 139 + 140 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index 90010757..585aebc6 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 2.0.0 + 2.0.1 CFBundleVersion - 139 + 140 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index 5a97ea25..af6bca1c 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 2.0.0 + 2.0.1 CFBundleVersion - 139 + 140 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index 5a97ea25..af6bca1c 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 2.0.0 + 2.0.1 CFBundleVersion - 139 + 140