Compare commits
22 Commits
1e7bfac13c
...
9ab95dfc43
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 9ab95dfc43 | |
Shadowfacts | c34ce758dd | |
Shadowfacts | 2c9f00d19f | |
Shadowfacts | f7127b84d8 | |
Shadowfacts | fdb21cd1fb | |
Shadowfacts | 9f0c1eece8 | |
Shadowfacts | e18a09f4ac | |
Shadowfacts | 005001b081 | |
Shadowfacts | 90f17693f1 | |
Shadowfacts | 698b045f86 | |
Shadowfacts | 654f84363a | |
Shadowfacts | 4dd510f3af | |
Shadowfacts | 1c36dfcc5f | |
Shadowfacts | b0bd27db31 | |
Shadowfacts | daa1a9eef7 | |
Shadowfacts | c737354ed3 | |
Shadowfacts | 8ea15d3bab | |
Shadowfacts | 13a4221fce | |
Shadowfacts | a896573a5e | |
Shadowfacts | edd89450aa | |
Shadowfacts | 5f5ef8fcea | |
Shadowfacts | a3b59c990b |
18
CHANGELOG.md
18
CHANGELOG.md
|
@ -1,5 +1,23 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2021.1 (18)
|
||||||
|
Polls! They're finally here. There will likely be another build in the next several weeks to polish some things off before WWDC, so if you've encountered any issues, now's the time to let me know :)
|
||||||
|
|
||||||
|
Features/Improvements:
|
||||||
|
- Show polls on posts
|
||||||
|
- Add authoring polls to Compose screen
|
||||||
|
- Add poll completed notifications
|
||||||
|
- Add preference for requiring confirmation before reblogging
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Fix cursor movement not working in Compse text field when an emoji was entered
|
||||||
|
- Fix several crashes related to network requests failing
|
||||||
|
- Show assets in attachment picker immediately after permissions is initially granted
|
||||||
|
- Fix crash when tapping non-HTTP(S) link with the In-App Safari preference enabled
|
||||||
|
|
||||||
|
Known Issues:
|
||||||
|
- Polls with between 23h30m and 24h left show as "0 weeks remaining"
|
||||||
|
|
||||||
## 2021.1 (17)
|
## 2021.1 (17)
|
||||||
The main improvement this build is a complete overhaul of the Conversation screen, along with fixes for a few different crashes.
|
The main improvement this build is a complete overhaul of the Conversation screen, along with fixes for a few different crashes.
|
||||||
|
|
||||||
|
|
|
@ -298,7 +298,10 @@ public class Client {
|
||||||
sensitive: Bool? = nil,
|
sensitive: Bool? = nil,
|
||||||
spoilerText: String? = nil,
|
spoilerText: String? = nil,
|
||||||
visibility: Status.Visibility? = nil,
|
visibility: Status.Visibility? = nil,
|
||||||
language: String? = nil) -> Request<Status> {
|
language: String? = nil,
|
||||||
|
pollOptions: [String]? = nil,
|
||||||
|
pollExpiresIn: Int? = nil,
|
||||||
|
pollMultiple: Bool? = nil) -> Request<Status> {
|
||||||
return Request<Status>(method: .post, path: "/api/v1/statuses", body: ParametersBody([
|
return Request<Status>(method: .post, path: "/api/v1/statuses", body: ParametersBody([
|
||||||
"status" => text,
|
"status" => text,
|
||||||
"content_type" => contentType.mimeType,
|
"content_type" => contentType.mimeType,
|
||||||
|
@ -306,8 +309,10 @@ public class Client {
|
||||||
"sensitive" => sensitive,
|
"sensitive" => sensitive,
|
||||||
"spoiler_text" => spoilerText,
|
"spoiler_text" => spoilerText,
|
||||||
"visibility" => visibility?.rawValue,
|
"visibility" => visibility?.rawValue,
|
||||||
"language" => language
|
"language" => language,
|
||||||
] + "media_ids" => media?.map { $0.id }))
|
"poll[expires_in]" => pollExpiresIn,
|
||||||
|
"poll[multiple]" => pollMultiple,
|
||||||
|
] + "media_ids" => media?.map { $0.id } + "poll[options]" => pollOptions))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Timelines
|
// MARK: - Timelines
|
||||||
|
|
|
@ -55,6 +55,7 @@ extension Notification {
|
||||||
case favourite
|
case favourite
|
||||||
case follow
|
case follow
|
||||||
case followRequest = "follow_request"
|
case followRequest = "follow_request"
|
||||||
|
case poll
|
||||||
case unknown
|
case unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
//
|
||||||
|
// Poll.swift
|
||||||
|
// Pachyderm
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/25/21.
|
||||||
|
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public final class Poll: Codable {
|
||||||
|
public let id: String
|
||||||
|
public let expiresAt: Date?
|
||||||
|
public let expired: Bool
|
||||||
|
public let multiple: Bool
|
||||||
|
public let votesCount: Int
|
||||||
|
public let votersCount: Int?
|
||||||
|
public let voted: Bool?
|
||||||
|
public let ownVotes: [Int]?
|
||||||
|
public let options: [Option]
|
||||||
|
public let emojis: [Emoji]
|
||||||
|
|
||||||
|
public var effectiveExpired: Bool {
|
||||||
|
expired || (expiresAt != nil && expiresAt! < Date())
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func vote(_ pollID: String, choices: [Int]) -> Request<Poll> {
|
||||||
|
return Request<Poll>(method: .post, path: "/api/v1/polls/\(pollID)/votes", body: FormDataBody("choices" => choices, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case expiresAt = "expires_at"
|
||||||
|
case expired
|
||||||
|
case multiple
|
||||||
|
case votesCount = "votes_count"
|
||||||
|
case votersCount = "voters_count"
|
||||||
|
case voted
|
||||||
|
case ownVotes = "own_votes"
|
||||||
|
case options
|
||||||
|
case emojis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Poll {
|
||||||
|
public final class Option: Codable {
|
||||||
|
public let title: String
|
||||||
|
public let votesCount: Int?
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case title
|
||||||
|
case votesCount = "votes_count"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,6 +37,7 @@ public final class Status: /*StatusProtocol,*/ Decodable {
|
||||||
public let pinned: Bool?
|
public let pinned: Bool?
|
||||||
public let bookmarked: Bool?
|
public let bookmarked: Bool?
|
||||||
public let card: Card?
|
public let card: Card?
|
||||||
|
public let poll: Poll?
|
||||||
|
|
||||||
public var applicationName: String? { application?.name }
|
public var applicationName: String? { application?.name }
|
||||||
|
|
||||||
|
@ -132,6 +133,7 @@ public final class Status: /*StatusProtocol,*/ Decodable {
|
||||||
case pinned
|
case pinned
|
||||||
case bookmarked
|
case bookmarked
|
||||||
case card
|
case card
|
||||||
|
case poll
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,10 @@ extension String {
|
||||||
let name = "\(name)[]"
|
let name = "\(name)[]"
|
||||||
return values.map { Parameter(name: name, value: $0) }
|
return values.map { Parameter(name: name, value: $0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func =>(name: String, values: [Int]) -> [Parameter] {
|
||||||
|
return name => values.map { $0.description }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Parameter: CustomStringConvertible {
|
extension Parameter: CustomStringConvertible {
|
||||||
|
|
|
@ -85,6 +85,10 @@
|
||||||
D62275A624F1C81800B82A16 /* ComposeReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62275A524F1C81800B82A16 /* ComposeReplyView.swift */; };
|
D62275A624F1C81800B82A16 /* ComposeReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62275A524F1C81800B82A16 /* ComposeReplyView.swift */; };
|
||||||
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */; };
|
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */; };
|
||||||
D62275AA24F1E01C00B82A16 /* ComposeTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62275A924F1E01C00B82A16 /* ComposeTextView.swift */; };
|
D62275AA24F1E01C00B82A16 /* ComposeTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62275A924F1E01C00B82A16 /* ComposeTextView.swift */; };
|
||||||
|
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A53C2635F5590095BD04 /* StatusPollView.swift */; };
|
||||||
|
D623A53F2635F6910095BD04 /* Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A53E2635F6910095BD04 /* Poll.swift */; };
|
||||||
|
D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A5402635FB3C0095BD04 /* PollOptionView.swift */; };
|
||||||
|
D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A542263634100095BD04 /* PollOptionCheckboxView.swift */; };
|
||||||
D625E4822588262A0074BB2B /* DraggableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */; };
|
D625E4822588262A0074BB2B /* DraggableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */; };
|
||||||
D626493323BD751600612E6E /* ShowCameraCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */; };
|
D626493323BD751600612E6E /* ShowCameraCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */; };
|
||||||
D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */; };
|
D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */; };
|
||||||
|
@ -154,6 +158,9 @@
|
||||||
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 */; };
|
||||||
|
D662AEEF263A3B880082A153 /* PollFinishedTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D662AEED263A3B880082A153 /* PollFinishedTableViewCell.swift */; };
|
||||||
|
D662AEF0263A3B880082A153 /* PollFinishedTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D662AEEE263A3B880082A153 /* PollFinishedTableViewCell.xib */; };
|
||||||
|
D662AEF2263A4BE10082A153 /* ComposePollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D662AEF1263A4BE10082A153 /* ComposePollView.swift */; };
|
||||||
D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */; };
|
D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */; };
|
||||||
D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */; };
|
D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */; };
|
||||||
D663626221360B1900C9CBA2 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626121360B1900C9CBA2 /* Preferences.swift */; };
|
D663626221360B1900C9CBA2 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626121360B1900C9CBA2 /* Preferences.swift */; };
|
||||||
|
@ -213,8 +220,8 @@
|
||||||
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */; };
|
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */; };
|
||||||
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */; };
|
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */; };
|
||||||
D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9F240C8384002843CE /* EmojiLabel.swift */; };
|
D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9F240C8384002843CE /* EmojiLabel.swift */; };
|
||||||
D6969EA4240DD28D002843CE /* UnknownNotificationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6969EA2240DD28D002843CE /* UnknownNotificationTableViewCell.xib */; };
|
|
||||||
D69CCBBF249E6EFD000AF167 /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = D69CCBBE249E6EFD000AF167 /* CrashReporter */; };
|
D69CCBBF249E6EFD000AF167 /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = D69CCBBE249E6EFD000AF167 /* CrashReporter */; };
|
||||||
|
D6A00B1D26379FC900316AD4 /* PollOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */; };
|
||||||
D6A3BC7723218E1300FD64D5 /* TimelineSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */; };
|
D6A3BC7723218E1300FD64D5 /* TimelineSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */; };
|
||||||
D6A3BC7923218E9200FD64D5 /* NotificationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7823218E9200FD64D5 /* NotificationGroup.swift */; };
|
D6A3BC7923218E9200FD64D5 /* NotificationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7823218E9200FD64D5 /* NotificationGroup.swift */; };
|
||||||
D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7A232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift */; };
|
D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7A232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift */; };
|
||||||
|
@ -269,7 +276,7 @@
|
||||||
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */; };
|
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */; };
|
||||||
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */; };
|
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */; };
|
||||||
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; };
|
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; };
|
||||||
D6C143DA253510F4007DC240 /* ComposeContentWarningTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143D9253510F4007DC240 /* ComposeContentWarningTextField.swift */; };
|
D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */; };
|
||||||
D6C143E025354E34007DC240 /* EmojiPickerCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143DF25354E34007DC240 /* EmojiPickerCollectionViewController.swift */; };
|
D6C143E025354E34007DC240 /* EmojiPickerCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143DF25354E34007DC240 /* EmojiPickerCollectionViewController.swift */; };
|
||||||
D6C143FD25354FD0007DC240 /* EmojiCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143FB25354FD0007DC240 /* EmojiCollectionViewCell.swift */; };
|
D6C143FD25354FD0007DC240 /* EmojiCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143FB25354FD0007DC240 /* EmojiCollectionViewCell.swift */; };
|
||||||
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */; };
|
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */; };
|
||||||
|
@ -454,6 +461,10 @@
|
||||||
D62275A524F1C81800B82A16 /* ComposeReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeReplyView.swift; sourceTree = "<group>"; };
|
D62275A524F1C81800B82A16 /* ComposeReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeReplyView.swift; sourceTree = "<group>"; };
|
||||||
D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeReplyContentView.swift; sourceTree = "<group>"; };
|
D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeReplyContentView.swift; sourceTree = "<group>"; };
|
||||||
D62275A924F1E01C00B82A16 /* ComposeTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeTextView.swift; sourceTree = "<group>"; };
|
D62275A924F1E01C00B82A16 /* ComposeTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeTextView.swift; sourceTree = "<group>"; };
|
||||||
|
D623A53C2635F5590095BD04 /* StatusPollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPollView.swift; sourceTree = "<group>"; };
|
||||||
|
D623A53E2635F6910095BD04 /* Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Poll.swift; sourceTree = "<group>"; };
|
||||||
|
D623A5402635FB3C0095BD04 /* PollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionView.swift; sourceTree = "<group>"; };
|
||||||
|
D623A542263634100095BD04 /* PollOptionCheckboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionCheckboxView.swift; sourceTree = "<group>"; };
|
||||||
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableTableViewCell.swift; sourceTree = "<group>"; };
|
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ShowCameraCollectionViewCell.xib; sourceTree = "<group>"; };
|
D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ShowCameraCollectionViewCell.xib; sourceTree = "<group>"; };
|
||||||
D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachmentData.swift; sourceTree = "<group>"; };
|
D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachmentData.swift; sourceTree = "<group>"; };
|
||||||
|
@ -526,6 +537,9 @@
|
||||||
D65F613523AFD65900F3CFD3 /* Ambassador.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Ambassador.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
D65F613523AFD65900F3CFD3 /* Ambassador.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Ambassador.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D65F613723AFD65D00F3CFD3 /* Embassy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Embassy.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
D65F613723AFD65D00F3CFD3 /* Embassy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Embassy.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusStateResolver.swift; sourceTree = "<group>"; };
|
D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusStateResolver.swift; sourceTree = "<group>"; };
|
||||||
|
D662AEED263A3B880082A153 /* PollFinishedTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFinishedTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
D662AEEE263A3B880082A153 /* PollFinishedTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PollFinishedTableViewCell.xib; sourceTree = "<group>"; };
|
||||||
|
D662AEF1263A4BE10082A153 /* ComposePollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposePollView.swift; sourceTree = "<group>"; };
|
||||||
D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConversationMainStatusTableViewCell.xib; sourceTree = "<group>"; };
|
D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConversationMainStatusTableViewCell.xib; sourceTree = "<group>"; };
|
||||||
D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMainStatusTableViewCell.swift; sourceTree = "<group>"; };
|
D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMainStatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D663626121360B1900C9CBA2 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
D663626121360B1900C9CBA2 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
||||||
|
@ -585,7 +599,7 @@
|
||||||
D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeSceneDelegate.swift; sourceTree = "<group>"; };
|
D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeSceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextAttachment+Emoji.swift"; sourceTree = "<group>"; };
|
D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextAttachment+Emoji.swift"; sourceTree = "<group>"; };
|
||||||
D6969E9F240C8384002843CE /* EmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiLabel.swift; sourceTree = "<group>"; };
|
D6969E9F240C8384002843CE /* EmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiLabel.swift; sourceTree = "<group>"; };
|
||||||
D6969EA2240DD28D002843CE /* UnknownNotificationTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = UnknownNotificationTableViewCell.xib; sourceTree = "<group>"; };
|
D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionsView.swift; sourceTree = "<group>"; };
|
||||||
D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSegment.swift; sourceTree = "<group>"; };
|
D6A3BC7323218C6E00FD64D5 /* TimelineSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSegment.swift; sourceTree = "<group>"; };
|
||||||
D6A3BC7823218E9200FD64D5 /* NotificationGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationGroup.swift; sourceTree = "<group>"; };
|
D6A3BC7823218E9200FD64D5 /* NotificationGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationGroup.swift; sourceTree = "<group>"; };
|
||||||
D6A3BC7A232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionNotificationGroupTableViewCell.swift; sourceTree = "<group>"; };
|
D6A3BC7A232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionNotificationGroupTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
@ -636,7 +650,7 @@
|
||||||
D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesPageViewController.swift; sourceTree = "<group>"; };
|
D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesPageViewController.swift; sourceTree = "<group>"; };
|
||||||
D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsViewController.swift; sourceTree = "<group>"; };
|
D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsViewController.swift; sourceTree = "<group>"; };
|
||||||
D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = "<group>"; };
|
D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D6C143D9253510F4007DC240 /* ComposeContentWarningTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeContentWarningTextField.swift; sourceTree = "<group>"; };
|
D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeEmojiTextField.swift; sourceTree = "<group>"; };
|
||||||
D6C143DF25354E34007DC240 /* EmojiPickerCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerCollectionViewController.swift; sourceTree = "<group>"; };
|
D6C143DF25354E34007DC240 /* EmojiPickerCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerCollectionViewController.swift; sourceTree = "<group>"; };
|
||||||
D6C143FB25354FD0007DC240 /* EmojiCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiCollectionViewCell.swift; sourceTree = "<group>"; };
|
D6C143FB25354FD0007DC240 /* EmojiCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardView.swift; sourceTree = "<group>"; };
|
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -837,6 +851,7 @@
|
||||||
D6109A062145756700432DC2 /* LoginSettings.swift */,
|
D6109A062145756700432DC2 /* LoginSettings.swift */,
|
||||||
D61099F22145688600432DC2 /* Mention.swift */,
|
D61099F22145688600432DC2 /* Mention.swift */,
|
||||||
D61099F4214568C300432DC2 /* Notification.swift */,
|
D61099F4214568C300432DC2 /* Notification.swift */,
|
||||||
|
D623A53E2635F6910095BD04 /* Poll.swift */,
|
||||||
D61099F62145693500432DC2 /* PushSubscription.swift */,
|
D61099F62145693500432DC2 /* PushSubscription.swift */,
|
||||||
D6109A022145722C00432DC2 /* RegisteredApplication.swift */,
|
D6109A022145722C00432DC2 /* RegisteredApplication.swift */,
|
||||||
D61099F82145698900432DC2 /* Relationship.swift */,
|
D61099F82145698900432DC2 /* Relationship.swift */,
|
||||||
|
@ -892,6 +907,17 @@
|
||||||
path = "Instance Cell";
|
path = "Instance Cell";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D623A53B2635F4E20095BD04 /* Poll */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D623A53C2635F5590095BD04 /* StatusPollView.swift */,
|
||||||
|
D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */,
|
||||||
|
D623A5402635FB3C0095BD04 /* PollOptionView.swift */,
|
||||||
|
D623A542263634100095BD04 /* PollOptionCheckboxView.swift */,
|
||||||
|
);
|
||||||
|
path = Poll;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D626494023C122C800612E6E /* Asset Picker */ = {
|
D626494023C122C800612E6E /* Asset Picker */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1087,13 +1113,14 @@
|
||||||
D6A57407255C53EC00674551 /* ComposeTextViewCaretScrolling.swift */,
|
D6A57407255C53EC00674551 /* ComposeTextViewCaretScrolling.swift */,
|
||||||
D62275A924F1E01C00B82A16 /* ComposeTextView.swift */,
|
D62275A924F1E01C00B82A16 /* ComposeTextView.swift */,
|
||||||
D6C99FCA24FADC91005C74D3 /* MainComposeTextView.swift */,
|
D6C99FCA24FADC91005C74D3 /* MainComposeTextView.swift */,
|
||||||
|
D662AEF1263A4BE10082A153 /* ComposePollView.swift */,
|
||||||
D622757324EDF1CD00B82A16 /* ComposeAttachmentsList.swift */,
|
D622757324EDF1CD00B82A16 /* ComposeAttachmentsList.swift */,
|
||||||
D622757924EE21D900B82A16 /* ComposeAttachmentRow.swift */,
|
D622757924EE21D900B82A16 /* ComposeAttachmentRow.swift */,
|
||||||
D622757724EE133700B82A16 /* ComposeAssetPicker.swift */,
|
D622757724EE133700B82A16 /* ComposeAssetPicker.swift */,
|
||||||
D62275A524F1C81800B82A16 /* ComposeReplyView.swift */,
|
D62275A524F1C81800B82A16 /* ComposeReplyView.swift */,
|
||||||
D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */,
|
D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */,
|
||||||
D6E4267625327FB400C02E1C /* ComposeAutocompleteView.swift */,
|
D6E4267625327FB400C02E1C /* ComposeAutocompleteView.swift */,
|
||||||
D6C143D9253510F4007DC240 /* ComposeContentWarningTextField.swift */,
|
D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */,
|
||||||
D6C143DF25354E34007DC240 /* EmojiPickerCollectionViewController.swift */,
|
D6C143DF25354E34007DC240 /* EmojiPickerCollectionViewController.swift */,
|
||||||
D6C143FB25354FD0007DC240 /* EmojiCollectionViewCell.swift */,
|
D6C143FB25354FD0007DC240 /* EmojiCollectionViewCell.swift */,
|
||||||
D670F8B52537DC890046588A /* EmojiPickerWrapper.swift */,
|
D670F8B52537DC890046588A /* EmojiPickerWrapper.swift */,
|
||||||
|
@ -1163,7 +1190,8 @@
|
||||||
D6A3BC7F2321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib */,
|
D6A3BC7F2321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib */,
|
||||||
D64BC18D23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift */,
|
D64BC18D23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift */,
|
||||||
D64BC18E23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib */,
|
D64BC18E23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib */,
|
||||||
D6969EA2240DD28D002843CE /* UnknownNotificationTableViewCell.xib */,
|
D662AEED263A3B880082A153 /* PollFinishedTableViewCell.swift */,
|
||||||
|
D662AEEE263A3B880082A153 /* PollFinishedTableViewCell.xib */,
|
||||||
);
|
);
|
||||||
path = Notifications;
|
path = Notifications;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1390,6 +1418,7 @@
|
||||||
D61959D0241E842400A37B8E /* Draft Cell */,
|
D61959D0241E842400A37B8E /* Draft Cell */,
|
||||||
D641C78A213DD926004B4513 /* Status */,
|
D641C78A213DD926004B4513 /* Status */,
|
||||||
D6C7D27B22B6EBE200071952 /* Attachments */,
|
D6C7D27B22B6EBE200071952 /* Attachments */,
|
||||||
|
D623A53B2635F4E20095BD04 /* Poll */,
|
||||||
D641C78B213DD92F004B4513 /* Profile Header */,
|
D641C78B213DD92F004B4513 /* Profile Header */,
|
||||||
D641C78C213DD937004B4513 /* Notifications */,
|
D641C78C213DD937004B4513 /* Notifications */,
|
||||||
D6A3BC872321F78000FD64D5 /* Account Cell */,
|
D6A3BC872321F78000FD64D5 /* Account Cell */,
|
||||||
|
@ -1769,7 +1798,6 @@
|
||||||
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */,
|
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */,
|
||||||
D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */,
|
D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */,
|
||||||
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */,
|
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */,
|
||||||
D6969EA4240DD28D002843CE /* UnknownNotificationTableViewCell.xib in Resources */,
|
|
||||||
D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */,
|
D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */,
|
||||||
D6093FB125BE0B01004811E6 /* TrendingHashtagTableViewCell.xib in Resources */,
|
D6093FB125BE0B01004811E6 /* TrendingHashtagTableViewCell.xib in Resources */,
|
||||||
D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */,
|
D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */,
|
||||||
|
@ -1780,6 +1808,7 @@
|
||||||
D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */,
|
D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */,
|
||||||
D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */,
|
D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */,
|
||||||
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */,
|
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */,
|
||||||
|
D662AEF0263A3B880082A153 /* PollFinishedTableViewCell.xib in Resources */,
|
||||||
D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */,
|
D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -1870,6 +1899,7 @@
|
||||||
D61099F5214568C300432DC2 /* Notification.swift in Sources */,
|
D61099F5214568C300432DC2 /* Notification.swift in Sources */,
|
||||||
D61099EF214566C000432DC2 /* Instance.swift in Sources */,
|
D61099EF214566C000432DC2 /* Instance.swift in Sources */,
|
||||||
D61099D22144B2E600432DC2 /* Body.swift in Sources */,
|
D61099D22144B2E600432DC2 /* Body.swift in Sources */,
|
||||||
|
D623A53F2635F6910095BD04 /* Poll.swift in Sources */,
|
||||||
D63569E023908A8D003DD353 /* StatusState.swift in Sources */,
|
D63569E023908A8D003DD353 /* StatusState.swift in Sources */,
|
||||||
D6109A0121456B0800432DC2 /* Hashtag.swift in Sources */,
|
D6109A0121456B0800432DC2 /* Hashtag.swift in Sources */,
|
||||||
D61099FD21456A1D00432DC2 /* SearchResults.swift in Sources */,
|
D61099FD21456A1D00432DC2 /* SearchResults.swift in Sources */,
|
||||||
|
@ -1896,6 +1926,7 @@
|
||||||
D600613E25D07E170067FAD6 /* ProfileDirectoryFilterView.swift in Sources */,
|
D600613E25D07E170067FAD6 /* ProfileDirectoryFilterView.swift in Sources */,
|
||||||
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */,
|
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */,
|
||||||
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
||||||
|
D662AEEF263A3B880082A153 /* PollFinishedTableViewCell.swift in Sources */,
|
||||||
D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */,
|
D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */,
|
||||||
D62275A624F1C81800B82A16 /* ComposeReplyView.swift in Sources */,
|
D62275A624F1C81800B82A16 /* ComposeReplyView.swift in Sources */,
|
||||||
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */,
|
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */,
|
||||||
|
@ -1952,6 +1983,7 @@
|
||||||
D63A8D0B2561C27F00D9DFFF /* ProfileStatusesViewController.swift in Sources */,
|
D63A8D0B2561C27F00D9DFFF /* ProfileStatusesViewController.swift in Sources */,
|
||||||
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */,
|
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */,
|
||||||
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */,
|
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */,
|
||||||
|
D662AEF2263A4BE10082A153 /* ComposePollView.swift in Sources */,
|
||||||
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */,
|
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */,
|
||||||
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
|
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
|
||||||
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */,
|
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */,
|
||||||
|
@ -1982,7 +2014,9 @@
|
||||||
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */,
|
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */,
|
||||||
D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */,
|
D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */,
|
||||||
D6B17255254F88B800128392 /* OppositeCollapseKeywordsView.swift in Sources */,
|
D6B17255254F88B800128392 /* OppositeCollapseKeywordsView.swift in Sources */,
|
||||||
|
D6A00B1D26379FC900316AD4 /* PollOptionsView.swift in Sources */,
|
||||||
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */,
|
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */,
|
||||||
|
D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */,
|
||||||
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */,
|
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */,
|
||||||
D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */,
|
D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */,
|
||||||
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,
|
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,
|
||||||
|
@ -1993,6 +2027,7 @@
|
||||||
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */,
|
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */,
|
||||||
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
|
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
|
||||||
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
|
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
|
||||||
|
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */,
|
||||||
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */,
|
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */,
|
||||||
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
||||||
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
||||||
|
@ -2010,7 +2045,7 @@
|
||||||
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
|
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
|
||||||
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
|
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
|
||||||
D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */,
|
D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */,
|
||||||
D6C143DA253510F4007DC240 /* ComposeContentWarningTextField.swift in Sources */,
|
D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */,
|
||||||
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */,
|
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */,
|
||||||
D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */,
|
D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */,
|
||||||
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */,
|
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */,
|
||||||
|
@ -2105,6 +2140,7 @@
|
||||||
D6A6C10525B6138A00298D0F /* StatusTablePrefetching.swift in Sources */,
|
D6A6C10525B6138A00298D0F /* StatusTablePrefetching.swift in Sources */,
|
||||||
D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */,
|
D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */,
|
||||||
D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */,
|
D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */,
|
||||||
|
D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */,
|
||||||
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */,
|
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */,
|
||||||
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */,
|
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */,
|
||||||
);
|
);
|
||||||
|
@ -2409,7 +2445,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 = 17;
|
CURRENT_PROJECT_VERSION = 18;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
@ -2438,7 +2474,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 = 17;
|
CURRENT_PROJECT_VERSION = 18;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
|
|
@ -46,6 +46,9 @@ class MastodonController: ObservableObject {
|
||||||
@Published private(set) var instance: Instance!
|
@Published private(set) var instance: Instance!
|
||||||
private(set) var customEmojis: [Emoji]?
|
private(set) var customEmojis: [Emoji]?
|
||||||
|
|
||||||
|
private var pendingOwnInstanceRequestCallbacks = [(Instance) -> Void]()
|
||||||
|
private var ownInstanceRequest: URLSessionTask?
|
||||||
|
|
||||||
var loggedIn: Bool {
|
var loggedIn: Bool {
|
||||||
accountInfo != nil
|
accountInfo != nil
|
||||||
}
|
}
|
||||||
|
@ -115,17 +118,56 @@ class MastodonController: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: this should dedup requests
|
|
||||||
func getOwnInstance(completion: ((Instance) -> Void)? = nil) {
|
func getOwnInstance(completion: ((Instance) -> Void)? = nil) {
|
||||||
|
getOwnInstanceInternal(retryAttempt: 0, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getOwnInstanceInternal(retryAttempt: Int, completion: ((Instance) -> Void)?) {
|
||||||
|
// this is main thread only to prevent concurrent access to ownInstanceRequest and pendingOwnInstanceRequestCallbacks
|
||||||
|
assert(Thread.isMainThread)
|
||||||
|
|
||||||
if let instance = self.instance {
|
if let instance = self.instance {
|
||||||
completion?(instance)
|
completion?(instance)
|
||||||
} else {
|
} else {
|
||||||
|
if let completion = completion {
|
||||||
|
pendingOwnInstanceRequestCallbacks.append(completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ownInstanceRequest == nil {
|
||||||
let request = Client.getInstance()
|
let request = Client.getInstance()
|
||||||
run(request) { (response) in
|
ownInstanceRequest = run(request) { (response) in
|
||||||
guard case let .success(instance, _) = response else { fatalError() }
|
switch response {
|
||||||
|
case .failure(_):
|
||||||
|
let delay: DispatchTimeInterval
|
||||||
|
switch retryAttempt {
|
||||||
|
case 0:
|
||||||
|
delay = .seconds(1)
|
||||||
|
case 1:
|
||||||
|
delay = .seconds(5)
|
||||||
|
case 2:
|
||||||
|
delay = .seconds(30)
|
||||||
|
case 3:
|
||||||
|
delay = .seconds(60)
|
||||||
|
default:
|
||||||
|
// if we've failed four times, just give up :/
|
||||||
|
return
|
||||||
|
}
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
||||||
|
// completion is nil because in this invocation of getOwnInstanceInternal we've already added it to the pending callbacks array
|
||||||
|
self.getOwnInstanceInternal(retryAttempt: retryAttempt + 1, completion: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
case let .success(instance, _):
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
self.ownInstanceRequest = nil
|
||||||
self.instance = instance
|
self.instance = instance
|
||||||
completion?(instance)
|
|
||||||
|
for completion in self.pendingOwnInstanceRequestCallbacks {
|
||||||
|
completion(instance)
|
||||||
|
}
|
||||||
|
self.pendingOwnInstanceRequestCallbacks = []
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,15 +65,15 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
private func upsert(status: Status, incrementReferenceCount: Bool) -> StatusMO {
|
private func upsert(status: Status, incrementReferenceCount: Bool, context: NSManagedObjectContext) -> StatusMO {
|
||||||
if let statusMO = self.status(for: status.id, in: self.backgroundContext) {
|
if let statusMO = self.status(for: status.id, in: context) {
|
||||||
statusMO.updateFrom(apiStatus: status, container: self)
|
statusMO.updateFrom(apiStatus: status, container: self)
|
||||||
if incrementReferenceCount {
|
if incrementReferenceCount {
|
||||||
statusMO.incrementReferenceCount()
|
statusMO.incrementReferenceCount()
|
||||||
}
|
}
|
||||||
return statusMO
|
return statusMO
|
||||||
} else {
|
} else {
|
||||||
let statusMO = StatusMO(apiStatus: status, container: self, context: self.backgroundContext)
|
let statusMO = StatusMO(apiStatus: status, container: self, context: context)
|
||||||
if incrementReferenceCount {
|
if incrementReferenceCount {
|
||||||
statusMO.incrementReferenceCount()
|
statusMO.incrementReferenceCount()
|
||||||
}
|
}
|
||||||
|
@ -81,11 +81,12 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addOrUpdate(status: Status, incrementReferenceCount: Bool, completion: ((StatusMO) -> Void)? = nil) {
|
func addOrUpdate(status: Status, incrementReferenceCount: Bool, context: NSManagedObjectContext? = nil, completion: ((StatusMO) -> Void)? = nil) {
|
||||||
backgroundContext.perform {
|
let context = context ?? backgroundContext
|
||||||
let statusMO = self.upsert(status: status, incrementReferenceCount: incrementReferenceCount)
|
context.perform {
|
||||||
if self.backgroundContext.hasChanges {
|
let statusMO = self.upsert(status: status, incrementReferenceCount: incrementReferenceCount, context: context)
|
||||||
try! self.backgroundContext.save()
|
if context.hasChanges {
|
||||||
|
try! context.save()
|
||||||
}
|
}
|
||||||
completion?(statusMO)
|
completion?(statusMO)
|
||||||
self.statusSubject.send(status.id)
|
self.statusSubject.send(status.id)
|
||||||
|
@ -94,7 +95,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
|
|
||||||
func addAll(statuses: [Status], completion: (() -> Void)? = nil) {
|
func addAll(statuses: [Status], completion: (() -> Void)? = nil) {
|
||||||
backgroundContext.perform {
|
backgroundContext.perform {
|
||||||
statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true) }
|
statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true, context: self.backgroundContext) }
|
||||||
if self.backgroundContext.hasChanges {
|
if self.backgroundContext.hasChanges {
|
||||||
try! self.backgroundContext.save()
|
try! self.backgroundContext.save()
|
||||||
}
|
}
|
||||||
|
@ -194,7 +195,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
// filter out mentions, otherwise we would double increment the reference count of those accounts
|
// filter out mentions, otherwise we would double increment the reference count of those accounts
|
||||||
// since the status has the same account as the notification
|
// since the status has the same account as the notification
|
||||||
let accounts = notifications.filter { $0.kind != .mention }.map { $0.account }
|
let accounts = notifications.filter { $0.kind != .mention }.map { $0.account }
|
||||||
statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true) }
|
statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true, context: self.backgroundContext) }
|
||||||
accounts.forEach { self.upsert(account: $0, incrementReferenceCount: true) }
|
accounts.forEach { self.upsert(account: $0, incrementReferenceCount: true) }
|
||||||
if self.backgroundContext.hasChanges {
|
if self.backgroundContext.hasChanges {
|
||||||
try! self.backgroundContext.save()
|
try! self.backgroundContext.save()
|
||||||
|
@ -214,7 +215,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
accounts.forEach { self.upsert(account: $0, incrementReferenceCount: true) }
|
accounts.forEach { self.upsert(account: $0, incrementReferenceCount: true) }
|
||||||
updatedAccounts.append(contentsOf: accounts.map { $0.id })
|
updatedAccounts.append(contentsOf: accounts.map { $0.id })
|
||||||
}, { (statuses) in
|
}, { (statuses) in
|
||||||
statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true) }
|
statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true, context: self.backgroundContext) }
|
||||||
updatedStatuses.append(contentsOf: statuses.map { $0.id })
|
updatedStatuses.append(contentsOf: statuses.map { $0.id })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@ public final class StatusMO: NSManagedObject, StatusProtocol {
|
||||||
@NSManaged public var uri: String // todo: are both uri and url necessary?
|
@NSManaged public var uri: String // todo: are both uri and url necessary?
|
||||||
@NSManaged public var url: URL?
|
@NSManaged public var url: URL?
|
||||||
@NSManaged private var visibilityString: String
|
@NSManaged private var visibilityString: String
|
||||||
|
@NSManaged private var pollData: Data?
|
||||||
@NSManaged public var account: AccountMO
|
@NSManaged public var account: AccountMO
|
||||||
@NSManaged public var reblog: StatusMO?
|
@NSManaged public var reblog: StatusMO?
|
||||||
|
|
||||||
|
@ -60,6 +61,9 @@ public final class StatusMO: NSManagedObject, StatusProtocol {
|
||||||
@LazilyDecoding(from: \StatusMO.cardData, fallback: nil)
|
@LazilyDecoding(from: \StatusMO.cardData, fallback: nil)
|
||||||
public var card: Card?
|
public var card: Card?
|
||||||
|
|
||||||
|
@LazilyDecoding(from: \StatusMO.pollData, fallback: nil)
|
||||||
|
public var poll: Poll?
|
||||||
|
|
||||||
public var pinned: Bool? { pinnedInternal }
|
public var pinned: Bool? { pinnedInternal }
|
||||||
public var bookmarked: Bool? { bookmarkedInternal }
|
public var bookmarked: Bool? { bookmarkedInternal }
|
||||||
|
|
||||||
|
@ -129,6 +133,8 @@ extension StatusMO {
|
||||||
self.uri = status.uri
|
self.uri = status.uri
|
||||||
self.url = status.url
|
self.url = status.url
|
||||||
self.visibility = status.visibility
|
self.visibility = status.visibility
|
||||||
|
self.poll = status.poll
|
||||||
|
|
||||||
if let existing = container.account(for: status.account.id, in: context) {
|
if let existing = container.account(for: status.account.id, in: context) {
|
||||||
existing.updateFrom(apiAccount: status.account, container: container)
|
existing.updateFrom(apiAccount: status.account, container: container)
|
||||||
self.account = existing
|
self.account = existing
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17510.1" systemVersion="19G2021" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="18154" systemVersion="20D91" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||||
<entity name="Account" representedClassName="AccountMO" syncable="YES">
|
<entity name="Account" representedClassName="AccountMO" syncable="YES">
|
||||||
<attribute name="acct" attributeType="String"/>
|
<attribute name="acct" attributeType="String"/>
|
||||||
<attribute name="avatar" attributeType="URI"/>
|
<attribute name="avatar" attributeType="URI"/>
|
||||||
|
@ -57,6 +57,7 @@
|
||||||
<attribute name="mentionsData" attributeType="Binary"/>
|
<attribute name="mentionsData" attributeType="Binary"/>
|
||||||
<attribute name="muted" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
<attribute name="muted" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
<attribute name="pinnedInternal" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
<attribute name="pinnedInternal" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="pollData" optional="YES" attributeType="Binary"/>
|
||||||
<attribute name="reblogged" attributeType="Boolean" usesScalarValueType="YES"/>
|
<attribute name="reblogged" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
<attribute name="reblogsCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="reblogsCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="referenceCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="referenceCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
@ -74,8 +75,8 @@
|
||||||
</uniquenessConstraints>
|
</uniquenessConstraints>
|
||||||
</entity>
|
</entity>
|
||||||
<elements>
|
<elements>
|
||||||
<element name="Account" positionX="169.21875" positionY="78.9609375" width="128" height="343"/>
|
<element name="Account" positionX="169.21875" positionY="78.9609375" width="128" height="329"/>
|
||||||
<element name="Relationship" positionX="63" positionY="135" width="128" height="208"/>
|
<element name="Relationship" positionX="63" positionY="135" width="128" height="208"/>
|
||||||
<element name="Status" positionX="-63" positionY="-18" width="128" height="433"/>
|
<element name="Status" positionX="-63" positionY="-18" width="128" height="434"/>
|
||||||
</elements>
|
</elements>
|
||||||
</model>
|
</model>
|
|
@ -33,6 +33,11 @@
|
||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
<key>LSApplicationCategoryType</key>
|
<key>LSApplicationCategoryType</key>
|
||||||
<string>public.app-category.social-networking</string>
|
<string>public.app-category.social-networking</string>
|
||||||
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>gopher</string>
|
||||||
|
<string>gemini</string>
|
||||||
|
</array>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
|
|
|
@ -20,13 +20,15 @@ class Draft: Codable, ObservableObject {
|
||||||
@Published var attachments: [CompositionAttachment]
|
@Published var attachments: [CompositionAttachment]
|
||||||
@Published var inReplyToID: String?
|
@Published var inReplyToID: String?
|
||||||
@Published var visibility: Status.Visibility
|
@Published var visibility: Status.Visibility
|
||||||
|
@Published var poll: Poll?
|
||||||
|
|
||||||
var initialText: String
|
var initialText: String
|
||||||
|
|
||||||
var hasContent: Bool {
|
var hasContent: Bool {
|
||||||
(!text.isEmpty && text != initialText) ||
|
(!text.isEmpty && text != initialText) ||
|
||||||
(contentWarningEnabled && !contentWarning.isEmpty) ||
|
(contentWarningEnabled && !contentWarning.isEmpty) ||
|
||||||
attachments.count > 0
|
attachments.count > 0 ||
|
||||||
|
poll?.hasContent == true
|
||||||
}
|
}
|
||||||
|
|
||||||
var textForPosting: String {
|
var textForPosting: String {
|
||||||
|
@ -46,6 +48,7 @@ class Draft: Codable, ObservableObject {
|
||||||
self.attachments = []
|
self.attachments = []
|
||||||
self.inReplyToID = nil
|
self.inReplyToID = nil
|
||||||
self.visibility = Preferences.shared.defaultPostVisibility
|
self.visibility = Preferences.shared.defaultPostVisibility
|
||||||
|
self.poll = nil
|
||||||
|
|
||||||
self.initialText = ""
|
self.initialText = ""
|
||||||
}
|
}
|
||||||
|
@ -75,6 +78,7 @@ class Draft: Codable, ObservableObject {
|
||||||
self.attachments = try container.decode([CompositionAttachment].self, forKey: .attachments)
|
self.attachments = try container.decode([CompositionAttachment].self, forKey: .attachments)
|
||||||
self.inReplyToID = try container.decode(String?.self, forKey: .inReplyToID)
|
self.inReplyToID = try container.decode(String?.self, forKey: .inReplyToID)
|
||||||
self.visibility = try container.decode(Status.Visibility.self, forKey: .visibility)
|
self.visibility = try container.decode(Status.Visibility.self, forKey: .visibility)
|
||||||
|
self.poll = try container.decode(Poll.self, forKey: .poll)
|
||||||
|
|
||||||
self.initialText = try container.decode(String.self, forKey: .initialText)
|
self.initialText = try container.decode(String.self, forKey: .initialText)
|
||||||
}
|
}
|
||||||
|
@ -92,6 +96,7 @@ class Draft: Codable, ObservableObject {
|
||||||
try container.encode(attachments, forKey: .attachments)
|
try container.encode(attachments, forKey: .attachments)
|
||||||
try container.encode(inReplyToID, forKey: .inReplyToID)
|
try container.encode(inReplyToID, forKey: .inReplyToID)
|
||||||
try container.encode(visibility, forKey: .visibility)
|
try container.encode(visibility, forKey: .visibility)
|
||||||
|
try container.encode(poll, forKey: .poll)
|
||||||
|
|
||||||
try container.encode(initialText, forKey: .initialText)
|
try container.encode(initialText, forKey: .initialText)
|
||||||
}
|
}
|
||||||
|
@ -115,11 +120,68 @@ extension Draft {
|
||||||
case attachments
|
case attachments
|
||||||
case inReplyToID
|
case inReplyToID
|
||||||
case visibility
|
case visibility
|
||||||
|
case poll
|
||||||
|
|
||||||
case initialText
|
case initialText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Draft {
|
||||||
|
class Poll: Codable, ObservableObject {
|
||||||
|
@Published var options: [Option]
|
||||||
|
@Published var multiple: Bool
|
||||||
|
@Published var duration: TimeInterval
|
||||||
|
|
||||||
|
var hasContent: Bool {
|
||||||
|
options.contains { !$0.text.isEmpty }
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.options = [Option(""), Option("")]
|
||||||
|
self.multiple = false
|
||||||
|
self.duration = 24 * 60 * 60 // 1 day
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.options = try container.decode([Option].self, forKey: .options)
|
||||||
|
self.multiple = try container.decode(Bool.self, forKey: .multiple)
|
||||||
|
self.duration = try container.decode(TimeInterval.self, forKey: .duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(options, forKey: .options)
|
||||||
|
try container.encode(multiple, forKey: .multiple)
|
||||||
|
try container.encode(duration, forKey: .duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case options
|
||||||
|
case multiple
|
||||||
|
case duration
|
||||||
|
}
|
||||||
|
|
||||||
|
class Option: Identifiable, Codable, ObservableObject {
|
||||||
|
let id = UUID()
|
||||||
|
@Published var text: String
|
||||||
|
|
||||||
|
init(_ text: String) {
|
||||||
|
self.text = text
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws {
|
||||||
|
self.text = try decoder.singleValueContainer().decode(String.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.singleValueContainer()
|
||||||
|
try container.encode(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension MastodonController {
|
extension MastodonController {
|
||||||
|
|
||||||
func createDraft(inReplyToID: String? = nil, mentioningAcct: String? = nil) -> Draft {
|
func createDraft(inReplyToID: String? = nil, mentioningAcct: String? = nil) -> Draft {
|
||||||
|
|
|
@ -58,6 +58,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
self.expandAllContentWarnings = try container.decodeIfPresent(Bool.self, forKey: .expandAllContentWarnings) ?? false
|
self.expandAllContentWarnings = try container.decodeIfPresent(Bool.self, forKey: .expandAllContentWarnings) ?? false
|
||||||
self.collapseLongPosts = try container.decodeIfPresent(Bool.self, forKey: .collapseLongPosts) ?? true
|
self.collapseLongPosts = try container.decodeIfPresent(Bool.self, forKey: .collapseLongPosts) ?? true
|
||||||
self.oppositeCollapseKeywords = try container.decodeIfPresent([String].self, forKey: .oppositeCollapseKeywords) ?? []
|
self.oppositeCollapseKeywords = try container.decodeIfPresent([String].self, forKey: .oppositeCollapseKeywords) ?? []
|
||||||
|
self.confirmBeforeReblog = try container.decodeIfPresent(Bool.self, forKey: .confirmBeforeReblog) ?? false
|
||||||
|
|
||||||
self.showFavoriteAndReblogCounts = try container.decode(Bool.self, forKey: .showFavoriteAndReblogCounts)
|
self.showFavoriteAndReblogCounts = try container.decode(Bool.self, forKey: .showFavoriteAndReblogCounts)
|
||||||
self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType)
|
self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType)
|
||||||
|
@ -91,6 +92,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
try container.encode(expandAllContentWarnings, forKey: .expandAllContentWarnings)
|
try container.encode(expandAllContentWarnings, forKey: .expandAllContentWarnings)
|
||||||
try container.encode(collapseLongPosts, forKey: .collapseLongPosts)
|
try container.encode(collapseLongPosts, forKey: .collapseLongPosts)
|
||||||
try container.encode(oppositeCollapseKeywords, forKey: .oppositeCollapseKeywords)
|
try container.encode(oppositeCollapseKeywords, forKey: .oppositeCollapseKeywords)
|
||||||
|
try container.encode(confirmBeforeReblog, forKey: .confirmBeforeReblog)
|
||||||
|
|
||||||
try container.encode(showFavoriteAndReblogCounts, forKey: .showFavoriteAndReblogCounts)
|
try container.encode(showFavoriteAndReblogCounts, forKey: .showFavoriteAndReblogCounts)
|
||||||
try container.encode(defaultNotificationsMode, forKey: .defaultNotificationsType)
|
try container.encode(defaultNotificationsMode, forKey: .defaultNotificationsType)
|
||||||
|
@ -125,6 +127,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
@Published var expandAllContentWarnings = false
|
@Published var expandAllContentWarnings = false
|
||||||
@Published var collapseLongPosts = true
|
@Published var collapseLongPosts = true
|
||||||
@Published var oppositeCollapseKeywords: [String] = []
|
@Published var oppositeCollapseKeywords: [String] = []
|
||||||
|
@Published var confirmBeforeReblog = false
|
||||||
|
|
||||||
// MARK: Digital Wellness
|
// MARK: Digital Wellness
|
||||||
@Published var showFavoriteAndReblogCounts = true
|
@Published var showFavoriteAndReblogCounts = true
|
||||||
|
@ -157,6 +160,7 @@ class Preferences: Codable, ObservableObject {
|
||||||
case expandAllContentWarnings
|
case expandAllContentWarnings
|
||||||
case collapseLongPosts
|
case collapseLongPosts
|
||||||
case oppositeCollapseKeywords
|
case oppositeCollapseKeywords
|
||||||
|
case confirmBeforeReblog
|
||||||
|
|
||||||
case showFavoriteAndReblogCounts
|
case showFavoriteAndReblogCounts
|
||||||
case defaultNotificationsType
|
case defaultNotificationsType
|
||||||
|
|
|
@ -69,6 +69,7 @@ class AssetCollectionViewController: UICollectionViewController {
|
||||||
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed))
|
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed))
|
||||||
|
|
||||||
collectionView.alwaysBounceVertical = true
|
collectionView.alwaysBounceVertical = true
|
||||||
|
collectionView.allowsMultipleSelection = true
|
||||||
|
|
||||||
collectionView.register(UINib(nibName: "AssetCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: reuseIdentifier)
|
collectionView.register(UINib(nibName: "AssetCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: reuseIdentifier)
|
||||||
collectionView.register(UINib(nibName: "ShowCameraCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: cameraReuseIdentifier)
|
collectionView.register(UINib(nibName: "ShowCameraCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: cameraReuseIdentifier)
|
||||||
|
@ -97,19 +98,6 @@ class AssetCollectionViewController: UICollectionViewController {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let options = PHFetchOptions()
|
|
||||||
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
|
|
||||||
fetchResult = fetchAssets(with: options)
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
|
||||||
snapshot.appendSections([.assets])
|
|
||||||
var items: [Item] = [.showCamera]
|
|
||||||
fetchResult.enumerateObjects { (asset, _, _) in
|
|
||||||
items.append(.asset(asset))
|
|
||||||
}
|
|
||||||
snapshot.appendItems(items)
|
|
||||||
dataSource.apply(snapshot, animatingDifferences: false)
|
|
||||||
|
|
||||||
collectionView.allowsMultipleSelection = true
|
|
||||||
setEditing(true, animated: false)
|
setEditing(true, animated: false)
|
||||||
|
|
||||||
updateItemsSelectedCount()
|
updateItemsSelectedCount()
|
||||||
|
@ -122,6 +110,12 @@ class AssetCollectionViewController: UICollectionViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
loadAssets()
|
||||||
|
}
|
||||||
|
|
||||||
override func viewWillLayoutSubviews() {
|
override func viewWillLayoutSubviews() {
|
||||||
super.viewWillLayoutSubviews()
|
super.viewWillLayoutSubviews()
|
||||||
|
|
||||||
|
@ -137,6 +131,40 @@ class AssetCollectionViewController: UICollectionViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func loadAssets() {
|
||||||
|
switch PHPhotoLibrary.authorizationStatus(for: .readWrite) {
|
||||||
|
case .notDetermined:
|
||||||
|
PHPhotoLibrary.requestAuthorization(for: .readWrite) { (_) in
|
||||||
|
self.loadAssets()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
case .restricted, .denied:
|
||||||
|
// todo: better UI for this
|
||||||
|
return
|
||||||
|
|
||||||
|
case .authorized, .limited:
|
||||||
|
// todo: show "add more" button for limited access
|
||||||
|
break
|
||||||
|
|
||||||
|
@unknown default:
|
||||||
|
// who knows, just try anyways
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
let options = PHFetchOptions()
|
||||||
|
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
|
||||||
|
fetchResult = fetchAssets(with: options)
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
|
snapshot.appendSections([.assets])
|
||||||
|
var items: [Item] = [.showCamera]
|
||||||
|
fetchResult.enumerateObjects { (asset, _, _) in
|
||||||
|
items.append(.asset(asset))
|
||||||
|
}
|
||||||
|
snapshot.appendItems(items)
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
}
|
||||||
|
|
||||||
open func fetchAssets(with options: PHFetchOptions) -> PHFetchResult<PHAsset> {
|
open func fetchAssets(with options: PHFetchOptions) -> PHFetchResult<PHAsset> {
|
||||||
return PHAsset.fetchAssets(with: options)
|
return PHAsset.fetchAssets(with: options)
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,14 @@ struct ComposeAttachmentsList: View {
|
||||||
.foregroundColor(.blue)
|
.foregroundColor(.blue)
|
||||||
.frame(height: cellHeight / 2)
|
.frame(height: cellHeight / 2)
|
||||||
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
|
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
|
||||||
|
|
||||||
|
Button(action: self.togglePoll) {
|
||||||
|
Label(draft.poll == nil ? "Add a poll" : "Remove poll", systemImage: "chart.bar.doc.horizontal")
|
||||||
|
}
|
||||||
|
.disabled(!canAddPoll)
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
.frame(height: cellHeight / 2)
|
||||||
|
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
|
||||||
}
|
}
|
||||||
.frame(height: totalListHeight)
|
.frame(height: totalListHeight)
|
||||||
.onAppear(perform: self.didAppear)
|
.onAppear(perform: self.didAppear)
|
||||||
|
@ -84,14 +92,25 @@ struct ComposeAttachmentsList: View {
|
||||||
case .pleroma:
|
case .pleroma:
|
||||||
return true
|
return true
|
||||||
case .mastodon:
|
case .mastodon:
|
||||||
return draft.attachments.count < 4 && draft.attachments.allSatisfy { $0.data.type == .image }
|
return draft.attachments.count < 4 && draft.attachments.allSatisfy { $0.data.type == .image } && draft.poll == nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var canAddPoll: Bool {
|
||||||
|
switch mastodonController.instance?.instanceType {
|
||||||
|
case nil:
|
||||||
|
return false
|
||||||
|
case .pleroma:
|
||||||
|
return true
|
||||||
|
case .mastodon:
|
||||||
|
return draft.attachments.isEmpty
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var totalListHeight: CGFloat {
|
private var totalListHeight: CGFloat {
|
||||||
let totalRowHeights = rowHeights.values.reduce(0, +)
|
let totalRowHeights = rowHeights.values.reduce(0, +)
|
||||||
let totalPadding = CGFloat(draft.attachments.count) * cellPadding
|
let totalPadding = CGFloat(draft.attachments.count) * cellPadding
|
||||||
let addButtonHeight = cellHeight + cellPadding * 2
|
let addButtonHeight = 3 * (cellHeight / 2 + cellPadding)
|
||||||
return totalRowHeights + totalPadding + addButtonHeight
|
return totalRowHeights + totalPadding + addButtonHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,6 +174,14 @@ struct ComposeAttachmentsList: View {
|
||||||
uiState.composeDrawingMode = .createNew
|
uiState.composeDrawingMode = .createNew
|
||||||
uiState.delegate?.presentComposeDrawing()
|
uiState.delegate?.presentComposeDrawing()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func togglePoll() {
|
||||||
|
UIApplication.shared.sendAction(#selector(UIView.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||||
|
|
||||||
|
withAnimation {
|
||||||
|
draft.poll = draft.poll == nil ? Draft.Poll() : nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//struct ComposeAttachmentsList_Previews: PreviewProvider {
|
//struct ComposeAttachmentsList_Previews: PreviewProvider {
|
||||||
|
|
|
@ -8,22 +8,50 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ComposeContentWarningTextField: UIViewRepresentable {
|
struct ComposeEmojiTextField: UIViewRepresentable {
|
||||||
typealias UIViewType = UITextField
|
typealias UIViewType = UITextField
|
||||||
|
|
||||||
@Binding var text: String
|
|
||||||
|
|
||||||
@EnvironmentObject private var uiState: ComposeUIState
|
@EnvironmentObject private var uiState: ComposeUIState
|
||||||
|
|
||||||
|
@Binding private var text: String
|
||||||
|
private let placeholder: String
|
||||||
|
private var didChange: ((String) -> Void)?
|
||||||
|
private var didEndEditing: (() -> Void)?
|
||||||
|
private var backgroundColor: UIColor? = nil
|
||||||
|
|
||||||
|
init(text: Binding<String>, placeholder: String) {
|
||||||
|
self._text = text
|
||||||
|
self.placeholder = placeholder
|
||||||
|
self.didChange = nil
|
||||||
|
self.didEndEditing = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func didChange(_ didChange: @escaping (String) -> Void) -> Self {
|
||||||
|
self.didChange = didChange
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func didEndEditing(_ didEndEditing: @escaping () -> Void) -> Self {
|
||||||
|
self.didEndEditing = didEndEditing
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func backgroundColor(_ color: UIColor) -> Self {
|
||||||
|
self.backgroundColor = color
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
func makeUIView(context: Context) -> UITextField {
|
func makeUIView(context: Context) -> UITextField {
|
||||||
let view = UITextField()
|
let view = UITextField()
|
||||||
|
|
||||||
view.placeholder = "Write your warning here"
|
view.placeholder = placeholder
|
||||||
view.borderStyle = .roundedRect
|
view.borderStyle = .roundedRect
|
||||||
|
|
||||||
view.delegate = context.coordinator
|
view.delegate = context.coordinator
|
||||||
view.addTarget(context.coordinator, action: #selector(Coordinator.didChange(_:)), for: .editingChanged)
|
view.addTarget(context.coordinator, action: #selector(Coordinator.didChange(_:)), for: .editingChanged)
|
||||||
|
|
||||||
|
view.backgroundColor = backgroundColor
|
||||||
|
|
||||||
context.coordinator.textField = view
|
context.coordinator.textField = view
|
||||||
context.coordinator.uiState = uiState
|
context.coordinator.uiState = uiState
|
||||||
context.coordinator.text = $text
|
context.coordinator.text = $text
|
||||||
|
@ -32,8 +60,14 @@ struct ComposeContentWarningTextField: UIViewRepresentable {
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUIView(_ uiView: UITextField, context: Context) {
|
func updateUIView(_ uiView: UITextField, context: Context) {
|
||||||
|
if context.coordinator.skipSettingTextOnNextUpdate {
|
||||||
|
context.coordinator.skipSettingTextOnNextUpdate = false
|
||||||
|
} else {
|
||||||
uiView.text = text
|
uiView.text = text
|
||||||
}
|
}
|
||||||
|
context.coordinator.didChange = didChange
|
||||||
|
context.coordinator.didEndEditing = didEndEditing
|
||||||
|
}
|
||||||
|
|
||||||
func makeCoordinator() -> Coordinator {
|
func makeCoordinator() -> Coordinator {
|
||||||
return Coordinator()
|
return Coordinator()
|
||||||
|
@ -43,9 +77,14 @@ struct ComposeContentWarningTextField: UIViewRepresentable {
|
||||||
weak var textField: UITextField?
|
weak var textField: UITextField?
|
||||||
var text: Binding<String>!
|
var text: Binding<String>!
|
||||||
var uiState: ComposeUIState!
|
var uiState: ComposeUIState!
|
||||||
|
var didChange: ((String) -> Void)?
|
||||||
|
var didEndEditing: (() -> Void)?
|
||||||
|
|
||||||
|
var skipSettingTextOnNextUpdate = false
|
||||||
|
|
||||||
@objc func didChange(_ textField: UITextField) {
|
@objc func didChange(_ textField: UITextField) {
|
||||||
text.wrappedValue = textField.text ?? ""
|
text.wrappedValue = textField.text ?? ""
|
||||||
|
didChange?(text.wrappedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
func textFieldDidBeginEditing(_ textField: UITextField) {
|
func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||||
|
@ -55,14 +94,13 @@ struct ComposeContentWarningTextField: UIViewRepresentable {
|
||||||
|
|
||||||
func textFieldDidEndEditing(_ textField: UITextField) {
|
func textFieldDidEndEditing(_ textField: UITextField) {
|
||||||
updateAutocompleteState(textField: textField)
|
updateAutocompleteState(textField: textField)
|
||||||
|
didEndEditing?()
|
||||||
}
|
}
|
||||||
|
|
||||||
func textFieldDidChangeSelection(_ textField: UITextField) {
|
func textFieldDidChangeSelection(_ textField: UITextField) {
|
||||||
// Update text binding before potentially triggering SwiftUI view update.
|
// see MainComposeTextView.Coordinator.textViewDidChangeSelection(_:)
|
||||||
// See comment in MainComposeTextView.Coordinator.textViewDidChangeSelection
|
skipSettingTextOnNextUpdate = true
|
||||||
text.wrappedValue = textField.text ?? ""
|
self.updateAutocompleteState(textField: textField)
|
||||||
|
|
||||||
updateAutocompleteState(textField: textField)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func autocomplete(with string: String) {
|
func autocomplete(with string: String) {
|
|
@ -0,0 +1,208 @@
|
||||||
|
//
|
||||||
|
// ComposePollView.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/28/21.
|
||||||
|
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ComposePollView: View {
|
||||||
|
private static let formatter: DateComponentsFormatter = {
|
||||||
|
let f = DateComponentsFormatter()
|
||||||
|
f.maximumUnitCount = 1
|
||||||
|
f.unitsStyle = .full
|
||||||
|
f.allowedUnits = [.weekOfMonth, .day, .hour, .minute]
|
||||||
|
return f
|
||||||
|
}()
|
||||||
|
|
||||||
|
@ObservedObject var draft: Draft
|
||||||
|
@ObservedObject var poll: Draft.Poll
|
||||||
|
|
||||||
|
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||||
|
|
||||||
|
@State private var duration: Duration
|
||||||
|
|
||||||
|
init(draft: Draft, poll: Draft.Poll) {
|
||||||
|
self.draft = draft
|
||||||
|
self.poll = poll
|
||||||
|
|
||||||
|
self._duration = State(initialValue: .fromTimeInterval(poll.duration) ?? .oneDay)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
Text("Poll")
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button(action: self.removePoll) {
|
||||||
|
Image(systemName: "xmark")
|
||||||
|
.imageScale(.small)
|
||||||
|
.padding(4)
|
||||||
|
}
|
||||||
|
.accentColor(buttonForegroundColor)
|
||||||
|
.background(Circle().foregroundColor(buttonBackgroundColor))
|
||||||
|
}
|
||||||
|
|
||||||
|
ForEach(Array(poll.options.enumerated()), id: \.element.id) { (e) in
|
||||||
|
ComposePollOption(poll: poll, option: e.element, optionIndex: e.offset)
|
||||||
|
}
|
||||||
|
.transition(.slide)
|
||||||
|
|
||||||
|
Button(action: self.addOption) {
|
||||||
|
Label("Add Option", systemImage: "plus")
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
// use .animation(nil) on the binding and .frame(maxWidth: .infinity) on labels so frame doesn't have a size change animation when the text changes
|
||||||
|
Picker(selection: $poll.multiple.animation(nil), label: Text(poll.multiple ? "Allow multiple choices" : "Single choice").frame(maxWidth: .infinity)) {
|
||||||
|
Text("Allow multiple choices").tag(true)
|
||||||
|
Text("Single choice").tag(false)
|
||||||
|
}
|
||||||
|
.pickerStyle(MenuPickerStyle())
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
|
||||||
|
Picker(selection: $duration.animation(nil), label: Text(verbatim: ComposePollView.formatter.string(from: duration.timeInterval)!).frame(maxWidth: .infinity)) {
|
||||||
|
ForEach(Duration.allCases, id: \.self) { (duration) in
|
||||||
|
Text(ComposePollView.formatter.string(from: duration.timeInterval)!).tag(duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(MenuPickerStyle())
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(8)
|
||||||
|
.background(
|
||||||
|
backgroundColor
|
||||||
|
.cornerRadius(10)
|
||||||
|
)
|
||||||
|
.onChange(of: duration, perform: { (value) in
|
||||||
|
poll.duration = value.timeInterval
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private var backgroundColor: Color {
|
||||||
|
// in light mode, .secondarySystemBackground has a blue-ish hue, which we don't want
|
||||||
|
colorScheme == .dark ? Color(UIColor.secondarySystemBackground) : Color(white: 0.95)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var buttonBackgroundColor: Color {
|
||||||
|
Color(white: colorScheme == .dark ? 0.1 : 0.8)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var buttonForegroundColor: Color {
|
||||||
|
Color(UIColor.label)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func removePoll() {
|
||||||
|
withAnimation {
|
||||||
|
self.draft.poll = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addOption() {
|
||||||
|
withAnimation(.easeInOut(duration: 0.25)) {
|
||||||
|
poll.options.append(Draft.Poll.Option(""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ComposePollView {
|
||||||
|
enum Duration: Hashable, Equatable, CaseIterable {
|
||||||
|
case fiveMinutes, thirtyMinutes, oneHour, sixHours, oneDay, threeDays, sevenDays
|
||||||
|
|
||||||
|
static func fromTimeInterval(_ ti: TimeInterval) -> Duration? {
|
||||||
|
for it in allCases where it.timeInterval == ti {
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeInterval: TimeInterval {
|
||||||
|
switch self {
|
||||||
|
case .fiveMinutes:
|
||||||
|
return 5 * 60
|
||||||
|
case .thirtyMinutes:
|
||||||
|
return 30 * 60
|
||||||
|
case .oneHour:
|
||||||
|
return 60 * 60
|
||||||
|
case .sixHours:
|
||||||
|
return 6 * 60 * 60
|
||||||
|
case .oneDay:
|
||||||
|
return 24 * 60 * 60
|
||||||
|
case .threeDays:
|
||||||
|
return 3 * 24 * 60 * 60
|
||||||
|
case .sevenDays:
|
||||||
|
return 7 * 24 * 60 * 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ComposePollOption: View {
|
||||||
|
@ObservedObject var poll: Draft.Poll
|
||||||
|
@ObservedObject var option: Draft.Poll.Option
|
||||||
|
let optionIndex: Int
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Checkbox(radiusFraction: poll.multiple ? 0.1 : 0.5, borderWidth: 2)
|
||||||
|
.animation(.default)
|
||||||
|
|
||||||
|
|
||||||
|
textField
|
||||||
|
|
||||||
|
Button(action: self.removeOption) {
|
||||||
|
Image(systemName: "minus.circle.fill")
|
||||||
|
}
|
||||||
|
.foregroundColor(poll.options.count == 1 ? .gray : .red)
|
||||||
|
.disabled(poll.options.count == 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var textField: some View {
|
||||||
|
var field = ComposeEmojiTextField(text: $option.text, placeholder: "Option \(optionIndex + 1)")
|
||||||
|
return field.backgroundColor(.systemBackground)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func removeOption() {
|
||||||
|
_ = withAnimation {
|
||||||
|
poll.options.remove(at: optionIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Checkbox: View {
|
||||||
|
private let radiusFraction: CGFloat
|
||||||
|
private let size: CGFloat = 20
|
||||||
|
private let innerSize: CGFloat
|
||||||
|
|
||||||
|
init(radiusFraction: CGFloat, borderWidth: CGFloat) {
|
||||||
|
self.radiusFraction = radiusFraction
|
||||||
|
self.innerSize = self.size - 2 * borderWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
Rectangle()
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.cornerRadius(radiusFraction * size)
|
||||||
|
|
||||||
|
Rectangle()
|
||||||
|
.foregroundColor(Color(UIColor.systemBackground))
|
||||||
|
.frame(width: innerSize, height: innerSize)
|
||||||
|
.cornerRadius(radiusFraction * innerSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//struct ComposePollView_Previews: PreviewProvider {
|
||||||
|
// static var previews: some View {
|
||||||
|
// ComposePollView()
|
||||||
|
// }
|
||||||
|
//}
|
|
@ -40,7 +40,7 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var postButtonEnabled: Bool {
|
var postButtonEnabled: Bool {
|
||||||
draft.hasContent && charactersRemaining >= 0 && !isPosting && !requiresAttachmentDescriptions
|
draft.hasContent && charactersRemaining >= 0 && !isPosting && !requiresAttachmentDescriptions && (draft.poll == nil || draft.poll!.options.allSatisfy { !$0.text.isEmpty })
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -101,7 +101,7 @@ struct ComposeView: View {
|
||||||
header
|
header
|
||||||
|
|
||||||
if draft.contentWarningEnabled {
|
if draft.contentWarningEnabled {
|
||||||
ComposeContentWarningTextField(text: $draft.contentWarning)
|
ComposeEmojiTextField(text: $draft.contentWarning, placeholder: "Write your warning here")
|
||||||
}
|
}
|
||||||
|
|
||||||
MainComposeTextView(
|
MainComposeTextView(
|
||||||
|
@ -109,6 +109,13 @@ struct ComposeView: View {
|
||||||
placeholder: Text("What's on your mind?")
|
placeholder: Text("What's on your mind?")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if let poll = draft.poll {
|
||||||
|
ComposePollView(draft: draft, poll: poll)
|
||||||
|
.transition(.opacity.combined(with: .asymmetric(insertion: .scale(scale: 0.5, anchor: .leading), removal: .scale(scale: 0.5, anchor: .trailing))))
|
||||||
|
.animation(.default)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
ComposeAttachmentsList(
|
ComposeAttachmentsList(
|
||||||
draft: draft
|
draft: draft
|
||||||
)
|
)
|
||||||
|
@ -213,7 +220,10 @@ struct ComposeView: View {
|
||||||
sensitive: sensitive,
|
sensitive: sensitive,
|
||||||
spoilerText: contentWarning,
|
spoilerText: contentWarning,
|
||||||
visibility: draft.visibility,
|
visibility: draft.visibility,
|
||||||
language: nil)
|
language: nil,
|
||||||
|
pollOptions: draft.poll?.options.map(\.text),
|
||||||
|
pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration),
|
||||||
|
pollMultiple: draft.poll?.multiple)
|
||||||
self.mastodonController.run(request) { (response) in
|
self.mastodonController.run(request) { (response) in
|
||||||
switch response {
|
switch response {
|
||||||
case let .failure(error):
|
case let .failure(error):
|
||||||
|
|
|
@ -135,7 +135,11 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUIView(_ uiView: UITextView, context: Context) {
|
func updateUIView(_ uiView: UITextView, context: Context) {
|
||||||
|
if context.coordinator.skipSettingTextOnNextUpdate {
|
||||||
|
context.coordinator.skipSettingTextOnNextUpdate = false
|
||||||
|
} else {
|
||||||
uiView.text = text
|
uiView.text = text
|
||||||
|
}
|
||||||
|
|
||||||
if let visibilityButton = visibilityButton {
|
if let visibilityButton = visibilityButton {
|
||||||
visibilityButton.image = UIImage(systemName: visibility.imageName)
|
visibilityButton.image = UIImage(systemName: visibility.imageName)
|
||||||
|
@ -199,6 +203,8 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
var uiState: ComposeUIState
|
var uiState: ComposeUIState
|
||||||
var caretScrollPositionAnimator: UIViewPropertyAnimator?
|
var caretScrollPositionAnimator: UIViewPropertyAnimator?
|
||||||
|
|
||||||
|
var skipSettingTextOnNextUpdate = false
|
||||||
|
|
||||||
init(text: Binding<String>, uiState: ComposeUIState, didChange: @escaping (UITextView) -> Void) {
|
init(text: Binding<String>, uiState: ComposeUIState, didChange: @escaping (UITextView) -> Void) {
|
||||||
self.text = text
|
self.text = text
|
||||||
self.didChange = didChange
|
self.didChange = didChange
|
||||||
|
@ -259,18 +265,11 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
||||||
}
|
}
|
||||||
|
|
||||||
func textViewDidChangeSelection(_ textView: UITextView) {
|
func textViewDidChangeSelection(_ textView: UITextView) {
|
||||||
// Update the value of the text binding.
|
// Setting the text view's text causes it to move the cursor to the end (though only
|
||||||
// Sometimes, when the user accepts an autocomplete suggestion from the system keyboard, the system
|
// when the text contains an emoji :/), so skip setting the text on the next SwiftUI update
|
||||||
// calls didChangeSelection before textDidChange, resulting in a loop where the updating the Tusker autocomplete
|
// that's triggered by setting the autocomplete state.
|
||||||
// state in didChangeSection (via updateAutocompleteState) triggers a new SwiftUI view update,
|
skipSettingTextOnNextUpdate = true
|
||||||
// but when that SwiftUI update is handled, the model still has the old text (from prior to accepting the autocomplete
|
self.updateAutocompleteState()
|
||||||
// suggestion), meaning the UITextView's text gets set back to whatever it was prior to the system autocomplete.
|
|
||||||
// To work around that, we also update the text binding in didChangeSelection, to ensure that, if the autocomplete state
|
|
||||||
// does change and trigger a SwiftUI update, the binding will have the correct text that was produced by the system
|
|
||||||
// autocompletion.
|
|
||||||
text.wrappedValue = textView.text ?? ""
|
|
||||||
|
|
||||||
updateAutocompleteState()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func autocomplete(with string: String) {
|
func autocomplete(with string: String) {
|
||||||
|
|
|
@ -118,7 +118,7 @@ class ProfileDirectoryFilterView: UICollectionReusableView {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func filterChanged() {
|
@objc private func filterChanged() {
|
||||||
let scope = Scope(rawValue: scope.selectedSegmentIndex)!
|
let scope = Scope(rawValue: self.scope.selectedSegmentIndex)!
|
||||||
let order = sort.selectedSegmentIndex == 0 ? DirectoryOrder.active : .new
|
let order = sort.selectedSegmentIndex == 0 ? DirectoryOrder.active : .new
|
||||||
onFilterChanged?(scope, order)
|
onFilterChanged?(scope, order)
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,11 +94,11 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
||||||
loadingVC = LoadingViewController()
|
loadingVC = LoadingViewController()
|
||||||
embedChild(loadingVC!)
|
embedChild(loadingVC!)
|
||||||
imageRequest = cache.get(url, loadOriginal: true) { [weak self] (data, image) in
|
imageRequest = cache.get(url, loadOriginal: true) { [weak self] (data, image) in
|
||||||
guard let self = self else { return }
|
guard let self = self, let image = image else { return }
|
||||||
self.imageRequest = nil
|
self.imageRequest = nil
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.loadingVC?.removeViewAndController()
|
self.loadingVC?.removeViewAndController()
|
||||||
self.createLargeImage(data: data!, image: image!, url: self.url)
|
self.createLargeImage(data: data, image: image, url: self.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -168,7 +168,13 @@ class MainSidebarViewController: UIViewController {
|
||||||
.profileDirectory,
|
.profileDirectory,
|
||||||
], toSection: .discover)
|
], toSection: .discover)
|
||||||
}
|
}
|
||||||
|
let prevSelected = collectionView.indexPathsForSelectedItems
|
||||||
|
|
||||||
dataSource.apply(snapshot, animatingDifferences: false)
|
dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
|
||||||
|
if let prevSelected = prevSelected?.first {
|
||||||
|
collectionView.selectItem(at: prevSelected, animated: false, scrollPosition: .top)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func reloadLists() {
|
private func reloadLists() {
|
||||||
|
|
|
@ -164,7 +164,10 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Switch the tab bar to focus the same item as the sidebar has selected
|
// Switch the tab bar to focus the same item as the sidebar has selected
|
||||||
switch sidebar.selectedItem! {
|
switch sidebar.selectedItem {
|
||||||
|
case nil:
|
||||||
|
break
|
||||||
|
|
||||||
case let .tab(tab):
|
case let .tab(tab):
|
||||||
// sidebar items that map 1 <-> 1 can be transferred directly
|
// sidebar items that map 1 <-> 1 can be transferred directly
|
||||||
tabBarViewController.select(tab: tab)
|
tabBarViewController.select(tab: tab)
|
||||||
|
|
|
@ -15,6 +15,7 @@ class NotificationsTableViewController: TimelineLikeTableViewController<Notifica
|
||||||
private let actionGroupCell = "actionGroupCell"
|
private let actionGroupCell = "actionGroupCell"
|
||||||
private let followGroupCell = "followGroupCell"
|
private let followGroupCell = "followGroupCell"
|
||||||
private let followRequestCell = "followRequestCell"
|
private let followRequestCell = "followRequestCell"
|
||||||
|
private let pollCell = "pollCell"
|
||||||
private let unknownCell = "unknownCell"
|
private let unknownCell = "unknownCell"
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
@ -49,6 +50,7 @@ class NotificationsTableViewController: TimelineLikeTableViewController<Notifica
|
||||||
tableView.register(UINib(nibName: "ActionNotificationGroupTableViewCell", bundle: .main), forCellReuseIdentifier: actionGroupCell)
|
tableView.register(UINib(nibName: "ActionNotificationGroupTableViewCell", bundle: .main), forCellReuseIdentifier: actionGroupCell)
|
||||||
tableView.register(UINib(nibName: "FollowNotificationGroupTableViewCell", bundle: .main), forCellReuseIdentifier: followGroupCell)
|
tableView.register(UINib(nibName: "FollowNotificationGroupTableViewCell", bundle: .main), forCellReuseIdentifier: followGroupCell)
|
||||||
tableView.register(UINib(nibName: "FollowRequestNotificationTableViewCell", bundle: .main), forCellReuseIdentifier: followRequestCell)
|
tableView.register(UINib(nibName: "FollowRequestNotificationTableViewCell", bundle: .main), forCellReuseIdentifier: followRequestCell)
|
||||||
|
tableView.register(UINib(nibName: "PollFinishedTableViewCell", bundle: .main), forCellReuseIdentifier: pollCell)
|
||||||
tableView.register(UINib(nibName: "BasicTableViewCell", bundle: .main), forCellReuseIdentifier: unknownCell)
|
tableView.register(UINib(nibName: "BasicTableViewCell", bundle: .main), forCellReuseIdentifier: unknownCell)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,6 +162,13 @@ class NotificationsTableViewController: TimelineLikeTableViewController<Notifica
|
||||||
cell.updateUI(notification: notification)
|
cell.updateUI(notification: notification)
|
||||||
return cell
|
return cell
|
||||||
|
|
||||||
|
case .poll:
|
||||||
|
guard let notification = group.notifications.first,
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: pollCell, for: indexPath) as? PollFinishedTableViewCell else { fatalError() }
|
||||||
|
cell.delegate = self
|
||||||
|
cell.updateUI(notification: notification)
|
||||||
|
return cell
|
||||||
|
|
||||||
case .unknown:
|
case .unknown:
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: unknownCell, for: indexPath)
|
let cell = tableView.dequeueReusableCell(withIdentifier: unknownCell, for: indexPath)
|
||||||
cell.textLabel!.text = NSLocalizedString("Unknown Notification", comment: "unknown notification fallback cell text")
|
cell.textLabel!.text = NSLocalizedString("Unknown Notification", comment: "unknown notification fallback cell text")
|
||||||
|
|
|
@ -13,6 +13,7 @@ struct BehaviorPrefsView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
|
untitledSection
|
||||||
linksSection
|
linksSection
|
||||||
contentWarningsSection
|
contentWarningsSection
|
||||||
}
|
}
|
||||||
|
@ -20,7 +21,15 @@ struct BehaviorPrefsView: View {
|
||||||
.navigationBarTitle(Text("Behavior"))
|
.navigationBarTitle(Text("Behavior"))
|
||||||
}
|
}
|
||||||
|
|
||||||
var linksSection: some View {
|
private var untitledSection: some View {
|
||||||
|
Section {
|
||||||
|
Toggle(isOn: $preferences.confirmBeforeReblog) {
|
||||||
|
Text("Require Confirmation Before Reblogging")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var linksSection: some View {
|
||||||
Section(header: Text("Links")) {
|
Section(header: Text("Links")) {
|
||||||
Toggle(isOn: $preferences.openLinksInApps) {
|
Toggle(isOn: $preferences.openLinksInApps) {
|
||||||
Text("Open Links in Apps")
|
Text("Open Links in Apps")
|
||||||
|
@ -34,7 +43,7 @@ struct BehaviorPrefsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentWarningsSection: some View {
|
private var contentWarningsSection: some View {
|
||||||
Section(header: Text("Content Warnings")) {
|
Section(header: Text("Content Warnings")) {
|
||||||
Toggle(isOn: $preferences.collapseLongPosts) {
|
Toggle(isOn: $preferences.collapseLongPosts) {
|
||||||
Text("Collapse Long Posts")
|
Text("Collapse Long Posts")
|
||||||
|
|
|
@ -72,7 +72,10 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
|
||||||
let request = Client.getStatuses(timeline: timeline)
|
let request = Client.getStatuses(timeline: timeline)
|
||||||
|
|
||||||
mastodonController?.run(request) { (response) in
|
mastodonController?.run(request) { (response) in
|
||||||
guard case let .success(statuses, pagination) = response else { fatalError() }
|
guard case let .success(statuses, pagination) = response else {
|
||||||
|
completion([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
self.newer = pagination?.newer
|
self.newer = pagination?.newer
|
||||||
self.older = pagination?.older
|
self.older = pagination?.older
|
||||||
|
@ -92,7 +95,10 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
|
||||||
let request = Client.getStatuses(timeline: timeline, range: older)
|
let request = Client.getStatuses(timeline: timeline, range: older)
|
||||||
|
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
guard case let .success(statuses, pagination) = response else { fatalError() }
|
guard case let .success(statuses, pagination) = response else {
|
||||||
|
completion([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
self.older = pagination?.older
|
self.older = pagination?.older
|
||||||
|
|
||||||
|
@ -111,7 +117,10 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
|
||||||
let request = Client.getStatuses(timeline: timeline, range: newer)
|
let request = Client.getStatuses(timeline: timeline, range: newer)
|
||||||
|
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
guard case let .success(statuses, pagination) = response else { fatalError() }
|
guard case let .success(statuses, pagination) = response else {
|
||||||
|
completion([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// if there are no new statuses, pagination is nil
|
// if there are no new statuses, pagination is nil
|
||||||
// if we were to then overwrite self.newer, future refreshes would fail
|
// if we were to then overwrite self.newer, future refreshes would fail
|
||||||
|
|
|
@ -176,7 +176,7 @@ extension MenuPreviewProvider {
|
||||||
|
|
||||||
if mastodonController.account != nil && mastodonController.account.id == status.account.id {
|
if mastodonController.account != nil && mastodonController.account.id == status.account.id {
|
||||||
let pinned = status.pinned ?? false
|
let pinned = status.pinned ?? false
|
||||||
actionsSection.append(createAction(identifier: "", title: pinned ? "Unpin" : "Pin", systemImageName: pinned ? "pin.slash" : "pin", handler: { [weak self] (_) in
|
actionsSection.append(createAction(identifier: "pin", title: pinned ? "Unpin" : "Pin", systemImageName: pinned ? "pin.slash" : "pin", handler: { [weak self] (_) in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
let request = (pinned ? Status.unpin : Status.pin)(status.id)
|
let request = (pinned ? Status.unpin : Status.pin)(status.id)
|
||||||
self.mastodonController?.run(request, completion: { [weak self] (response) in
|
self.mastodonController?.run(request, completion: { [weak self] (response) in
|
||||||
|
@ -188,6 +188,20 @@ extension MenuPreviewProvider {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if status.poll != nil {
|
||||||
|
actionsSection.insert(createAction(identifier: "refresh", title: "Refresh Poll", systemImageName: "arrow.clockwise", handler: { [weak self] (_) in
|
||||||
|
guard let mastodonController = self?.mastodonController else { return }
|
||||||
|
let request = Client.getStatus(id: status.id)
|
||||||
|
mastodonController.run(request, completion: { (response) in
|
||||||
|
if case let .success(status, _) = response {
|
||||||
|
// todo: this shouldn't really use the viewContext, but for some reason saving the
|
||||||
|
// backgroundContext with the new version of the status isn't updating the viewContext
|
||||||
|
mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false, context: mastodonController.persistentContainer.viewContext)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}), at: 0)
|
||||||
|
}
|
||||||
|
|
||||||
var shareSection = [
|
var shareSection = [
|
||||||
openInSafariAction(url: status.url!),
|
openInSafariAction(url: status.url!),
|
||||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
||||||
|
|
|
@ -65,11 +65,18 @@ class TimelineLikeTableViewController<Item>: EnhancedTableViewController, Refres
|
||||||
|
|
||||||
func loadInitial() {
|
func loadInitial() {
|
||||||
guard !loaded else { return }
|
guard !loaded else { return }
|
||||||
|
// set loaded immediately so we don't trigger another request while the current one is running
|
||||||
loaded = true
|
loaded = true
|
||||||
|
|
||||||
loadInitialItems() { (items) in
|
loadInitialItems() { (items) in
|
||||||
guard items.count > 0 else { return }
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
guard items.count > 0 else {
|
||||||
|
// set loaded back to false so the next time the VC appears, we try to load again
|
||||||
|
// todo: this should probably retry automatically
|
||||||
|
self.loaded = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if self.sections.count < self.headerSectionsCount() {
|
if self.sections.count < self.headerSectionsCount() {
|
||||||
self.sections.insert(contentsOf: Array(repeating: [], count: self.headerSectionsCount() - self.sections.count), at: 0)
|
self.sections.insert(contentsOf: Array(repeating: [], count: self.headerSectionsCount() - self.sections.count), at: 0)
|
||||||
}
|
}
|
||||||
|
@ -97,6 +104,8 @@ class TimelineLikeTableViewController<Item>: EnhancedTableViewController, Refres
|
||||||
return "Refresh"
|
return "Refresh"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo: these three should use Result<[Item], Client.Error> so we can differentiate between failed requests and there actually being no results
|
||||||
|
|
||||||
func loadInitialItems(completion: @escaping ([Item]) -> Void) {
|
func loadInitialItems(completion: @escaping ([Item]) -> Void) {
|
||||||
fatalError("loadInitialItems(completion:) must be implemented by subclasses")
|
fatalError("loadInitialItems(completion:) must be implemented by subclasses")
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,16 +46,26 @@ extension TuskerNavigationDelegate {
|
||||||
|
|
||||||
func selected(url: URL, allowUniversalLinks: Bool = true) {
|
func selected(url: URL, allowUniversalLinks: Bool = true) {
|
||||||
func openSafari() {
|
func openSafari() {
|
||||||
if Preferences.shared.useInAppSafari {
|
if Preferences.shared.useInAppSafari,
|
||||||
|
url.scheme == "https" || url.scheme == "http" {
|
||||||
let config = SFSafariViewController.Configuration()
|
let config = SFSafariViewController.Configuration()
|
||||||
config.entersReaderIfAvailable = Preferences.shared.inAppSafariAutomaticReaderMode
|
config.entersReaderIfAvailable = Preferences.shared.inAppSafariAutomaticReaderMode
|
||||||
present(SFSafariViewController(url: url, configuration: config), animated: true)
|
present(SFSafariViewController(url: url, configuration: config), animated: true)
|
||||||
} else {
|
} else if UIApplication.shared.canOpenURL(url) {
|
||||||
UIApplication.shared.open(url, options: [:])
|
UIApplication.shared.open(url, options: [:])
|
||||||
|
} else {
|
||||||
|
var message = "The URL could not be opened."
|
||||||
|
if let scheme = url.scheme {
|
||||||
|
message += " This can happen if you do not have an app installed for '\(scheme)://' URLs."
|
||||||
|
}
|
||||||
|
let alert = UIAlertController(title: "Invalid URL", message: message, preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
|
||||||
|
present(alert, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if allowUniversalLinks && Preferences.shared.openLinksInApps {
|
if allowUniversalLinks && Preferences.shared.openLinksInApps,
|
||||||
|
url.scheme == "https" || url.scheme == "http" {
|
||||||
UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { (success) in
|
UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { (success) in
|
||||||
if (!success) {
|
if (!success) {
|
||||||
openSafari()
|
openSafari()
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17504.1"/>
|
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17154" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17124"/>
|
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
//
|
||||||
|
// PollFinishedTableViewCell.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/28/21.
|
||||||
|
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
import SwiftSoup
|
||||||
|
|
||||||
|
class PollFinishedTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
weak var delegate: TuskerNavigationDelegate?
|
||||||
|
var mastodonController: MastodonController? { delegate?.apiController }
|
||||||
|
|
||||||
|
@IBOutlet weak var displayNameLabel: EmojiLabel!
|
||||||
|
@IBOutlet weak var timestampLabel: UILabel!
|
||||||
|
@IBOutlet weak var statusContentLabel: UILabel!
|
||||||
|
@IBOutlet weak var pollView: StatusPollView!
|
||||||
|
|
||||||
|
var notification: Pachyderm.Notification?
|
||||||
|
|
||||||
|
private var updateTimestampWorkItem: DispatchWorkItem?
|
||||||
|
|
||||||
|
func updateUI(notification: Pachyderm.Notification) {
|
||||||
|
guard let statusID = notification.status?.id,
|
||||||
|
let status = delegate?.apiController.persistentContainer.status(for: statusID),
|
||||||
|
let poll = status.poll else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.notification = notification
|
||||||
|
|
||||||
|
updateTimestamp()
|
||||||
|
|
||||||
|
displayNameLabel.text = notification.account.displayName
|
||||||
|
displayNameLabel.setEmojis(notification.account.emojis, identifier: notification.account.id)
|
||||||
|
|
||||||
|
let doc = try! SwiftSoup.parse(status.content)
|
||||||
|
statusContentLabel.text = try! doc.text()
|
||||||
|
|
||||||
|
pollView.updateUI(status: status, poll: poll)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateTimestamp() {
|
||||||
|
guard let notification = notification else { return }
|
||||||
|
|
||||||
|
timestampLabel.text = notification.createdAt.timeAgoString()
|
||||||
|
|
||||||
|
let delay: DispatchTimeInterval?
|
||||||
|
switch notification.createdAt.timeAgo().1 {
|
||||||
|
case .second:
|
||||||
|
delay = .seconds(10)
|
||||||
|
case .minute:
|
||||||
|
delay = .seconds(60)
|
||||||
|
default:
|
||||||
|
delay = nil
|
||||||
|
}
|
||||||
|
if let delay = delay {
|
||||||
|
if updateTimestampWorkItem == nil {
|
||||||
|
updateTimestampWorkItem = DispatchWorkItem { [weak self] in
|
||||||
|
self?.updateTimestamp()
|
||||||
|
}
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateTimestampWorkItem = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
|
||||||
|
updateTimestampWorkItem?.cancel()
|
||||||
|
updateTimestampWorkItem = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PollFinishedTableViewCell: SelectableTableViewCell {
|
||||||
|
func didSelectCell() {
|
||||||
|
guard let delegate = delegate,
|
||||||
|
let status = notification?.status else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let vc = delegate.conversation(mainStatusID: status.id, state: .unknown)
|
||||||
|
delegate.show(vc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PollFinishedTableViewCell: MenuPreviewProvider {
|
||||||
|
var navigationDelegate: TuskerNavigationDelegate? { delegate }
|
||||||
|
|
||||||
|
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||||
|
guard let delegate = delegate,
|
||||||
|
let statusID = notification?.status?.id,
|
||||||
|
let status = delegate.apiController.persistentContainer.status(for: statusID) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return (content: {
|
||||||
|
delegate.conversation(mainStatusID: statusID, state: .unknown)
|
||||||
|
}, actions: {
|
||||||
|
self.actionsForStatus(status, sourceView: self)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="102" id="KGk-i7-Jjw" customClass="PollFinishedTableViewCell" customModule="Tusker" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="320" height="102"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="320" height="102"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="eSw-Oo-Scy">
|
||||||
|
<rect key="frame" x="72" y="11" width="232" height="80"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="69j-GL-yd7">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="232" height="20.5"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="A poll has finished" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="9He-JX-i6Z">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="208" height="20.5"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
|
<nil key="textColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" text="2m" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Bsi-QS-utc">
|
||||||
|
<rect key="frame" x="208" y="0.0" width="24" height="20.5"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="light" pointSize="17"/>
|
||||||
|
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" text="Person" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zwM-Iw-Hob" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="24.5" width="232" height="20.5"/>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
|
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Content" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bLL-8K-VWn">
|
||||||
|
<rect key="frame" x="0.0" y="49" width="232" height="20.5"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
|
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ybA-ob-sHe" customClass="StatusPollView" customModule="Tusker" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="73.5" width="232" height="6.5"/>
|
||||||
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
|
</view>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="checkmark.square.fill" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="cqi-cV-ejs">
|
||||||
|
<rect key="frame" x="34" y="10" width="30" height="32"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="30" id="E9e-iF-rqo"/>
|
||||||
|
<constraint firstAttribute="width" constant="30" id="Efu-VP-pjH"/>
|
||||||
|
</constraints>
|
||||||
|
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="large"/>
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="cqi-cV-ejs" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="34" id="8hN-WG-IsT"/>
|
||||||
|
<constraint firstAttribute="bottomMargin" secondItem="eSw-Oo-Scy" secondAttribute="bottom" id="9Hx-wD-Rfx"/>
|
||||||
|
<constraint firstItem="eSw-Oo-Scy" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="topMargin" id="CxC-Ch-JAx"/>
|
||||||
|
<constraint firstAttribute="trailingMargin" secondItem="eSw-Oo-Scy" secondAttribute="trailing" id="OPc-Wi-cHD"/>
|
||||||
|
<constraint firstItem="cqi-cV-ejs" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="topMargin" id="jQZ-cM-UuM"/>
|
||||||
|
<constraint firstItem="eSw-Oo-Scy" firstAttribute="leading" secondItem="cqi-cV-ejs" secondAttribute="trailing" constant="8" id="wMo-VG-1DK"/>
|
||||||
|
</constraints>
|
||||||
|
</tableViewCellContentView>
|
||||||
|
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="displayNameLabel" destination="zwM-Iw-Hob" id="3VF-5X-B94"/>
|
||||||
|
<outlet property="pollView" destination="ybA-ob-sHe" id="lpi-94-dvu"/>
|
||||||
|
<outlet property="statusContentLabel" destination="bLL-8K-VWn" id="GZo-ko-eaD"/>
|
||||||
|
<outlet property="timestampLabel" destination="Bsi-QS-utc" id="ufI-re-iM2"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="-62.318840579710148" y="-22.767857142857142"/>
|
||||||
|
</tableViewCell>
|
||||||
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<image name="checkmark.square.fill" catalog="system" width="128" height="114"/>
|
||||||
|
<systemColor name="secondaryLabelColor">
|
||||||
|
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</systemColor>
|
||||||
|
<systemColor name="systemBackgroundColor">
|
||||||
|
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
</systemColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
|
@ -1,35 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16092.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
|
||||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
|
||||||
<dependencies>
|
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16082.1"/>
|
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
|
||||||
</dependencies>
|
|
||||||
<objects>
|
|
||||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
|
||||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="KGk-i7-Jjw">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
|
||||||
<subviews>
|
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Unknown Notification" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="BC8-kp-x12">
|
|
||||||
<rect key="frame" x="16" y="11.5" width="164" height="21"/>
|
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
|
||||||
<nil key="textColor"/>
|
|
||||||
<nil key="highlightedColor"/>
|
|
||||||
</label>
|
|
||||||
</subviews>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="BC8-kp-x12" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" id="MQf-82-GMw"/>
|
|
||||||
<constraint firstItem="BC8-kp-x12" firstAttribute="centerY" secondItem="H2p-sc-9uM" secondAttribute="centerY" id="gHv-BQ-CXd"/>
|
|
||||||
</constraints>
|
|
||||||
</tableViewCellContentView>
|
|
||||||
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
|
||||||
<point key="canvasLocation" x="132" y="154"/>
|
|
||||||
</tableViewCell>
|
|
||||||
</objects>
|
|
||||||
</document>
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
//
|
||||||
|
// PollOptionCheckboxView.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/25/21.
|
||||||
|
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class PollOptionCheckboxView: UIView {
|
||||||
|
|
||||||
|
var isChecked: Bool = false {
|
||||||
|
didSet {
|
||||||
|
updateStyle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var readOnly: Bool = true {
|
||||||
|
didSet {
|
||||||
|
updateStyle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var voted: Bool = false {
|
||||||
|
didSet {
|
||||||
|
updateStyle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let imageView: UIImageView
|
||||||
|
|
||||||
|
init(multiple: Bool) {
|
||||||
|
imageView = UIImageView(image: UIImage(systemName: "checkmark")!)
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
let size: CGFloat = 20
|
||||||
|
layer.cornerRadius = (multiple ? 0.1 : 0.5) * size
|
||||||
|
layer.borderWidth = 2
|
||||||
|
|
||||||
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
imageView.isHidden = true
|
||||||
|
addSubview(imageView)
|
||||||
|
|
||||||
|
updateStyle()
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
widthAnchor.constraint(equalTo: heightAnchor),
|
||||||
|
widthAnchor.constraint(equalToConstant: size),
|
||||||
|
|
||||||
|
imageView.widthAnchor.constraint(equalTo: widthAnchor, constant: -3),
|
||||||
|
imageView.heightAnchor.constraint(equalTo: heightAnchor, constant: -3),
|
||||||
|
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||||
|
imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateStyle() {
|
||||||
|
imageView.isHidden = !isChecked
|
||||||
|
if voted || readOnly {
|
||||||
|
layer.borderColor = UIColor.clear.cgColor
|
||||||
|
} else if isChecked {
|
||||||
|
layer.borderColor = tintColor.cgColor
|
||||||
|
} else {
|
||||||
|
layer.borderColor = UIColor.gray.cgColor
|
||||||
|
}
|
||||||
|
backgroundColor = isChecked && !voted ? tintColor : .clear
|
||||||
|
imageView.tintColor = voted ? .label : .white
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
//
|
||||||
|
// PollOptionView.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/25/21.
|
||||||
|
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
class PollOptionView: UIView {
|
||||||
|
|
||||||
|
private let unselectedBackgroundColor = UIColor(white: 0.5, alpha: 0.25)
|
||||||
|
|
||||||
|
let checkbox: PollOptionCheckboxView
|
||||||
|
|
||||||
|
init(poll: Poll, option: Poll.Option) {
|
||||||
|
checkbox = PollOptionCheckboxView(multiple: poll.multiple)
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
let minHeight: CGFloat = 35
|
||||||
|
layer.cornerRadius = 0.1 * minHeight
|
||||||
|
backgroundColor = unselectedBackgroundColor
|
||||||
|
|
||||||
|
checkbox.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(checkbox)
|
||||||
|
|
||||||
|
let label = EmojiLabel()
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
label.numberOfLines = 0
|
||||||
|
label.text = option.title
|
||||||
|
label.setEmojis(poll.emojis, identifier: poll.id)
|
||||||
|
addSubview(label)
|
||||||
|
|
||||||
|
let percentLabel = UILabel()
|
||||||
|
percentLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
percentLabel.text = "100%"
|
||||||
|
percentLabel.isHidden = true
|
||||||
|
addSubview(percentLabel)
|
||||||
|
|
||||||
|
if (poll.voted ?? false) || poll.effectiveExpired,
|
||||||
|
let optionVotes = option.votesCount {
|
||||||
|
let frac = poll.votesCount == 0 ? 0 : CGFloat(optionVotes) / CGFloat(poll.votesCount)
|
||||||
|
|
||||||
|
percentLabel.isHidden = false
|
||||||
|
percentLabel.text = String(format: "%.0f%%", frac * 100)
|
||||||
|
|
||||||
|
let fillView = UIView()
|
||||||
|
fillView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
fillView.backgroundColor = tintColor.withAlphaComponent(0.6)
|
||||||
|
fillView.layer.zPosition = -1
|
||||||
|
fillView.layer.cornerRadius = layer.cornerRadius
|
||||||
|
addSubview(fillView)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
fillView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
fillView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: frac),
|
||||||
|
fillView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
fillView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
let minHeightConstraint = heightAnchor.constraint(greaterThanOrEqualToConstant: minHeight)
|
||||||
|
// on the first layout, something is weird and this becomes ambiguous even though it's fine on subsequent layouts
|
||||||
|
// this keeps autolayout from complaining
|
||||||
|
minHeightConstraint.priority = .required - 1
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
minHeightConstraint,
|
||||||
|
|
||||||
|
checkbox.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||||
|
checkbox.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
|
||||||
|
|
||||||
|
label.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
label.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
label.leadingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: 8),
|
||||||
|
label.trailingAnchor.constraint(equalTo: percentLabel.leadingAnchor),
|
||||||
|
|
||||||
|
percentLabel.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
percentLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
percentLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,180 @@
|
||||||
|
//
|
||||||
|
// PollOptionsView.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/26/21.
|
||||||
|
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
class PollOptionsView: UIControl {
|
||||||
|
|
||||||
|
var checkedOptionIndices: [Int] {
|
||||||
|
options.enumerated().filter { $0.element.checkbox.isChecked }.map(\.offset)
|
||||||
|
}
|
||||||
|
var checkedOptionsChanged: (() -> Void)?
|
||||||
|
|
||||||
|
private let stack: UIStackView
|
||||||
|
private var options: [PollOptionView] = []
|
||||||
|
|
||||||
|
private var poll: Poll!
|
||||||
|
private var animator: UIViewPropertyAnimator!
|
||||||
|
private var currentSelectedOptionIndex: Int?
|
||||||
|
|
||||||
|
private let animationDuration: TimeInterval = 0.1
|
||||||
|
private let scaledTransform = CGAffineTransform(scaleX: 0.95, y: 0.95)
|
||||||
|
|
||||||
|
private let generator = UIImpactFeedbackGenerator(style: .soft)
|
||||||
|
|
||||||
|
override var isEnabled: Bool {
|
||||||
|
didSet {
|
||||||
|
options.forEach { $0.checkbox.readOnly = !isEnabled }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
stack = UIStackView()
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stack.axis = .vertical
|
||||||
|
stack.spacing = 4
|
||||||
|
addSubview(stack)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
stack.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
stack.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
stack.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
stack.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUI(poll: Poll) {
|
||||||
|
self.poll = poll
|
||||||
|
|
||||||
|
options.forEach { $0.removeFromSuperview() }
|
||||||
|
|
||||||
|
options = poll.options.enumerated().map { (index, opt) in
|
||||||
|
let optionView = PollOptionView(poll: poll, option: opt)
|
||||||
|
optionView.checkbox.readOnly = !isEnabled
|
||||||
|
optionView.checkbox.isChecked = poll.ownVotes?.contains(index) ?? false
|
||||||
|
optionView.checkbox.voted = poll.voted ?? false
|
||||||
|
stack.addArrangedSubview(optionView)
|
||||||
|
return optionView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func selectOption(_ option: PollOptionView) {
|
||||||
|
if poll.multiple {
|
||||||
|
option.checkbox.isChecked.toggle()
|
||||||
|
} else {
|
||||||
|
for opt in options {
|
||||||
|
if opt === option {
|
||||||
|
opt.checkbox.isChecked = true
|
||||||
|
} else {
|
||||||
|
opt.checkbox.isChecked = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkedOptionsChanged?()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
// don't let subviews receive touch events
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UIControl
|
||||||
|
|
||||||
|
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||||
|
guard isEnabled else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (index, view) in options.enumerated() {
|
||||||
|
if view.point(inside: touch.location(in: view), with: event) {
|
||||||
|
currentSelectedOptionIndex = index
|
||||||
|
|
||||||
|
animator = UIViewPropertyAnimator(duration: animationDuration, curve: .easeInOut) {
|
||||||
|
view.transform = self.scaledTransform
|
||||||
|
}
|
||||||
|
animator.startAnimation()
|
||||||
|
|
||||||
|
generator.impactOccurred()
|
||||||
|
generator.prepare()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||||
|
var newIndex: Int? = nil
|
||||||
|
for (index, view) in options.enumerated() {
|
||||||
|
if view.point(inside: touch.location(in: view), with: event) {
|
||||||
|
newIndex = index
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if newIndex != currentSelectedOptionIndex {
|
||||||
|
currentSelectedOptionIndex = newIndex
|
||||||
|
|
||||||
|
UIView.animate(withDuration: animationDuration, delay: 0, options: .curveEaseInOut) {
|
||||||
|
for (index, view) in self.options.enumerated() {
|
||||||
|
view.transform = index == newIndex ? self.scaledTransform : .identity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if newIndex != nil {
|
||||||
|
generator.impactOccurred()
|
||||||
|
generator.prepare()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
|
||||||
|
super.endTracking(touch, with: event)
|
||||||
|
|
||||||
|
func selectOption() {
|
||||||
|
guard let index = currentSelectedOptionIndex else { return }
|
||||||
|
let option = options[index]
|
||||||
|
animator = UIViewPropertyAnimator(duration: animationDuration, curve: .easeInOut) {
|
||||||
|
option.transform = .identity
|
||||||
|
self.selectOption(option)
|
||||||
|
}
|
||||||
|
animator.startAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
if animator.isRunning {
|
||||||
|
animator.addCompletion { (_) in
|
||||||
|
selectOption()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectOption()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func cancelTracking(with event: UIEvent?) {
|
||||||
|
super.cancelTracking(with: event)
|
||||||
|
UIView.animate(withDuration: animationDuration, delay: 0, options: .curveEaseInOut) {
|
||||||
|
for view in self.options {
|
||||||
|
view.transform = .identity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,163 @@
|
||||||
|
//
|
||||||
|
// StatusPollView.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/25/21.
|
||||||
|
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
class StatusPollView: UIView {
|
||||||
|
|
||||||
|
private static let formatter: DateComponentsFormatter = {
|
||||||
|
let f = DateComponentsFormatter()
|
||||||
|
f.includesTimeRemainingPhrase = true
|
||||||
|
f.maximumUnitCount = 1
|
||||||
|
f.unitsStyle = .full
|
||||||
|
f.allowedUnits = [.weekOfMonth, .day, .hour, .minute]
|
||||||
|
return f
|
||||||
|
}()
|
||||||
|
|
||||||
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
|
private var statusID: String!
|
||||||
|
private var poll: Poll!
|
||||||
|
|
||||||
|
private var optionsView: PollOptionsView!
|
||||||
|
private var voteButton: UIButton!
|
||||||
|
private var infoLabel: UILabel!
|
||||||
|
private var options: [PollOptionView] = []
|
||||||
|
|
||||||
|
private var canVote = true
|
||||||
|
private var animator: UIViewPropertyAnimator!
|
||||||
|
private var currentSelectedOptionIndex: Int!
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
backgroundColor = .clear
|
||||||
|
|
||||||
|
optionsView = PollOptionsView(frame: .zero)
|
||||||
|
optionsView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
optionsView.checkedOptionsChanged = self.checkedOptionsChanged
|
||||||
|
addSubview(optionsView)
|
||||||
|
|
||||||
|
infoLabel = UILabel()
|
||||||
|
infoLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
infoLabel.textColor = .secondaryLabel
|
||||||
|
infoLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .callout), size: 0)
|
||||||
|
infoLabel.adjustsFontSizeToFitWidth = true
|
||||||
|
addSubview(infoLabel)
|
||||||
|
|
||||||
|
voteButton = UIButton(type: .system)
|
||||||
|
voteButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
voteButton.addTarget(self, action: #selector(votePressed), for: .touchUpInside)
|
||||||
|
voteButton.setTitle("Vote", for: .normal)
|
||||||
|
voteButton.setTitleColor(.secondaryLabel, for: .disabled)
|
||||||
|
voteButton.titleLabel!.font = infoLabel.font
|
||||||
|
addSubview(voteButton)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
optionsView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
optionsView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
optionsView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
|
||||||
|
infoLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
infoLabel.topAnchor.constraint(equalTo: optionsView.bottomAnchor),
|
||||||
|
infoLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
infoLabel.trailingAnchor.constraint(lessThanOrEqualTo: voteButton.leadingAnchor, constant: -8),
|
||||||
|
|
||||||
|
voteButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 44),
|
||||||
|
voteButton.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
voteButton.topAnchor.constraint(equalTo: optionsView.bottomAnchor),
|
||||||
|
voteButton.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUI(status: StatusMO, poll: Poll) {
|
||||||
|
self.statusID = status.id
|
||||||
|
self.poll = poll
|
||||||
|
|
||||||
|
options.forEach { $0.removeFromSuperview() }
|
||||||
|
|
||||||
|
// poll.voted is nil if there is no user (e.g., public timeline), in which case the poll also cannot be voted upon
|
||||||
|
if (poll.voted ?? true) || poll.expired || status.account.id == mastodonController.account.id {
|
||||||
|
canVote = false
|
||||||
|
} else {
|
||||||
|
canVote = true
|
||||||
|
}
|
||||||
|
|
||||||
|
optionsView.isEnabled = canVote
|
||||||
|
optionsView.updateUI(poll: poll)
|
||||||
|
|
||||||
|
var expired = false
|
||||||
|
let expiryText: String?
|
||||||
|
if let expiresAt = poll.expiresAt {
|
||||||
|
if expiresAt > Date() {
|
||||||
|
expiryText = StatusPollView.formatter.string(from: Date(), to: expiresAt)
|
||||||
|
} else {
|
||||||
|
expired = true
|
||||||
|
expiryText = nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expiryText = "Does not expire"
|
||||||
|
}
|
||||||
|
|
||||||
|
let format = NSLocalizedString("poll votes count", comment: "poll total votes count")
|
||||||
|
infoLabel.text = String.localizedStringWithFormat(format, poll.votesCount)
|
||||||
|
if let expiryText = expiryText {
|
||||||
|
infoLabel.text! += ", \(expiryText)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if expired {
|
||||||
|
voteButton.setTitle("Expired", for: .disabled)
|
||||||
|
} else if poll.voted ?? false {
|
||||||
|
if status.account.id == mastodonController.account.id {
|
||||||
|
voteButton.setTitle("", for: .disabled)
|
||||||
|
} else {
|
||||||
|
voteButton.setTitle("Voted", for: .disabled)
|
||||||
|
}
|
||||||
|
} else if poll.multiple {
|
||||||
|
voteButton.setTitle("Select multiple", for: .disabled)
|
||||||
|
} else {
|
||||||
|
voteButton.setTitle("Select one", for: .disabled)
|
||||||
|
}
|
||||||
|
voteButton.isEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func checkedOptionsChanged() {
|
||||||
|
voteButton.isEnabled = optionsView.checkedOptionIndices.count > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func votePressed() {
|
||||||
|
optionsView.isEnabled = false
|
||||||
|
voteButton.isEnabled = false
|
||||||
|
voteButton.setTitle("Voted", for: .disabled)
|
||||||
|
|
||||||
|
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
||||||
|
|
||||||
|
let request = Poll.vote(poll.id, choices: optionsView.checkedOptionIndices)
|
||||||
|
mastodonController.run(request) { (response) in
|
||||||
|
switch response {
|
||||||
|
case let .failure(error):
|
||||||
|
fatalError("error voting in poll: \(error)")
|
||||||
|
|
||||||
|
case let .success(poll, _):
|
||||||
|
let container = self.mastodonController.persistentContainer
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard let status = container.status(for: self.statusID) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
status.poll = poll
|
||||||
|
if container.viewContext.hasChanges {
|
||||||
|
try! container.viewContext.save()
|
||||||
|
}
|
||||||
|
container.statusSubject.send(status.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ protocol StatusTableViewCellDelegate: TuskerNavigationDelegate {
|
||||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell)
|
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell)
|
||||||
}
|
}
|
||||||
|
|
||||||
class BaseStatusTableViewCell: UITableViewCell {
|
class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
|
||||||
|
|
||||||
weak var delegate: StatusTableViewCellDelegate? {
|
weak var delegate: StatusTableViewCellDelegate? {
|
||||||
didSet {
|
didSet {
|
||||||
|
@ -34,6 +34,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
@IBOutlet weak var contentTextView: StatusContentTextView!
|
@IBOutlet weak var contentTextView: StatusContentTextView!
|
||||||
@IBOutlet weak var cardView: StatusCardView!
|
@IBOutlet weak var cardView: StatusCardView!
|
||||||
@IBOutlet weak var attachmentsView: AttachmentsContainerView!
|
@IBOutlet weak var attachmentsView: AttachmentsContainerView!
|
||||||
|
@IBOutlet weak var pollView: StatusPollView!
|
||||||
@IBOutlet weak var replyButton: UIButton!
|
@IBOutlet weak var replyButton: UIButton!
|
||||||
@IBOutlet weak var favoriteButton: UIButton!
|
@IBOutlet weak var favoriteButton: UIButton!
|
||||||
@IBOutlet weak var reblogButton: UIButton!
|
@IBOutlet weak var reblogButton: UIButton!
|
||||||
|
@ -213,6 +214,14 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
// keep menu in sync with changed states e.g. bookmarked, muted
|
// keep menu in sync with changed states e.g. bookmarked, muted
|
||||||
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actionsForStatus(status, sourceView: moreButton))
|
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actionsForStatus(status, sourceView: moreButton))
|
||||||
|
|
||||||
|
if let poll = status.poll {
|
||||||
|
pollView.isHidden = false
|
||||||
|
pollView.mastodonController = mastodonController
|
||||||
|
pollView.updateUI(status: status, poll: poll)
|
||||||
|
} else {
|
||||||
|
pollView.isHidden = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI(account: AccountMO) {
|
func updateUI(account: AccountMO) {
|
||||||
|
@ -330,6 +339,15 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
showStatusAutomatically = false
|
showStatusAutomatically = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - MenuPreviewProvider
|
||||||
|
var navigationDelegate: TuskerNavigationDelegate? { return delegate }
|
||||||
|
|
||||||
|
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Interaction
|
||||||
|
|
||||||
@IBAction func collapseButtonPressed() {
|
@IBAction func collapseButtonPressed() {
|
||||||
setCollapsed(!collapsed, animated: true)
|
setCollapsed(!collapsed, animated: true)
|
||||||
delegate?.statusCellCollapsedStateChanged(self)
|
delegate?.statusCellCollapsedStateChanged(self)
|
||||||
|
@ -403,6 +421,23 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
@IBAction func reblogPressed() {
|
@IBAction func reblogPressed() {
|
||||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
|
guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
|
||||||
|
|
||||||
|
// if we are about to reblog and the user has confirmation enabled
|
||||||
|
if !reblogged,
|
||||||
|
Preferences.shared.confirmBeforeReblog {
|
||||||
|
let alert = UIAlertController(title: "Confirm Reblog", message: "Are you sure you want to reblog this post by @\(status.account.acct)?", preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
|
||||||
|
alert.addAction(UIAlertAction(title: "Reblog", style: .default) { (_) in
|
||||||
|
self.toggleReblogInternal()
|
||||||
|
})
|
||||||
|
delegate?.present(alert, animated: true)
|
||||||
|
} else {
|
||||||
|
toggleReblogInternal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func toggleReblogInternal() {
|
||||||
|
guard let status = mastodonController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
|
||||||
|
|
||||||
let oldValue = reblogged
|
let oldValue = reblogged
|
||||||
reblogged = !reblogged
|
reblogged = !reblogged
|
||||||
|
|
||||||
|
@ -431,10 +466,6 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
@objc func accountPressed() {
|
@objc func accountPressed() {
|
||||||
delegate?.selected(account: accountID)
|
delegate?.selected(account: accountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStatusCellPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BaseStatusTableViewCell: AttachmentViewDelegate {
|
extension BaseStatusTableViewCell: AttachmentViewDelegate {
|
||||||
|
@ -509,21 +540,6 @@ extension BaseStatusTableViewCell: AVPlayerViewControllerDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BaseStatusTableViewCell: MenuPreviewProvider {
|
|
||||||
var navigationDelegate: TuskerNavigationDelegate? { return delegate }
|
|
||||||
|
|
||||||
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
|
||||||
guard let mastodonController = mastodonController else { return nil }
|
|
||||||
if avatarImageView.frame.contains(location) {
|
|
||||||
return (
|
|
||||||
content: { ProfileViewController(accountID: self.accountID, mastodonController: mastodonController) },
|
|
||||||
actions: { self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return self.getStatusCellPreviewProviders(for: location, sourceViewController: sourceViewController)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension BaseStatusTableViewCell: UIDragInteractionDelegate {
|
extension BaseStatusTableViewCell: UIDragInteractionDelegate {
|
||||||
func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] {
|
func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] {
|
||||||
guard let currentAccountID = mastodonController.accountInfo?.id,
|
guard let currentAccountID = mastodonController.accountInfo?.id,
|
||||||
|
|
|
@ -36,6 +36,8 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
accessibilityElements = [profileAccessibilityElement!, contentWarningLabel!, collapseButton!, contentTextView!, totalFavoritesButton!, totalReblogsButton!, timestampAndClientLabel!, replyButton!, favoriteButton!, reblogButton!, moreButton!]
|
accessibilityElements = [profileAccessibilityElement!, contentWarningLabel!, collapseButton!, contentTextView!, totalFavoritesButton!, totalReblogsButton!, timestampAndClientLabel!, replyButton!, favoriteButton!, reblogButton!, moreButton!]
|
||||||
|
|
||||||
contentTextView.defaultFont = .systemFont(ofSize: 18)
|
contentTextView.defaultFont = .systemFont(ofSize: 18)
|
||||||
|
|
||||||
|
profileDetailContainerView.addInteraction(UIContextMenuInteraction(delegate: self))
|
||||||
}
|
}
|
||||||
|
|
||||||
override func doUpdateUI(status: StatusMO, state: StatusState) {
|
override func doUpdateUI(status: StatusMO, state: StatusState) {
|
||||||
|
@ -86,3 +88,13 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ConversationMainStatusTableViewCell: UIContextMenuInteractionDelegate {
|
||||||
|
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
|
return UIContextMenuConfiguration(identifier: nil) {
|
||||||
|
ProfileViewController(accountID: self.accountID, mastodonController: self.mastodonController)
|
||||||
|
} actionProvider: { (_) in
|
||||||
|
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17504.1"/>
|
|
||||||
<capability name="Image references" minToolsVersion="12.0"/>
|
<capability name="Image references" minToolsVersion="12.0"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
|
@ -90,26 +89,30 @@
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" verticalCompressionResistancePriority="749" scrollEnabled="NO" delaysContentTouches="NO" editable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="z0g-HN-gS0" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
|
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" verticalCompressionResistancePriority="749" scrollEnabled="NO" delaysContentTouches="NO" editable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="z0g-HN-gS0" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="124.5" width="343" height="47.5"/>
|
<rect key="frame" x="0.0" y="124.5" width="343" height="0.0"/>
|
||||||
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
|
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
|
||||||
<color key="textColor" systemColor="labelColor"/>
|
<color key="textColor" systemColor="labelColor"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="20"/>
|
<fontDescription key="fontDescription" type="system" pointSize="20"/>
|
||||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||||
</textView>
|
</textView>
|
||||||
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="QqC-GR-TLC" customClass="StatusCardView" customModule="Tusker" customModuleProvider="target">
|
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="QqC-GR-TLC" customClass="StatusCardView" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="176" width="343" height="0.0"/>
|
<rect key="frame" x="0.0" y="128.5" width="343" height="0.0"/>
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="height" priority="999" constant="65" id="Tdo-Hv-ITE"/>
|
<constraint firstAttribute="height" priority="999" constant="65" id="Tdo-Hv-ITE"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="IF9-9U-Gk0" customClass="AttachmentsContainerView" customModule="Tusker" customModuleProvider="target">
|
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="IF9-9U-Gk0" customClass="AttachmentsContainerView" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="176" width="343" height="0.0"/>
|
<rect key="frame" x="0.0" y="128.5" width="343" height="0.0"/>
|
||||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
|
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="height" secondItem="IF9-9U-Gk0" secondAttribute="width" multiplier="9:16" priority="999" id="5oh-eK-J5d"/>
|
<constraint firstAttribute="height" secondItem="IF9-9U-Gk0" secondAttribute="width" multiplier="9:16" priority="999" id="5oh-eK-J5d"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="TLv-Xu-tT1" customClass="StatusPollView" customModule="Tusker" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="132.5" width="343" height="39.5"/>
|
||||||
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
|
</view>
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ejU-sO-Og5">
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ejU-sO-Og5">
|
||||||
<rect key="frame" x="0.0" y="180" width="343" height="0.5"/>
|
<rect key="frame" x="0.0" y="180" width="343" height="0.5"/>
|
||||||
<color key="backgroundColor" systemColor="opaqueSeparatorColor"/>
|
<color key="backgroundColor" systemColor="opaqueSeparatorColor"/>
|
||||||
|
@ -221,6 +224,7 @@
|
||||||
<constraint firstItem="ejU-sO-Og5" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="biK-oQ-SLy"/>
|
<constraint firstItem="ejU-sO-Og5" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="biK-oQ-SLy"/>
|
||||||
<constraint firstItem="3Bg-XP-d13" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="iIq-gh-90O"/>
|
<constraint firstItem="3Bg-XP-d13" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="iIq-gh-90O"/>
|
||||||
<constraint firstItem="3Fp-Nj-sVj" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="kfI-WN-ouW"/>
|
<constraint firstItem="3Fp-Nj-sVj" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="kfI-WN-ouW"/>
|
||||||
|
<constraint firstItem="TLv-Xu-tT1" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="v87-hd-fd4"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</stackView>
|
</stackView>
|
||||||
</subviews>
|
</subviews>
|
||||||
|
@ -244,6 +248,7 @@
|
||||||
<outlet property="favoriteAndReblogCountStackView" destination="HZv-qj-gi6" id="jC9-cA-dXg"/>
|
<outlet property="favoriteAndReblogCountStackView" destination="HZv-qj-gi6" id="jC9-cA-dXg"/>
|
||||||
<outlet property="favoriteButton" destination="DhN-rJ-jdA" id="b2Q-ch-kSP"/>
|
<outlet property="favoriteButton" destination="DhN-rJ-jdA" id="b2Q-ch-kSP"/>
|
||||||
<outlet property="moreButton" destination="Ujo-Ap-dmK" id="2ba-5w-HDx"/>
|
<outlet property="moreButton" destination="Ujo-Ap-dmK" id="2ba-5w-HDx"/>
|
||||||
|
<outlet property="pollView" destination="TLv-Xu-tT1" id="hJX-YD-lNr"/>
|
||||||
<outlet property="profileDetailContainerView" destination="Cnd-Fj-B7l" id="wco-VB-VQx"/>
|
<outlet property="profileDetailContainerView" destination="Cnd-Fj-B7l" id="wco-VB-VQx"/>
|
||||||
<outlet property="reblogButton" destination="GUG-f7-Hdy" id="WtT-Ph-DQm"/>
|
<outlet property="reblogButton" destination="GUG-f7-Hdy" id="WtT-Ph-DQm"/>
|
||||||
<outlet property="replyButton" destination="2cc-lE-AdG" id="My8-JV-Nho"/>
|
<outlet property="replyButton" destination="2cc-lE-AdG" id="My8-JV-Nho"/>
|
||||||
|
|
|
@ -50,6 +50,8 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
replyButton.imageView!.leadingAnchor.constraint(equalTo: contentTextView.leadingAnchor).isActive = true
|
replyButton.imageView!.leadingAnchor.constraint(equalTo: contentTextView.leadingAnchor).isActive = true
|
||||||
|
|
||||||
contentTextView.defaultFont = .systemFont(ofSize: 16)
|
contentTextView.defaultFont = .systemFont(ofSize: 16)
|
||||||
|
|
||||||
|
avatarImageView.addInteraction(UIContextMenuInteraction(delegate: self))
|
||||||
}
|
}
|
||||||
|
|
||||||
override func createObserversIfNecessary() {
|
override func createObserversIfNecessary() {
|
||||||
|
@ -181,7 +183,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
reply()
|
reply()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func getStatusCellPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> BaseStatusTableViewCell.PreviewProviders? {
|
override func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> BaseStatusTableViewCell.PreviewProviders? {
|
||||||
guard let mastodonController = mastodonController,
|
guard let mastodonController = mastodonController,
|
||||||
let status = mastodonController.persistentContainer.status(for: statusID) else {
|
let status = mastodonController.persistentContainer.status(for: statusID) else {
|
||||||
return nil
|
return nil
|
||||||
|
@ -303,3 +305,13 @@ extension TimelineStatusTableViewCell: DraggableTableViewCell {
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension TimelineStatusTableViewCell: UIContextMenuInteractionDelegate {
|
||||||
|
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
|
return UIContextMenuConfiguration(identifier: nil) {
|
||||||
|
ProfileViewController(accountID: self.accountID, mastodonController: self.mastodonController)
|
||||||
|
} actionProvider: { (_) in
|
||||||
|
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17504.1"/>
|
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
@ -109,26 +108,30 @@
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" delaysContentTouches="NO" editable="NO" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="waJ-f5-LKv" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
|
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" delaysContentTouches="NO" editable="NO" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="waJ-f5-LKv" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="83" width="277" height="86.5"/>
|
<rect key="frame" x="0.0" y="83" width="277" height="82.5"/>
|
||||||
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
|
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
|
||||||
<color key="textColor" systemColor="labelColor"/>
|
<color key="textColor" systemColor="labelColor"/>
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||||
</textView>
|
</textView>
|
||||||
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LKo-VB-XWl" customClass="StatusCardView" customModule="Tusker" customModuleProvider="target">
|
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LKo-VB-XWl" customClass="StatusCardView" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="169.5" width="277" height="0.0"/>
|
<rect key="frame" x="0.0" y="167.5" width="277" height="0.0"/>
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="height" priority="999" constant="65" id="khY-jm-CPn"/>
|
<constraint firstAttribute="height" priority="999" constant="65" id="khY-jm-CPn"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
<view hidden="YES" contentMode="scaleToFill" verticalCompressionResistancePriority="1" translatesAutoresizingMaskIntoConstraints="NO" id="nbq-yr-2mA" customClass="AttachmentsContainerView" customModule="Tusker" customModuleProvider="target">
|
<view hidden="YES" contentMode="scaleToFill" verticalCompressionResistancePriority="1" translatesAutoresizingMaskIntoConstraints="NO" id="nbq-yr-2mA" customClass="AttachmentsContainerView" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="169.5" width="277" height="0.0"/>
|
<rect key="frame" x="0.0" y="167.5" width="277" height="0.0"/>
|
||||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
|
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="height" secondItem="nbq-yr-2mA" secondAttribute="width" multiplier="9:16" priority="999" id="Rvt-zs-fkd"/>
|
<constraint firstAttribute="height" secondItem="nbq-yr-2mA" secondAttribute="width" multiplier="9:16" priority="999" id="Rvt-zs-fkd"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="x3b-Zl-9F0" customClass="StatusPollView" customModule="Tusker" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="169.5" width="277" height="0.0"/>
|
||||||
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
|
</view>
|
||||||
</subviews>
|
</subviews>
|
||||||
</stackView>
|
</stackView>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="oie-wK-IpU">
|
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="oie-wK-IpU">
|
||||||
|
@ -254,6 +257,7 @@
|
||||||
<outlet property="favoriteButton" destination="x0t-TR-jJ4" id="guV-yz-Lm6"/>
|
<outlet property="favoriteButton" destination="x0t-TR-jJ4" id="guV-yz-Lm6"/>
|
||||||
<outlet property="moreButton" destination="982-J4-NGl" id="Pux-tL-aWe"/>
|
<outlet property="moreButton" destination="982-J4-NGl" id="Pux-tL-aWe"/>
|
||||||
<outlet property="pinImageView" destination="wtt-8G-Ua1" id="mE8-oe-m1l"/>
|
<outlet property="pinImageView" destination="wtt-8G-Ua1" id="mE8-oe-m1l"/>
|
||||||
|
<outlet property="pollView" destination="x3b-Zl-9F0" id="WIF-Oz-cnm"/>
|
||||||
<outlet property="reblogButton" destination="6tW-z8-Qh9" id="u2t-8D-kOn"/>
|
<outlet property="reblogButton" destination="6tW-z8-Qh9" id="u2t-8D-kOn"/>
|
||||||
<outlet property="reblogLabel" destination="lDH-50-AJZ" id="uJf-Pt-cEP"/>
|
<outlet property="reblogLabel" destination="lDH-50-AJZ" id="uJf-Pt-cEP"/>
|
||||||
<outlet property="replyButton" destination="rKF-yF-KIa" id="rka-q1-o4a"/>
|
<outlet property="replyButton" destination="rKF-yF-KIa" id="rka-q1-o4a"/>
|
||||||
|
|
|
@ -2,10 +2,26 @@
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>poll votes count</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
<string>%2$#@votes@</string>
|
||||||
|
<key>votes</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringFormatSpecTypeKey</key>
|
||||||
|
<string>NSStringPluralRuleType</string>
|
||||||
|
<key>NSStringFormatValueTypeKey</key>
|
||||||
|
<string>u</string>
|
||||||
|
<key>one</key>
|
||||||
|
<string>1 vote</string>
|
||||||
|
<key>other</key>
|
||||||
|
<string>%u votes</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
<key>trending hashtag info</key>
|
<key>trending hashtag info</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSStringLocalizedFormatKey</key>
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
<string>%#@accounts@, %#@posts@ recently</string>
|
<string>%1$#@accounts@, %2$#@posts@ recently</string>
|
||||||
<key>posts</key>
|
<key>posts</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSStringFormatSpecTypeKey</key>
|
<key>NSStringFormatSpecTypeKey</key>
|
||||||
|
|
Loading…
Reference in New Issue