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
|
||||
|
||||
## 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)
|
||||
Features/Improvements:
|
||||
- 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 {
|
||||
if animated,
|
||||
case .ducked(_, placeholder: let placeholder) = state {
|
||||
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
|
||||
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
||||
let origConstant = placeholder.topConstraint.constant
|
||||
UIView.animateKeyframes(withDuration: 0.4, delay: 0) {
|
||||
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")
|
||||
}
|
||||
|
||||
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> {
|
||||
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 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> {
|
||||
return Request<ConversationContext>(method: .get, path: "/api/v1/statuses/\(statusID)/context")
|
||||
}
|
||||
|
|
|
@ -2372,7 +2372,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 69;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
|
@ -2380,7 +2380,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2023.3;
|
||||
MARKETING_VERSION = 2023.4;
|
||||
OTHER_CODE_SIGN_FLAGS = "";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@ -2437,7 +2437,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 69;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||
|
@ -2446,7 +2446,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2023.3;
|
||||
MARKETING_VERSION = 2023.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -2588,7 +2588,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 69;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
|
@ -2596,7 +2596,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2023.3;
|
||||
MARKETING_VERSION = 2023.4;
|
||||
OTHER_CODE_SIGN_FLAGS = "";
|
||||
OTHER_LDFLAGS = "";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
||||
|
@ -2616,7 +2616,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 69;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
|
@ -2624,7 +2624,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2023.3;
|
||||
MARKETING_VERSION = 2023.4;
|
||||
OTHER_CODE_SIGN_FLAGS = "";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@ -2721,7 +2721,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 69;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||
|
@ -2730,7 +2730,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2023.3;
|
||||
MARKETING_VERSION = 2023.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -2747,7 +2747,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 69;
|
||||
CURRENT_PROJECT_VERSION = 70;
|
||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||
|
@ -2756,7 +2756,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2023.3;
|
||||
MARKETING_VERSION = 2023.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
|
|
@ -55,7 +55,21 @@ struct HTMLConverter {
|
|||
case let node as Element:
|
||||
let attributed = NSMutableAttributedString(string: "", attributes: [.font: font, .foregroundColor: color])
|
||||
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"))
|
||||
|
||||
if appendEllipsis {
|
||||
attributed.append(NSAttributedString("…"))
|
||||
}
|
||||
}
|
||||
|
||||
switch node.tagName() {
|
||||
|
|
|
@ -44,6 +44,7 @@ class Preferences: Codable, ObservableObject {
|
|||
self.showIsStatusReplyIcon = try container.decode(Bool.self, forKey: .showIsStatusReplyIcon)
|
||||
self.alwaysShowStatusVisibilityIcon = try container.decode(Bool.self, forKey: .alwaysShowStatusVisibilityIcon)
|
||||
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.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(alwaysShowStatusVisibilityIcon, forKey: .alwaysShowStatusVisibilityIcon)
|
||||
try container.encode(hideActionsInTimeline, forKey: .hideActionsInTimeline)
|
||||
try container.encode(showLinkPreviews, forKey: .showLinkPreviews)
|
||||
try container.encode(leadingStatusSwipeActions, forKey: .leadingStatusSwipeActions)
|
||||
try container.encode(trailingStatusSwipeActions, forKey: .trailingStatusSwipeActions)
|
||||
|
||||
|
@ -144,6 +146,7 @@ class Preferences: Codable, ObservableObject {
|
|||
@Published var showIsStatusReplyIcon = false
|
||||
@Published var alwaysShowStatusVisibilityIcon = false
|
||||
@Published var hideActionsInTimeline = false
|
||||
@Published var showLinkPreviews = true
|
||||
@Published var leadingStatusSwipeActions: [StatusSwipeAction] = [.favorite, .reblog]
|
||||
@Published var trailingStatusSwipeActions: [StatusSwipeAction] = [.reply, .share]
|
||||
|
||||
|
@ -205,6 +208,7 @@ class Preferences: Codable, ObservableObject {
|
|||
case showIsStatusReplyIcon
|
||||
case alwaysShowStatusVisibilityIcon
|
||||
case hideActionsInTimeline
|
||||
case showLinkPreviews
|
||||
case leadingStatusSwipeActions
|
||||
case trailingStatusSwipeActions
|
||||
|
||||
|
|
|
@ -54,6 +54,7 @@ struct ComposeToolbar: View {
|
|||
.font(.system(size: imageSize))
|
||||
.padding(5)
|
||||
.hoverEffect()
|
||||
.transition(.opacity.animation(.linear(duration: 0.2)))
|
||||
}
|
||||
|
||||
if let currentInput = uiState.currentInput,
|
||||
|
@ -74,16 +75,11 @@ struct ComposeToolbar: View {
|
|||
.accessibilityLabel(format.accessibilityLabel)
|
||||
.padding(5)
|
||||
.hoverEffect()
|
||||
.transition(.opacity.animation(.linear(duration: 0.2)))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: self.draftsButtonPressed) {
|
||||
Text("Drafts")
|
||||
}
|
||||
.padding(5)
|
||||
.hoverEffect()
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.frame(minWidth: minWidth)
|
||||
|
@ -119,10 +115,6 @@ struct ComposeToolbar: View {
|
|||
uiState.currentInput?.beginAutocompletingEmoji()
|
||||
}
|
||||
|
||||
private func draftsButtonPressed() {
|
||||
uiState.isShowingDraftsList = true
|
||||
}
|
||||
|
||||
private func formatAction(_ format: StatusFormat) -> () -> Void {
|
||||
{
|
||||
uiState.currentInput?.applyFormat(format)
|
||||
|
|
|
@ -239,16 +239,25 @@ struct ComposeView: View {
|
|||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var postButton: some View {
|
||||
Button {
|
||||
Task {
|
||||
await self.postStatus()
|
||||
if draft.hasContent {
|
||||
Button {
|
||||
Task {
|
||||
await self.postStatus()
|
||||
}
|
||||
} label: {
|
||||
Text("Post")
|
||||
}
|
||||
.keyboardShortcut(.return, modifiers: .command)
|
||||
.disabled(!postButtonEnabled)
|
||||
} else {
|
||||
Button {
|
||||
uiState.isShowingDraftsList = true
|
||||
} label: {
|
||||
Text("Drafts")
|
||||
}
|
||||
} label: {
|
||||
Text("Post")
|
||||
}
|
||||
.keyboardShortcut(.return, modifiers: .command)
|
||||
.disabled(!postButtonEnabled)
|
||||
}
|
||||
|
||||
private func cancel() {
|
||||
|
|
|
@ -98,6 +98,7 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
if context.coordinator.skipSettingTextOnNextUpdate {
|
||||
context.coordinator.skipSettingTextOnNextUpdate = false
|
||||
} else {
|
||||
context.coordinator.skipNextAutocompleteUpdate = true
|
||||
uiView.text = text
|
||||
}
|
||||
|
||||
|
@ -185,6 +186,7 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
var caretScrollPositionAnimator: UIViewPropertyAnimator?
|
||||
|
||||
var skipSettingTextOnNextUpdate = false
|
||||
var skipNextAutocompleteUpdate = false
|
||||
|
||||
var toolbarElements: [ComposeUIState.ToolbarElement] {
|
||||
[.emojiPicker, .formattingButtons]
|
||||
|
@ -324,6 +326,10 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
}
|
||||
|
||||
private func updateAutocompleteState() {
|
||||
guard !skipNextAutocompleteUpdate else {
|
||||
skipNextAutocompleteUpdate = false
|
||||
return
|
||||
}
|
||||
guard let textView = textView,
|
||||
let text = textView.text,
|
||||
let (lastWordStartIndex, foundFirstAtSign) = findAutocompleteLastWord() else {
|
||||
|
|
|
@ -189,11 +189,11 @@ class ConversationViewController: UIViewController {
|
|||
indicator.startAnimating()
|
||||
state = .loading(indicator)
|
||||
|
||||
let url = WebURL(url)!
|
||||
let request = Client.search(query: url.serialized(), types: [.statuses], resolve: true)
|
||||
let url = WebURL(url)!.serialized(excludingFragment: true)
|
||||
let request = Client.search(query: url, types: [.statuses], resolve: true)
|
||||
do {
|
||||
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()
|
||||
}
|
||||
_ = mastodonController.persistentContainer.addOrUpdateOnViewContext(status: status)
|
||||
|
|
|
@ -24,7 +24,7 @@ class FastAccountSwitcherViewController: UIViewController {
|
|||
|
||||
private(set) var accountViews: [FastSwitchingAccountView] = []
|
||||
private var lastSelectedAccountViewIndex: Int?
|
||||
private var selectionChangedFeedbackGenerator: UIImpactFeedbackGenerator?
|
||||
private var selectionChangedFeedbackGenerator: UISelectionFeedbackGenerator?
|
||||
private var touchBeganFeedbackWorkItem: DispatchWorkItem?
|
||||
|
||||
var itemOrientation: ItemOrientation = .iconsTrailing
|
||||
|
@ -148,7 +148,7 @@ class FastAccountSwitcherViewController: UIViewController {
|
|||
private func switchAccount(newIndex: Int, hapticFeedback: Bool = true) {
|
||||
if newIndex == 0 { // add account placeholder
|
||||
if hapticFeedback {
|
||||
selectionChangedFeedbackGenerator?.impactOccurred()
|
||||
selectionChangedFeedbackGenerator?.selectionChanged()
|
||||
}
|
||||
selectionChangedFeedbackGenerator = nil
|
||||
|
||||
|
@ -160,7 +160,7 @@ class FastAccountSwitcherViewController: UIViewController {
|
|||
|
||||
if account.id != LocalData.shared.mostRecentAccountID {
|
||||
if hapticFeedback {
|
||||
selectionChangedFeedbackGenerator?.impactOccurred()
|
||||
selectionChangedFeedbackGenerator?.selectionChanged()
|
||||
}
|
||||
selectionChangedFeedbackGenerator = nil
|
||||
|
||||
|
@ -178,8 +178,8 @@ class FastAccountSwitcherViewController: UIViewController {
|
|||
@objc private func handleLongPress(_ recognizer: UIGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
selectionChangedFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
|
||||
selectionChangedFeedbackGenerator?.impactOccurred()
|
||||
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
|
||||
selectionChangedFeedbackGenerator = UISelectionFeedbackGenerator()
|
||||
selectionChangedFeedbackGenerator?.prepare()
|
||||
|
||||
show()
|
||||
|
@ -231,7 +231,7 @@ class FastAccountSwitcherViewController: UIViewController {
|
|||
lastSelectedAccountViewIndex = selectedAccountViewIndex
|
||||
|
||||
if hapticFeedback {
|
||||
selectionChangedFeedbackGenerator?.impactOccurred(intensity: 0.5)
|
||||
selectionChangedFeedbackGenerator?.selectionChanged()
|
||||
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 tap ends very quickly, this will be cancelled
|
||||
touchBeganFeedbackWorkItem = DispatchWorkItem {
|
||||
self.selectionChangedFeedbackGenerator?.impactOccurred(intensity: 0.5)
|
||||
self.selectionChangedFeedbackGenerator?.selectionChanged()
|
||||
self.selectionChangedFeedbackGenerator?.prepare()
|
||||
self.touchBeganFeedbackWorkItem = nil
|
||||
}
|
||||
|
|
|
@ -84,6 +84,9 @@ struct AppearancePrefsView : View {
|
|||
Toggle(isOn: $preferences.hideActionsInTimeline) {
|
||||
Text("Hide Actions on Timeline")
|
||||
}
|
||||
Toggle(isOn: $preferences.showLinkPreviews) {
|
||||
Text("Show Link Previews")
|
||||
}
|
||||
NavigationLink("Leading Swipe Actions") {
|
||||
SwipeActionsPrefsView(selection: $preferences.leadingStatusSwipeActions)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
|
|
|
@ -97,8 +97,9 @@ extension MenuActionProvider {
|
|||
}))
|
||||
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.blockAction(for: $0, mastodonController: $1) }))
|
||||
suppressSection.append(createAction(identifier: "report", title: "Report", systemImageName: "flag", handler: { [unowned self] _ in
|
||||
let view = ReportView(report: EditedReport(accountID: accountID), mastodonController: mastodonController)
|
||||
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? {
|
||||
|
|
|
@ -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 {
|
||||
case .image:
|
||||
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() {
|
||||
if let delegate = delegate,
|
||||
let gallery = delegate.attachmentViewGallery(startingAt: index) {
|
||||
|
@ -328,6 +395,13 @@ fileprivate extension AttachmentView {
|
|||
case gifData(URL, Data)
|
||||
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 {
|
||||
|
|
|
@ -146,7 +146,7 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
|
|||
do {
|
||||
_ = try await mastodonController.run(request)
|
||||
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
||||
self.actionButtonsStackView.isHidden = true
|
||||
self.addLabel(NSLocalizedString("Rejected", comment: "rejected follow request label"))
|
||||
} catch let error as Client.Error {
|
||||
|
@ -172,7 +172,7 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
|
|||
do {
|
||||
_ = try await mastodonController.run(request)
|
||||
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
||||
self.actionButtonsStackView.isHidden = true
|
||||
self.addLabel(NSLocalizedString("Accepted", comment: "accepted follow request label"))
|
||||
} catch let error as Client.Error {
|
||||
|
|
|
@ -26,7 +26,7 @@ class PollOptionsView: UIControl {
|
|||
private let animationDuration: TimeInterval = 0.1
|
||||
private let scaledTransform = CGAffineTransform(scaleX: 0.95, y: 0.95)
|
||||
|
||||
private let generator = UIImpactFeedbackGenerator(style: .soft)
|
||||
private let generator = UISelectionFeedbackGenerator()
|
||||
|
||||
override var isEnabled: Bool {
|
||||
didSet {
|
||||
|
@ -110,7 +110,7 @@ class PollOptionsView: UIControl {
|
|||
}
|
||||
animator.startAnimation()
|
||||
|
||||
generator.impactOccurred()
|
||||
generator.selectionChanged()
|
||||
generator.prepare()
|
||||
|
||||
return true
|
||||
|
@ -139,7 +139,7 @@ class PollOptionsView: UIControl {
|
|||
}
|
||||
|
||||
if newIndex != nil {
|
||||
generator.impactOccurred()
|
||||
generator.selectionChanged()
|
||||
generator.prepare()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -148,7 +148,7 @@ class StatusPollView: UIView {
|
|||
voteButton.isEnabled = false
|
||||
voteButton.disabledTitle = "Voted"
|
||||
|
||||
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
||||
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
||||
|
||||
let request = Poll.vote(poll!.id, choices: optionsView.checkedOptionIndices)
|
||||
mastodonController.run(request) { (response) in
|
||||
|
|
|
@ -98,8 +98,8 @@ class StatusCardView: UIView {
|
|||
hStack.spacing = 4
|
||||
hStack.clipsToBounds = true
|
||||
hStack.layer.borderWidth = 0.5
|
||||
hStack.layer.borderColor = UIColor.lightGray.cgColor
|
||||
hStack.backgroundColor = inactiveBackgroundColor
|
||||
updateBorderColor()
|
||||
|
||||
addSubview(hStack)
|
||||
|
||||
|
@ -132,11 +132,24 @@ class StatusCardView: UIView {
|
|||
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
updateBorderColor()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
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) {
|
||||
guard status.id != statusID else {
|
||||
return
|
||||
|
|
|
@ -83,17 +83,21 @@ extension StatusCollectionViewCell {
|
|||
accountID = status.account.id
|
||||
|
||||
updateAccountUI(account: status.account)
|
||||
updateUIForPreferences(status: status)
|
||||
|
||||
contentContainer.contentTextView.setTextFrom(status: status, precomputed: precomputedContent)
|
||||
contentContainer.contentTextView.navigationDelegate = delegate
|
||||
contentContainer.attachmentsView.delegate = self
|
||||
contentContainer.attachmentsView.updateUI(status: status)
|
||||
contentContainer.cardView.updateUI(status: status)
|
||||
contentContainer.cardView.isHidden = status.card == nil
|
||||
contentContainer.cardView.navigationDelegate = delegate
|
||||
contentContainer.cardView.actionProvider = delegate
|
||||
if Preferences.shared.showLinkPreviews {
|
||||
contentContainer.cardView.updateUI(status: status)
|
||||
contentContainer.cardView.isHidden = status.card == nil
|
||||
contentContainer.cardView.navigationDelegate = delegate
|
||||
contentContainer.cardView.actionProvider = delegate
|
||||
} else {
|
||||
contentContainer.cardView.isHidden = true
|
||||
}
|
||||
|
||||
updateUIForPreferences(status: status)
|
||||
updateStatusState(status: status)
|
||||
|
||||
contentWarningLabel.text = status.spoilerText
|
||||
|
@ -150,6 +154,12 @@ extension StatusCollectionViewCell {
|
|||
|
||||
func baseUpdateUIForPreferences(status: StatusMO) {
|
||||
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 {
|
||||
case .never:
|
||||
contentContainer.attachmentsView.contentHidden = false
|
||||
|
|
Loading…
Reference in New Issue