diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist
index 5350483f..f9aa0a6a 100644
--- a/NotificationService/Info.plist
+++ b/NotificationService/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleVersion
- 125
+ 128
NSExtension
NSExtensionPointIdentifier
diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist
index 4b75f20f..6758123f 100644
--- a/ShareExtension/Info.plist
+++ b/ShareExtension/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
2.0.0
CFBundleVersion
- 125
+ 128
NSExtension
NSExtensionAttributes
diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift
index f36b14e6..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.4.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/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/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/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/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/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/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" = "草稿";
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/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/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift
index 44d8e9e7..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
@@ -1135,6 +1140,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..b638837c 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)
@@ -142,6 +145,10 @@ public struct StatusView: View {
// content
if viewModel.isContentReveal {
contentView
+
+ if viewModel.isTranslateButtonDisplay {
+ translateButton
+ }
}
// media
if !viewModel.mediaViewModels.isEmpty {
@@ -153,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
@@ -182,6 +190,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 {
@@ -420,6 +431,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,
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..9307a5d7 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,10 @@ 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 isMyself: Bool = false
+
// @Published public var authenticationContext: AuthenticationContext? // me
// @Published public var userAuthenticationContext: AuthenticationContext?
//
@@ -62,22 +65,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,14 +92,39 @@ 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
}
+ // 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:
@@ -110,7 +139,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 +149,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 +211,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
@@ -211,8 +238,9 @@ extension UserView.ViewModel {
}
public enum MenuAction: Hashable {
+ case openInNewWindowForAccount
case signOut
- case remove
+ case removeListMember
}
}
@@ -320,31 +348,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 +359,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 +412,7 @@ extension UserView.ViewModel {
// end init
// user
+ platform = .twitter
user.publisher(for: \.profileImageURL)
.map { _ in user.avatarImageURL() }
.assign(to: &$avatarURL)
@@ -474,6 +438,7 @@ extension UserView.ViewModel {
// end init
// user
+ platform = .mastodon
user.publisher(for: \.avatar)
.compactMap { $0.flatMap { URL(string: $0) } }
.assign(to: &$avatarURL)
@@ -485,3 +450,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..1c703746 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 {
@@ -120,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)
@@ -130,7 +168,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 +190,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 +215,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 +245,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 +313,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 +333,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 +454,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 +855,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/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/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.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj
index c4b7e5a2..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 = 125;
+ 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 = 125;
+ 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 = 125;
+ 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 = 125;
+ 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 = 125;
+ 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 = 125;
+ 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 = 125;
+ 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 = 125;
+ 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 = 125;
+ 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 = 125;
+ 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 = 125;
+ 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 = 125;
+ 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 = 125;
+ 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 = 125;
+ 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 = 125;
+ 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 = 125;
+ 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 = 125;
+ 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 = 125;
+ CURRENT_PROJECT_VERSION = 128;
DEVELOPMENT_TEAM = 7LFDZ96332;
INFOPLIST_FILE = TwidereXIntent/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent;
diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved
index f9f16352..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": "94732843985776a7b59d8e24cb4e026f20ae8942",
- "version": "0.4.0"
+ "revision": "cf2f33c96e7b9f86dbee23111cd523f9a9294099",
+ "version": "0.7.0"
}
},
{
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/Info.plist b/TwidereX/Info.plist
index 697cf1d3..33f4967d 100644
--- a/TwidereX/Info.plist
+++ b/TwidereX/Info.plist
@@ -40,7 +40,7 @@
CFBundleShortVersionString
2.0.0
CFBundleVersion
- 125
+ 128
ITSAppUsesNonExemptEncryption
LSRequiresIPhoneOS
@@ -62,7 +62,7 @@
UIApplicationSceneManifest
UIApplicationSupportsMultipleScenes
-
+
UISceneConfigurations
UIWindowSceneSessionRoleApplication
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/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 3bd1d7bf..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,47 +36,59 @@ 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 {
+ assertionFailure()
+ return
+ }
try await DataSourceFacade.responseToUserSignOut(
dependency: self,
- user: viewModel.user.asRecord
+ user: user
)
} // end Task
- case .remove:
- assertionFailure("Override in view controller")
- }
- }
-
-// 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
-// }
-
+
+ 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
+ }
}
// MARK: - friendship button
@@ -92,53 +106,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/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/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/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)
diff --git a/TwidereX/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift b/TwidereX/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift
index b12bb5df..d69bc375 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)
}
}
@@ -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
+ }
+}
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:
diff --git a/TwidereX/Supporting Files/SceneDelegate.swift b/TwidereX/Supporting Files/SceneDelegate.swift
index baa2a997..e39aa675 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()
@@ -150,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 {
@@ -213,6 +213,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 {
diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist
index 051466d7..57f097f7 100644
--- a/TwidereXIntent/Info.plist
+++ b/TwidereXIntent/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
2.0.0
CFBundleVersion
- 125
+ 128
NSExtension
NSExtensionAttributes
diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist
index 2cc5125d..e0d1d83e 100644
--- a/TwidereXTests/Info.plist
+++ b/TwidereXTests/Info.plist
@@ -17,6 +17,6 @@
CFBundleShortVersionString
2.0.0
CFBundleVersion
- 125
+ 128
diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist
index 2cc5125d..e0d1d83e 100644
--- a/TwidereXUITests/Info.plist
+++ b/TwidereXUITests/Info.plist
@@ -17,6 +17,6 @@
CFBundleShortVersionString
2.0.0
CFBundleVersion
- 125
+ 128