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