diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index f9aa0a6a..4f6e87f4 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 128 + 129 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 6758123f..8810f44f 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 128 + 129 NSExtension NSExtensionAttributes diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index 24193981..007e3ef9 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -51,7 +51,7 @@ let package = Package( .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.6.0"), .package(url: "https://github.com/tid-kijyun/Kanna.git", from: "5.2.7"), .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"), - .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.7.0"), + .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.8.0"), .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"), .package(name: "CoverFlowStackLayout", path: "../CoverFlowStackLayout"), ], diff --git a/TwidereSDK/Sources/CoreDataStack/Utility/ManagedObjectRecord.swift b/TwidereSDK/Sources/CoreDataStack/Utility/ManagedObjectRecord.swift index de4915be..63a361be 100644 --- a/TwidereSDK/Sources/CoreDataStack/Utility/ManagedObjectRecord.swift +++ b/TwidereSDK/Sources/CoreDataStack/Utility/ManagedObjectRecord.swift @@ -18,7 +18,12 @@ public class ManagedObjectRecord: Hashable { } public func object(in managedObjectContext: NSManagedObjectContext) -> T? { - return managedObjectContext.object(with: objectID) as? T + do { + return try managedObjectContext.existingObject(with: objectID) as? T + } catch { + assertionFailure(error.localizedDescription) + return nil + } } public static func == (lhs: ManagedObjectRecord, rhs: ManagedObjectRecord) -> Bool { diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.imageset/Contents.json new file mode 100644 index 00000000..8b028c9a --- /dev/null +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "message.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.imageset/message.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.imageset/message.pdf new file mode 100644 index 00000000..f9a4bb81 Binary files /dev/null and b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.imageset/message.pdf differ diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.mini.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.mini.imageset/Contents.json new file mode 100644 index 00000000..fffdd98b --- /dev/null +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.mini.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "message.mini.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.mini.imageset/message.mini.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.mini.imageset/message.mini.pdf new file mode 100644 index 00000000..78dfc7cd Binary files /dev/null and b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Communication/text.bubble.mini.imageset/message.mini.pdf differ diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.imageset/Contents.json new file mode 100644 index 00000000..bcacd54f --- /dev/null +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "lock and repeat.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.imageset/lock and repeat.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.imageset/lock and repeat.pdf new file mode 100644 index 00000000..6b2ad0cb Binary files /dev/null and b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.imageset/lock and repeat.pdf differ diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.mini.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.mini.imageset/Contents.json new file mode 100644 index 00000000..bcacd54f --- /dev/null +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.mini.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "lock and repeat.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.mini.imageset/lock and repeat.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.mini.imageset/lock and repeat.pdf new file mode 100644 index 00000000..f148daea Binary files /dev/null and b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.lock.mini.imageset/lock and repeat.pdf differ diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat-off.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.off.imageset/Contents.json similarity index 100% rename from TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat-off.imageset/Contents.json rename to TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.off.imageset/Contents.json diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat-off.imageset/repeat-off.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.off.imageset/repeat-off.pdf similarity index 100% rename from TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat-off.imageset/repeat-off.pdf rename to TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.off.imageset/repeat-off.pdf diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.off.mini.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.off.mini.imageset/Contents.json new file mode 100644 index 00000000..1fe64dc8 --- /dev/null +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.off.mini.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Retweet-Off.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.off.mini.imageset/Retweet-Off.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.off.mini.imageset/Retweet-Off.pdf new file mode 100644 index 00000000..a4f1818a Binary files /dev/null and b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Media/repeat.off.mini.imageset/Retweet-Off.pdf differ diff --git a/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift b/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift index d244bd3b..1730b999 100644 --- a/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift +++ b/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift @@ -87,6 +87,8 @@ public enum Asset { public static let ellipsisBubblePlus = ImageAsset(name: "Communication/ellipsis.bubble.plus") public static let mail = ImageAsset(name: "Communication/mail") public static let mailMiniInline = ImageAsset(name: "Communication/mail.mini.inline") + public static let textBubble = ImageAsset(name: "Communication/text.bubble") + public static let textBubbleMini = ImageAsset(name: "Communication/text.bubble.mini") public static let textBubbleSmall = ImageAsset(name: "Communication/text.bubble.small") } public enum Editing { @@ -148,9 +150,12 @@ public enum Asset { public static let altRectangle = ImageAsset(name: "Media/alt.rectangle") public static let gifRectangle = ImageAsset(name: "Media/gif.rectangle") public static let playerRectangle = ImageAsset(name: "Media/player.rectangle") - public static let repeatOff = ImageAsset(name: "Media/repeat-off") public static let `repeat` = ImageAsset(name: "Media/repeat") + public static let repeatLock = ImageAsset(name: "Media/repeat.lock") + public static let repeatLockMini = ImageAsset(name: "Media/repeat.lock.mini") public static let repeatMini = ImageAsset(name: "Media/repeat.mini") + public static let repeatOff = ImageAsset(name: "Media/repeat.off") + public static let repeatOffMini = ImageAsset(name: "Media/repeat.off.mini") } public enum ObjectTools { public static let bell = ImageAsset(name: "Object&Tools/bell") diff --git a/TwidereSDK/Sources/TwidereCore/Error/Twitter/TwitterAPIError.swift b/TwidereSDK/Sources/TwidereCore/Error/Twitter/TwitterAPIError.swift index dd4d6123..1bc90779 100644 --- a/TwidereSDK/Sources/TwidereCore/Error/Twitter/TwitterAPIError.swift +++ b/TwidereSDK/Sources/TwidereCore/Error/Twitter/TwitterAPIError.swift @@ -13,6 +13,8 @@ extension Twitter.API.Error.TwitterAPIError: LocalizedError { public var errorDescription: String? { switch self { + case .notAuthorizedToViewTheSpecifiedUser: + return L10n.Common.Alerts.PermissionDeniedNotAuthorized.title case .userHasBeenSuspended: return L10n.Common.Alerts.AccountSuspended.title case .rateLimitExceeded: @@ -32,6 +34,8 @@ extension Twitter.API.Error.TwitterAPIError: LocalizedError { public var failureReason: String? { switch self { + case .notAuthorizedToViewTheSpecifiedUser: + return L10n.Common.Alerts.PermissionDeniedNotAuthorized.message case .userHasBeenSuspended: let twitterRules = L10n.Common.Alerts.AccountSuspended.twitterRules return L10n.Common.Alerts.AccountSuspended.message(twitterRules) diff --git a/TwidereSDK/Sources/TwidereCore/Model/User/UserObject.swift b/TwidereSDK/Sources/TwidereCore/Model/User/UserObject.swift index ff69aa4d..2a1e0705 100644 --- a/TwidereSDK/Sources/TwidereCore/Model/User/UserObject.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/User/UserObject.swift @@ -99,6 +99,15 @@ extension UserObject { return object.avatar.flatMap { URL(string: $0) } } } + + public var protected: Bool { + switch self { + case .twitter(let object): + return object.protected + case .mastodon(let object): + return object.locked + } + } } extension UserObject { diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift index c847bfeb..c18a5d56 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift @@ -26,6 +26,7 @@ extension StatusFetchViewModel.Timeline.User { public struct TwitterFetchContext: Hashable { public let authenticationContext: TwitterAuthenticationContext public let userID: Twitter.Entity.V2.User.ID + public let protected: Bool public let paginationToken: String? public let maxID: Twitter.Entity.V2.Tweet.ID? public let maxResults: Int? @@ -37,6 +38,7 @@ extension StatusFetchViewModel.Timeline.User { public init( authenticationContext: TwitterAuthenticationContext, userID: Twitter.Entity.V2.User.ID, + protected: Bool, paginationToken: String?, maxID: Twitter.Entity.V2.Tweet.ID?, maxResults: Int?, @@ -45,6 +47,7 @@ extension StatusFetchViewModel.Timeline.User { ) { self.authenticationContext = authenticationContext self.userID = userID + self.protected = protected self.paginationToken = paginationToken self.maxID = maxID self.maxResults = maxResults @@ -56,6 +59,7 @@ extension StatusFetchViewModel.Timeline.User { return TwitterFetchContext( authenticationContext: authenticationContext, userID: userID, + protected: protected, paginationToken: paginationToken, maxID: maxID, maxResults: maxResults, @@ -68,6 +72,7 @@ extension StatusFetchViewModel.Timeline.User { return TwitterFetchContext( authenticationContext: authenticationContext, userID: userID, + protected: protected, paginationToken: paginationToken, maxID: maxID, maxResults: maxResults, @@ -157,6 +162,9 @@ extension StatusFetchViewModel.Timeline.User { switch fetchContext.timelineKind { case .status, .media: do { + guard !fetchContext.protected else { + throw Twitter.API.Error.ResponseError(httpResponseStatus: .ok, twitterAPIError: .rateLimitExceeded) + } guard !fetchContext.needsAPIFallback else { throw Twitter.API.Error.ResponseError(httpResponseStatus: .ok, twitterAPIError: .rateLimitExceeded) } diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift index c871bd12..c62fcd5c 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift @@ -50,25 +50,26 @@ extension StatusFetchViewModel.Timeline.Kind { public class UserTimelineContext { public let timelineKind: TimelineKind + @Published public var protected: Bool? @Published public var userIdentifier: UserIdentifier? public init( timelineKind: TimelineKind, - userIdentifier: Published.Publisher? + protected protectedPublisher: Published.Publisher?, + userIdentifier userIdentifierPublisher: Published.Publisher? ) { self.timelineKind = timelineKind - - if let userIdentifier = userIdentifier { - userIdentifier.assign(to: &self.$userIdentifier) - - } + protectedPublisher?.assign(to: &$protected) + userIdentifierPublisher?.assign(to: &$userIdentifier) } public init( timelineKind: TimelineKind, + protected: Bool, userIdentifier: UserIdentifier? ) { self.timelineKind = timelineKind + self.protected = protected self.userIdentifier = userIdentifier } @@ -450,6 +451,7 @@ extension StatusFetchViewModel.Timeline { return .user(.twitter(.init( authenticationContext: authenticationContext, userID: userIdentifier.id, + protected: userTimelineContext.protected ?? false, paginationToken: nil, maxID: nil, maxResults: nil, diff --git a/TwidereSDK/Sources/TwidereUI/Container/MediaStackContainerView.swift b/TwidereSDK/Sources/TwidereUI/Container/MediaStackContainerView.swift index 1462e880..b843376b 100644 --- a/TwidereSDK/Sources/TwidereUI/Container/MediaStackContainerView.swift +++ b/TwidereSDK/Sources/TwidereUI/Container/MediaStackContainerView.swift @@ -28,19 +28,23 @@ public struct MediaStackContainerView: View { let dimension = min(root.size.width, root.size.height) switch viewModel.items.count { case 1: - MediaView(viewModel: viewModel.items[0]) - .frame(width: dimension, height: dimension) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .overlay(alignment: .bottom) { - MediaMetaIndicatorView(viewModel: viewModel.items[0]) - } - .overlay( - RoundedRectangle(cornerRadius: 12) - .stroke(Color(uiColor: .placeholderText).opacity(0.5), lineWidth: 1) - ) - .onTapGesture { - handler(viewModel.items[0], .preview) - } + VStack { + Spacer() + MediaView(viewModel: viewModel.items[0]) + .frame(width: dimension, height: dimension) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .overlay(alignment: .bottom) { + MediaMetaIndicatorView(viewModel: viewModel.items[0]) + } + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color(uiColor: .placeholderText).opacity(0.5), lineWidth: 1) + ) + .onTapGesture { + handler(viewModel.items[0], .preview) + } + Spacer() + } default: CoverFlowStackScrollView { HStack(spacing: .zero) { diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift index 00d5c0ea..17992bac 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusToolbarView.swift @@ -68,9 +68,9 @@ extension StatusToolbarView { image: { switch viewModel.style { case .inline: - return Asset.Arrows.arrowTurnUpLeftMini.image.withRenderingMode(.alwaysTemplate) + return Asset.Communication.textBubbleMini.image.withRenderingMode(.alwaysTemplate) case .plain: - return Asset.Arrows.arrowTurnUpLeft.image.withRenderingMode(.alwaysTemplate) + return Asset.Communication.textBubble.image.withRenderingMode(.alwaysTemplate) } }(), count: isMetricCountDisplay ? viewModel.replyCount : nil, @@ -78,50 +78,99 @@ extension StatusToolbarView { ) } + enum RepostButtonImage { + case repost + case repostOff + case repostLock + + func image(style: StatusToolbarView.Style) -> UIImage { + switch self { + case .repost: + switch style { + case .inline: return Asset.Media.repeatMini.image.withRenderingMode(.alwaysTemplate) + case .plain: return Asset.Media.repeat.image.withRenderingMode(.alwaysTemplate) + } + case .repostOff: + switch style { + case .inline: return Asset.Media.repeatOffMini.image.withRenderingMode(.alwaysTemplate) + case .plain: return Asset.Media.repeatOff.image.withRenderingMode(.alwaysTemplate) + } + case .repostLock: + switch style { + case .inline: return Asset.Media.repeatLockMini.image.withRenderingMode(.alwaysTemplate) + case .plain: return Asset.Media.repeatLock.image.withRenderingMode(.alwaysTemplate) + } + } // end switch + } // end func + + static func kind( + platform: Platform, + isReposeRestricted: Bool, + isMyself: Bool + ) -> Self { + switch platform { + case .twitter: + if isMyself { return .repost } + if isReposeRestricted { return .repostOff } + return .repost + case .mastodon: + if isReposeRestricted { + return isMyself ? .repostLock : .repostOff + } + return .repost + case .none: + return .repost + } // end switch + } + } + public var repostButton: some View { ToolbarButton( handler: { action in + guard viewModel.isRepostable else { return } logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): repost") handler(action) }, action: .repost, image: { - switch viewModel.style { - case .inline: - return Asset.Media.repeatMini.image.withRenderingMode(.alwaysTemplate) - case .plain: - return Asset.Media.repeat.image.withRenderingMode(.alwaysTemplate) - } + return RepostButtonImage.kind( + platform: viewModel.platform, + isReposeRestricted: viewModel.isReposeRestricted, + isMyself: viewModel.isMyself + ).image(style: viewModel.style) }(), count: isMetricCountDisplay ? viewModel.repostCount : nil, tintColor: viewModel.isReposted ? Asset.Scene.Status.Toolbar.repost.color : nil ) + .opacity(viewModel.isRepostable ? 1 : 0.5) } public var repostMenu: some View { Menu { - // repost - Button { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): repost") - handler(.repost) - } label: { - Label { - let text = viewModel.isReposted ? L10n.Common.Controls.Status.Actions.undoRetweet : L10n.Common.Controls.Status.Actions.retweet - Text(text) - } icon: { - let image = viewModel.isReposted ? Asset.Media.repeatOff.image.withRenderingMode(.alwaysTemplate) : Asset.Media.repeat.image.withRenderingMode(.alwaysTemplate) - Image(uiImage: image) + if viewModel.isRepostable { + // repost + Button { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): repost") + handler(.repost) + } label: { + Label { + let text = viewModel.isReposted ? L10n.Common.Controls.Status.Actions.undoRetweet : L10n.Common.Controls.Status.Actions.retweet + Text(text) + } icon: { + let image = viewModel.isReposted ? Asset.Media.repeatOff.image.withRenderingMode(.alwaysTemplate) : Asset.Media.repeat.image.withRenderingMode(.alwaysTemplate) + Image(uiImage: image) + } } - } - // quote - Button { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): quote") - handler(.quote) - } label: { - Label { - Text(L10n.Common.Controls.Status.Actions.quote) - } icon: { - Image(uiImage: Asset.TextFormatting.textQuote.image.withRenderingMode(.alwaysTemplate)) + // quote + Button { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): quote") + handler(.quote) + } label: { + Label { + Text(L10n.Common.Controls.Status.Actions.quote) + } icon: { + Image(uiImage: Asset.TextFormatting.textQuote.image.withRenderingMode(.alwaysTemplate)) + } } } } label: { @@ -190,6 +239,7 @@ extension StatusToolbarView { // input @Published var platform: Platform = .none @Published var style: Style = .inline + @Published var replyCount: Int? @Published var repostCount: Int? @Published var likeCount: Int? @@ -197,6 +247,13 @@ extension StatusToolbarView { @Published var isReposted: Bool = false @Published var isLiked: Bool = false + @Published var isReposeRestricted: Bool = false + @Published var isMyself: Bool = false + + var isRepostable: Bool { + return isMyself || !isReposeRestricted + } + public init() { // end init } @@ -208,6 +265,7 @@ extension StatusToolbarView { case inline case plain } + public enum Action: Hashable, CaseIterable { case reply case repost diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index db740060..6357e160 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -55,20 +55,22 @@ extension StatusView { @Published public var authorName: MetaContent = PlaintextMetaContent(string: "") @Published public var authorUsernme = "" - @Published public var authorUserIdentifier: UserIdentifier? - + public let authorUserIdentifier: UserIdentifier? + + @Published public var protected: Bool = false + public let isMyself: Bool + // static let pollOptionOrdinalNumberFormatter: NumberFormatter = { // let formatter = NumberFormatter() // formatter.numberStyle = .ordinal // return formatter // }() // -// @Published public var userIdentifier: UserIdentifier? // @Published public var authorAvatarImage: UIImage? // @Published public var authorAvatarImageURL: URL? // @Published public var authorUsername: String? // -// @Published public var protected: Bool = false + // content @Published public var spoilerContent: MetaContent? @@ -121,9 +123,6 @@ extension StatusView { default: return true } } - -// @Published public var isRepost = false -// @Published public var isRepostEnabled = true // poll @Published public var pollViewModel: PollView.ViewModel? @@ -147,9 +146,7 @@ extension StatusView { return nil } } -// @Published public var replySettings: Twitter.Entity.V2.Tweet.ReplySettings? -////// // @Published public var groupedAccessibilityLabel = "" // timestamp @@ -168,6 +165,9 @@ extension StatusView { guard let authorUserIdentifier = self.authorUserIdentifier else { return false } return authContext.authenticationContext.userIdentifier == authorUserIdentifier } + + // reply settings banner + @Published public var replySettingBannerViewModel: ReplySettingBannerView.ViewModel? // conversation link @Published public var isTopConversationLinkLineViewDisplay = false @@ -186,22 +186,23 @@ extension StatusView { self.authContext = authContext self.kind = kind self.delegate = delegate + let _authorUserIdentifier: UserIdentifier = { + switch author { + case .twitter(let author): + return .twitter(.init(id: author.id)) + case .mastodon(let author): + return .mastodon(.init(domain: author.domain, id: author.id)) + } + }() + self.authorUserIdentifier = _authorUserIdentifier + self.isMyself = { + guard let myUserIdentifier = authContext?.authenticationContext.userIdentifier else { return false } + return myUserIdentifier == _authorUserIdentifier + }() // end init viewLayoutFramePublisher?.assign(to: &$viewLayoutFrame) -// // isMyself -// Publishers.CombineLatest( -// $authenticationContext, -// $userIdentifier -// ) -// .map { authenticationContext, userIdentifier in -// guard let authenticationContext = authenticationContext, -// let userIdentifier = userIdentifier -// else { return false } -// return authenticationContext.userIdentifier == userIdentifier -// } -// .assign(to: &$isMyself) // // isContentSensitive // Publishers.CombineLatest( // $platform, @@ -271,6 +272,8 @@ extension StatusView { self.author = nil self.authContext = nil self.kind = .timeline + self.authorUserIdentifier = nil + self.isMyself = false // end init viewLayoutFramePublisher?.assign(to: &$viewLayoutFrame) @@ -1095,6 +1098,16 @@ extension StatusView.ViewModel { viewLayoutFramePublisher: viewLayoutFramePublisher ) } + + // reply settings + replySettingBannerViewModel = status.replySettings + .flatMap { object in Twitter.Entity.V2.Tweet.ReplySettings(rawValue: object.value) } + .flatMap { replaySettings in + ReplySettingBannerView.ViewModel( + replaySettings: replaySettings, + authorUsername: status.author.username + ) + } // author status.author.publisher(for: \.profileImageURL) @@ -1105,7 +1118,8 @@ extension StatusView.ViewModel { .assign(to: &$authorName) status.author.publisher(for: \.username) .assign(to: &$authorUsernme) - authorUserIdentifier = .twitter(.init(id: status.author.id)) + status.author.publisher(for: \.protected) + .assign(to: &$protected) // timestamp switch kind { @@ -1181,6 +1195,7 @@ extension StatusView.ViewModel { // toolbar toolbarViewModel.platform = .twitter + toolbarViewModel.isMyself = isMyself status.publisher(for: \.replyCount) .map { Int($0) } .assign(to: &toolbarViewModel.$replyCount) @@ -1190,6 +1205,8 @@ extension StatusView.ViewModel { status.publisher(for: \.likeCount) .map { Int($0) } .assign(to: &toolbarViewModel.$likeCount) + status.author.publisher(for: \.protected) + .assign(to: &toolbarViewModel.$isReposeRestricted) if case let .twitter(authenticationContext) = authContext?.authenticationContext { status.publisher(for: \.likeBy) .map { users -> Bool in @@ -1264,8 +1281,9 @@ extension StatusView.ViewModel { status.author.publisher(for: \.username) .map { _ in status.author.acct } .assign(to: &$authorUsernme) - authorUserIdentifier = .mastodon(.init(domain: status.author.domain, id: status.author.id)) - + status.author.publisher(for: \.locked) + .assign(to: &$protected) + // visibility visibility = status.visibility @@ -1331,6 +1349,7 @@ extension StatusView.ViewModel { // toolbar toolbarViewModel.platform = .mastodon + toolbarViewModel.isMyself = isMyself status.publisher(for: \.replyCount) .map { Int($0) } .assign(to: &toolbarViewModel.$replyCount) @@ -1340,6 +1359,17 @@ extension StatusView.ViewModel { status.publisher(for: \.likeCount) .map { Int($0) } .assign(to: &toolbarViewModel.$likeCount) + toolbarViewModel.isReposeRestricted = { + switch status.visibility { + case .public: return false + case .unlisted: return false + case .direct: return true + case .private: return true + case ._other: + assertionFailure() + return false + } + }() if case let .mastodon(authenticationContext) = authContext?.authenticationContext { status.publisher(for: \.likeBy) .map { users -> Bool in diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index b638837c..d318ecd0 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -68,6 +68,7 @@ public struct StatusView: View { @Environment(\.dynamicTypeSize) var dynamicTypeSize @ScaledMetric(relativeTo: .subheadline) private var visibilityIconImageDimension: CGFloat = 16 @ScaledMetric(relativeTo: .headline) private var inlineAvatarButtonDimension: CGFloat = 20 + @ScaledMetric(relativeTo: .headline) private var lockImageDimension: CGFloat = 16 public init(viewModel: StatusView.ViewModel) { self.viewModel = viewModel @@ -208,28 +209,50 @@ public struct StatusView: View { // metric if let metricViewModel = viewModel.metricViewModel { StatusMetricView(viewModel: metricViewModel) { action in - + // TODO: } .padding(.vertical, 8) } // toolbar if viewModel.hasToolbar { - toolbarView - .overlay(alignment: .top) { - switch viewModel.kind { - case .conversationRoot: - VStack(spacing: .zero) { - Color.clear - .frame(height: 1) - Divider() - .frame(width: viewModel.viewLayoutFrame.safeAreaLayoutFrame.width) - .fixedSize() - Spacer() + VStack(spacing: .zero) { + toolbarView + .overlay(alignment: .top) { + switch viewModel.kind { + case .conversationRoot: + // toolbar top divider + VStack(spacing: .zero) { + Color.clear + .frame(height: 1) + Divider() + .frame(width: viewModel.viewLayoutFrame.safeAreaLayoutFrame.width) + .fixedSize() + Spacer() + } + default: + EmptyView() } - default: - EmptyView() + } + if viewModel.kind == .conversationRoot, + let replySettingBannerViewModel = viewModel.replySettingBannerViewModel, + !replySettingBannerViewModel.shouldHidden + { + HStack { + ReplySettingBannerView(viewModel: replySettingBannerViewModel) + Spacer() + } + .background { + Color(uiColor: Asset.Colors.hightLight.color.withAlphaComponent(0.6)) + .frame(width: viewModel.viewLayoutFrame.safeAreaLayoutFrame.width) + .overlay(alignment: .top) { + // reply settings banner top divider + VStack(spacing: .zero) { + Divider() + } + } } } + } // end VStack } } // end VStack .padding(.top, viewModel.margin) // container margin @@ -246,13 +269,12 @@ public struct StatusView: View { .frame(height: 1) } case .conversationRoot: + // cell bottom divider VStack(spacing: .zero) { Spacer() Divider() .frame(width: viewModel.viewLayoutFrame.safeAreaLayoutFrame.width) .fixedSize() - Color.clear - .frame(height: 1) } default: EmptyView() @@ -313,16 +335,25 @@ extension StatusView { } }() nameLayout { - // name - LabelRepresentable( - metaContent: viewModel.authorName, - textStyle: .statusAuthorName, - setupLabel: { label in - label.setContentHuggingPriority(.defaultHigh, for: .horizontal) - label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + HStack(spacing: 4) { + // name + LabelRepresentable( + metaContent: viewModel.authorName, + textStyle: .statusAuthorName, + setupLabel: { label in + label.setContentHuggingPriority(.defaultHigh, for: .horizontal) + label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + } + ) + .fixedSize(horizontal: false, vertical: true) + // lock + if viewModel.protected { + Image(uiImage: Asset.ObjectTools.lockMini.image.withRenderingMode(.alwaysTemplate)) + .resizable() + .frame(width: lockImageDimension, height: lockImageDimension) + .foregroundColor(.secondary) } - ) - .fixedSize(horizontal: false, vertical: true) + } .layoutPriority(0.618) // username LabelRepresentable( diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift index 9307a5d7..de83f88f 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift @@ -43,6 +43,7 @@ extension UserView { @Published public var platform: Platform = .none @Published public var isMyself: Bool = false + @Published public var protected: Bool = false // @Published public var authenticationContext: AuthenticationContext? // me // @Published public var userAuthenticationContext: AuthenticationContext? @@ -55,10 +56,9 @@ extension UserView { // @Published public var name: MetaContent? = PlaintextMetaContent(string: " ") // @Published public var username: String? // -// @Published public var protected: Bool = false -// + // @Published public var followerCount: Int? -// + // follow request @Published public var isFollowRequestActionDisplay = false @Published public var isFollowRequestBusy = false @@ -421,6 +421,8 @@ extension UserView.ViewModel { .assign(to: &$name) user.publisher(for: \.username) .assign(to: &$username) + user.publisher(for: \.protected) + .assign(to: &$protected) } public convenience init( @@ -448,6 +450,8 @@ extension UserView.ViewModel { user.publisher(for: \.username) .map { _ in user.acctWithDomain } .assign(to: &$username) + user.publisher(for: \.locked) + .assign(to: &$protected) } } @@ -467,6 +471,7 @@ extension UserView.ViewModel { username = "username" platform = .twitter notificationBadgeCount = 10 + protected = true } } #endif diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift index 1c703746..e4022696 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift @@ -26,38 +26,74 @@ public struct UserView: View { @ObservedObject public private(set) var viewModel: ViewModel + @Environment(\.dynamicTypeSize) var dynamicTypeSize + @ScaledMetric(relativeTo: .headline) private var lockImageDimension: CGFloat = 16 + public init(viewModel: UserView.ViewModel) { self.viewModel = viewModel } public var body: some View { - HStack(alignment: .center, spacing: .zero) { - // avatar - avatarButton - .padding(.trailing, StatusView.hangingAvatarButtonTrailingSpacing) - // info - VStackLayout(alignment: .leading, spacing: .zero) { - headlineView - subheadlineView - } - .frame(alignment: .leading) - Spacer() - // accessory view - accessoryView - } // end HStack - .padding(.vertical, viewModel.verticalMargin) - .overlay { - if viewModel.isSeparateLineDisplay { - HStack(spacing: .zero) { - Color.clear.frame(width: StatusView.hangingAvatarButtonDimension + StatusView.hangingAvatarButtonTrailingSpacing) - VStack(spacing: .zero) { + Group { + if dynamicTypeSize < .accessibility1 { + HStack(alignment: .center, spacing: .zero) { + // avatar + avatarButton + .padding(.trailing, StatusView.hangingAvatarButtonTrailingSpacing) + // info + VStackLayout(alignment: .leading, spacing: .zero) { + headlineView + subheadlineView + } + .frame(alignment: .leading) + Spacer() + // accessory view + accessoryView + } // end HStack + .padding(.vertical, viewModel.verticalMargin) + .overlay { + if viewModel.isSeparateLineDisplay { + HStack(spacing: .zero) { + Color.clear.frame(width: StatusView.hangingAvatarButtonDimension + StatusView.hangingAvatarButtonTrailingSpacing) + VStack(spacing: .zero) { + Spacer() + Divider() + Color.clear.frame(height: 1) + } + } // end HStack + } // end if + } // end .overlay + } else { + VStack(spacing: .zero) { + HStack { + // avatar + avatarButton + .padding(.trailing, StatusView.hangingAvatarButtonTrailingSpacing) Spacer() - Divider() - Color.clear.frame(height: 1) + // accessory view + accessoryView + } + // info + VStackLayout(alignment: .leading, spacing: .zero) { + headlineView + subheadlineView } + .frame(alignment: .leading) } // end HStack - } // end if - } // end .overlay + .padding(.vertical, viewModel.verticalMargin) + .overlay { + if viewModel.isSeparateLineDisplay { + HStack(spacing: .zero) { + VStack(spacing: .zero) { + Spacer() + Divider() + Color.clear.frame(height: 1) + } + } // end HStack + } // end if + } // end .overlay + } + } // Group } } @@ -258,26 +294,17 @@ extension UserView { var headlineView: some View { Group { switch viewModel.kind { - case .account: - nameLabel - case .search: - nameLabel - case .friend: - nameLabel - case .history: - nameLabel - case .notification: - nameLabel - case .mentionPick: - nameLabel - case .listMember: - nameLabel - case .addListMember: - nameLabel - case .settingAccountSection: - nameLabel - case .plain: - nameLabel + default: + HStack(spacing: 6) { + nameLabel + if viewModel.protected { + Image(uiImage: Asset.ObjectTools.lockMini.image.withRenderingMode(.alwaysTemplate)) + .resizable() + .frame(width: lockImageDimension, height: lockImageDimension) + .foregroundColor(.secondary) + Spacer() + } + } } } // end Group } diff --git a/TwidereSDK/Sources/TwidereUI/Control/ReplySettingBannerView.swift b/TwidereSDK/Sources/TwidereUI/Control/ReplySettingBannerView.swift index f8acb018..b8b347ee 100644 --- a/TwidereSDK/Sources/TwidereUI/Control/ReplySettingBannerView.swift +++ b/TwidereSDK/Sources/TwidereUI/Control/ReplySettingBannerView.swift @@ -6,76 +6,96 @@ // import UIKit +import SwiftUI +import TwitterSDK import TwidereAsset +import TwidereLocalization -final public class ReplySettingBannerView: UIView { +public struct ReplySettingBannerView: View { - let topSeparator = SeparatorLineView() - - let overflowBackgroundView = UIView() - - let stackView = UIStackView() - - public let imageView = UIImageView() - - public let label: UILabel = { - let label = UILabel() - label.font = .preferredFont(forTextStyle: .callout) - label.numberOfLines = 0 - return label - }() - - public override init(frame: CGRect) { - super.init(frame: frame) - _init() - } + public let viewModel: ViewModel + + @ScaledMetric(relativeTo: .callout) private var imageDimension: CGFloat = 16 + - public required init?(coder: NSCoder) { - super.init(coder: coder) - _init() + public var body: some View { + HStack(spacing: 4) { + Image(uiImage: viewModel.icon) + .resizable() + .frame(width: imageDimension, height: imageDimension) + Text(viewModel.title) + } + .font(.callout) + .foregroundColor(.white) + .padding(.vertical, 8) } } extension ReplySettingBannerView { - private func _init() { - // Hack the background view to fill the table width - overflowBackgroundView.translatesAutoresizingMaskIntoConstraints = false - addSubview(overflowBackgroundView) - NSLayoutConstraint.activate([ - overflowBackgroundView.topAnchor.constraint(equalTo: topAnchor), - leadingAnchor.constraint(equalTo: overflowBackgroundView.leadingAnchor, constant: 400), - overflowBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 400), - overflowBackgroundView.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - - // Hack the line to fill the table width - topSeparator.translatesAutoresizingMaskIntoConstraints = false - addSubview(topSeparator) - NSLayoutConstraint.activate([ - topSeparator.topAnchor.constraint(equalTo: topSeparator.topAnchor), - leadingAnchor.constraint(equalTo: topSeparator.leadingAnchor, constant: 400), - topSeparator.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 400), - ]) + public class ViewModel: ObservableObject { + // input + public let replaySettings: Twitter.Entity.V2.Tweet.ReplySettings + public let authorUsername: String - // stackView: H - [ icon | label ] - stackView.axis = .horizontal - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.alignment = .center - stackView.spacing = 4 - addSubview(stackView) - NSLayoutConstraint.activate([ - stackView.topAnchor.constraint(equalTo: topAnchor, constant: 8), - stackView.leadingAnchor.constraint(equalTo: leadingAnchor), - stackView.trailingAnchor.constraint(equalTo: trailingAnchor), - bottomAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 8), - ]) + // output + public let icon: UIImage + public let title: String + public let shouldHidden: Bool - stackView.addArrangedSubview(imageView) - stackView.addArrangedSubview(label) - - overflowBackgroundView.backgroundColor = Asset.Colors.hightLight.color.withAlphaComponent(0.6) - imageView.tintColor = .white - label.textColor = .white + public init( + replaySettings: Twitter.Entity.V2.Tweet.ReplySettings, + authorUsername: String + ) { + self.replaySettings = replaySettings + self.authorUsername = authorUsername + self.icon = { + switch replaySettings { + case .everyone: + fallthrough + case .following: + return Asset.Communication.at.image.withRenderingMode(.alwaysTemplate) + case .mentionedUsers: + return Asset.Human.personCheckMini.image.withRenderingMode(.alwaysTemplate) + } + }() + self.title = { + switch replaySettings { + case .everyone: + return "" + case .following: + return L10n.Common.Controls.Status.ReplySettings.peopleUserFollowsOrMentionedCanReply("@\(authorUsername)") + case .mentionedUsers: + return L10n.Common.Controls.Status.ReplySettings.peopleUserMentionedCanReply("@\(authorUsername)") + } + }() + self.shouldHidden = replaySettings == .everyone + // end init + } } } + +#if canImport(SwiftUI) && DEBUG +import SwiftUI + +@available(iOS 13.0, *) +struct ReplySettingBannerView_Previews: PreviewProvider { + + static var previews: some View { + Group { + ReplySettingBannerView(viewModel: .init( + replaySettings: .following, + authorUsername: "alice" + )) + ReplySettingBannerView(viewModel: .init( + replaySettings: .mentionedUsers, + authorUsername: "alice" + )) + } + .background(Color.black) + } + +} + +#endif + diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 6839e4d6..9840a86c 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3431,7 +3431,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3458,7 +3458,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3481,7 +3481,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3505,7 +3505,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3532,7 +3532,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3561,7 +3561,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3590,7 +3590,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3618,7 +3618,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3644,7 +3644,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3671,7 +3671,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3825,7 +3825,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3857,7 +3857,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3884,7 +3884,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3909,7 +3909,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3932,7 +3932,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3955,7 +3955,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3979,7 +3979,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -4006,7 +4006,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 128; + CURRENT_PROJECT_VERSION = 129; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index 049913c7..33da201f 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -258,8 +258,8 @@ "repositoryURL": "https://github.com/TwidereProject/TwitterSDK.git", "state": { "branch": null, - "revision": "cf2f33c96e7b9f86dbee23111cd523f9a9294099", - "version": "0.7.0" + "revision": "1b2998a2fc5b7abc421e61b075ba3807ba694991", + "version": "0.8.0" } }, { diff --git a/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift b/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift index fa9defd3..7c081596 100644 --- a/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift +++ b/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift @@ -121,7 +121,18 @@ extension TabBarItem { fatalError() case .likes: let _viewController = UserLikeTimelineViewController() - _viewController.viewModel = UserLikeTimelineViewModel(context: context, authContext: authContext, timelineContext: .init(timelineKind: .like, userIdentifier: authContext.authenticationContext.userIdentifier)) + _viewController.viewModel = UserLikeTimelineViewModel( + context: context, + authContext: authContext, + timelineContext: .init( + timelineKind: .like, + protected: { + guard let user = authContext.authenticationContext.user(in: context.managedObjectContext) else { return false } + return user.protected + }(), + userIdentifier: authContext.authenticationContext.userIdentifier + ) + ) viewController = _viewController case .history: let _viewController = HistoryViewController() diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index 33f4967d..5f7a8ad6 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 128 + 129 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereX/Scene/Profile/ProfileViewController.swift b/TwidereX/Scene/Profile/ProfileViewController.swift index 9e6487dd..18c67e5c 100644 --- a/TwidereX/Scene/Profile/ProfileViewController.swift +++ b/TwidereX/Scene/Profile/ProfileViewController.swift @@ -57,6 +57,7 @@ final class ProfileViewController: UIViewController, NeedsDependency, DrawerSide authContext: authContext, coordinator: coordinator, displayLikeTimeline: viewModel.displayLikeTimeline, + protected: viewModel.$protected, userIdentifier: viewModel.$userIdentifier ) return profilePagingViewController diff --git a/TwidereX/Scene/Profile/ProfileViewModel.swift b/TwidereX/Scene/Profile/ProfileViewModel.swift index 6dfb433c..ffb797e9 100644 --- a/TwidereX/Scene/Profile/ProfileViewModel.swift +++ b/TwidereX/Scene/Profile/ProfileViewModel.swift @@ -30,6 +30,7 @@ class ProfileViewModel: ObservableObject { let displayLikeTimeline: Bool @Published var userRecord: UserRecord? @Published var userIdentifier: UserIdentifier? = nil + @Published var protected: Bool? = nil let relationshipViewModel = RelationshipViewModel() // let suspended = CurrentValueSubject(false) @@ -52,7 +53,7 @@ class ProfileViewModel: ObservableObject { } $user - .map { user in user.flatMap { UserRecord(object: $0) } } + .map { $0?.asRecord } .assign(to: &$userRecord) $user @@ -67,6 +68,10 @@ class ProfileViewModel: ObservableObject { } } .assign(to: &$userIdentifier) + + $user + .map { $0?.protected } + .assign(to: &$protected) // bind active authentication Task { diff --git a/TwidereX/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift b/TwidereX/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift index 87b5d25a..8aa1be67 100644 --- a/TwidereX/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift +++ b/TwidereX/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift @@ -27,6 +27,7 @@ final class ProfilePagingViewModel: NSObject { authContext: AuthContext, coordinator: SceneCoordinator, displayLikeTimeline: Bool, + protected: Published.Publisher?, userIdentifier: Published.Publisher? ) { self.context = context @@ -39,6 +40,7 @@ final class ProfilePagingViewModel: NSObject { authContext: authContext, timelineContext: .init( timelineKind: .status, + protected: protected, userIdentifier: userIdentifier ) ) @@ -55,6 +57,7 @@ final class ProfilePagingViewModel: NSObject { authContext: authContext, timelineContext: .init( timelineKind: .media, + protected: protected, userIdentifier: userIdentifier ) ) @@ -75,6 +78,7 @@ final class ProfilePagingViewModel: NSObject { authContext: authContext, timelineContext: .init( timelineKind: .like, + protected: protected, userIdentifier: userIdentifier ) ) diff --git a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift index e71a8679..e4712687 100644 --- a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift +++ b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift @@ -210,7 +210,18 @@ extension DrawerSidebarViewController: UICollectionViewDelegate { let federatedTimelineViewModel = FederatedTimelineViewModel(context: context, authContext: viewModel.authContext, isLocal: false) coordinator.present(scene: .federatedTimeline(viewModel: federatedTimelineViewModel), from: presentingViewController, transition: .show) case .likes: - let userLikeTimelineViewModel = UserLikeTimelineViewModel(context: context, authContext: viewModel.authContext, timelineContext: .init(timelineKind: .like, userIdentifier: viewModel.authContext.authenticationContext.userIdentifier)) + let userLikeTimelineViewModel = UserLikeTimelineViewModel( + context: context, + authContext: viewModel.authContext, + timelineContext: .init( + timelineKind: .like, + protected: { + guard let user = viewModel.authContext.authenticationContext.user(in: context.managedObjectContext) else { return false } + return user.protected + }(), + userIdentifier: viewModel.authContext.authenticationContext.userIdentifier + ) + ) coordinator.present(scene: .userLikeTimeline(viewModel: userLikeTimelineViewModel), from: presentingViewController, transition: .show) case .history: let historyViewModel = HistoryViewModel(context: context, coordinator: coordinator, authContext: viewModel.authContext) diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index 57f097f7..14cabbb5 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 128 + 129 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index e0d1d83e..f33f97dc 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 128 + 129 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index e0d1d83e..f33f97dc 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 128 + 129