Compare commits

..

10 Commits

27 changed files with 635 additions and 37 deletions

View File

@ -1,5 +1,18 @@
# Changelog # Changelog
## 2023.1 (61)
Features/Improvements:
- Add report UI
- Add accent color preference
- Start playing videos immediately when gallery opens
Bugfixes:
- Fix crash when trying to load deleted status for state restoration/sync
- Fix crash when trying to restore state for non-pinned timeline
- Fix crash due to relationships being cached longer than their corresponding accounts
- Fix crash if preferences change when there are cells that haven't yet been displayed
- Fix crash when displaying poll finished notifications
## 2023.1 (60) ## 2023.1 (60)
Features/Improvements: Features/Improvements:
- Allow sharing gifv attachments - Allow sharing gifv attachments

View File

@ -323,11 +323,20 @@ public class Client {
return Request<[Report]>(method: .get, path: "/api/v1/reports") return Request<[Report]>(method: .get, path: "/api/v1/reports")
} }
public static func report(account: Account, statuses: [Status], comment: String) -> Request<Report> { public static func report(
account: String,
statuses: [String],
comment: String,
forward: Bool,
category: String,
ruleIDs: [String]
) -> Request<Report> {
return Request<Report>(method: .post, path: "/api/v1/reports", body: ParametersBody([ return Request<Report>(method: .post, path: "/api/v1/reports", body: ParametersBody([
"account_id" => account.id, "account_id" => account,
"comment" => comment "comment" => comment,
] + "status_ids" => statuses.map { $0.id })) "forward" => forward,
"category" => category,
] + "status_ids" => statuses + "rule_ids" => ruleIDs))
} }
// MARK: - Search // MARK: - Search

View File

