Skip to content

Commit

Permalink
feat: どこでも新規投稿をウィンドウで (編集以外)
Browse files Browse the repository at this point in the history
  • Loading branch information
rinsuki committed Feb 12, 2024
1 parent 3898a7f commit 5212ae7
Show file tree
Hide file tree
Showing 16 changed files with 238 additions and 61 deletions.
4 changes: 2 additions & 2 deletions Sources/Core/Mastodon/API/MastodonPost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -369,14 +369,14 @@ extension MastodonEndpoint {
visibility: MastodonPostVisibility? = nil,
mediaIds: [MastodonID] = [],
spoiler: String = "", sensitive: Bool = false,
inReplyToPost: MastodonPost? = nil
inReplyToPostID: MastodonID? = nil
) {
self.status = status
self.visibility = visibility
self.mediaIds = mediaIds
self.spoiler = spoiler
self.sensitive = sensitive
self.inReplyToPostId = inReplyToPost?.id
self.inReplyToPostId = inReplyToPostID
}

public typealias Response = MastodonPost
Expand Down
4 changes: 4 additions & 0 deletions Sources/Core/Package/Sources/iMastPackage/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@

@attached(accessor)
public macro UserInfoProperty(_ key: String) = #externalMacro(module: "iMastPackageMacros", type: "UserInfoPropertyMacro")

@attached(accessor)
public macro UserInfoCodableProperty(_ key: String) = #externalMacro(module: "iMastPackageMacros", type: "UserInfoCodablePropertyMacro")

56 changes: 56 additions & 0 deletions Sources/Core/Package/Sources/iMastPackageMacros/PackageMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,65 @@ public struct UserInfoPropertyMacro: AccessorMacro {
}
}

public struct UserInfoCodablePropertyMacro: AccessorMacro {
public static func expansion(
of node: AttributeSyntax,
providingAccessorsOf decl: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AccessorDeclSyntax] {
// extract key part
guard case .argumentList(let arguments) = node.arguments else {
// TODO: throw error like "arguments required"
return []
}
guard arguments.count == 1, let key = arguments.first else {
// TODO: throw error like "arguments count wrong"
return []
}

// extract type part
guard let decl = decl.as(VariableDeclSyntax.self) else {
// TODO: throw error like "this macro should be attached to var decl"
return []
}
guard decl.bindings.count == 1, let binding = decl.bindings.first else {
// TODO: throw error like "bindings count wrong"
return []
}
guard let typeAnot = binding.typeAnnotation else {
// TODO: throw error like "type annotation is required"
return []
}
guard let optionalInnerType = typeAnot.type.as(OptionalTypeSyntax.self)?.wrappedType else {
// TODO: throw error like "type should be optional wrapped"
return []
}
return [
"""
get {
guard let data = userInfo?[\(key)] as? Data else {
return nil
}
return try? JSONDecoder().decode(\(optionalInnerType).self, from: data)
}
""",
"""
set {
if let newValue {
addUserInfoEntries(from: [\(key): try? JSONEncoder().encode(newValue)])
} else {
userInfo?.removeValue(forKey: \(key))
}
}
""",
]
}
}

@main
struct PackagePlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
UserInfoPropertyMacro.self,
UserInfoCodablePropertyMacro.self,
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,9 @@ class NewPostAccountSelectCushionViewController: AccoutSelectCushionBaseViewCont
var appendBottomString = ""

