Compare commits

...

6 Commits

14 changed files with 190 additions and 61 deletions

View File

@ -22,8 +22,7 @@ extension NSTextAttachment {
image.draw(in: CGRect(origin: .zero, size: imageSizeMatchingFontSize))
}
self.init()
self.image = attachmentImage
self.init(image: attachmentImage)
}
convenience init(emojiPlaceholderIn font: UIFont) {
@ -31,7 +30,6 @@ extension NSTextAttachment {
// assumes emoji are mostly square
let size = CGSize(width: adjustedCapHeight, height: adjustedCapHeight)
let image = UIGraphicsImageRenderer(size: size).image { (_) in }
self.init()
self.image = image
self.init(image: image)
}
}

View File

@ -11,11 +11,10 @@ import Pachyderm
extension StatusState {
func resolveFor(status: StatusMO, text: String?) {
func resolveFor(status: StatusMO, height: CGFloat) {
let longEnoughToCollapse: Bool
if Preferences.shared.collapseLongPosts,
let text = text,
text.count > 500 {
height > 500 {
longEnoughToCollapse = true
} else {
longEnoughToCollapse = false

View File

@ -64,3 +64,18 @@ extension Status.Visibility {
}
}
extension Status.Visibility: Comparable {
public static func < (lhs: Pachyderm.Status.Visibility, rhs: Pachyderm.Status.Visibility) -> Bool {
switch (lhs, rhs) {
case (.direct, .public), (.private, .public), (.unlisted, .public):
return true
case (.direct, .unlisted), (.private, .unlisted):
return true
case (.direct, .private):
return true
default:
return false
}
}
}

View File

@ -187,14 +187,14 @@ extension MastodonController {
func createDraft(inReplyToID: String? = nil, mentioningAcct: String? = nil) -> Draft {
var acctsToMention = [String]()
var visibility = Preferences.shared.defaultPostVisibility
var visibility = inReplyToID != nil ? Preferences.shared.defaultReplyVisibility.resolved : Preferences.shared.defaultPostVisibility
var contentWarning = ""
if let inReplyToID = inReplyToID,
let inReplyTo = persistentContainer.status(for: inReplyToID) {
acctsToMention.append(inReplyTo.account.acct)
acctsToMention.append(contentsOf: inReplyTo.mentions.map(\.acct))
visibility = inReplyTo.visibility
visibility = min(visibility, inReplyTo.visibility)
if !inReplyTo.spoilerText.isEmpty {
switch Preferences.shared.contentWarningCopyMode {

View File

@ -45,12 +45,14 @@ class Preferences: Codable, ObservableObject {
self.hideActionsInTimeline = try container.decodeIfPresent(Bool.self, forKey: .hideActionsInTimeline) ?? false
self.defaultPostVisibility = try container.decode(Status.Visibility.self, forKey: .defaultPostVisibility)
self.defaultReplyVisibility = try container.decodeIfPresent(ReplyVisibility.self, forKey: .defaultReplyVisibility) ?? .sameAsPost
self.automaticallySaveDrafts = try container.decode(Bool.self, forKey: .automaticallySaveDrafts)
self.requireAttachmentDescriptions = try container.decode(Bool.self, forKey: .requireAttachmentDescriptions)
self.contentWarningCopyMode = try container.decode(ContentWarningCopyMode.self, forKey: .contentWarningCopyMode)
self.mentionReblogger = try container.decode(Bool.self, forKey: .mentionReblogger)
self.blurAllMedia = try container.decode(Bool.self, forKey: .blurAllMedia)
self.blurMediaBehindContentWarning = try container.decodeIfPresent(Bool.self, forKey: .blurMediaBehindContentWarning) ?? true
self.automaticallyPlayGifs = try container.decode(Bool.self, forKey: .automaticallyPlayGifs)
self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps)
@ -84,12 +86,14 @@ class Preferences: Codable, ObservableObject {
try container.encode(hideActionsInTimeline, forKey: .hideActionsInTimeline)
try container.encode(defaultPostVisibility, forKey: .defaultPostVisibility)
try container.encode(defaultReplyVisibility, forKey: .defaultReplyVisibility)
try container.encode(automaticallySaveDrafts, forKey: .automaticallySaveDrafts)
try container.encode(requireAttachmentDescriptions, forKey: .requireAttachmentDescriptions)
try container.encode(contentWarningCopyMode, forKey: .contentWarningCopyMode)
try container.encode(mentionReblogger, forKey: .mentionReblogger)
try container.encode(blurAllMedia, forKey: .blurAllMedia)
try container.encode(blurMediaBehindContentWarning, forKey: .blurMediaBehindContentWarning)
try container.encode(automaticallyPlayGifs, forKey: .automaticallyPlayGifs)
try container.encode(openLinksInApps, forKey: .openLinksInApps)
@ -122,13 +126,21 @@ class Preferences: Codable, ObservableObject {
// MARK: Composing
@Published var defaultPostVisibility = Status.Visibility.public
@Published var defaultReplyVisibility = ReplyVisibility.sameAsPost
@Published var automaticallySaveDrafts = true
@Published var requireAttachmentDescriptions = false
@Published var contentWarningCopyMode = ContentWarningCopyMode.asIs
@Published var mentionReblogger = false
// MARK: Media
@Published var blurAllMedia = false
@Published var blurAllMedia = false {
didSet {
if blurAllMedia {
blurMediaBehindContentWarning = true
}
}
}
@Published var blurMediaBehindContentWarning = true
@Published var automaticallyPlayGifs = true
// MARK: Behavior
@ -164,12 +176,14 @@ class Preferences: Codable, ObservableObject {
case hideActionsInTimeline
case defaultPostVisibility
case defaultReplyVisibility
case automaticallySaveDrafts
case requireAttachmentDescriptions
case contentWarningCopyMode
case mentionReblogger
case blurAllMedia
case blurMediaBehindContentWarning
case automaticallyPlayGifs
case openLinksInApps
@ -194,4 +208,40 @@ class Preferences: Codable, ObservableObject {
}
extension Preferences {
enum ReplyVisibility: Codable, Hashable, CaseIterable {
case sameAsPost
case visibility(Status.Visibility)
static var allCases: [Preferences.ReplyVisibility] = [.sameAsPost] + Status.Visibility.allCases.map { .visibility($0) }
var resolved: Status.Visibility {
switch self {
case .sameAsPost:
return Preferences.shared.defaultPostVisibility
case .visibility(let vis):
return vis
}
}
var displayName: String {
switch self {
case .sameAsPost:
return "Same as Default"
case .visibility(let vis):
return vis.displayName
}
}
var imageName: String? {
switch self {
case .sameAsPost:
return nil
case .visibility(let vis):
return vis.imageName
}
}
}
}
extension UIUserInterfaceStyle: Codable {}

View File

@ -14,6 +14,30 @@ protocol ComposeDrawingViewControllerDelegate: AnyObject {
func composeDrawingViewController(_ drawingController: ComposeDrawingViewController, saveDrawing drawing: PKDrawing)
}
class ComposeDrawingNavigationController: UINavigationController {
override var preferredStatusBarStyle: UIStatusBarStyle {
.darkContent
}
init(editing initialDrawing: PKDrawing, delegate: ComposeDrawingViewControllerDelegate) {
let vc = ComposeDrawingViewController(editing: initialDrawing)
vc.delegate = delegate
super.init(rootViewController: vc)
modalPresentationStyle = .fullScreen
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
overrideUserInterfaceStyle = .light
}
}
class ComposeDrawingViewController: UIViewController {
weak var delegate: ComposeDrawingViewControllerDelegate?
@ -63,7 +87,8 @@ class ComposeDrawingViewController: UIViewController {
canvasView.drawingPolicy = .anyInput
canvasView.minimumZoomScale = 0.5
canvasView.maximumZoomScale = 2
canvasView.backgroundColor = .systemBackground
canvasView.backgroundColor = .white
canvasView.overrideUserInterfaceStyle = .light
canvasView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(canvasView)
NSLayoutConstraint.activate([
@ -74,6 +99,7 @@ class ComposeDrawingViewController: UIViewController {
])
toolPicker = PKToolPicker()
toolPicker.overrideUserInterfaceStyle = .light
toolPicker.setVisible(true, forFirstResponder: canvasView)
toolPicker.addObserver(canvasView)
toolPicker.addObserver(self)

View File

@ -360,11 +360,7 @@ extension ComposeHostingController: ComposeUIStateDelegate {
drawing = PKDrawing()
}
let drawingVC = ComposeDrawingViewController(editing: drawing)
drawingVC.delegate = self
let nav = UINavigationController(rootViewController: drawingVC)
nav.modalPresentationStyle = .fullScreen
present(nav, animated: true)
present(ComposeDrawingNavigationController(editing: drawing, delegate: self), animated: true)
}
}

View File

@ -14,6 +14,7 @@ struct ComposingPrefsView: View {
var body: some View {
List {
visibilitySection
composingSection
replyingSection
}
@ -21,9 +22,9 @@ struct ComposingPrefsView: View {
.navigationBarTitle("Composing")
}
var composingSection: some View {
Section(header: Text("Composing")) {
Picker(selection: $preferences.defaultPostVisibility, label: Text("Default Post Visibility")) {
var visibilitySection: some View {
Section {
Picker(selection: $preferences.defaultPostVisibility, label: Text("Default Visibility")) {
ForEach(Status.Visibility.allCases, id: \.self) { visibility in
HStack {
Image(systemName: visibility.imageName)
@ -33,6 +34,26 @@ struct ComposingPrefsView: View {
}//.navigationBarTitle("Default Post Visibility")
// navbar title on the ForEach is currently incorrectly applied when the picker is not expanded, see FB6838291
}
Picker(selection: $preferences.defaultReplyVisibility, label: Text("Reply Visibility")) {
ForEach(Preferences.ReplyVisibility.allCases, id: \.self) { visibility in
HStack {
if let imageName = visibility.imageName {
Image(systemName: imageName)
}
Text(visibility.displayName)
}
.tag(visibility)
}
}
} header: {
Text("Visibility")
} footer: {
Text("When starting a reply, Tusker will use your preferred visibility or the visibility of the post to which you're replying, whichever is narrower.")
}
}
var composingSection: some View {
Section(header: Text("Composing")) {
Toggle(isOn: $preferences.automaticallySaveDrafts) {
Text("Automatically Save Drafts")
}

View File

@ -24,6 +24,12 @@ struct MediaPrefsView: View {
Toggle(isOn: $preferences.blurAllMedia) {
Text("Blur All Media")
}
Toggle(isOn: $preferences.blurMediaBehindContentWarning) {
Text("Blur Media Behind Content Warning")
}
.disabled(preferences.blurAllMedia)
Toggle(isOn: $preferences.automaticallyPlayGifs) {
Text("Automatically Play GIFs")
}

View File

@ -222,7 +222,17 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
}
var snapshot = dataSource.snapshot()
let items = statuses.map { Item.status(id: $0.id, state: .unknown, pinned: true) }
let existingPinned = snapshot.itemIdentifiers(inSection: .pinned)
let items = statuses.map {
let item = Item.status(id: $0.id, state: .unknown, pinned: true)
// try to keep the existing status state
if let existing = existingPinned.first(where: { $0 == item }) {
return existing
} else {
return item
}
}
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .pinned))
snapshot.appendItems(items, toSection: .pinned)
await apply(snapshot, animatingDifferences: true)
}

View File

@ -236,27 +236,9 @@ class AttachmentsContainerView: UIView {
// Make sure accessibilityElements is set every time the UI is updated, otherwise it holds
// on to strong references to the old set of attachment views
self.accessibilityElements = accessibilityElements
contentHidden = Preferences.shared.blurAllMedia || status.sensitive
}
private func createAttachmentView(index: Int, hSize: RelativeSize, vSize: RelativeSize) -> AttachmentView {
let width: CGFloat
switch hSize {
case .full:
width = bounds.width
case .half:
width = (bounds.width - 4) / 2
}
let height: CGFloat
switch vSize {
case .full:
height = bounds.height
case .half:
height = (bounds.height - 4) / 2
}
let size = CGSize(width: width, height: height)
let attachmentView = AttachmentView(attachment: attachments[index], index: index)
attachmentView.delegate = delegate
attachmentView.translatesAutoresizingMaskIntoConstraints = false

View File

@ -86,28 +86,31 @@ extension BaseEmojiLabel {
func buildStringWithEmojisReplaced(usePlaceholders: Bool) -> NSAttributedString {
let mutAttrString = NSMutableAttributedString(attributedString: attributedString)
// lock once for the entire loop, rather than lock/unlocking for each iteration to do the lookup
// OSAllocatedUnfairLock.withLock expects a @Sendable closure, so this warns about captures of non-sendable types (attribute dstrings, text checking results)
// even though the closures is invoked on the same thread that withLock is called, so it's unclear why it needs to be @Sendable (FB11494878)
// so, just ignore the warnings
emojiImages.withLock { emojiImages in
// replaces the emojis starting from the end of the string as to not alter the indices of preceeding emojis
for match in matches.reversed() {
let shortcode = (attributedString.string as NSString).substring(with: match.range(at: 1))
let attachment: NSTextAttachment
if let emojiImage = emojiImages[shortcode] {
attachment = NSTextAttachment(emojiImage: emojiImage, in: self.emojiFont, with: self.emojiTextColor)
} else if usePlaceholders {
attachment = NSTextAttachment(emojiPlaceholderIn: self.emojiFont)
} else {
continue
}
let attachmentStr = NSAttributedString(attachment: attachment)
mutAttrString.replaceCharacters(in: match.range, with: attachmentStr)
let emojiAttachments = emojiImages.withLock {
$0.mapValues { image in
NSTextAttachment(emojiImage: image, in: self.emojiFont, with: self.emojiTextColor)
}
}
let placeholder = usePlaceholders ? NSTextAttachment(emojiPlaceholderIn: self.emojiFont) : nil
// replaces the emojis starting from the end of the string as to not alter the indices of preceeding emojis
for match in matches.reversed() {
let shortcode = (attributedString.string as NSString).substring(with: match.range(at: 1))
let attachment: NSTextAttachment
if let emoji = emojiAttachments[shortcode] {
attachment = emoji
} else if usePlaceholders {
attachment = placeholder!
} else {
continue
}
let attachmentStr = NSAttributedString(attachment: attachment)
mutAttrString.replaceCharacters(in: match.range, with: attachmentStr)
}
return mutAttrString
}

View File

@ -182,7 +182,8 @@ class BaseStatusTableViewCell: UITableViewCell {
updateStatusIconsForPreferences(status)
if state.unknown {
state.resolveFor(status: status, text: contentTextView.text)
layoutIfNeeded()
state.resolveFor(status: status, height: contentTextView.bounds.height)
if state.collapsible! && showStatusAutomatically {
state.collapsed = false
}
@ -230,7 +231,17 @@ class BaseStatusTableViewCell: UITableViewCell {
func updateUIForPreferences(account: AccountMO, status: StatusMO) {
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
attachmentsView.contentHidden = Preferences.shared.blurAllMedia || status.sensitive
if Preferences.shared.blurAllMedia {
attachmentsView.contentHidden = true
} else if status.sensitive {
if !Preferences.shared.blurMediaBehindContentWarning && !status.spoilerText.isEmpty {
attachmentsView.contentHidden = false
} else {
attachmentsView.contentHidden = true
}
} else {
attachmentsView.contentHidden = false
}
updateStatusIconsForPreferences(status)

View File

@ -104,7 +104,9 @@ extension StatusCollectionViewCell {
favoriteButton.isEnabled = mastodonController.loggedIn
if statusState.unknown {
statusState.resolveFor(status: status, text: contentContainer.contentTextView.text)
// layout so that we can take the content height into consideration when deciding whether to collapse
layoutIfNeeded()
statusState.resolveFor(status: status, height: contentContainer.contentTextView.bounds.height)
if statusState.collapsible! && showStatusAutomatically {
statusState.collapsed = false
}
@ -145,7 +147,17 @@ extension StatusCollectionViewCell {
func baseUpdateUIForPreferences(status: StatusMO) {
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * Self.avatarImageViewSize
contentContainer.attachmentsView.contentHidden = Preferences.shared.blurAllMedia || status.sensitive
if Preferences.shared.blurAllMedia {
contentContainer.attachmentsView.contentHidden = true
} else if status.sensitive {
if !Preferences.shared.blurMediaBehindContentWarning && !status.spoilerText.isEmpty {
contentContainer.attachmentsView.contentHidden = false
} else {
contentContainer.attachmentsView.contentHidden = true
}
} else {
contentContainer.attachmentsView.contentHidden = false
}
let reblogButtonImage: UIImage
if Preferences.shared.alwaysShowStatusVisibilityIcon || reblogEnabled(status: status) {