@ -94,11 +94,12 @@ public final class Account: AccountProtocol, Decodable {
return request return request
} }
public static func getStatuses(_ accountID: String, range: RequestRange = .default, onlyMedia: Bool? = nil, pinned: Bool? = nil, excludeReplies: Bool? = nil) -> Request<[Status]> { public static func getStatuses(_ accountID: String, range: RequestRange = .default, onlyMedia: Bool? = nil, pinned: Bool? = nil, excludeReplies: Bool? = nil, excludeReblogs: Bool? = nil) -> Request<[Status]> {
var request = Request<[Status]>(method: .get, path: "/api/v1/accounts/\(accountID)/statuses", queryParameters: [ var request = Request<[Status]>(method: .get, path: "/api/v1/accounts/\(accountID)/statuses", queryParameters: [
"only_media" => onlyMedia, "only_media" => onlyMedia,
"pinned" => pinned, "pinned" => pinned,
"exclude_replies" => excludeReplies "exclude_replies" => excludeReplies,
"exclude_reblogs" => excludeReblogs,
]) ])
request.range = range request.range = range
return request return request

View File

@ -20,6 +20,7 @@ public class Instance: Decodable {
public let languages: [String]? public let languages: [String]?
public let stats: Stats? public let stats: Stats?
public let configuration: Configuration? public let configuration: Configuration?
public let rules: [Rule]?
// pleroma doesn't currently implement these // pleroma doesn't currently implement these
public let contactAccount: Account? public let contactAccount: Account?
@ -57,6 +58,7 @@ public class Instance: Decodable {
self.thumbnail = try? container.decodeIfPresent(URL.self, forKey: .thumbnail) self.thumbnail = try? container.decodeIfPresent(URL.self, forKey: .thumbnail)
self.configuration = try? container.decodeIfPresent(Configuration.self, forKey: .configuration) self.configuration = try? container.decodeIfPresent(Configuration.self, forKey: .configuration)
self.rules = try? container.decodeIfPresent([Rule].self, forKey: .rules)
if let maxTootCharacters = try? container.decodeIfPresent(Int.self, forKey: .maxTootCharacters) { if let maxTootCharacters = try? container.decodeIfPresent(Int.self, forKey: .maxTootCharacters) {
self.maxTootCharacters = maxTootCharacters self.maxTootCharacters = maxTootCharacters
@ -83,6 +85,7 @@ public class Instance: Decodable {
case stats case stats
case configuration case configuration
case contactAccount = "contact_account" case contactAccount = "contact_account"
case rules
case maxTootCharacters = "max_toot_chars" case maxTootCharacters = "max_toot_chars"
case pollLimits = "poll_limits" case pollLimits = "poll_limits"
@ -167,3 +170,10 @@ extension Instance {
} }
} }
} }
extension Instance {
public struct Rule: Decodable, Identifiable {
public let id: String
public let text: String
}
}

View File

@ -145,6 +145,11 @@
D6552367289870790048A653 /* ScreenCorners in Frameworks */ = {isa = PBXBuildFile; productRef = D6552366289870790048A653 /* ScreenCorners */; }; D6552367289870790048A653 /* ScreenCorners in Frameworks */ = {isa = PBXBuildFile; productRef = D6552366289870790048A653 /* ScreenCorners */; };
D659F35E2953A212002D944A /* TTTKit in Frameworks */ = {isa = PBXBuildFile; productRef = D659F35D2953A212002D944A /* TTTKit */; }; D659F35E2953A212002D944A /* TTTKit in Frameworks */ = {isa = PBXBuildFile; productRef = D659F35D2953A212002D944A /* TTTKit */; };
D659F36229541065002D944A /* TTTView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D659F36129541065002D944A /* TTTView.swift */; }; D659F36229541065002D944A /* TTTView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D659F36129541065002D944A /* TTTView.swift */; };
D65B4B542971F71D00DABDFB /* EditedReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B532971F71D00DABDFB /* EditedReport.swift */; };
D65B4B562971F98300DABDFB /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B552971F98300DABDFB /* ReportView.swift */; };
D65B4B58297203A700DABDFB /* ReportSelectRulesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B57297203A700DABDFB /* ReportSelectRulesView.swift */; };
D65B4B5A29720AB000DABDFB /* ReportStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B5929720AB000DABDFB /* ReportStatusView.swift */; };
D65B4B5E2973040D00DABDFB /* ReportAddStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B5D2973040D00DABDFB /* ReportAddStatusView.swift */; };
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */; }; D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */; };
D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */; }; D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */; };
D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */; }; D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */; };
@ -527,6 +532,11 @@
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = "<group>"; }; D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = "<group>"; };
D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffableTimelineLikeTableViewController.swift; sourceTree = "<group>"; }; D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffableTimelineLikeTableViewController.swift; sourceTree = "<group>"; };
D659F36129541065002D944A /* TTTView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TTTView.swift; sourceTree = "<group>"; }; D659F36129541065002D944A /* TTTView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TTTView.swift; sourceTree = "<group>"; };
D65B4B532971F71D00DABDFB /* EditedReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditedReport.swift; sourceTree = "<group>"; };
D65B4B552971F98300DABDFB /* ReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = "<group>"; };
D65B4B57297203A700DABDFB /* ReportSelectRulesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportSelectRulesView.swift; sourceTree = "<group>"; };
D65B4B5929720AB000DABDFB /* ReportStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusView.swift; sourceTree = "<group>"; };
D65B4B5D2973040D00DABDFB /* ReportAddStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportAddStatusView.swift; sourceTree = "<group>"; };
D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundableViewController.swift; sourceTree = "<group>"; }; D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundableViewController.swift; sourceTree = "<group>"; };
D65F612D23AE990C00F3CFD3 /* Embassy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Embassy.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D65F612D23AE990C00F3CFD3 /* Embassy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Embassy.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D65F613023AE99E000F3CFD3 /* Ambassador.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Ambassador.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D65F613023AE99E000F3CFD3 /* Ambassador.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Ambassador.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -810,6 +820,7 @@
D677284D24ECC01D00C732D3 /* Draft.swift */, D677284D24ECC01D00C732D3 /* Draft.swift */,
D627FF75217E923E00CC0648 /* DraftsManager.swift */, D627FF75217E923E00CC0648 /* DraftsManager.swift */,
D61F75AE293AF50C00C0B37F /* EditedFilter.swift */, D61F75AE293AF50C00C0B37F /* EditedFilter.swift */,
D65B4B532971F71D00DABDFB /* EditedReport.swift */,
); );
path = Models; path = Models;
sourceTree = "<group>"; sourceTree = "<group>";
@ -963,6 +974,7 @@
D641C783213DD7FE004B4513 /* Onboarding */, D641C783213DD7FE004B4513 /* Onboarding */,
D641C789213DD87E004B4513 /* Preferences */, D641C789213DD87E004B4513 /* Preferences */,
D641C784213DD819004B4513 /* Profile */, D641C784213DD819004B4513 /* Profile */,
D65B4B522971F6E300DABDFB /* Report */,
D6BC9DD8232D8BCA002CA326 /* Search */, D6BC9DD8232D8BCA002CA326 /* Search */,
D6A3BC8C2321FF9B00FD64D5 /* Status Action Account List */, D6A3BC8C2321FF9B00FD64D5 /* Status Action Account List */,
D641C781213DD7DD004B4513 /* Timeline */, D641C781213DD7DD004B4513 /* Timeline */,
@ -1173,6 +1185,17 @@
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D65B4B522971F6E300DABDFB /* Report */ = {
isa = PBXGroup;
children = (
D65B4B552971F98300DABDFB /* ReportView.swift */,
D65B4B57297203A700DABDFB /* ReportSelectRulesView.swift */,
D65B4B5929720AB000DABDFB /* ReportStatusView.swift */,
D65B4B5D2973040D00DABDFB /* ReportAddStatusView.swift */,
);
path = Report;
sourceTree = "<group>";
};
D663626021360A9600C9CBA2 /* Preferences */ = { D663626021360A9600C9CBA2 /* Preferences */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1975,6 +1998,7 @@
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */, D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */,
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */, D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
D6E9CDA8281A427800BBC98E /* PostService.swift in Sources */, D6E9CDA8281A427800BBC98E /* PostService.swift in Sources */,
D65B4B5E2973040D00DABDFB /* ReportAddStatusView.swift in Sources */,
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */, D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */, D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */,
D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */, D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */,
@ -2020,6 +2044,7 @@
D6BEA24B291C6A2B002F4D01 /* AlertWithData.swift in Sources */, D6BEA24B291C6A2B002F4D01 /* AlertWithData.swift in Sources */,
D61ABEFE28F1C92600B29151 /* FavoriteService.swift in Sources */, D61ABEFE28F1C92600B29151 /* FavoriteService.swift in Sources */,
D61F75AB293AF11400C0B37F /* FilterKeywordMO.swift in Sources */, D61F75AB293AF11400C0B37F /* FilterKeywordMO.swift in Sources */,
D65B4B5A29720AB000DABDFB /* ReportStatusView.swift in Sources */,
D663626221360B1900C9CBA2 /* Preferences.swift in Sources */, D663626221360B1900C9CBA2 /* Preferences.swift in Sources */,
D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */, D626493823C0FD0000612E6E /* AllPhotosTableViewCell.swift in Sources */,
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */, D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
@ -2065,7 +2090,9 @@
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */, D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
D6E77D0B286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift in Sources */, D6E77D0B286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift in Sources */,
D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */, D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */,
D65B4B58297203A700DABDFB /* ReportSelectRulesView.swift in Sources */,
D6B81F442560390300F6E31D /* MenuController.swift in Sources */, D6B81F442560390300F6E31D /* MenuController.swift in Sources */,
D65B4B542971F71D00DABDFB /* EditedReport.swift in Sources */,
D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */, D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */,
D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */, D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */,
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */, D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
@ -2127,6 +2154,7 @@
D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */, D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */,
D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */, D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */,
D6A3A380295515550036B6EF /* ProfileHeaderButton.swift in Sources */, D6A3A380295515550036B6EF /* ProfileHeaderButton.swift in Sources */,
D65B4B562971F98300DABDFB /* ReportView.swift in Sources */,
D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */, D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */,
D68A76E829527884001DA1B3 /* PinnedTimelinesView.swift in Sources */, D68A76E829527884001DA1B3 /* PinnedTimelinesView.swift in Sources */,
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */, D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */,
@ -2286,7 +2314,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 = 60; CURRENT_PROJECT_VERSION = 61;
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;
@ -2354,7 +2382,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 = 60; CURRENT_PROJECT_VERSION = 61;
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;
@ -2505,7 +2533,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 = 60; CURRENT_PROJECT_VERSION = 61;
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;
@ -2534,7 +2562,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 = 60; CURRENT_PROJECT_VERSION = 61;
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;
@ -2644,7 +2672,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 = 60; CURRENT_PROJECT_VERSION = 61;
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;
@ -2670,7 +2698,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 = 60; CURRENT_PROJECT_VERSION = 61;
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;

View File

@ -321,7 +321,9 @@ class MastodonController: ObservableObject {
let request = Client.getCustomEmoji() let request = Client.getCustomEmoji()
run(request) { (response) in run(request) { (response) in
if case let .success(emojis, _) = response { if case let .success(emojis, _) = response {
DispatchQueue.main.async {
self.customEmojis = emojis self.customEmojis = emojis
}
completion(emojis) completion(emojis)
} else { } else {
completion([]) completion([])

View File

@ -20,7 +20,7 @@
<attribute name="url" attributeType="URI"/> <attribute name="url" attributeType="URI"/>
<attribute name="username" attributeType="String"/> <attribute name="username" attributeType="String"/>
<relationship name="movedTo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account"/> <relationship name="movedTo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account"/>
<relationship name="relationship" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Relationship" inverseName="account" inverseEntity="Relationship"/> <relationship name="relationship" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="Relationship" inverseName="account" inverseEntity="Relationship"/>
<relationship name="statuses" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="Status" inverseName="account" inverseEntity="Status"/> <relationship name="statuses" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="Status" inverseName="account" inverseEntity="Status"/>
<uniquenessConstraints> <uniquenessConstraints>
<uniquenessConstraint> <uniquenessConstraint>

View File

@ -0,0 +1,43 @@
//
// EditedReport.swift
// Tusker
//
// Created by Shadowfacts on 1/13/23.
// Copyright © 2023 Shadowfacts. All rights reserved.
//
import Foundation
import Pachyderm
class EditedReport: ObservableObject {
let accountID: String
@Published var statusIDs = [String]()
@Published var comment = ""
@Published var forward = false
@Published var reason: Reason = .spam
init(accountID: String) {
self.accountID = accountID
}
func makeRequest() -> Request<Report>? {
let category: String
let ruleIDs: [String]
switch reason {
case .spam:
category = "spam"
ruleIDs = []
case .rules(let ids):
category = "violation"
ruleIDs = ids
}
return Client.report(account: accountID, statuses: statusIDs, comment: comment, forward: forward, category: category, ruleIDs: ruleIDs)
}
}
extension EditedReport {
enum Reason {
case spam
case rules([String])
}
}

View File

@ -38,6 +38,7 @@ class Preferences: Codable, ObservableObject {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
self.theme = try container.decode(UIUserInterfaceStyle.self, forKey: .theme) self.theme = try container.decode(UIUserInterfaceStyle.self, forKey: .theme)
self.accentColor = try container.decodeIfPresent(AccentColor.self, forKey: .accentColor) ?? .default
self.avatarStyle = try container.decode(AvatarStyle.self, forKey: .avatarStyle) self.avatarStyle = try container.decode(AvatarStyle.self, forKey: .avatarStyle)
self.hideCustomEmojiInUsernames = try container.decode(Bool.self, forKey: .hideCustomEmojiInUsernames) self.hideCustomEmojiInUsernames = try container.decode(Bool.self, forKey: .hideCustomEmojiInUsernames)
self.showIsStatusReplyIcon = try container.decode(Bool.self, forKey: .showIsStatusReplyIcon) self.showIsStatusReplyIcon = try container.decode(Bool.self, forKey: .showIsStatusReplyIcon)
@ -90,6 +91,7 @@ class Preferences: Codable, ObservableObject {
var container = encoder.container(keyedBy: CodingKeys.self) var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(theme, forKey: .theme) try container.encode(theme, forKey: .theme)
try container.encode(accentColor, forKey: .accentColor)
try container.encode(avatarStyle, forKey: .avatarStyle) try container.encode(avatarStyle, forKey: .avatarStyle)
try container.encode(hideCustomEmojiInUsernames, forKey: .hideCustomEmojiInUsernames) try container.encode(hideCustomEmojiInUsernames, forKey: .hideCustomEmojiInUsernames)
try container.encode(showIsStatusReplyIcon, forKey: .showIsStatusReplyIcon) try container.encode(showIsStatusReplyIcon, forKey: .showIsStatusReplyIcon)
@ -136,6 +138,7 @@ class Preferences: Codable, ObservableObject {
// MARK: Appearance // MARK: Appearance
@Published var theme = UIUserInterfaceStyle.unspecified @Published var theme = UIUserInterfaceStyle.unspecified
@Published var accentColor = AccentColor.default
@Published var avatarStyle = AvatarStyle.roundRect @Published var avatarStyle = AvatarStyle.roundRect
@Published var hideCustomEmojiInUsernames = false @Published var hideCustomEmojiInUsernames = false
@Published var showIsStatusReplyIcon = false @Published var showIsStatusReplyIcon = false
@ -196,6 +199,7 @@ class Preferences: Codable, ObservableObject {
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case theme case theme
case accentColor
case avatarStyle case avatarStyle
case hideCustomEmojiInUsernames case hideCustomEmojiInUsernames
case showIsStatusReplyIcon case showIsStatusReplyIcon
@ -299,3 +303,83 @@ extension Preferences {
} }
extension UIUserInterfaceStyle: Codable {} extension UIUserInterfaceStyle: Codable {}
extension Preferences {
enum AccentColor: String, Codable, CaseIterable {
case `default`
case purple
case indigo
case blue
case cyan
case teal
case mint
case green
// case yellow
case orange
case red
case pink
// case brown
var color: UIColor? {
switch self {
case .default:
return nil
case .blue:
return .systemBlue
// case .brown:
// return .systemBrown
case .cyan:
return .systemCyan
case .green:
return .systemGreen
case .indigo:
return .systemIndigo
case .mint:
return .systemMint
case .orange:
return .systemOrange
case .pink:
return .systemPink
case .purple:
return .systemPurple
case .red:
return .systemRed
case .teal:
return .systemTeal
// case .yellow:
// return .systemYellow
}
}
var name: String {
switch self {
case .default:
return "Default"
case .blue:
return "Blue"
// case .brown:
// return "Brown"
case .cyan:
return "Cyan"
case .green:
return "Green"
case .indigo:
return "Indigo"
case .mint:
return "Mint"
case .orange:
return "Orange"
case .pink:
return "Pink"
case .purple:
return "Purple"
case .red:
return "Red"
case .teal:
return "Teal"
// case .yellow:
// return "Yellow"
}
}
}
}

View File

@ -113,5 +113,6 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate {
@objc private func themePrefChanged() { @objc private func themePrefChanged() {
window?.overrideUserInterfaceStyle = Preferences.shared.theme window?.overrideUserInterfaceStyle = Preferences.shared.theme
window?.tintColor = Preferences.shared.accentColor.color
} }
} }

View File

@ -101,6 +101,7 @@ class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate {
@objc private func themePrefChanged() { @objc private func themePrefChanged() {
window?.overrideUserInterfaceStyle = Preferences.shared.theme window?.overrideUserInterfaceStyle = Preferences.shared.theme
window?.tintColor = Preferences.shared.accentColor.color
} }
} }

View File

@ -244,6 +244,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
@objc func themePrefChanged() { @objc func themePrefChanged() {
window?.overrideUserInterfaceStyle = Preferences.shared.theme window?.overrideUserInterfaceStyle = Preferences.shared.theme
window?.tintColor = Preferences.shared.accentColor.color
} }
func showAddAccount() { func showAddAccount() {

View File

@ -16,6 +16,8 @@ class GalleryPlayerViewController: UIViewController {
var attachment: Attachment! var attachment: Attachment!
private var isFirstAppearance = true
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
@ -44,6 +46,13 @@ class GalleryPlayerViewController: UIViewController {
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {
AudioSessionHelper.enable() AudioSessionHelper.enable()
AudioSessionHelper.setVideoPlayback() AudioSessionHelper.setVideoPlayback()
DispatchQueue.main.async {
if self.isFirstAppearance {
self.isFirstAppearance = false
self.playerVC.player?.play()
}
}
} }
} }

View File

@ -69,7 +69,8 @@ struct MuteAccountView: View {
} }
.frame(height: 50) .frame(height: 50)
.listRowBackground(EmptyView()) .listRowBackground(EmptyView())
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) // vertical insets are so the rounded corners fo the section don't affect the avatar
.listRowInsets(EdgeInsets(top: 5, leading: 0, bottom: 5, trailing: 0))
} }
.accessibilityHidden(true) .accessibilityHidden(true)

View File

@ -10,14 +10,20 @@ import SwiftUI
struct AppearancePrefsView : View { struct AppearancePrefsView : View {
@ObservedObject var preferences = Preferences.shared @ObservedObject var preferences = Preferences.shared
var theme: Binding<UIUserInterfaceStyle> = Binding(get: { private var theme: Binding<UIUserInterfaceStyle> = Binding(get: {
Preferences.shared.theme Preferences.shared.theme
}, set: { }, set: {
Preferences.shared.theme = $0 Preferences.shared.theme = $0
NotificationCenter.default.post(name: .themePreferenceChanged, object: nil) NotificationCenter.default.post(name: .themePreferenceChanged, object: nil)
}) })
private var accentColor: Binding<Preferences.AccentColor> = Binding {
Preferences.shared.accentColor
} set: {
Preferences.shared.accentColor = $0
NotificationCenter.default.post(name: .themePreferenceChanged, object: nil)
}
var useCircularAvatars: Binding<Bool> = Binding(get: { private var useCircularAvatars: Binding<Bool> = Binding(get: {
Preferences.shared.avatarStyle == .circle Preferences.shared.avatarStyle == .circle
}) { }) {
Preferences.shared.avatarStyle = $0 ? .circle : .roundRect Preferences.shared.avatarStyle = $0 ? .circle : .roundRect
@ -25,11 +31,7 @@ struct AppearancePrefsView : View {
var body: some View { var body: some View {
List { List {
Picker(selection: theme, label: Text("Theme")) { themeSection
Text("Use System Theme").tag(UIUserInterfaceStyle.unspecified)
Text("Light").tag(UIUserInterfaceStyle.light)
Text("Dark").tag(UIUserInterfaceStyle.dark)
}
accountsSection accountsSection
postsSection postsSection
} }
@ -37,6 +39,29 @@ struct AppearancePrefsView : View {
.navigationBarTitle(Text("Appearance")) .navigationBarTitle(Text("Appearance"))
} }
private var themeSection: some View {
Section {
Picker(selection: theme, label: Text("Theme")) {
Text("Use System Theme").tag(UIUserInterfaceStyle.unspecified)
Text("Light").tag(UIUserInterfaceStyle.light)
Text("Dark").tag(UIUserInterfaceStyle.dark)
}
Picker(selection: accentColor, label: Text("Accent Color")) {
ForEach(Preferences.AccentColor.allCases, id: \.rawValue) { color in
HStack {
Text(color.name)
if let color = color.color {
Spacer()
Image(uiImage: UIImage(systemName: "circle.fill")!.withTintColor(color, renderingMode: .alwaysTemplate).withRenderingMode(.alwaysOriginal))
}
}
.tag(color)
}
}
}
}
private var accountsSection: some View { private var accountsSection: some View {
Section(header: Text("Accounts")) { Section(header: Text("Accounts")) {
Toggle(isOn: useCircularAvatars) { Toggle(isOn: useCircularAvatars) {

View File

@ -0,0 +1,58 @@
//
// ReportAddStatusView.swift
// Tusker
//
// Created by Shadowfacts on 1/14/23.
// Copyright © 2023 Shadowfacts. All rights reserved.
//
import SwiftUI
import Pachyderm
struct ReportAddStatusView: View {
@ObservedObject var report: EditedReport
let mastodonController: MastodonController
@Environment(\.dismiss) private var dismiss
@State private var statuses: [StatusMO]?
@State private var error: Error?
var body: some View {
statusesListIfLoaded
.navigationTitle("Add Posts")
}
@ViewBuilder
private var statusesListIfLoaded: some View {
if let statuses {
List {
ForEach(statuses, id: \.id) { status in
Button {
report.statusIDs.append(status.id)
dismiss()
} label: {
ReportStatusView(status: status, mastodonController: mastodonController)
}
}
}
} else {
ProgressView()
.progressViewStyle(.circular)
.alertWithData("Error Loading Posts", data: $error, actions: { _ in
Button("OK") {}
}, message: { error in
Text(error.localizedDescription)
})
.task { @MainActor in
do {
let req = Account.getStatuses(report.accountID, excludeReplies: false, excludeReblogs: true)
let (statuses, _) = try await mastodonController.run(req)
await mastodonController.persistentContainer.addAll(statuses: statuses)
self.statuses = statuses.compactMap { mastodonController.persistentContainer.status(for: $0.id) }
} catch {
self.error = error
}
}
}
}
}

View File

@ -0,0 +1,59 @@
//
// ReportSelectRulesView.swift
// Tusker
//
// Created by Shadowfacts on 1/13/23.
// Copyright © 2023 Shadowfacts. All rights reserved.
//
import SwiftUI
struct ReportSelectRulesView: View {
@ObservedObject var mastodonController: MastodonController
@ObservedObject var report: EditedReport
var selectedRuleIDs: [String] {
get {
if case .rules(let ids) = report.reason {
return ids
} else {
return []
}
}
nonmutating set {
report.reason = .rules(newValue)
}
}
init(mastodonController: MastodonController, report: EditedReport) {
self.mastodonController = mastodonController
self.report = report
}
var body: some View {
List(mastodonController.instance.rules!) { rule in
Button {
if selectedRuleIDs.contains(rule.id) {
selectedRuleIDs.removeAll(where: { $0 == rule.id })
} else {
selectedRuleIDs.append(rule.id)
}
} label: {
HStack {
Text(rule.text)
.foregroundColor(.primary)
Spacer()
Image(systemName: "checkmark")
.foregroundColor(selectedRuleIDs.contains(rule.id) ? .accentColor : .clear)
}
}
}
.navigationTitle("Rules")
}
}
//struct ReportSelectRulesView_Previews: PreviewProvider {
// static var previews: some View {
// ReportSelectRulesView()
// }
//}

View File

@ -0,0 +1,38 @@
//
// ReportStatusView.swift
// Tusker
//
// Created by Shadowfacts on 1/13/23.
// Copyright © 2023 Shadowfacts. All rights reserved.
//
import SwiftUI
import SwiftSoup
private var converter = HTMLConverter()
struct ReportStatusView: View {
let status: StatusMO
let mastodonController: MastodonController
private var text: AttributedString {
let str = AttributedString(converter.convert(status.content))
return str.transformingAttributes(\.link) { transformer in
if transformer.value != nil {
transformer.replace(with: \.foregroundColor, value: .accentColor)
}
}
}
var body: some View {
VStack(alignment: .leading) {
Text(text)
if !status.attachments.isEmpty {
Text("^[\(status.attachments.count) attachments](inflect: true)")
.foregroundColor(.secondary)
.font(.caption.bold())
}
}
}
}

View File

@ -0,0 +1,172 @@
//
// ReportView.swift
// Tusker
//
// Created by Shadowfacts on 1/13/23.
// Copyright © 2023 Shadowfacts. All rights reserved.
//
import SwiftUI
struct ReportView: View {
let account: AccountMO
@ObservedObject var mastodonController: MastodonController
@StateObject var report: EditedReport
@Environment(\.dismiss) private var dismiss
@ObservedObject private var preferences = Preferences.shared
@State private var isReporting = false
@State private var error: Error?
init(report: EditedReport, mastodonController: MastodonController) {
self.account = mastodonController.persistentContainer.account(for: report.accountID)!
self.mastodonController = mastodonController
self._report = StateObject(wrappedValue: report)
if mastodonController.instance.rules == nil {
report.reason = .spam
}
}
var body: some View {
if #available(iOS 16.0, *) {
NavigationStack {
navigationViewContent
}
} else {
NavigationView {
navigationViewContent
}
.navigationViewStyle(.stack)
}
}
private var navigationViewContent: some View {
Form {
Section {
HStack {
ComposeAvatarImageView(url: account.avatar)
.frame(width: 50, height: 50)
.cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 50)
VStack(alignment: .leading) {
AccountDisplayNameLabel(account: account, textStyle: .headline, emojiSize: 17)
Text("@\(account.acct)")
.fontWeight(.light)
.foregroundColor(.secondary)
}
}
.frame(height: 50)
.listRowBackground(EmptyView())
// vertical insets are so the rounded corners fo the section don't affect the avatar
.listRowInsets(EdgeInsets(top: 5, leading: 0, bottom: 5, trailing: 0))
}
.accessibilityHidden(true)
Section {
Button {
report.reason = .spam
} label: {
HStack {
Text("Spam")
Spacer()
if case .spam = report.reason {
Image(systemName: "checkmark")
}
}
}
if mastodonController.instance.rules != nil {
NavigationLink {
ReportSelectRulesView(mastodonController: mastodonController, report: report)
} label: {
HStack {
Text("Instance Rule")
Spacer()
if case .rules(_) = report.reason {
Image(systemName: "checkmark")
}
}
.foregroundColor(.accentColor)
}
}
} header: {
Text("Reason")
}
Section {
ComposeTextView(text: $report.comment, placeholder: Text("Add any additional comments"))
.backgroundColor(.clear)
.listRowInsets(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8))
}
Section {
ForEach(report.statusIDs, id: \.self) { id in
ReportStatusView(status: mastodonController.persistentContainer.status(for: id)!, mastodonController: mastodonController)
}
.onDelete { indices in
report.statusIDs.remove(atOffsets: indices)
}
NavigationLink {
ReportAddStatusView(report: report, mastodonController: mastodonController)
} label: {
Label("Add Posts…", systemImage: "plus")
.foregroundColor(.accentColor)
}
} footer: {
Text("Attach posts to your report to provide additional context for moderators.")
}
Section {
Toggle("Forward", isOn: $report.forward)
} footer: {
Text("You can choose to anonymously forward your report to the moderators of **\(account.url.host!)**.")
}
Button(action: self.sendReport) {
if isReporting {
Text("Sending Report")
Spacer()
ProgressView()
.progressViewStyle(.circular)
} else {
Text("Send Report")
}
}
.disabled(isReporting)
}
.alertWithData("Error Reporting", data: $error, actions: { error in
Button("OK") {}
}, message: { error in
Text(error.localizedDescription)
})
.navigationTitle("Report \(account.displayOrUserName)")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
self.dismiss()
}
}
}
}
private func sendReport() {
isReporting = true
Task {
do {
_ = try await mastodonController.run(report.makeRequest()!)
self.dismiss()
} catch {
self.error = error
self.isReporting = false
}
}
}
}
//struct ReportView_Previews: PreviewProvider {
// static var previews: some View {
// ReportView()
// }
//}

View File

@ -334,25 +334,31 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
return false return false
} }
loadViewIfNeeded() loadViewIfNeeded()
var loaded = false
await controller.restoreInitial { await controller.restoreInitial {
await loadStatusesToRestore(position: position) let hasStatusesToRestore = await loadStatusesToRestore(position: position)
if hasStatusesToRestore {
applyItemsToRestore(position: position) applyItemsToRestore(position: position)
loaded = true
} }
return true }
return loaded
} }
@MainActor @MainActor
private func loadStatusesToRestore(position: TimelinePosition) async { private func loadStatusesToRestore(position: TimelinePosition) async -> Bool {
let unloaded = position.statusIDs.filter({ mastodonController.persistentContainer.status(for: $0) == nil }) let unloaded = position.statusIDs.filter({ mastodonController.persistentContainer.status(for: $0) == nil })
guard !unloaded.isEmpty else { guard !unloaded.isEmpty else {
return return true
} }
let statuses = await withTaskGroup(of: Status?.self) { group -> [Status] in let statuses = await withTaskGroup(of: Status?.self) { group -> [Status] in
for id in unloaded { for id in unloaded {
group.addTask { @MainActor in group.addTask {
if let (status, _) = try? await self.mastodonController.run(Client.getStatus(id: id)) { do {
let (status, _) = try await self.mastodonController.run(Client.getStatus(id: id))
return status return status
} else { } catch {
print(error)
return nil return nil
} }
} }
@ -364,6 +370,20 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
} }
} }
await mastodonController.persistentContainer.addAll(statuses: statuses, in: mastodonController.persistentContainer.viewContext) await mastodonController.persistentContainer.addAll(statuses: statuses, in: mastodonController.persistentContainer.viewContext)
// update the timeline position in case some statuses couldn't be loaded
if let center = position.centerStatusID {
let nearestLoadedStatusToCenter = position.statusIDs[position.statusIDs.firstIndex(of: center)!...].first(where: { id in
// was already loaded or was just now loaded
!unloaded.contains(id) || statuses.contains(where: { $0.id == id })
})
position.centerStatusID = nearestLoadedStatusToCenter
}
position.statusIDs = position.statusIDs.filter { id in
!unloaded.contains(id) || statuses.contains(where: { $0.id == id })
}
return !position.statusIDs.isEmpty
} }
private func applyItemsToRestore(position: TimelinePosition) { private func applyItemsToRestore(position: TimelinePosition) {

View File

@ -95,8 +95,11 @@ class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageView
return return
} }
let page = Page(mastodonController: mastodonController, timeline: timeline) let page = Page(mastodonController: mastodonController, timeline: timeline)
// the pinned timelines may have changed after an iCloud sync, in which case don't restore anything
if pages.contains(page) {
selectPage(page, animated: false) selectPage(page, animated: false)
} }
}
@objc private func customizePressed() { @objc private func customizePressed() {
present(UIHostingController(rootView: CustomizeTimelinesView(mastodonController: mastodonController)), animated: true) present(UIHostingController(rootView: CustomizeTimelinesView(mastodonController: mastodonController)), animated: true)

View File

@ -84,6 +84,11 @@ extension MenuActionProvider {
})) }))
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.blockAction(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(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)
self.navigationDelegate?.present(host, animated: true)
}))
} }
addOpenInNewWindow(actions: &shareSection, activity: UserActivityManager.showProfileActivity(id: accountID, accountID: loggedInAccountID)) addOpenInNewWindow(actions: &shareSection, activity: UserActivityManager.showProfileActivity(id: accountID, accountID: loggedInAccountID))
@ -210,7 +215,15 @@ extension MenuActionProvider {
}), at: 1) }), at: 1)
} }
var actionsSection: [UIAction] = [] var actionsSection: [UIAction] = [
createAction(identifier: "report", title: "Report", systemImageName: "flag", handler: { [weak self] _ in
let report = EditedReport(accountID: status.account.id)
report.statusIDs = [status.id]
let view = ReportView(report: report, mastodonController: mastodonController)
let host = UIHostingController(rootView: view)
self?.navigationDelegate?.present(host, animated: true)
})
]
if includeStatusButtonActions { if includeStatusButtonActions {
actionsSection.insert(createAction(identifier: "reply", title: "Reply", systemImageName: "arrowshape.turn.up.left", handler: { [weak self] (_) in actionsSection.insert(createAction(identifier: "reply", title: "Reply", systemImageName: "arrowshape.turn.up.left", handler: { [weak self] (_) in

View File

@ -55,6 +55,8 @@ class PollFinishedTableViewCell: UITableViewCell {
let doc = try! SwiftSoup.parseBodyFragment(status.content) let doc = try! SwiftSoup.parseBodyFragment(status.content)
statusContentLabel.text = try! doc.text() statusContentLabel.text = try! doc.text()
pollView.mastodonController = mastodonController
pollView.toastableViewController = delegate
pollView.updateUI(status: status, poll: poll) pollView.updateUI(status: status, poll: poll)
} }

View File

@ -48,12 +48,12 @@ class BaseStatusTableViewCell: UITableViewCell {
private var favorited = false { private var favorited = false {
didSet { didSet {
favoriteButton.tintColor = favorited ? UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1) : tintColor favoriteButton.tintColor = favorited ? UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1) : .tintColor
} }
} }
private var reblogged = false { private var reblogged = false {
didSet { didSet {
reblogButton.tintColor = reblogged ? UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1) : tintColor reblogButton.tintColor = reblogged ? UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1) : .tintColor
} }
} }

View File

@ -77,7 +77,7 @@
</label> </label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="8r8-O8-Agh" customClass="StatusCollapseButton" customModule="Tusker" customModuleProvider="target"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="8r8-O8-Agh" customClass="StatusCollapseButton" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="86.5" width="361" height="30"/> <rect key="frame" x="0.0" y="86.5" width="361" height="30"/>
<color key="backgroundColor" systemColor="systemBlueColor"/> <color key="backgroundColor" systemColor="tintColor"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="30" id="icD-3q-uJ6"/> <constraint firstAttribute="height" constant="30" id="icD-3q-uJ6"/>
</constraints> </constraints>
@ -169,6 +169,7 @@
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2cc-lE-AdG"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2cc-lE-AdG">
<rect key="frame" x="0.0" y="0.0" width="90.5" height="26"/> <rect key="frame" x="0.0" y="0.0" width="90.5" height="26"/>
<accessibility key="accessibilityConfiguration" label="Reply"/> <accessibility key="accessibilityConfiguration" label="Reply"/>
<color key="tintColor" systemColor="tintColor"/>
<state key="normal"> <state key="normal">
<imageReference key="image" image="arrowshape.turn.up.left.fill" catalog="system" symbolScale="large"/> <imageReference key="image" image="arrowshape.turn.up.left.fill" catalog="system" symbolScale="large"/>
</state> </state>
@ -179,6 +180,7 @@
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DhN-rJ-jdA"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DhN-rJ-jdA">
<rect key="frame" x="90.5" y="0.0" width="90" height="26"/> <rect key="frame" x="90.5" y="0.0" width="90" height="26"/>
<accessibility key="accessibilityConfiguration" label="Favorite"/> <accessibility key="accessibilityConfiguration" label="Favorite"/>
<color key="tintColor" systemColor="tintColor"/>
<state key="normal"> <state key="normal">
<imageReference key="image" image="star.fill" catalog="system" symbolScale="large"/> <imageReference key="image" image="star.fill" catalog="system" symbolScale="large"/>
</state> </state>
@ -189,6 +191,7 @@
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="GUG-f7-Hdy"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="GUG-f7-Hdy">
<rect key="frame" x="180.5" y="0.0" width="90.5" height="26"/> <rect key="frame" x="180.5" y="0.0" width="90.5" height="26"/>
<accessibility key="accessibilityConfiguration" label="Reblog"/> <accessibility key="accessibilityConfiguration" label="Reblog"/>
<color key="tintColor" systemColor="tintColor"/>
<state key="normal"> <state key="normal">
<imageReference key="image" image="repeat" catalog="system" symbolScale="large"/> <imageReference key="image" image="repeat" catalog="system" symbolScale="large"/>
</state> </state>
@ -199,6 +202,7 @@
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Ujo-Ap-dmK"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Ujo-Ap-dmK">
<rect key="frame" x="271" y="0.0" width="90" height="26"/> <rect key="frame" x="271" y="0.0" width="90" height="26"/>
<accessibility key="accessibilityConfiguration" label="More Actions"/> <accessibility key="accessibilityConfiguration" label="More Actions"/>
<color key="tintColor" systemColor="tintColor"/>
<state key="normal"> <state key="normal">
<imageReference key="image" image="ellipsis" catalog="system" symbolScale="large"/> <imageReference key="image" image="ellipsis" catalog="system" symbolScale="large"/>
</state> </state>
@ -278,7 +282,7 @@
<systemColor name="systemBackgroundColor"> <systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor> </systemColor>
<systemColor name="systemBlueColor"> <systemColor name="tintColor">
<color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor> </systemColor>
</resources> </resources>

View File

@ -668,6 +668,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
@objc private func preferencesChanged() { @objc private func preferencesChanged() {
guard let mastodonController, guard let mastodonController,
let statusID,
let status = mastodonController.persistentContainer.status(for: statusID) else { let status = mastodonController.persistentContainer.status(for: statusID) else {
return return
} }

View File

@ -46,7 +46,7 @@ class ToastView: UIView {
} }
private func setupView() { private func setupView() {
backgroundColor = .systemBlue backgroundColor = .tintColor
layer.shadowColor = UIColor.black.cgColor layer.shadowColor = UIColor.black.cgColor
layer.shadowRadius = 5 layer.shadowRadius = 5
layer.shadowOffset = CGSize(width: 0, height: 2.5) layer.shadowOffset = CGSize(width: 0, height: 2.5)