Compare commits
12 Commits
8fc915d6a0
...
fe32356bce
Author | SHA1 | Date |
---|---|---|
Shadowfacts | fe32356bce | |
Shadowfacts | 1f337613be | |
Shadowfacts | 3f4a62f5f9 | |
Shadowfacts | b506704716 | |
Shadowfacts | 6a3dcca9ee | |
Shadowfacts | edd1e55cbb | |
Shadowfacts | f1facea929 | |
Shadowfacts | d638ea054b | |
Shadowfacts | e11784904b | |
Shadowfacts | 9f1d3804d9 | |
Shadowfacts | 333295367a | |
Shadowfacts | e9d14c6cbf |
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -1,5 +1,19 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2023.4 (70)
|
||||||
|
Features/Improvements:
|
||||||
|
- Add GIF/ALT badges to attachments
|
||||||
|
- Add menu action to hide/show reblogs from specific accounts
|
||||||
|
- Apply Mastodon's link truncation
|
||||||
|
- Add preference to hide link preview cards
|
||||||
|
- Tweak link preview card border color in dark mode
|
||||||
|
- Unify haptic feedback across the app
|
||||||
|
- Move Drafts button to the nav bar when the post doesn't have any content, to reduce accidental presses
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Fix status URLs with fragments not being resolved
|
||||||
|
- Workaround for local-only posts not being decodable when logged in to Akkoma instances
|
||||||
|
|
||||||
## 2023.3 (69)
|
## 2023.3 (69)
|
||||||
Features/Improvements:
|
Features/Improvements:
|
||||||
- Add Tip Jar under Preferences
|
- Add Tip Jar under Preferences
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Haptic Feedback
|
||||||
|
|
||||||
|
## Selection changed
|
||||||
|
`UISelectionFeedbackGenerator`
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
On success, `UIImpactFeedbackGenerator` with the `.light` style. On error, `UINotificationFeedbackGenerator` with the `.error` type.
|
|
@ -62,7 +62,7 @@ public class DuckableContainerViewController: UIViewController, DuckableViewCont
|
||||||
guard case .idle = state else {
|
guard case .idle = state else {
|
||||||
if animated,
|
if animated,
|
||||||
case .ducked(_, placeholder: let placeholder) = state {
|
case .ducked(_, placeholder: let placeholder) = state {
|
||||||
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
|
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
||||||
let origConstant = placeholder.topConstraint.constant
|
let origConstant = placeholder.topConstraint.constant
|
||||||
UIView.animateKeyframes(withDuration: 0.4, delay: 0) {
|
UIView.animateKeyframes(withDuration: 0.4, delay: 0) {
|
||||||
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {
|
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {
|
||||||
|
|
|
@ -109,6 +109,12 @@ public final class Account: AccountProtocol, Decodable {
|
||||||
return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(accountID)/follow")
|
return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(accountID)/follow")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func setShowReblogs(_ accountID: String, showReblogs: Bool) -> Request<Relationship> {
|
||||||
|
return Request(method: .post, path: "/api/v1/accounts/\(accountID)/follow", body: ParametersBody([
|
||||||
|
"reblogs" => showReblogs
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
public static func unfollow(_ accountID: String) -> Request<Relationship> {
|
public static func unfollow(_ accountID: String) -> Request<Relationship> {
|
||||||
return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(accountID)/unfollow")
|
return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(accountID)/unfollow")
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,47 @@ public final class Status: StatusProtocol, Decodable {
|
||||||
|
|
||||||
public var applicationName: String? { application?.name }
|
public var applicationName: String? { application?.name }
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.id = try container.decode(String.self, forKey: .id)
|
||||||
|
self.uri = try container.decode(String.self, forKey: .uri)
|
||||||
|
self.url = try container.decodeIfPresent(WebURL.self, forKey: .url)
|
||||||
|
self.account = try container.decode(Account.self, forKey: .account)
|
||||||
|
self.inReplyToID = try container.decodeIfPresent(String.self, forKey: .inReplyToID)
|
||||||
|
self.inReplyToAccountID = try container.decodeIfPresent(String.self, forKey: .inReplyToAccountID)
|
||||||
|
self.reblog = try container.decodeIfPresent(Status.self, forKey: .reblog)
|
||||||
|
self.content = try container.decode(String.self, forKey: .content)
|
||||||
|
self.createdAt = try container.decode(Date.self, forKey: .createdAt)
|
||||||
|
self.emojis = try container.decode([Emoji].self, forKey: .emojis)
|
||||||
|
self.reblogsCount = try container.decode(Int.self, forKey: .reblogsCount)
|
||||||
|
self.favouritesCount = try container.decode(Int.self, forKey: .favouritesCount)
|
||||||
|
self.reblogged = try container.decodeIfPresent(Bool.self, forKey: .reblogged)
|
||||||
|
self.favourited = try container.decodeIfPresent(Bool.self, forKey: .favourited)
|
||||||
|
self.muted = try container.decodeIfPresent(Bool.self, forKey: .muted)
|
||||||
|
self.sensitive = try container.decode(Bool.self, forKey: .sensitive)
|
||||||
|
self.spoilerText = try container.decode(String.self, forKey: .spoilerText)
|
||||||
|
if let visibility = try? container.decode(Status.Visibility.self, forKey: .visibility) {
|
||||||
|
self.visibility = visibility
|
||||||
|
self.localOnly = try container.decodeIfPresent(Bool.self, forKey: .localOnly)
|
||||||
|
} else if let s = try? container.decode(String.self, forKey: .visibility),
|
||||||
|
s == "local" {
|
||||||
|
// hacky workaround for #332, akkoma describes local posts with a separate visibility
|
||||||
|
self.visibility = .public
|
||||||
|
self.localOnly = true
|
||||||
|
} else {
|
||||||
|
throw DecodingError.dataCorruptedError(forKey: .visibility, in: container, debugDescription: "Could not decode visibility")
|
||||||
|
}
|
||||||
|
self.attachments = try container.decode([Attachment].self, forKey: .attachments)
|
||||||
|
self.mentions = try container.decode([Mention].self, forKey: .mentions)
|
||||||
|
self.hashtags = try container.decode([Hashtag].self, forKey: .hashtags)
|
||||||
|
self.application = try container.decodeIfPresent(Application.self, forKey: .application)
|
||||||
|
self.language = try container.decodeIfPresent(String.self, forKey: .language)
|
||||||
|
self.pinned = try container.decodeIfPresent(Bool.self, forKey: .pinned)
|
||||||
|
self.bookmarked = try container.decodeIfPresent(Bool.self, forKey: .bookmarked)
|
||||||
|
self.card = try container.decodeIfPresent(Card.self, forKey: .card)
|
||||||
|
self.poll = try container.decodeIfPresent(Poll.self, forKey: .poll)
|
||||||
|
}
|
||||||
|
|
||||||
public static func getContext(_ statusID: String) -> Request<ConversationContext> {
|
public static func getContext(_ statusID: String) -> Request<ConversationContext> {
|
||||||
return Request<ConversationContext>(method: .get, path: "/api/v1/statuses/\(statusID)/context")
|
return Request<ConversationContext>(method: .get, path: "/api/v1/statuses/\(statusID)/context")
|
||||||
}
|
}
|
||||||
|
|
|
@ -2372,7 +2372,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 69;
|
CURRENT_PROJECT_VERSION = 70;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
|
@ -2380,7 +2380,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2023.3;
|
MARKETING_VERSION = 2023.4;
|
||||||
OTHER_CODE_SIGN_FLAGS = "";
|
OTHER_CODE_SIGN_FLAGS = "";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
@ -2437,7 +2437,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 69;
|
CURRENT_PROJECT_VERSION = 70;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
|
@ -2446,7 +2446,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2023.3;
|
MARKETING_VERSION = 2023.4;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
@ -2588,7 +2588,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 69;
|
CURRENT_PROJECT_VERSION = 70;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
|
@ -2596,7 +2596,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2023.3;
|
MARKETING_VERSION = 2023.4;
|
||||||
OTHER_CODE_SIGN_FLAGS = "";
|
OTHER_CODE_SIGN_FLAGS = "";
|
||||||
OTHER_LDFLAGS = "";
|
OTHER_LDFLAGS = "";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
||||||
|
@ -2616,7 +2616,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 69;
|
CURRENT_PROJECT_VERSION = 70;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
|
@ -2624,7 +2624,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2023.3;
|
MARKETING_VERSION = 2023.4;
|
||||||
OTHER_CODE_SIGN_FLAGS = "";
|
OTHER_CODE_SIGN_FLAGS = "";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
@ -2721,7 +2721,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 69;
|
CURRENT_PROJECT_VERSION = 70;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
|
@ -2730,7 +2730,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2023.3;
|
MARKETING_VERSION = 2023.4;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
@ -2747,7 +2747,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 69;
|
CURRENT_PROJECT_VERSION = 70;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||||
|
@ -2756,7 +2756,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2023.3;
|
MARKETING_VERSION = 2023.4;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
|
|
@ -55,7 +55,21 @@ struct HTMLConverter {
|
||||||
case let node as Element:
|
case let node as Element:
|
||||||
let attributed = NSMutableAttributedString(string: "", attributes: [.font: font, .foregroundColor: color])
|
let attributed = NSMutableAttributedString(string: "", attributes: [.font: font, .foregroundColor: color])
|
||||||
for child in node.getChildNodes() {
|
for child in node.getChildNodes() {
|
||||||
|
var appendEllipsis = false
|
||||||
|
if node.tagName() == "a",
|
||||||
|
let el = child as? Element {
|
||||||
|
if el.hasClass("invisible") {
|
||||||
|
continue
|
||||||
|
} else if el.hasClass("ellipsis") {
|
||||||
|
appendEllipsis = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
attributed.append(attributedTextForHTMLNode(child, usePreformattedText: usePreformattedText || node.tagName() == "pre"))
|
attributed.append(attributedTextForHTMLNode(child, usePreformattedText: usePreformattedText || node.tagName() == "pre"))
|
||||||
|
|
||||||
|
if appendEllipsis {
|
||||||
|
attributed.append(NSAttributedString("…"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch node.tagName() {
|
switch node.tagName() {
|
||||||
|
|
|
@ -44,6 +44,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
self.showIsStatusReplyIcon = try container.decode(Bool.self, forKey: .showIsStatusReplyIcon)
|
self.showIsStatusReplyIcon = try container.decode(Bool.self, forKey: .showIsStatusReplyIcon)
|
||||||
self.alwaysShowStatusVisibilityIcon = try container.decode(Bool.self, forKey: .alwaysShowStatusVisibilityIcon)
|
self.alwaysShowStatusVisibilityIcon = try container.decode(Bool.self, forKey: .alwaysShowStatusVisibilityIcon)
|
||||||
self.hideActionsInTimeline = try container.decodeIfPresent(Bool.self, forKey: .hideActionsInTimeline) ?? false
|
self.hideActionsInTimeline = try container.decodeIfPresent(Bool.self, forKey: .hideActionsInTimeline) ?? false
|
||||||
|
self.showLinkPreviews = try container.decodeIfPresent(Bool.self, forKey: .showLinkPreviews) ?? true
|
||||||
self.leadingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .leadingStatusSwipeActions) ?? leadingStatusSwipeActions
|
self.leadingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .leadingStatusSwipeActions) ?? leadingStatusSwipeActions
|
||||||
self.trailingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .trailingStatusSwipeActions) ?? trailingStatusSwipeActions
|
self.trailingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .trailingStatusSwipeActions) ?? trailingStatusSwipeActions
|
||||||
|
|
||||||
|
@ -97,6 +98,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
try container.encode(showIsStatusReplyIcon, forKey: .showIsStatusReplyIcon)
|
try container.encode(showIsStatusReplyIcon, forKey: .showIsStatusReplyIcon)
|
||||||
try container.encode(alwaysShowStatusVisibilityIcon, forKey: .alwaysShowStatusVisibilityIcon)
|
try container.encode(alwaysShowStatusVisibilityIcon, forKey: .alwaysShowStatusVisibilityIcon)
|
||||||
try container.encode(hideActionsInTimeline, forKey: .hideActionsInTimeline)
|
try container.encode(hideActionsInTimeline, forKey: .hideActionsInTimeline)
|
||||||
|
try container.encode(showLinkPreviews, forKey: .showLinkPreviews)
|
||||||
try container.encode(leadingStatusSwipeActions, forKey: .leadingStatusSwipeActions)
|
try container.encode(leadingStatusSwipeActions, forKey: .leadingStatusSwipeActions)
|
||||||
try container.encode(trailingStatusSwipeActions, forKey: .trailingStatusSwipeActions)
|
try container.encode(trailingStatusSwipeActions, forKey: .trailingStatusSwipeActions)
|
||||||
|
|
||||||
|
@ -144,6 +146,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
@Published var showIsStatusReplyIcon = false
|
@Published var showIsStatusReplyIcon = false
|
||||||
@Published var alwaysShowStatusVisibilityIcon = false
|
@Published var alwaysShowStatusVisibilityIcon = false
|
||||||
@Published var hideActionsInTimeline = false
|
@Published var hideActionsInTimeline = false
|
||||||
|
@Published var showLinkPreviews = true
|
||||||
@Published var leadingStatusSwipeActions: [StatusSwipeAction] = [.favorite, .reblog]
|
@Published var leadingStatusSwipeActions: [StatusSwipeAction] = [.favorite, .reblog]
|
||||||
@Published var trailingStatusSwipeActions: [StatusSwipeAction] = [.reply, .share]
|
@Published var trailingStatusSwipeActions: [StatusSwipeAction] = [.reply, .share]
|
||||||
|
|
||||||
|
@ -205,6 +208,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
case showIsStatusReplyIcon
|
case showIsStatusReplyIcon
|
||||||
case alwaysShowStatusVisibilityIcon
|
case alwaysShowStatusVisibilityIcon
|
||||||
case hideActionsInTimeline
|
case hideActionsInTimeline
|
||||||
|
case showLinkPreviews
|
||||||
case leadingStatusSwipeActions
|
case leadingStatusSwipeActions
|
||||||
case trailingStatusSwipeActions
|
case trailingStatusSwipeActions
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,7 @@ struct ComposeToolbar: View {
|
||||||
.font(.system(size: imageSize))
|
.font(.system(size: imageSize))
|
||||||
.padding(5)
|
.padding(5)
|
||||||
.hoverEffect()
|
.hoverEffect()
|
||||||
|
.transition(.opacity.animation(.linear(duration: 0.2)))
|
||||||
}
|
}
|
||||||
|
|
||||||
if let currentInput = uiState.currentInput,
|
if let currentInput = uiState.currentInput,
|
||||||
|
@ -74,16 +75,11 @@ struct ComposeToolbar: View {
|
||||||
.accessibilityLabel(format.accessibilityLabel)
|
.accessibilityLabel(format.accessibilityLabel)
|
||||||
.padding(5)
|
.padding(5)
|
||||||
.hoverEffect()
|
.hoverEffect()
|
||||||
|
.transition(.opacity.animation(.linear(duration: 0.2)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Button(action: self.draftsButtonPressed) {
|
|
||||||
Text("Drafts")
|
|
||||||
}
|
|
||||||
.padding(5)
|
|
||||||
.hoverEffect()
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.frame(minWidth: minWidth)
|
.frame(minWidth: minWidth)
|
||||||
|
@ -119,10 +115,6 @@ struct ComposeToolbar: View {
|
||||||
uiState.currentInput?.beginAutocompletingEmoji()
|
uiState.currentInput?.beginAutocompletingEmoji()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func draftsButtonPressed() {
|
|
||||||
uiState.isShowingDraftsList = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private func formatAction(_ format: StatusFormat) -> () -> Void {
|
private func formatAction(_ format: StatusFormat) -> () -> Void {
|
||||||
{
|
{
|
||||||
uiState.currentInput?.applyFormat(format)
|
uiState.currentInput?.applyFormat(format)
|
||||||
|
|
|
@ -239,7 +239,9 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
private var postButton: some View {
|
private var postButton: some View {
|
||||||
|
if draft.hasContent {
|
||||||
Button {
|
Button {
|
||||||
Task {
|
Task {
|
||||||
await self.postStatus()
|
await self.postStatus()
|
||||||
|
@ -249,6 +251,13 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
.keyboardShortcut(.return, modifiers: .command)
|
.keyboardShortcut(.return, modifiers: .command)
|
||||||
.disabled(!postButtonEnabled)
|
.disabled(!postButtonEnabled)
|
||||||
|
} else {
|
||||||
|
Button {
|
||||||
|
uiState.isShowingDraftsList = true
|
||||||
|
} label: {
|
||||||
|
Text("Drafts")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func cancel() {
|
private func cancel() {
|
||||||
|
|
|
@ -98,6 +98,7 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
if context.coordinator.skipSettingTextOnNextUpdate {
|
if context.coordinator.skipSettingTextOnNextUpdate {
|
||||||
context.coordinator.skipSettingTextOnNextUpdate = false
|
context.coordinator.skipSettingTextOnNextUpdate = false
|
||||||
} else {
|
} else {
|
||||||
|
context.coordinator.skipNextAutocompleteUpdate = true
|
||||||
uiView.text = text
|
uiView.text = text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,6 +186,7 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
var caretScrollPositionAnimator: UIViewPropertyAnimator?
|
var caretScrollPositionAnimator: UIViewPropertyAnimator?
|
||||||
|
|
||||||
var skipSettingTextOnNextUpdate = false
|
var skipSettingTextOnNextUpdate = false
|
||||||
|
var skipNextAutocompleteUpdate = false
|
||||||
|
|
||||||
var toolbarElements: [ComposeUIState.ToolbarElement] {
|
var toolbarElements: [ComposeUIState.ToolbarElement] {
|
||||||
[.emojiPicker, .formattingButtons]
|
[.emojiPicker, .formattingButtons]
|
||||||
|
@ -324,6 +326,10 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateAutocompleteState() {
|
private func updateAutocompleteState() {
|
||||||
|
guard !skipNextAutocompleteUpdate else {
|
||||||
|
skipNextAutocompleteUpdate = false
|
||||||
|
return
|
||||||
|
}
|
||||||
guard let textView = textView,
|
guard let textView = textView,
|
||||||
let text = textView.text,
|
let text = textView.text,
|
||||||
let (lastWordStartIndex, foundFirstAtSign) = findAutocompleteLastWord() else {
|
let (lastWordStartIndex, foundFirstAtSign) = findAutocompleteLastWord() else {
|
||||||
|
|
|
@ -189,11 +189,11 @@ class ConversationViewController: UIViewController {
|
||||||
indicator.startAnimating()
|
indicator.startAnimating()
|
||||||
state = .loading(indicator)
|
state = .loading(indicator)
|
||||||
|
|
||||||
let url = WebURL(url)!
|
let url = WebURL(url)!.serialized(excludingFragment: true)
|
||||||
let request = Client.search(query: url.serialized(), types: [.statuses], resolve: true)
|
let request = Client.search(query: url, types: [.statuses], resolve: true)
|
||||||
do {
|
do {
|
||||||
let (results, _) = try await mastodonController.run(request)
|
let (results, _) = try await mastodonController.run(request)
|
||||||
guard let status = results.statuses.first(where: { $0.url == url }) else {
|
guard let status = results.statuses.first(where: { $0.url?.serialized() == url }) else {
|
||||||
throw UnableToResolveError()
|
throw UnableToResolveError()
|
||||||
}
|
}
|
||||||
_ = mastodonController.persistentContainer.addOrUpdateOnViewContext(status: status)
|
_ = mastodonController.persistentContainer.addOrUpdateOnViewContext(status: status)
|
||||||
|
|
|
@ -24,7 +24,7 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
|
|
||||||
private(set) var accountViews: [FastSwitchingAccountView] = []
|
private(set) var accountViews: [FastSwitchingAccountView] = []
|
||||||
private var lastSelectedAccountViewIndex: Int?
|
private var lastSelectedAccountViewIndex: Int?
|
||||||
private var selectionChangedFeedbackGenerator: UIImpactFeedbackGenerator?
|
private var selectionChangedFeedbackGenerator: UISelectionFeedbackGenerator?
|
||||||
private var touchBeganFeedbackWorkItem: DispatchWorkItem?
|
private var touchBeganFeedbackWorkItem: DispatchWorkItem?
|
||||||
|
|
||||||
var itemOrientation: ItemOrientation = .iconsTrailing
|
var itemOrientation: ItemOrientation = .iconsTrailing
|
||||||
|
@ -148,7 +148,7 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
private func switchAccount(newIndex: Int, hapticFeedback: Bool = true) {
|
private func switchAccount(newIndex: Int, hapticFeedback: Bool = true) {
|
||||||
if newIndex == 0 { // add account placeholder
|
if newIndex == 0 { // add account placeholder
|
||||||
if hapticFeedback {
|
if hapticFeedback {
|
||||||
selectionChangedFeedbackGenerator?.impactOccurred()
|
selectionChangedFeedbackGenerator?.selectionChanged()
|
||||||
}
|
}
|
||||||
selectionChangedFeedbackGenerator = nil
|
selectionChangedFeedbackGenerator = nil
|
||||||
|
|
||||||
|
@ -160,7 +160,7 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
|
|
||||||
if account.id != LocalData.shared.mostRecentAccountID {
|
if account.id != LocalData.shared.mostRecentAccountID {
|
||||||
if hapticFeedback {
|
if hapticFeedback {
|
||||||
selectionChangedFeedbackGenerator?.impactOccurred()
|
selectionChangedFeedbackGenerator?.selectionChanged()
|
||||||
}
|
}
|
||||||
selectionChangedFeedbackGenerator = nil
|
selectionChangedFeedbackGenerator = nil
|
||||||
|
|
||||||
|
@ -178,8 +178,8 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
@objc private func handleLongPress(_ recognizer: UIGestureRecognizer) {
|
@objc private func handleLongPress(_ recognizer: UIGestureRecognizer) {
|
||||||
switch recognizer.state {
|
switch recognizer.state {
|
||||||
case .began:
|
case .began:
|
||||||
selectionChangedFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
|
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
|
||||||
selectionChangedFeedbackGenerator?.impactOccurred()
|
selectionChangedFeedbackGenerator = UISelectionFeedbackGenerator()
|
||||||
selectionChangedFeedbackGenerator?.prepare()
|
selectionChangedFeedbackGenerator?.prepare()
|
||||||
|
|
||||||
show()
|
show()
|
||||||
|
@ -231,7 +231,7 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
lastSelectedAccountViewIndex = selectedAccountViewIndex
|
lastSelectedAccountViewIndex = selectedAccountViewIndex
|
||||||
|
|
||||||
if hapticFeedback {
|
if hapticFeedback {
|
||||||
selectionChangedFeedbackGenerator?.impactOccurred(intensity: 0.5)
|
selectionChangedFeedbackGenerator?.selectionChanged()
|
||||||
selectionChangedFeedbackGenerator?.prepare()
|
selectionChangedFeedbackGenerator?.prepare()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -260,7 +260,7 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
// if the user is merely tapping, not initiating a pan, we don't want to trigger a double-impact
|
// if the user is merely tapping, not initiating a pan, we don't want to trigger a double-impact
|
||||||
// if the tap ends very quickly, this will be cancelled
|
// if the tap ends very quickly, this will be cancelled
|
||||||
touchBeganFeedbackWorkItem = DispatchWorkItem {
|
touchBeganFeedbackWorkItem = DispatchWorkItem {
|
||||||
self.selectionChangedFeedbackGenerator?.impactOccurred(intensity: 0.5)
|
self.selectionChangedFeedbackGenerator?.selectionChanged()
|
||||||
self.selectionChangedFeedbackGenerator?.prepare()
|
self.selectionChangedFeedbackGenerator?.prepare()
|
||||||
self.touchBeganFeedbackWorkItem = nil
|
self.touchBeganFeedbackWorkItem = nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,6 +84,9 @@ struct AppearancePrefsView : View {
|
||||||
Toggle(isOn: $preferences.hideActionsInTimeline) {
|
Toggle(isOn: $preferences.hideActionsInTimeline) {
|
||||||
Text("Hide Actions on Timeline")
|
Text("Hide Actions on Timeline")
|
||||||
}
|
}
|
||||||
|
Toggle(isOn: $preferences.showLinkPreviews) {
|
||||||
|
Text("Show Link Previews")
|
||||||
|
}
|
||||||
NavigationLink("Leading Swipe Actions") {
|
NavigationLink("Leading Swipe Actions") {
|
||||||
SwipeActionsPrefsView(selection: $preferences.leadingStatusSwipeActions)
|
SwipeActionsPrefsView(selection: $preferences.leadingStatusSwipeActions)
|
||||||
.edgesIgnoringSafeArea(.all)
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
|
|
@ -97,8 +97,9 @@ extension MenuActionProvider {
|
||||||
}))
|
}))
|
||||||
elementHandler([UIMenu(title: "Add to List", image: UIImage(systemName: "list.bullet"), children: listActions)])
|
elementHandler([UIMenu(title: "Add to List", image: UIImage(systemName: "list.bullet"), children: listActions)])
|
||||||
}))
|
}))
|
||||||
suppressSection.append(relationshipAction(fetchRelationship, accountID: accountID, mastodonController: mastodonController, builder: { [unowned self] in self.blockAction(for: $0, mastodonController: $1) }))
|
suppressSection.append(relationshipAction(fetchRelationship, accountID: accountID, mastodonController: mastodonController, builder: { [unowned self] in self.hideReblogsAction(for: $0, mastodonController: $1) }))
|
||||||
suppressSection.append(relationshipAction(fetchRelationship, accountID: accountID, mastodonController: mastodonController, builder: { [unowned self] in self.muteAction(for: $0, mastodonController: $1) }))
|
suppressSection.append(relationshipAction(fetchRelationship, accountID: accountID, mastodonController: mastodonController, builder: { [unowned self] in self.muteAction(for: $0, mastodonController: $1) }))
|
||||||
|
suppressSection.append(relationshipAction(fetchRelationship, accountID: accountID, mastodonController: mastodonController, builder: { [unowned self] in self.blockAction(for: $0, mastodonController: $1) }))
|
||||||
suppressSection.append(createAction(identifier: "report", title: "Report", systemImageName: "flag", handler: { [unowned self] _ in
|
suppressSection.append(createAction(identifier: "report", title: "Report", systemImageName: "flag", handler: { [unowned self] _ in
|
||||||
let view = ReportView(report: EditedReport(accountID: accountID), mastodonController: mastodonController)
|
let view = ReportView(report: EditedReport(accountID: accountID), mastodonController: mastodonController)
|
||||||
let host = UIHostingController(rootView: view)
|
let host = UIHostingController(rootView: view)
|
||||||
|
@ -538,6 +539,24 @@ extension MenuActionProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func hideReblogsAction(for relationship: RelationshipMO, mastodonController: MastodonController) -> UIMenuElement {
|
||||||
|
let title = relationship.showingReblogs ? "Hide Reblogs" : "Show Reblogs"
|
||||||
|
// todo: need alternate repeat icon to use here
|
||||||
|
return UIAction(title: title, image: nil) { [weak self] _ in
|
||||||
|
let req = Account.setShowReblogs(relationship.accountID, showReblogs: !relationship.showingReblogs)
|
||||||
|
mastodonController.run(req) { response in
|
||||||
|
switch response {
|
||||||
|
case .failure(let error):
|
||||||
|
self?.handleError(error, title: "Error \(relationship.showingReblogs ? "Hiding" : "Showing") Reblogs")
|
||||||
|
case .success(let relationship, _):
|
||||||
|
mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
|
||||||
|
self?.handleSuccess(title: relationship.showingReblogs ? "Reblogs Shown" : "Reblogs Hidden")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fetchRelationship(accountID: String, mastodonController: MastodonController) async -> RelationshipMO? {
|
private func fetchRelationship(accountID: String, mastodonController: MastodonController) async -> RelationshipMO? {
|
||||||
|
|
|
@ -127,6 +127,15 @@ class AttachmentView: GIFImageView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var badges: Badges = []
|
||||||
|
if attachment.description?.isEmpty == false {
|
||||||
|
badges.formUnion(.alt)
|
||||||
|
}
|
||||||
|
if attachment.kind == .gifv || attachment.url.pathExtension == "gif" {
|
||||||
|
badges.formUnion(.gif)
|
||||||
|
}
|
||||||
|
createBadgesView(badges)
|
||||||
|
|
||||||
switch attachment.kind {
|
switch attachment.kind {
|
||||||
case .image:
|
case .image:
|
||||||
loadImage()
|
loadImage()
|
||||||
|
@ -302,6 +311,64 @@ class AttachmentView: GIFImageView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func createBadgesView(_ badges: Badges) {
|
||||||
|
guard !badges.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let stack = UIStackView()
|
||||||
|
stack.axis = .horizontal
|
||||||
|
stack.spacing = 2
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
let font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 13, weight: .bold))
|
||||||
|
func makeBadgeView(text: String) {
|
||||||
|
let container = UIView()
|
||||||
|
container.backgroundColor = .secondarySystemBackground.resolvedColor(with: UITraitCollection(userInterfaceStyle: .dark))
|
||||||
|
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = font
|
||||||
|
label.adjustsFontForContentSizeCategory = true
|
||||||
|
label.textColor = .white
|
||||||
|
label.text = text
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
container.addSubview(label)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
label.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 2),
|
||||||
|
label.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -2),
|
||||||
|
label.topAnchor.constraint(equalTo: container.topAnchor, constant: 2),
|
||||||
|
label.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -2),
|
||||||
|
])
|
||||||
|
stack.addArrangedSubview(container)
|
||||||
|
}
|
||||||
|
|
||||||
|
if badges.contains(.gif) {
|
||||||
|
makeBadgeView(text: "GIF")
|
||||||
|
}
|
||||||
|
if badges.contains(.alt) {
|
||||||
|
makeBadgeView(text: "ALT")
|
||||||
|
}
|
||||||
|
|
||||||
|
let first = stack.arrangedSubviews.first!
|
||||||
|
first.layer.masksToBounds = true
|
||||||
|
first.layer.cornerRadius = 4
|
||||||
|
if stack.arrangedSubviews.count > 1 {
|
||||||
|
first.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
|
||||||
|
let last = stack.arrangedSubviews.last!
|
||||||
|
last.layer.masksToBounds = true
|
||||||
|
last.layer.cornerRadius = 4
|
||||||
|
last.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
|
||||||
|
}
|
||||||
|
|
||||||
|
addSubview(stack)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
stack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 4),
|
||||||
|
stack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Interaction
|
||||||
|
|
||||||
func showGallery() {
|
func showGallery() {
|
||||||
if let delegate = delegate,
|
if let delegate = delegate,
|
||||||
let gallery = delegate.attachmentViewGallery(startingAt: index) {
|
let gallery = delegate.attachmentViewGallery(startingAt: index) {
|
||||||
|
@ -328,6 +395,13 @@ fileprivate extension AttachmentView {
|
||||||
case gifData(URL, Data)
|
case gifData(URL, Data)
|
||||||
case cgImage(URL, CGImage)
|
case cgImage(URL, CGImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Badges: OptionSet {
|
||||||
|
static let gif = Badges(rawValue: 1 << 0)
|
||||||
|
static let alt = Badges(rawValue: 1 << 1)
|
||||||
|
|
||||||
|
let rawValue: Int
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AttachmentView: UIContextMenuInteractionDelegate {
|
extension AttachmentView: UIContextMenuInteractionDelegate {
|
||||||
|
|
|
@ -146,7 +146,7 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
|
||||||
do {
|
do {
|
||||||
_ = try await mastodonController.run(request)
|
_ = try await mastodonController.run(request)
|
||||||
|
|
||||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
||||||
self.actionButtonsStackView.isHidden = true
|
self.actionButtonsStackView.isHidden = true
|
||||||
self.addLabel(NSLocalizedString("Rejected", comment: "rejected follow request label"))
|
self.addLabel(NSLocalizedString("Rejected", comment: "rejected follow request label"))
|
||||||
} catch let error as Client.Error {
|
} catch let error as Client.Error {
|
||||||
|
@ -172,7 +172,7 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
|
||||||
do {
|
do {
|
||||||
_ = try await mastodonController.run(request)
|
_ = try await mastodonController.run(request)
|
||||||
|
|
||||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
||||||
self.actionButtonsStackView.isHidden = true
|
self.actionButtonsStackView.isHidden = true
|
||||||
self.addLabel(NSLocalizedString("Accepted", comment: "accepted follow request label"))
|
self.addLabel(NSLocalizedString("Accepted", comment: "accepted follow request label"))
|
||||||
} catch let error as Client.Error {
|
} catch let error as Client.Error {
|
||||||
|
|
|
@ -26,7 +26,7 @@ class PollOptionsView: UIControl {
|
||||||
private let animationDuration: TimeInterval = 0.1
|
private let animationDuration: TimeInterval = 0.1
|
||||||
private let scaledTransform = CGAffineTransform(scaleX: 0.95, y: 0.95)
|
private let scaledTransform = CGAffineTransform(scaleX: 0.95, y: 0.95)
|
||||||
|
|
||||||
private let generator = UIImpactFeedbackGenerator(style: .soft)
|
private let generator = UISelectionFeedbackGenerator()
|
||||||
|
|
||||||
override var isEnabled: Bool {
|
override var isEnabled: Bool {
|
||||||
didSet {
|
didSet {
|
||||||
|
@ -110,7 +110,7 @@ class PollOptionsView: UIControl {
|
||||||
}
|
}
|
||||||
animator.startAnimation()
|
animator.startAnimation()
|
||||||
|
|
||||||
generator.impactOccurred()
|
generator.selectionChanged()
|
||||||
generator.prepare()
|
generator.prepare()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
@ -139,7 +139,7 @@ class PollOptionsView: UIControl {
|
||||||
}
|
}
|
||||||
|
|
||||||
if newIndex != nil {
|
if newIndex != nil {
|
||||||
generator.impactOccurred()
|
generator.selectionChanged()
|
||||||
generator.prepare()
|
generator.prepare()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -148,7 +148,7 @@ class StatusPollView: UIView {
|
||||||
voteButton.isEnabled = false
|
voteButton.isEnabled = false
|
||||||
voteButton.disabledTitle = "Voted"
|
voteButton.disabledTitle = "Voted"
|
||||||
|
|
||||||
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
||||||
|
|
||||||
let request = Poll.vote(poll!.id, choices: optionsView.checkedOptionIndices)
|
let request = Poll.vote(poll!.id, choices: optionsView.checkedOptionIndices)
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
|
|
|
@ -98,8 +98,8 @@ class StatusCardView: UIView {
|
||||||
hStack.spacing = 4
|
hStack.spacing = 4
|
||||||
hStack.clipsToBounds = true
|
hStack.clipsToBounds = true
|
||||||
hStack.layer.borderWidth = 0.5
|
hStack.layer.borderWidth = 0.5
|
||||||
hStack.layer.borderColor = UIColor.lightGray.cgColor
|
|
||||||
hStack.backgroundColor = inactiveBackgroundColor
|
hStack.backgroundColor = inactiveBackgroundColor
|
||||||
|
updateBorderColor()
|
||||||
|
|
||||||
addSubview(hStack)
|
addSubview(hStack)
|
||||||
|
|
||||||
|
@ -132,11 +132,24 @@ class StatusCardView: UIView {
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
updateBorderColor()
|
||||||
|
}
|
||||||
|
|
||||||
override func layoutSubviews() {
|
override func layoutSubviews() {
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
hStack.layer.cornerRadius = 0.1 * bounds.height
|
hStack.layer.cornerRadius = 0.1 * bounds.height
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func updateBorderColor() {
|
||||||
|
if traitCollection.userInterfaceStyle == .dark {
|
||||||
|
hStack.layer.borderColor = UIColor.darkGray.cgColor
|
||||||
|
} else {
|
||||||
|
hStack.layer.borderColor = UIColor.lightGray.cgColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func updateUI(status: StatusMO) {
|
func updateUI(status: StatusMO) {
|
||||||
guard status.id != statusID else {
|
guard status.id != statusID else {
|
||||||
return
|
return
|
||||||
|
|
|
@ -83,17 +83,21 @@ extension StatusCollectionViewCell {
|
||||||
accountID = status.account.id
|
accountID = status.account.id
|
||||||
|
|
||||||
updateAccountUI(account: status.account)
|
updateAccountUI(account: status.account)
|
||||||
updateUIForPreferences(status: status)
|
|
||||||
|
|
||||||
contentContainer.contentTextView.setTextFrom(status: status, precomputed: precomputedContent)
|
contentContainer.contentTextView.setTextFrom(status: status, precomputed: precomputedContent)
|
||||||
contentContainer.contentTextView.navigationDelegate = delegate
|
contentContainer.contentTextView.navigationDelegate = delegate
|
||||||
contentContainer.attachmentsView.delegate = self
|
contentContainer.attachmentsView.delegate = self
|
||||||
contentContainer.attachmentsView.updateUI(status: status)
|
contentContainer.attachmentsView.updateUI(status: status)
|
||||||
|
if Preferences.shared.showLinkPreviews {
|
||||||
contentContainer.cardView.updateUI(status: status)
|
contentContainer.cardView.updateUI(status: status)
|
||||||
contentContainer.cardView.isHidden = status.card == nil
|
contentContainer.cardView.isHidden = status.card == nil
|
||||||
contentContainer.cardView.navigationDelegate = delegate
|
contentContainer.cardView.navigationDelegate = delegate
|
||||||
contentContainer.cardView.actionProvider = delegate
|
contentContainer.cardView.actionProvider = delegate
|
||||||
|
} else {
|
||||||
|
contentContainer.cardView.isHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUIForPreferences(status: status)
|
||||||
updateStatusState(status: status)
|
updateStatusState(status: status)
|
||||||
|
|
||||||
contentWarningLabel.text = status.spoilerText
|
contentWarningLabel.text = status.spoilerText
|
||||||
|
@ -150,6 +154,12 @@ extension StatusCollectionViewCell {
|
||||||
|
|
||||||
func baseUpdateUIForPreferences(status: StatusMO) {
|
func baseUpdateUIForPreferences(status: StatusMO) {
|
||||||
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * Self.avatarImageViewSize
|
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * Self.avatarImageViewSize
|
||||||
|
|
||||||
|
let newCardHidden = !Preferences.shared.showLinkPreviews || status.card == nil
|
||||||
|
if contentContainer.cardView.isHidden != newCardHidden {
|
||||||
|
delegate?.statusCellNeedsReconfigure(self, animated: false, completion: nil)
|
||||||
|
}
|
||||||
|
|
||||||
switch Preferences.shared.attachmentBlurMode {
|
switch Preferences.shared.attachmentBlurMode {
|
||||||
case .never:
|
case .never:
|
||||||
contentContainer.attachmentsView.contentHidden = false
|
contentContainer.attachmentsView.contentHidden = false
|
||||||
|
|
Loading…
Reference in New Issue