override func showVC(userToken: MastodonUserToken) {
let newPost = NewPostViewController()
newPost.userToken = userToken
newPost.appendBottomString = appendBottomString
let userActivity = NSUserActivity(newPostWithMastodonUserToken: userToken)
userActivity.newPostSuffix = appendBottomString
let newPost = NewPostViewController(userActivity: userActivity)
show(newPost, sender: self)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,9 @@ class MastodonPostDetailReactionBarViewController: UIViewController, Instantiata

@objc func openReplyVC() {
let post = self.input.originalPost
let vc = NewPostViewController()
vc.userToken = environment
vc.replyToPost = post
self.navigationController?.pushViewController(vc, animated: true)
showAsWindow(userActivity: .init(newPostWithMastodonUserToken: environment) {
$0.setNewPostReplyInfo(post)
}, fallback: .push)
}

@objc func boostButtonTapped() {
Expand Down Expand Up @@ -234,8 +233,8 @@ class MastodonPostDetailReactionBarViewController: UIViewController, Instantiata
Task {
do {
let source = try await MastodonEndpoint.GetPostSource(input.id).request(with: environment)
let vc = NewPostViewController()
vc.userToken = environment
let userActivity = NSUserActivity(newPostWithMastodonUserToken: environment)
let vc = NewPostViewController(userActivity: userActivity)
vc.editPost = (post: input, source: source)
present(ModalNavigationViewController(rootViewController: vc), animated: true)
} catch {
Expand Down
74 changes: 74 additions & 0 deletions Sources/iOS/App/Extensions/UIViewController+showAsWindow.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//
// UIViewController+showAsWindow.swift
//
// iMast https://github.com/cinderella-project/iMast
//
// Created by user on 2024/02/12.
//
// ------------------------------------------------------------------------
//
// Copyright 2017-2021 rinsuki and other contributors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import UIKit
import Ikemen
import iMastiOSCore

extension UIViewController {
enum ShowAsWindowFallbackOption {
case timeline
case push
case modal
}

func showAsWindow(userActivity: NSUserActivity, fallback: ShowAsWindowFallbackOption) {
if Defaults.openAsAnotherWindow {
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: userActivity, options: UIWindowScene.ActivationRequestOptions() {
$0.requestingScene = view.window?.windowScene
$0.preferredPresentationStyle = .prominent
}) { [weak self] error in
self?.showAsWindowFallback(userActivity: userActivity, fallback: fallback)
}
return
}
showAsWindowFallback(userActivity: userActivity, fallback: fallback)
}

fileprivate func showAsWindowFallback(userActivity: NSUserActivity, fallback: ShowAsWindowFallbackOption) {
guard let vc = UIViewController.viewController(from: userActivity) else {
return
}
switch fallback {
case .timeline:
showFromTimeline(vc)
case .push:
navigationController?.pushViewController(vc, animated: true)
case .modal:
present(ModalNavigationViewController(rootViewController: vc), animated: true)
}
}

static func viewController(from userActivity: NSUserActivity) -> UIViewController? {
switch userActivity.activityType {
case NSUserActivity.activityTypeNewPost:
let vc = NewPostViewController(userActivity: userActivity)
return vc
default:
#if DEBUG
fatalError("Unknown NSUserActivity: \(userActivity.activityType)")
#endif
return nil
}
}
}
7 changes: 2 additions & 5 deletions Sources/iOS/App/NewPostSceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,7 @@ class NewPostSceneDelegate: UIResponder, UIWindowSceneDelegate {

window = UIWindow(windowScene: windowScene)
if let window = window {
let newPostVC = NewPostViewController()
newPostVC.userToken = userToken
newPostVC.userActivity = activity
scene.userActivity = activity
let newPostVC = NewPostViewController(userActivity: activity)
window.rootViewController = UINavigationController(rootViewController: newPostVC)
window.makeKeyAndVisible()

Expand All @@ -62,6 +59,6 @@ class NewPostSceneDelegate: UIResponder, UIWindowSceneDelegate {

func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
print("state restoration activity", scene.userActivity)
return scene.userActivity
return ((scene as? UIWindowScene)?.keyWindow?.rootViewController as? UINavigationController)?.viewControllers.first?.userActivity
}
}
3 changes: 1 addition & 2 deletions Sources/iOS/App/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
guard let vc = windowScene.windows.first?.rootViewController else {
return completionHandler(false)
}
let newVC = NewPostViewController()
newVC.userToken = token
let newVC = NewPostViewController(userActivity: .init(newPostWithMastodonUserToken: token))
vc.present(ModalNavigationViewController(rootViewController: newVC), animated: true, completion: nil)
print("animated")
}
Expand Down
53 changes: 34 additions & 19 deletions Sources/iOS/App/Screens/NewPost/NewPostViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,19 @@ class NewPostViewController: UIViewController, UITextViewDelegate {
}
}
var editPost: (post: MastodonPost, source: MastodonPostSource)?
var replyToPost: MastodonPost?
// var replyToPost: MastodonPost?

var userToken: MastodonUserToken!

var appendBottomString: String = ""
init(userActivity: NSUserActivity) {
super.init(nibName: nil, bundle: nil)
self.userActivity = userActivity
self.userToken = userActivity.mastodonUserToken()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func loadView() {
contentView = .init()
Expand All @@ -80,21 +88,17 @@ class NewPostViewController: UIViewController, UITextViewDelegate {

contentView.currentAccountLabel.text = userToken.acct
navigationItem.largeTitleDisplayMode = .never
if let replyToPost = replyToPost {
contentView.currentAccountLabel.text! += "\n返信先: @\(replyToPost.account.acct): \(replyToPost.status.toPlainText().replacingOccurrences(of: "\n", with: " "))"
var replyAccounts = [replyToPost.account.acct]
for mention in replyToPost.mentions {
replyAccounts.append(mention.acct)
}
replyAccounts = replyAccounts.filter { $0 != userToken.screenName }.map { "@\($0) " }
contentView.textInput.text = replyAccounts.joined()
scope = replyToPost.visibility
if let userActivity, let replyPostID = userActivity.newPostReplyPostID, let replyPostAcct = userActivity.newPostReplyPostAcct {
let replyPostText = userActivity.newPostReplyPostText ?? ""
contentView.currentAccountLabel.text! += "\n返信先: @\(replyPostAcct): \(replyPostText.toPlainText().replacingOccurrences(of: "\n", with: " "))"
title = L10n.NewPost.reply
} else {
title = L10n.NewPost.title
}

if Defaults.usingDefaultVisibility && replyToPost == nil && editPost == nil {
if let scope = MastodonPostVisibility(rawValue: userActivity?.newPostVisibility ?? "") {
self.scope = scope
} else if Defaults.usingDefaultVisibility && editPost == nil {
setVisibilityFromUserInfo()
}

Expand All @@ -115,12 +119,17 @@ class NewPostViewController: UIViewController, UITextViewDelegate {
}

contentView.textInput.becomeFirstResponder()
// メンションとかの後を選択する
let nowCount = contentView.textInput.text.nsLength
DispatchQueue.main.async {
self.contentView.textInput.selectedRange.location = nowCount

if let userActivity {
contentView.textInput.text = userActivity.newPostCurrentText ?? ""
// メンションとかの後を選択する
let nowCount = contentView.textInput.text.nsLength
DispatchQueue.main.async {
self.contentView.textInput.selectedRange.location = nowCount
}
contentView.textInput.text += userActivity.newPostSuffix ?? ""
}
contentView.textInput.text += appendBottomString

addKeyCommand(.init(title: "投稿", action: #selector(sendPost(_:)), input: "\r", modifierFlags: .command, discoverabilityTitle: "投稿を送信"))

navigationItem.rightBarButtonItems = [
Expand Down Expand Up @@ -220,7 +229,7 @@ class NewPostViewController: UIViewController, UITextViewDelegate {
mediaIds: media.map { $0.id },
spoiler: spoilerText,
sensitive: isSensitive,
inReplyToPost: self.replyToPost
inReplyToPostID: self.userActivity.flatMap { $0.newPostReplyPostID }
).request(with: self.userToken)
}.then(in: .main) {
self.clearContent()
Expand Down Expand Up @@ -373,7 +382,7 @@ class NewPostViewController: UIViewController, UITextViewDelegate {
contentView.textInput.text = ""
media = []
isNSFW = false
if Defaults.usingDefaultVisibility && replyToPost == nil {
if Defaults.usingDefaultVisibility {
setVisibilityFromUserInfo()
} else {
scope = .public
Expand All @@ -388,6 +397,12 @@ class NewPostViewController: UIViewController, UITextViewDelegate {
}
}
}

override func updateUserActivityState(_ activity: NSUserActivity) {
if activity.activityType == NSUserActivity.activityTypeNewPost {
print("TODO: restoration")
}
}
}

extension NewPostViewController: UIPopoverPresentationControllerDelegate {
Expand Down
2 changes: 2 additions & 0 deletions Sources/iOS/App/Screens/OtherMenu/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ struct SettingsView: View {
@AppStorage(defaults: .$newFirstScreen) var newFirstScreen
@AppStorage(defaults: .$communicationNotificationsEnabled) var communicationNotificationsEnabled
@AppStorage(defaults: .$openAsHalfModalFromTimeline) var openAsHalfModalFromTimeline
@AppStorage(defaults: .$openAsAnotherWindow) var openAsAnotherWindow

var body: some View {
Section("実験的な要素") {
Expand All @@ -300,6 +301,7 @@ struct SettingsView: View {
Text("メンションのプッシュ通知に送信者のアイコンが付くようになります。")
}
Toggle("タイムラインから何かを開いた時にハーフモーダルにする", isOn: $openAsHalfModalFromTimeline)
Toggle("できるだけ新ウィンドウで開く", isOn: $openAsAnotherWindow)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ class HashtagTimelineViewController: TimelineViewController {
self.isNewPostAvailable = true
}

override func processNewPostVC(newPostVC: NewPostViewController) {
newPostVC.appendBottomString = " #\(hashtag)"
override func processNewPostVC(userActivity: NSUserActivity) {
userActivity.newPostSuffix = " #\(hashtag)"
}

required init?(coder aDecoder: NSCoder) {
Expand Down
23 changes: 4 additions & 19 deletions Sources/iOS/App/Screens/Timelines/TimelineViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -273,29 +273,14 @@ class TimelineViewController: UIViewController, Instantiatable {
}
}

func processNewPostVC(newPostVC: NewPostViewController) {
func processNewPostVC(userActivity: NSUserActivity) {
// オーバーライド用
}

@objc func openNewPostVC() {
let fallback = {
let vc = NewPostViewController()
vc.userToken = self.environment
self.processNewPostVC(newPostVC: vc)
self.showFromTimeline(vc)
}
// TODO: also allow for other idiom like .pad, after consider about corner worse case
if #available(iOS 17.0, *), traitCollection.userInterfaceIdiom == .vision {
let userActivity = NSUserActivity(newPostWithMastodonUserToken: environment)
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: userActivity, options: UIWindowScene.ActivationRequestOptions() {
$0.requestingScene = self.view.window?.windowScene
$0.preferredPresentationStyle = .prominent
}) { error in
fallback()
}
} else {
fallback()
}
let userActivity = NSUserActivity(newPostWithMastodonUserToken: environment)
processNewPostVC(userActivity: userActivity)
showAsWindow(userActivity: userActivity, fallback: .timeline)
}

@objc func postFabTapped(sender: UITapGestureRecognizer) {
Expand Down
Loading

0 comments on commit 5212ae7

Please sign in to comment.