Compare commits
No commits in common. "9ab95dfc431a283c342c863a684bea068ff8a31d" and "1e7bfac13ca39405b0a79cf5ca477728160d8a7b" have entirely different histories.
9ab95dfc43
...
1e7bfac13c
18
CHANGELOG.md
18
CHANGELOG.md
|
@ -1,23 +1,5 @@
|
|||
# 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)
|
||||
The main improvement this build is a complete overhaul of the Conversation screen, along with fixes for a few different crashes.
|
||||
|
||||
|
|
|
@ -298,10 +298,7 @@ public class Client {
|
|||
sensitive: Bool? = nil,
|
||||
spoilerText: String? = nil,
|
||||
visibility: Status.Visibility? = nil,
|
||||
language: String? = nil,
|
||||
pollOptions: [String]? = nil,
|
||||
pollExpiresIn: Int? = nil,
|
||||
pollMultiple: Bool? = nil) -> Request<Status> {
|
||||
language: String? = nil) -> Request<Status> {
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses", body: ParametersBody([
|
||||
"status" => text,
|
||||
"content_type" => contentType.mimeType,
|
||||
|
@ -309,10 +306,8 @@ public class Client {
|
|||
"sensitive" => sensitive,
|
||||
"spoiler_text" => spoilerText,
|
||||
"visibility" => visibility?.rawValue,
|
||||
"language" => language,
|
||||
"poll[expires_in]" => pollExpiresIn,
|
||||
"poll[multiple]" => pollMultiple,
|
||||
] + "media_ids" => media?.map { $0.id } + "poll[options]" => pollOptions))
|
||||
"language" => language
|
||||
] + "media_ids" => media?.map { $0.id }))
|
||||
}
|
||||
|
||||
// MARK: - Timelines
|
||||
|
|
|
@ -55,7 +55,6 @@ extension Notification {
|
|||
case favourite
|
||||
case follow
|
||||
case followRequest = "follow_request"
|
||||
case poll
|
||||
case unknown
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
//
|
||||
// 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,7 +37,6 @@ public final class Status: /*StatusProtocol,*/ Decodable {
|
|||
public let pinned: Bool?
|
||||
public let bookmarked: Bool?
|
||||
public let card: Card?
|
||||
public let poll: Poll?
|
||||
|
||||
public var applicationName: String? { application?.name }
|
||||
|
||||
|
@ -133,7 +132,6 @@ public final class Status: /*StatusProtocol,*/ Decodable {
|
|||
case pinned
|
||||
case bookmarked
|
||||
case card
|
||||
case poll
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -52,10 +52,6 @@ extension String {
|
|||
let name = "\(name)[]"
|
||||
return values.map { Parameter(name: name, value: $0) }
|
||||
}
|
||||
|
||||
static func =>(name: String, values: [Int]) -> [Parameter] {
|
||||
return name => values.map { $0.description }
|
||||
}
|
||||
}
|
||||
|
||||
extension Parameter: CustomStringConvertible {
|
||||
|
|
|
@ -85,10 +85,6 @@
|
|||
D62275A624F1C81800B82A16 /* ComposeReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62275A524F1C81800B82A16 /* ComposeReplyView.swift */; };
|
||||
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62275A724F1CA2800B82A16 /* ComposeReplyContentView.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 */; };
|
||||
D626493323BD751600612E6E /* ShowCameraCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D626493123BD751600612E6E /* ShowCameraCollectionViewCell.xib */; };
|
||||
D626493523BD94CE00612E6E /* CompositionAttachmentData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */; };
|
||||
|
@ -158,9 +154,6 @@
|
|||
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */; };
|
||||
D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.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 */; };
|
||||
D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */; };
|
||||
D663626221360B1900C9CBA2 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626121360B1900C9CBA2 /* Preferences.swift */; };
|
||||
|
@ -220,8 +213,8 @@
|
|||
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69693F925859A8000F4E116 /* ComposeSceneDelegate.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 */; };
|
||||
D6969EA4240DD28D002843CE /* UnknownNotificationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6969EA2240DD28D002843CE /* UnknownNotificationTableViewCell.xib */; };
|
||||
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 */; };
|
||||
D6A3BC7923218E9200FD64D5 /* NotificationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7823218E9200FD64D5 /* NotificationGroup.swift */; };
|
||||
D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A3BC7A232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift */; };
|
||||
|
@ -276,7 +269,7 @@
|
|||
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */; };
|
||||
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */; };
|
||||
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; };
|
||||
D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */; };
|
||||
D6C143DA253510F4007DC240 /* ComposeContentWarningTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143D9253510F4007DC240 /* ComposeContentWarningTextField.swift */; };
|
||||
D6C143E025354E34007DC240 /* EmojiPickerCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143DF25354E34007DC240 /* EmojiPickerCollectionViewController.swift */; };
|
||||
D6C143FD25354FD0007DC240 /* EmojiCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143FB25354FD0007DC240 /* EmojiCollectionViewCell.swift */; };
|
||||
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */; };
|
||||
|
@ -461,10 +454,6 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -537,9 +526,6 @@
|
|||
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; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -599,7 +585,7 @@
|
|||
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>"; };
|
||||
D6969E9F240C8384002843CE /* EmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiLabel.swift; sourceTree = "<group>"; };
|
||||
D6A00B1C26379FC900316AD4 /* PollOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionsView.swift; sourceTree = "<group>"; };
|
||||
D6969EA2240DD28D002843CE /* UnknownNotificationTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = UnknownNotificationTableViewCell.xib; 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>"; };
|
||||
D6A3BC7A232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionNotificationGroupTableViewCell.swift; sourceTree = "<group>"; };
|
||||
|
@ -650,7 +636,7 @@
|
|||
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>"; };
|
||||
D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeEmojiTextField.swift; sourceTree = "<group>"; };
|
||||
D6C143D9253510F4007DC240 /* ComposeContentWarningTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeContentWarningTextField.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>"; };
|
||||
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardView.swift; sourceTree = "<group>"; };
|
||||
|
@ -851,7 +837,6 @@
|
|||
D6109A062145756700432DC2 /* LoginSettings.swift */,
|
||||
D61099F22145688600432DC2 /* Mention.swift */,
|
||||
D61099F4214568C300432DC2 /* Notification.swift */,
|
||||
D623A53E2635F6910095BD04 /* Poll.swift */,
|
||||
D61099F62145693500432DC2 /* PushSubscription.swift */,
|
||||
D6109A022145722C00432DC2 /* RegisteredApplication.swift */,
|
||||
D61099F82145698900432DC2 /* Relationship.swift */,
|
||||
|
@ -907,17 +892,6 @@
|
|||
path = "Instance Cell";
|
||||
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 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1113,14 +1087,13 @@
|
|||
D6A57407255C53EC00674551 /* ComposeTextViewCaretScrolling.swift */,
|
||||
D62275A924F1E01C00B82A16 /* ComposeTextView.swift */,
|
||||
D6C99FCA24FADC91005C74D3 /* MainComposeTextView.swift */,
|
||||
D662AEF1263A4BE10082A153 /* ComposePollView.swift */,
|
||||
D622757324EDF1CD00B82A16 /* ComposeAttachmentsList.swift */,
|
||||
D622757924EE21D900B82A16 /* ComposeAttachmentRow.swift */,
|
||||
D622757724EE133700B82A16 /* ComposeAssetPicker.swift */,
|
||||
D62275A524F1C81800B82A16 /* ComposeReplyView.swift */,
|
||||
D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */,
|
||||
D6E4267625327FB400C02E1C /* ComposeAutocompleteView.swift */,
|
||||
D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */,
|
||||
D6C143D9253510F4007DC240 /* ComposeContentWarningTextField.swift */,
|
||||
D6C143DF25354E34007DC240 /* EmojiPickerCollectionViewController.swift */,
|
||||
D6C143FB25354FD0007DC240 /* EmojiCollectionViewCell.swift */,
|
||||
D670F8B52537DC890046588A /* EmojiPickerWrapper.swift */,
|
||||
|
@ -1190,8 +1163,7 @@
|
|||
D6A3BC7F2321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib */,
|
||||
D64BC18D23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift */,
|
||||
D64BC18E23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib */,
|
||||
D662AEED263A3B880082A153 /* PollFinishedTableViewCell.swift */,
|
||||
D662AEEE263A3B880082A153 /* PollFinishedTableViewCell.xib */,
|
||||
D6969EA2240DD28D002843CE /* UnknownNotificationTableViewCell.xib */,
|
||||
);
|
||||
path = Notifications;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1418,7 +1390,6 @@
|
|||
D61959D0241E842400A37B8E /* Draft Cell */,
|
||||
D641C78A213DD926004B4513 /* Status */,
|
||||
D6C7D27B22B6EBE200071952 /* Attachments */,
|
||||
D623A53B2635F4E20095BD04 /* Poll */,
|
||||
D641C78B213DD92F004B4513 /* Profile Header */,
|
||||
D641C78C213DD937004B4513 /* Notifications */,
|
||||
D6A3BC872321F78000FD64D5 /* Account Cell */,
|
||||
|
@ -1798,6 +1769,7 @@
|
|||
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */,
|
||||
D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */,
|
||||
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */,
|
||||
D6969EA4240DD28D002843CE /* UnknownNotificationTableViewCell.xib in Resources */,
|
||||
D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */,
|
||||
D6093FB125BE0B01004811E6 /* TrendingHashtagTableViewCell.xib in Resources */,
|
||||
D627FF79217E950100CC0648 /* DraftsTableViewController.xib in Resources */,
|
||||
|
@ -1808,7 +1780,6 @@
|
|||
D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */,
|
||||
D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */,
|
||||
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */,
|
||||
D662AEF0263A3B880082A153 /* PollFinishedTableViewCell.xib in Resources */,
|
||||
D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -1899,7 +1870,6 @@
|
|||
D61099F5214568C300432DC2 /* Notification.swift in Sources */,
|
||||
D61099EF214566C000432DC2 /* Instance.swift in Sources */,
|
||||
D61099D22144B2E600432DC2 /* Body.swift in Sources */,
|
||||
D623A53F2635F6910095BD04 /* Poll.swift in Sources */,
|
||||
D63569E023908A8D003DD353 /* StatusState.swift in Sources */,
|
||||
D6109A0121456B0800432DC2 /* Hashtag.swift in Sources */,
|
||||
D61099FD21456A1D00432DC2 /* SearchResults.swift in Sources */,
|
||||
|
@ -1926,7 +1896,6 @@
|
|||
D600613E25D07E170067FAD6 /* ProfileDirectoryFilterView.swift in Sources */,
|
||||
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */,
|
||||
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
||||
D662AEEF263A3B880082A153 /* PollFinishedTableViewCell.swift in Sources */,
|
||||
D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */,
|
||||
D62275A624F1C81800B82A16 /* ComposeReplyView.swift in Sources */,
|
||||
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */,
|
||||
|
@ -1983,7 +1952,6 @@
|
|||
D63A8D0B2561C27F00D9DFFF /* ProfileStatusesViewController.swift in Sources */,
|
||||
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */,
|
||||
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */,
|
||||
D662AEF2263A4BE10082A153 /* ComposePollView.swift in Sources */,
|
||||
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */,
|
||||
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
|
||||
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */,
|
||||
|
@ -2014,9 +1982,7 @@
|
|||
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */,
|
||||
D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */,
|
||||
D6B17255254F88B800128392 /* OppositeCollapseKeywordsView.swift in Sources */,
|
||||
D6A00B1D26379FC900316AD4 /* PollOptionsView.swift in Sources */,
|
||||
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */,
|
||||
D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */,
|
||||
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */,
|
||||
D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */,
|
||||
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,
|
||||
|
@ -2027,7 +1993,6 @@
|
|||
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */,
|
||||
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
|
||||
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
|
||||
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */,
|
||||
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */,
|
||||
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
||||
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
||||
|
@ -2045,7 +2010,7 @@
|
|||
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
|
||||
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
|
||||
D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */,
|
||||
D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */,
|
||||
D6C143DA253510F4007DC240 /* ComposeContentWarningTextField.swift in Sources */,
|
||||
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */,
|
||||
D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */,
|
||||
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */,
|
||||
|
@ -2140,7 +2105,6 @@
|
|||
D6A6C10525B6138A00298D0F /* StatusTablePrefetching.swift in Sources */,
|
||||
D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */,
|
||||
D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */,
|
||||
D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */,
|
||||
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */,
|
||||
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */,
|
||||
);
|
||||
|
@ -2445,7 +2409,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 18;
|
||||
CURRENT_PROJECT_VERSION = 17;
|
||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
|
@ -2474,7 +2438,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 18;
|
||||
CURRENT_PROJECT_VERSION = 17;
|
||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
|
|
|
@ -46,9 +46,6 @@ class MastodonController: ObservableObject {
|
|||
@Published private(set) var instance: Instance!
|
||||
private(set) var customEmojis: [Emoji]?
|
||||
|
||||
private var pendingOwnInstanceRequestCallbacks = [(Instance) -> Void]()
|
||||
private var ownInstanceRequest: URLSessionTask?
|
||||
|
||||
var loggedIn: Bool {
|
||||
accountInfo != nil
|
||||
}
|
||||
|
@ -118,56 +115,17 @@ class MastodonController: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
// todo: this should dedup requests
|
||||
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 {
|
||||
completion?(instance)
|
||||
} else {
|
||||
if let completion = completion {
|
||||
pendingOwnInstanceRequestCallbacks.append(completion)
|
||||
}
|
||||
|
||||
if ownInstanceRequest == nil {
|
||||
let request = Client.getInstance()
|
||||
ownInstanceRequest = run(request) { (response) in
|
||||
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 {
|
||||
self.ownInstanceRequest = nil
|
||||
self.instance = instance
|
||||
|
||||
for completion in self.pendingOwnInstanceRequestCallbacks {
|
||||
completion(instance)
|
||||
}
|
||||
self.pendingOwnInstanceRequestCallbacks = []
|
||||
}
|
||||
}
|
||||
let request = Client.getInstance()
|
||||
run(request) { (response) in
|
||||
guard case let .success(instance, _) = response else { fatalError() }
|
||||
DispatchQueue.main.async {
|
||||
self.instance = instance
|
||||
completion?(instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,15 +65,15 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
|||
}
|
||||
|
||||
@discardableResult
|
||||
private func upsert(status: Status, incrementReferenceCount: Bool, context: NSManagedObjectContext) -> StatusMO {
|
||||
if let statusMO = self.status(for: status.id, in: context) {
|
||||
private func upsert(status: Status, incrementReferenceCount: Bool) -> StatusMO {
|
||||
if let statusMO = self.status(for: status.id, in: self.backgroundContext) {
|
||||
statusMO.updateFrom(apiStatus: status, container: self)
|
||||
if incrementReferenceCount {
|
||||
statusMO.incrementReferenceCount()
|
||||
}
|
||||
return statusMO
|
||||
} else {
|
||||
let statusMO = StatusMO(apiStatus: status, container: self, context: context)
|
||||
let statusMO = StatusMO(apiStatus: status, container: self, context: self.backgroundContext)
|
||||
if incrementReferenceCount {
|
||||
statusMO.incrementReferenceCount()
|
||||
}
|
||||
|
@ -81,12 +81,11 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
|||
}
|
||||
}
|
||||
|
||||
func addOrUpdate(status: Status, incrementReferenceCount: Bool, context: NSManagedObjectContext? = nil, completion: ((StatusMO) -> Void)? = nil) {
|
||||
let context = context ?? backgroundContext
|
||||
context.perform {
|
||||
let statusMO = self.upsert(status: status, incrementReferenceCount: incrementReferenceCount, context: context)
|
||||
if context.hasChanges {
|
||||
try! context.save()
|
||||
func addOrUpdate(status: Status, incrementReferenceCount: Bool, completion: ((StatusMO) -> Void)? = nil) {
|
||||
backgroundContext.perform {
|
||||
let statusMO = self.upsert(status: status, incrementReferenceCount: incrementReferenceCount)
|
||||
if self.backgroundContext.hasChanges {
|
||||
try! self.backgroundContext.save()
|
||||
}
|
||||
completion?(statusMO)
|
||||
self.statusSubject.send(status.id)
|
||||
|
@ -95,7 +94,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
|||
|
||||
func addAll(statuses: [Status], completion: (() -> Void)? = nil) {
|
||||
backgroundContext.perform {
|
||||
statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true, context: self.backgroundContext) }
|
||||
statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true) }
|
||||
if self.backgroundContext.hasChanges {
|
||||
try! self.backgroundContext.save()
|
||||
}
|
||||
|
@ -195,7 +194,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
|||
// filter out mentions, otherwise we would double increment the reference count of those accounts
|
||||
// since the status has the same account as the notification
|
||||
let accounts = notifications.filter { $0.kind != .mention }.map { $0.account }
|
||||
statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true, context: self.backgroundContext) }
|
||||
statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true) }
|
||||
accounts.forEach { self.upsert(account: $0, incrementReferenceCount: true) }
|
||||
if self.backgroundContext.hasChanges {
|
||||
try! self.backgroundContext.save()
|
||||
|
@ -215,7 +214,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
|||
accounts.forEach { self.upsert(account: $0, incrementReferenceCount: true) }
|
||||
updatedAccounts.append(contentsOf: accounts.map { $0.id })
|
||||
}, { (statuses) in
|
||||
statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true, context: self.backgroundContext) }
|
||||
statuses.forEach { self.upsert(status: $0, incrementReferenceCount: true) }
|
||||
updatedStatuses.append(contentsOf: statuses.map { $0.id })
|
||||
})
|
||||
|
||||
|
|
|
@ -42,7 +42,6 @@ public final class StatusMO: NSManagedObject, StatusProtocol {
|
|||
@NSManaged public var uri: String // todo: are both uri and url necessary?
|
||||
@NSManaged public var url: URL?
|
||||
@NSManaged private var visibilityString: String
|
||||
@NSManaged private var pollData: Data?
|
||||
@NSManaged public var account: AccountMO
|
||||
@NSManaged public var reblog: StatusMO?
|
||||
|
||||
|
@ -61,9 +60,6 @@ public final class StatusMO: NSManagedObject, StatusProtocol {
|
|||
@LazilyDecoding(from: \StatusMO.cardData, fallback: nil)
|
||||
public var card: Card?
|
||||
|
||||
@LazilyDecoding(from: \StatusMO.pollData, fallback: nil)
|
||||
public var poll: Poll?
|
||||
|
||||
public var pinned: Bool? { pinnedInternal }
|
||||
public var bookmarked: Bool? { bookmarkedInternal }
|
||||
|
||||
|
@ -133,8 +129,6 @@ extension StatusMO {
|
|||
self.uri = status.uri
|
||||
self.url = status.url
|
||||
self.visibility = status.visibility
|
||||
self.poll = status.poll
|
||||
|
||||
if let existing = container.account(for: status.account.id, in: context) {
|
||||
existing.updateFrom(apiAccount: status.account, container: container)
|
||||
self.account = existing
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="18154" systemVersion="20D91" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17510.1" systemVersion="19G2021" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="AccountMO" syncable="YES">
|
||||
<attribute name="acct" attributeType="String"/>
|
||||
<attribute name="avatar" attributeType="URI"/>
|
||||
|
@ -57,7 +57,6 @@
|
|||
<attribute name="mentionsData" attributeType="Binary"/>
|
||||
<attribute name="muted" 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="reblogsCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="referenceCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
|
@ -75,8 +74,8 @@
|
|||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Account" positionX="169.21875" positionY="78.9609375" width="128" height="329"/>
|
||||
<element name="Account" positionX="169.21875" positionY="78.9609375" width="128" height="343"/>
|
||||
<element name="Relationship" positionX="63" positionY="135" width="128" height="208"/>
|
||||
<element name="Status" positionX="-63" positionY="-18" width="128" height="434"/>
|
||||
<element name="Status" positionX="-63" positionY="-18" width="128" height="433"/>
|
||||
</elements>
|
||||
</model>
|
|
@ -33,11 +33,6 @@
|
|||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.social-networking</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>gopher</string>
|
||||
<string>gemini</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
|
|
|
@ -20,15 +20,13 @@ class Draft: Codable, ObservableObject {
|
|||
@Published var attachments: [CompositionAttachment]
|
||||
@Published var inReplyToID: String?
|
||||
@Published var visibility: Status.Visibility
|
||||
@Published var poll: Poll?
|
||||
|
||||
var initialText: String
|
||||
|
||||
var hasContent: Bool {
|
||||
(!text.isEmpty && text != initialText) ||
|
||||
(contentWarningEnabled && !contentWarning.isEmpty) ||
|
||||
attachments.count > 0 ||
|
||||
poll?.hasContent == true
|
||||
attachments.count > 0
|
||||
}
|
||||
|
||||
var textForPosting: String {
|
||||
|
@ -48,7 +46,6 @@ class Draft: Codable, ObservableObject {
|
|||
self.attachments = []
|
||||
self.inReplyToID = nil
|
||||
self.visibility = Preferences.shared.defaultPostVisibility
|
||||
self.poll = nil
|
||||
|
||||
self.initialText = ""
|
||||
}
|
||||
|
@ -78,7 +75,6 @@ class Draft: Codable, ObservableObject {
|
|||
self.attachments = try container.decode([CompositionAttachment].self, forKey: .attachments)
|
||||
self.inReplyToID = try container.decode(String?.self, forKey: .inReplyToID)
|
||||
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)
|
||||
}
|
||||
|
@ -96,7 +92,6 @@ class Draft: Codable, ObservableObject {
|
|||
try container.encode(attachments, forKey: .attachments)
|
||||
try container.encode(inReplyToID, forKey: .inReplyToID)
|
||||
try container.encode(visibility, forKey: .visibility)
|
||||
try container.encode(poll, forKey: .poll)
|
||||
|
||||
try container.encode(initialText, forKey: .initialText)
|
||||
}
|
||||
|
@ -120,68 +115,11 @@ extension Draft {
|
|||
case attachments
|
||||
case inReplyToID
|
||||
case visibility
|
||||
case poll
|
||||
|
||||
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 {
|
||||
|
||||
func createDraft(inReplyToID: String? = nil, mentioningAcct: String? = nil) -> Draft {
|
||||
|
|
|
@ -58,7 +58,6 @@ class Preferences: Codable, ObservableObject {
|
|||
self.expandAllContentWarnings = try container.decodeIfPresent(Bool.self, forKey: .expandAllContentWarnings) ?? false
|
||||
self.collapseLongPosts = try container.decodeIfPresent(Bool.self, forKey: .collapseLongPosts) ?? true
|
||||
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.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType)
|
||||
|
@ -92,7 +91,6 @@ class Preferences: Codable, ObservableObject {
|
|||
try container.encode(expandAllContentWarnings, forKey: .expandAllContentWarnings)
|
||||
try container.encode(collapseLongPosts, forKey: .collapseLongPosts)
|
||||
try container.encode(oppositeCollapseKeywords, forKey: .oppositeCollapseKeywords)
|
||||
try container.encode(confirmBeforeReblog, forKey: .confirmBeforeReblog)
|
||||
|
||||
try container.encode(showFavoriteAndReblogCounts, forKey: .showFavoriteAndReblogCounts)
|
||||
try container.encode(defaultNotificationsMode, forKey: .defaultNotificationsType)
|
||||
|
@ -127,7 +125,6 @@ class Preferences: Codable, ObservableObject {
|
|||
@Published var expandAllContentWarnings = false
|
||||
@Published var collapseLongPosts = true
|
||||
@Published var oppositeCollapseKeywords: [String] = []
|
||||
@Published var confirmBeforeReblog = false
|
||||
|
||||
// MARK: Digital Wellness
|
||||
@Published var showFavoriteAndReblogCounts = true
|
||||
|
@ -160,7 +157,6 @@ class Preferences: Codable, ObservableObject {
|
|||
case expandAllContentWarnings
|
||||
case collapseLongPosts
|
||||
case oppositeCollapseKeywords
|
||||
case confirmBeforeReblog
|
||||
|
||||
case showFavoriteAndReblogCounts
|
||||
case defaultNotificationsType
|
||||
|
|
|
@ -69,7 +69,6 @@ class AssetCollectionViewController: UICollectionViewController {
|
|||
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed))
|
||||
|
||||
collectionView.alwaysBounceVertical = true
|
||||
collectionView.allowsMultipleSelection = true
|
||||
|
||||
collectionView.register(UINib(nibName: "AssetCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: reuseIdentifier)
|
||||
collectionView.register(UINib(nibName: "ShowCameraCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: cameraReuseIdentifier)
|
||||
|
@ -98,6 +97,19 @@ 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)
|
||||
|
||||
updateItemsSelectedCount()
|
||||
|
@ -110,12 +122,6 @@ class AssetCollectionViewController: UICollectionViewController {
|
|||
}
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
loadAssets()
|
||||
}
|
||||
|
||||
override func viewWillLayoutSubviews() {
|
||||
super.viewWillLayoutSubviews()
|
||||
|
||||
|
@ -131,40 +137,6 @@ 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> {
|
||||
return PHAsset.fetchAssets(with: options)
|
||||
}
|
||||
|
|
|
@ -60,14 +60,6 @@ struct ComposeAttachmentsList: View {
|
|||
.foregroundColor(.blue)
|
||||
.frame(height: cellHeight / 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)
|
||||
.onAppear(perform: self.didAppear)
|
||||
|
@ -92,25 +84,14 @@ struct ComposeAttachmentsList: View {
|
|||
case .pleroma:
|
||||
return true
|
||||
case .mastodon:
|
||||
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
|
||||
return draft.attachments.count < 4 && draft.attachments.allSatisfy { $0.data.type == .image }
|
||||
}
|
||||
}
|
||||
|
||||
private var totalListHeight: CGFloat {
|
||||
let totalRowHeights = rowHeights.values.reduce(0, +)
|
||||
let totalPadding = CGFloat(draft.attachments.count) * cellPadding
|
||||
let addButtonHeight = 3 * (cellHeight / 2 + cellPadding)
|
||||
let addButtonHeight = cellHeight + cellPadding * 2
|
||||
return totalRowHeights + totalPadding + addButtonHeight
|
||||
}
|
||||
|
||||
|
@ -174,14 +155,6 @@ struct ComposeAttachmentsList: View {
|
|||
uiState.composeDrawingMode = .createNew
|
||||
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 {
|
||||
|
|
|
@ -8,50 +8,22 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct ComposeEmojiTextField: UIViewRepresentable {
|
||||
struct ComposeContentWarningTextField: UIViewRepresentable {
|
||||
typealias UIViewType = UITextField
|
||||
|
||||
@Binding var text: String
|
||||
|
||||
@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 {
|
||||
let view = UITextField()
|
||||
|
||||
view.placeholder = placeholder
|
||||
view.placeholder = "Write your warning here"
|
||||
view.borderStyle = .roundedRect
|
||||
|
||||
view.delegate = context.coordinator
|
||||
view.addTarget(context.coordinator, action: #selector(Coordinator.didChange(_:)), for: .editingChanged)
|
||||
|
||||
view.backgroundColor = backgroundColor
|
||||
|
||||
context.coordinator.textField = view
|
||||
context.coordinator.uiState = uiState
|
||||
context.coordinator.text = $text
|
||||
|
@ -60,13 +32,7 @@ struct ComposeEmojiTextField: UIViewRepresentable {
|
|||
}
|
||||
|
||||
func updateUIView(_ uiView: UITextField, context: Context) {
|
||||
if context.coordinator.skipSettingTextOnNextUpdate {
|
||||
context.coordinator.skipSettingTextOnNextUpdate = false
|
||||
} else {
|
||||
uiView.text = text
|
||||
}
|
||||
context.coordinator.didChange = didChange
|
||||
context.coordinator.didEndEditing = didEndEditing
|
||||
uiView.text = text
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
|
@ -77,14 +43,9 @@ struct ComposeEmojiTextField: UIViewRepresentable {
|
|||
weak var textField: UITextField?
|
||||
var text: Binding<String>!
|
||||
var uiState: ComposeUIState!
|
||||
var didChange: ((String) -> Void)?
|
||||
var didEndEditing: (() -> Void)?
|
||||
|
||||
var skipSettingTextOnNextUpdate = false
|
||||
|
||||
@objc func didChange(_ textField: UITextField) {
|
||||
text.wrappedValue = textField.text ?? ""
|
||||
didChange?(text.wrappedValue)
|
||||
}
|
||||
|
||||
func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||
|
@ -94,13 +55,14 @@ struct ComposeEmojiTextField: UIViewRepresentable {
|
|||
|
||||
func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
updateAutocompleteState(textField: textField)
|
||||
didEndEditing?()
|
||||
}
|
||||
|
||||
func textFieldDidChangeSelection(_ textField: UITextField) {
|
||||
// see MainComposeTextView.Coordinator.textViewDidChangeSelection(_:)
|
||||
skipSettingTextOnNextUpdate = true
|
||||
self.updateAutocompleteState(textField: textField)
|
||||
// Update text binding before potentially triggering SwiftUI view update.
|
||||
// See comment in MainComposeTextView.Coordinator.textViewDidChangeSelection
|
||||
text.wrappedValue = textField.text ?? ""
|
||||
|
||||
updateAutocompleteState(textField: textField)
|
||||
}
|
||||
|
||||
func autocomplete(with string: String) {
|
|
@ -1,208 +0,0 @@
|
|||
//
|
||||
// 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 {
|
||||
draft.hasContent && charactersRemaining >= 0 && !isPosting && !requiresAttachmentDescriptions && (draft.poll == nil || draft.poll!.options.allSatisfy { !$0.text.isEmpty })
|
||||
draft.hasContent && charactersRemaining >= 0 && !isPosting && !requiresAttachmentDescriptions
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
@ -101,7 +101,7 @@ struct ComposeView: View {
|
|||
header
|
||||
|
||||
if draft.contentWarningEnabled {
|
||||
ComposeEmojiTextField(text: $draft.contentWarning, placeholder: "Write your warning here")
|
||||
ComposeContentWarningTextField(text: $draft.contentWarning)
|
||||
}
|
||||
|
||||
MainComposeTextView(
|
||||
|
@ -109,13 +109,6 @@ struct ComposeView: View {
|
|||
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(
|
||||
draft: draft
|
||||
)
|
||||
|
@ -220,10 +213,7 @@ struct ComposeView: View {
|
|||
sensitive: sensitive,
|
||||
spoilerText: contentWarning,
|
||||
visibility: draft.visibility,
|
||||
language: nil,
|
||||
pollOptions: draft.poll?.options.map(\.text),
|
||||
pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration),
|
||||
pollMultiple: draft.poll?.multiple)
|
||||
language: nil)
|
||||
self.mastodonController.run(request) { (response) in
|
||||
switch response {
|
||||
case let .failure(error):
|
||||
|
|
|
@ -135,11 +135,7 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
}
|
||||
|
||||
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 {
|
||||
visibilityButton.image = UIImage(systemName: visibility.imageName)
|
||||
|
@ -203,8 +199,6 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
var uiState: ComposeUIState
|
||||
var caretScrollPositionAnimator: UIViewPropertyAnimator?
|
||||
|
||||
var skipSettingTextOnNextUpdate = false
|
||||
|
||||
init(text: Binding<String>, uiState: ComposeUIState, didChange: @escaping (UITextView) -> Void) {
|
||||
self.text = text
|
||||
self.didChange = didChange
|
||||
|
@ -265,11 +259,18 @@ struct MainComposeWrappedTextView: UIViewRepresentable {
|
|||
}
|
||||
|
||||
func textViewDidChangeSelection(_ textView: UITextView) {
|
||||
// Setting the text view's text causes it to move the cursor to the end (though only
|
||||
// when the text contains an emoji :/), so skip setting the text on the next SwiftUI update
|
||||
// that's triggered by setting the autocomplete state.
|
||||
skipSettingTextOnNextUpdate = true
|
||||
self.updateAutocompleteState()
|
||||
// Update the value of the text binding.
|
||||
// Sometimes, when the user accepts an autocomplete suggestion from the system keyboard, the system
|
||||
// calls didChangeSelection before textDidChange, resulting in a loop where the updating the Tusker autocomplete
|
||||
// state in didChangeSection (via updateAutocompleteState) triggers a new SwiftUI view update,
|
||||
// but when that SwiftUI update is handled, the model still has the old text (from prior to accepting the autocomplete
|
||||
// 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) {
|
||||
|
|
|
@ -118,7 +118,7 @@ class ProfileDirectoryFilterView: UICollectionReusableView {
|
|||
}
|
||||
|
||||
@objc private func filterChanged() {
|
||||
let scope = Scope(rawValue: self.scope.selectedSegmentIndex)!
|
||||
let scope = Scope(rawValue: scope.selectedSegmentIndex)!
|
||||
let order = sort.selectedSegmentIndex == 0 ? DirectoryOrder.active : .new
|
||||
onFilterChanged?(scope, order)
|
||||
}
|
||||
|
|
|
@ -94,11 +94,11 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
|||
loadingVC = LoadingViewController()
|
||||
embedChild(loadingVC!)
|
||||
imageRequest = cache.get(url, loadOriginal: true) { [weak self] (data, image) in
|
||||
guard let self = self, let image = image else { return }
|
||||
guard let self = self else { return }
|
||||
self.imageRequest = nil
|
||||
DispatchQueue.main.async {
|
||||
self.loadingVC?.removeViewAndController()
|
||||
self.createLargeImage(data: data, image: image, url: self.url)
|
||||
self.createLargeImage(data: data!, image: image!, url: self.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -168,13 +168,7 @@ class MainSidebarViewController: UIViewController {
|
|||
.profileDirectory,
|
||||
], toSection: .discover)
|
||||
}
|
||||
let prevSelected = collectionView.indexPathsForSelectedItems
|
||||
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
if let prevSelected = prevSelected?.first {
|
||||
collectionView.selectItem(at: prevSelected, animated: false, scrollPosition: .top)
|
||||
}
|
||||
}
|
||||
|
||||
private func reloadLists() {
|
||||
|
|
|
@ -164,10 +164,7 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
|||
}
|
||||
|
||||
// Switch the tab bar to focus the same item as the sidebar has selected
|
||||
switch sidebar.selectedItem {
|
||||
case nil:
|
||||
break
|
||||
|
||||
switch sidebar.selectedItem! {
|
||||
case let .tab(tab):
|
||||
// sidebar items that map 1 <-> 1 can be transferred directly
|
||||
tabBarViewController.select(tab: tab)
|
||||
|
|
|
@ -15,7 +15,6 @@ class NotificationsTableViewController: TimelineLikeTableViewController<Notifica
|
|||
private let actionGroupCell = "actionGroupCell"
|
||||
private let followGroupCell = "followGroupCell"
|
||||
private let followRequestCell = "followRequestCell"
|
||||
private let pollCell = "pollCell"
|
||||
private let unknownCell = "unknownCell"
|
||||
|
||||
weak var mastodonController: MastodonController!
|
||||
|
@ -50,7 +49,6 @@ class NotificationsTableViewController: TimelineLikeTableViewController<Notifica
|
|||
tableView.register(UINib(nibName: "ActionNotificationGroupTableViewCell", bundle: .main), forCellReuseIdentifier: actionGroupCell)
|
||||
tableView.register(UINib(nibName: "FollowNotificationGroupTableViewCell", bundle: .main), forCellReuseIdentifier: followGroupCell)
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -162,13 +160,6 @@ class NotificationsTableViewController: TimelineLikeTableViewController<Notifica
|
|||
cell.updateUI(notification: notification)
|
||||
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:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: unknownCell, for: indexPath)
|
||||
cell.textLabel!.text = NSLocalizedString("Unknown Notification", comment: "unknown notification fallback cell text")
|
||||
|
|
|
@ -13,7 +13,6 @@ struct BehaviorPrefsView: View {
|
|||
|
||||
var body: some View {
|
||||
List {
|
||||
untitledSection
|
||||
linksSection
|
||||
contentWarningsSection
|
||||
}
|
||||
|
@ -21,15 +20,7 @@ struct BehaviorPrefsView: View {
|
|||
.navigationBarTitle(Text("Behavior"))
|
||||
}
|
||||
|
||||
private var untitledSection: some View {
|
||||
Section {
|
||||
Toggle(isOn: $preferences.confirmBeforeReblog) {
|
||||
Text("Require Confirmation Before Reblogging")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var linksSection: some View {
|
||||
var linksSection: some View {
|
||||
Section(header: Text("Links")) {
|
||||
Toggle(isOn: $preferences.openLinksInApps) {
|
||||
Text("Open Links in Apps")
|
||||
|
@ -43,7 +34,7 @@ struct BehaviorPrefsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private var contentWarningsSection: some View {
|
||||
var contentWarningsSection: some View {
|
||||
Section(header: Text("Content Warnings")) {
|
||||
Toggle(isOn: $preferences.collapseLongPosts) {
|
||||
Text("Collapse Long Posts")
|
||||
|
|
|
@ -72,10 +72,7 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
|
|||
let request = Client.getStatuses(timeline: timeline)
|
||||
|
||||
mastodonController?.run(request) { (response) in
|
||||
guard case let .success(statuses, pagination) = response else {
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
guard case let .success(statuses, pagination) = response else { fatalError() }
|
||||
|
||||
self.newer = pagination?.newer
|
||||
self.older = pagination?.older
|
||||
|
@ -95,10 +92,7 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
|
|||
let request = Client.getStatuses(timeline: timeline, range: older)
|
||||
|
||||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(statuses, pagination) = response else {
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
guard case let .success(statuses, pagination) = response else { fatalError() }
|
||||
|
||||
self.older = pagination?.older
|
||||
|
||||
|
@ -117,10 +111,7 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
|
|||
let request = Client.getStatuses(timeline: timeline, range: newer)
|
||||
|
||||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(statuses, pagination) = response else {
|
||||
completion([])
|
||||
return
|
||||
}
|
||||
guard case let .success(statuses, pagination) = response else { fatalError() }
|
||||
|
||||
// if there are no new statuses, pagination is nil
|
||||
// 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 {
|
||||
let pinned = status.pinned ?? false
|
||||
actionsSection.append(createAction(identifier: "pin", title: pinned ? "Unpin" : "Pin", systemImageName: pinned ? "pin.slash" : "pin", handler: { [weak self] (_) in
|
||||
actionsSection.append(createAction(identifier: "", title: pinned ? "Unpin" : "Pin", systemImageName: pinned ? "pin.slash" : "pin", handler: { [weak self] (_) in
|
||||
guard let self = self else { return }
|
||||
let request = (pinned ? Status.unpin : Status.pin)(status.id)
|
||||
self.mastodonController?.run(request, completion: { [weak self] (response) in
|
||||
|
@ -188,20 +188,6 @@ 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 = [
|
||||
openInSafariAction(url: status.url!),
|
||||
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in
|
||||
|
|
|
@ -65,18 +65,11 @@ class TimelineLikeTableViewController<Item>: EnhancedTableViewController, Refres
|
|||
|
||||
func loadInitial() {
|
||||
guard !loaded else { return }
|
||||
// set loaded immediately so we don't trigger another request while the current one is running
|
||||
loaded = true
|
||||
|
||||
loadInitialItems() { (items) in
|
||||
guard items.count > 0 else { return }
|
||||
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() {
|
||||
self.sections.insert(contentsOf: Array(repeating: [], count: self.headerSectionsCount() - self.sections.count), at: 0)
|
||||
}
|
||||
|
@ -104,8 +97,6 @@ class TimelineLikeTableViewController<Item>: EnhancedTableViewController, Refres
|
|||
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) {
|
||||
fatalError("loadInitialItems(completion:) must be implemented by subclasses")
|
||||
}
|
||||
|
|
|
@ -46,26 +46,16 @@ extension TuskerNavigationDelegate {
|
|||
|
||||
func selected(url: URL, allowUniversalLinks: Bool = true) {
|
||||
func openSafari() {
|
||||
if Preferences.shared.useInAppSafari,
|
||||
url.scheme == "https" || url.scheme == "http" {
|
||||
if Preferences.shared.useInAppSafari {
|
||||
let config = SFSafariViewController.Configuration()
|
||||
config.entersReaderIfAvailable = Preferences.shared.inAppSafariAutomaticReaderMode
|
||||
present(SFSafariViewController(url: url, configuration: config), animated: true)
|
||||
} else if UIApplication.shared.canOpenURL(url) {
|
||||
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)
|
||||
UIApplication.shared.open(url, options: [:])
|
||||
}
|
||||
}
|
||||
|
||||
if allowUniversalLinks && Preferences.shared.openLinksInApps,
|
||||
url.scheme == "https" || url.scheme == "http" {
|
||||
if allowUniversalLinks && Preferences.shared.openLinksInApps {
|
||||
UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { (success) in
|
||||
if (!success) {
|
||||
openSafari()
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<?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">
|
||||
<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">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17504.1"/>
|
||||
<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"/>
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<?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">
|
||||
<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">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17124"/>
|
||||
<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"/>
|
||||
|
|
|
@ -1,108 +0,0 @@
|
|||
//
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
<?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>
|
|
@ -0,0 +1,35 @@
|
|||
<?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>
|
|
@ -1,74 +0,0 @@
|
|||
//
|
||||
// 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
|
||||
}
|
||||
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
//
|
||||
// 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")
|
||||
}
|
||||
|
||||
}
|
|
@ -1,180 +0,0 @@
|
|||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,163 +0,0 @@
|
|||
//
|
||||
// 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)
|
||||
}
|
||||
|
||||
class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
|
||||
class BaseStatusTableViewCell: UITableViewCell {
|
||||
|
||||
weak var delegate: StatusTableViewCellDelegate? {
|
||||
didSet {
|
||||
|
@ -34,7 +34,6 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
|
|||
@IBOutlet weak var contentTextView: StatusContentTextView!
|
||||
@IBOutlet weak var cardView: StatusCardView!
|
||||
@IBOutlet weak var attachmentsView: AttachmentsContainerView!
|
||||
@IBOutlet weak var pollView: StatusPollView!
|
||||
@IBOutlet weak var replyButton: UIButton!
|
||||
@IBOutlet weak var favoriteButton: UIButton!
|
||||
@IBOutlet weak var reblogButton: UIButton!
|
||||
|
@ -214,14 +213,6 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
|
|||
|
||||
// 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))
|
||||
|
||||
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) {
|
||||
|
@ -339,15 +330,6 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
|
|||
showStatusAutomatically = false
|
||||
}
|
||||
|
||||
// MARK: - MenuPreviewProvider
|
||||
var navigationDelegate: TuskerNavigationDelegate? { return delegate }
|
||||
|
||||
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: - Interaction
|
||||
|
||||
@IBAction func collapseButtonPressed() {
|
||||
setCollapsed(!collapsed, animated: true)
|
||||
delegate?.statusCellCollapsedStateChanged(self)
|
||||
|
@ -421,23 +403,6 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
|
|||
@IBAction func reblogPressed() {
|
||||
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
|
||||
reblogged = !reblogged
|
||||
|
||||
|
@ -466,6 +431,10 @@ class BaseStatusTableViewCell: UITableViewCell, MenuPreviewProvider {
|
|||
@objc func accountPressed() {
|
||||
delegate?.selected(account: accountID)
|
||||
}
|
||||
|
||||
func getStatusCellPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
extension BaseStatusTableViewCell: AttachmentViewDelegate {
|
||||
|
@ -540,6 +509,21 @@ 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 {
|
||||
func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] {
|
||||
guard let currentAccountID = mastodonController.accountInfo?.id,
|
||||
|
|
|
@ -36,8 +36,6 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
|
|||
accessibilityElements = [profileAccessibilityElement!, contentWarningLabel!, collapseButton!, contentTextView!, totalFavoritesButton!, totalReblogsButton!, timestampAndClientLabel!, replyButton!, favoriteButton!, reblogButton!, moreButton!]
|
||||
|
||||
contentTextView.defaultFont = .systemFont(ofSize: 18)
|
||||
|
||||
profileDetailContainerView.addInteraction(UIContextMenuInteraction(delegate: self))
|
||||
}
|
||||
|
||||
override func doUpdateUI(status: StatusMO, state: StatusState) {
|
||||
|
@ -88,13 +86,3 @@ 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,8 +1,9 @@
|
|||
<?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">
|
||||
<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">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17504.1"/>
|
||||
<capability name="Image references" minToolsVersion="12.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
|
@ -89,30 +90,26 @@
|
|||
</connections>
|
||||
</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">
|
||||
<rect key="frame" x="0.0" y="124.5" width="343" height="0.0"/>
|
||||
<rect key="frame" x="0.0" y="124.5" width="343" height="47.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>
|
||||
<color key="textColor" systemColor="labelColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="20"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="QqC-GR-TLC" customClass="StatusCardView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="128.5" width="343" height="0.0"/>
|
||||
<rect key="frame" x="0.0" y="176" width="343" height="0.0"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" priority="999" constant="65" id="Tdo-Hv-ITE"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="IF9-9U-Gk0" customClass="AttachmentsContainerView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="128.5" width="343" height="0.0"/>
|
||||
<rect key="frame" x="0.0" y="176" width="343" height="0.0"/>
|
||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" secondItem="IF9-9U-Gk0" secondAttribute="width" multiplier="9:16" priority="999" id="5oh-eK-J5d"/>
|
||||
</constraints>
|
||||
</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">
|
||||
<rect key="frame" x="0.0" y="180" width="343" height="0.5"/>
|
||||
<color key="backgroundColor" systemColor="opaqueSeparatorColor"/>
|
||||
|
@ -224,7 +221,6 @@
|
|||
<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="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>
|
||||
</stackView>
|
||||
</subviews>
|
||||
|
@ -248,7 +244,6 @@
|
|||
<outlet property="favoriteAndReblogCountStackView" destination="HZv-qj-gi6" id="jC9-cA-dXg"/>
|
||||
<outlet property="favoriteButton" destination="DhN-rJ-jdA" id="b2Q-ch-kSP"/>
|
||||
<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="reblogButton" destination="GUG-f7-Hdy" id="WtT-Ph-DQm"/>
|
||||
<outlet property="replyButton" destination="2cc-lE-AdG" id="My8-JV-Nho"/>
|
||||
|
|
|
@ -50,8 +50,6 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
|||
replyButton.imageView!.leadingAnchor.constraint(equalTo: contentTextView.leadingAnchor).isActive = true
|
||||
|
||||
contentTextView.defaultFont = .systemFont(ofSize: 16)
|
||||
|
||||
avatarImageView.addInteraction(UIContextMenuInteraction(delegate: self))
|
||||
}
|
||||
|
||||
override func createObserversIfNecessary() {
|
||||
|
@ -183,7 +181,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
|||
reply()
|
||||
}
|
||||
|
||||
override func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> BaseStatusTableViewCell.PreviewProviders? {
|
||||
override func getStatusCellPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> BaseStatusTableViewCell.PreviewProviders? {
|
||||
guard let mastodonController = mastodonController,
|
||||
let status = mastodonController.persistentContainer.status(for: statusID) else {
|
||||
return nil
|
||||
|
@ -305,13 +303,3 @@ extension TimelineStatusTableViewCell: DraggableTableViewCell {
|
|||
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,8 +1,9 @@
|
|||
<?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">
|
||||
<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">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17504.1"/>
|
||||
<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"/>
|
||||
|
@ -108,30 +109,26 @@
|
|||
</connections>
|
||||
</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">
|
||||
<rect key="frame" x="0.0" y="83" width="277" height="82.5"/>
|
||||
<rect key="frame" x="0.0" y="83" width="277" height="86.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>
|
||||
<color key="textColor" systemColor="labelColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LKo-VB-XWl" customClass="StatusCardView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="167.5" width="277" height="0.0"/>
|
||||
<rect key="frame" x="0.0" y="169.5" width="277" height="0.0"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" priority="999" constant="65" id="khY-jm-CPn"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<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="167.5" width="277" height="0.0"/>
|
||||
<rect key="frame" x="0.0" y="169.5" width="277" height="0.0"/>
|
||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" secondItem="nbq-yr-2mA" secondAttribute="width" multiplier="9:16" priority="999" id="Rvt-zs-fkd"/>
|
||||
</constraints>
|
||||
</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>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="oie-wK-IpU">
|
||||
|
@ -257,7 +254,6 @@
|
|||
<outlet property="favoriteButton" destination="x0t-TR-jJ4" id="guV-yz-Lm6"/>
|
||||
<outlet property="moreButton" destination="982-J4-NGl" id="Pux-tL-aWe"/>
|
||||
<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="reblogLabel" destination="lDH-50-AJZ" id="uJf-Pt-cEP"/>
|
||||
<outlet property="replyButton" destination="rKF-yF-KIa" id="rka-q1-o4a"/>
|
||||
|
|
|
@ -2,26 +2,10 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<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>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%1$#@accounts@, %2$#@posts@ recently</string>
|
||||
<string>%#@accounts@, %#@posts@ recently</string>
|
||||
<key>posts</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
|
|
Loading…
Reference in New Issue