Compare commits
33 Commits
b623e348c2
...
0e5aab75df
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 0e5aab75df | |
Shadowfacts | c715d11fc2 | |
Shadowfacts | 8010e86711 | |
Shadowfacts | a41d27f18c | |
Shadowfacts | 083add273b | |
Shadowfacts | 64365bdf2b | |
Shadowfacts | 6adcad63b3 | |
Shadowfacts | 393a134648 | |
Shadowfacts | ba3e9e7491 | |
Shadowfacts | 920f926b48 | |
Shadowfacts | 6e27399e10 | |
Shadowfacts | c3c19b1994 | |
Shadowfacts | 1f40cc9928 | |
Shadowfacts | 66020b7847 | |
Shadowfacts | 00bf99334f | |
Shadowfacts | 3aef7d4d93 | |
Shadowfacts | a901af6be9 | |
Shadowfacts | 056346cee9 | |
Shadowfacts | 30c04b49e7 | |
Shadowfacts | 848022ec6e | |
Shadowfacts | 39e847bda8 | |
Shadowfacts | 5d751cd994 | |
Shadowfacts | d27bddb2ca | |
Shadowfacts | 36326e4469 | |
Shadowfacts | 6b7904ed52 | |
Shadowfacts | 61c6d63c67 | |
Shadowfacts | c0316f55ef | |
Shadowfacts | 803ba50f53 | |
Shadowfacts | 5d0c59e863 | |
Shadowfacts | c7b4d00da7 | |
Shadowfacts | f2a8b91769 | |
Shadowfacts | ce464dfb9f | |
Shadowfacts | d4bf289716 |
|
@ -0,0 +1,59 @@
|
|||
# Changelog
|
||||
|
||||
## 2020.1 (6)
|
||||
This is the pre-WWDC update with lots of bugfixes and some small features. There will likely be another build this week to fix any pressing issues that arise from iOS 14.
|
||||
|
||||
Features/Improvements:
|
||||
- Add mute/unmute conversation status action
|
||||
- iPadOS: Add pointer interactions to remove attachment button, gallery view share/dismiss buttons
|
||||
- Disable reblog button for direct/followers-only posts
|
||||
- On Pleroma, the reblog button is still enabled for your own followers-only posts to match Pleroma's "Boost to original audience" feature.
|
||||
- Add preference to always display status visibilities below account avatars
|
||||
- Add preference to show reply indicators for statuses in timelines
|
||||
- Show share/dismiss controls and image description for gifv attachments
|
||||
- 'Share' is currently disabled for gifv attachments, it will be enabled in a future build
|
||||
- Add crash report helper
|
||||
- If the app detects that it crashed the last time it was running, it will allow you to review the crash report and email it to me
|
||||
- Add Recognize Text context menu option for images on the Compose screen
|
||||
- This uses iOS' builtin Vision framework to perform on-device OCR and generate an image description from the recognized text
|
||||
- Tweak attachment previews to always have a 16:9 aspect ratio
|
||||
|
||||
Bugfixes:
|
||||
- Fix account/status More actions not working
|
||||
- Improve share sheet loading speed
|
||||
- Fix crash when loading bookmarks
|
||||
- Prompt for Photos access before showing photo picker. Prevents empty sheet displaying.
|
||||
- Fix profile fields not displaying and improve layout
|
||||
- Fix profile header image not displaying the first time an account is loaded
|
||||
- Don't show Follow action for your own account
|
||||
- Fix attachments on the Compose screen being cut-off above the home indicator on iPhone X-style devices
|
||||
- Fix audio being played by other apps pausing when displaying a gifv attachment on Mastodon
|
||||
|
||||
|
||||
## 2020.1 (5)
|
||||
The main focus of this update has been switching to using CoreData internally to cache/synchronize the most up-to-date versions of all statuses. Currently, this does not provide any new functionality, however, it lays the groundwork for several significant features coming in the future, including multiple window support on iPadOS and state restoration/persistence between launches.
|
||||
|
||||
Even though there aren't a huge number of new features in this build, a great deal has changed under the hood. As such, this build may suffer somewhat in the stability department. Please bear with me and report any issues you encounter; you can send me a message on the fediverse, email me at me@shadowfacts.net, or file an issue on the project issue tracker at https://git.shadowfacts.net/shadowfacts/Tusker/issues. Thank you!
|
||||
|
||||
Features:
|
||||
- iPadOS: Add pointer interactions to status action buttons and profile header button
|
||||
- iPadOS: Allow scrolling w/ trackpad/magic mouse to dismiss attachment gallery
|
||||
- iPadOS: Enable interactive push gesture with trackpad/magic mouse
|
||||
- Add drawing attachments using PencilKit
|
||||
- Long-press to open context menu on the 'Add Attachment' button on the Compose screen, select 'Draw Something'
|
||||
- Supports Apple Pencil on iPad, including tilt and pressure sensitivity
|
||||
- Add avatar and instance domain in accounts switcher in Preferences
|
||||
- Show gifv attachments on Mastodon
|
||||
- Currently doesn't show attachment description or share/close buttons
|
||||
- Add 'Clear Cache' option to Preferences -> Advanced for debugging
|
||||
|
||||
Bugfixes:
|
||||
- Fix size of attachment previews in context menu
|
||||
- Fix previewing audio/video attachments
|
||||
- Fix incorrect image size during attachment expand/shrink animation
|
||||
- Prevent avatars in grouped action notification from overflowing the cell and hiding the timestamp
|
||||
- Fix text in conversation main statuses not being de-selectable
|
||||
- Fix scroll-to-top sometimes not scrolling all the way to the top
|
||||
- Fix account profile descriptions being squashed in the follow notification account list
|
||||
|
||||
|
|
@ -80,28 +80,28 @@ public final class Status: /*StatusProtocol,*/ Decodable {
|
|||
return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/unfavourite")
|
||||
}
|
||||
|
||||
public static func pin(_ status: Status) -> Request<Status> {
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/pin")
|
||||
public static func pin(_ statusID: String) -> Request<Status> {
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/pin")
|
||||
}
|
||||
|
||||
public static func unpin(_ status: Status) -> Request<Status> {
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/unpin")
|
||||
public static func unpin(_ statusID: String) -> Request<Status> {
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/unpin")
|
||||
}
|
||||
|
||||
public static func bookmark(_ status: Status) -> Request<Status> {
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/bookmark")
|
||||
public static func bookmark(_ statusID: String) -> Request<Status> {
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/bookmark")
|
||||
}
|
||||
|
||||
public static func unbookmark(_ statusID: String) -> Request<Status> {
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/unbookmark")
|
||||
}
|
||||
|
||||
public static func muteConversation(_ status: Status) -> Request<Status> {
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/mute")
|
||||
public static func muteConversation(_ statusID: String) -> Request<Status> {
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/mute")
|
||||
}
|
||||
|
||||
public static func unmuteConversation(_ status: Status) -> Request<Status> {
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/unmute")
|
||||
public static func unmuteConversation(_ statusID: String) -> Request<Status> {
|
||||
return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/unmute")
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
|
|
|
@ -168,6 +168,11 @@
|
|||
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */; };
|
||||
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */; };
|
||||
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68015412401A74600D6103B /* MediaPrefsView.swift */; };
|
||||
D681A29A249AD62D0085E54E /* LargeImageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681A299249AD62D0085E54E /* LargeImageContentView.swift */; };
|
||||
D681E4D3246E2AFF0053414F /* MuteConversationActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D2246E2AFF0053414F /* MuteConversationActivity.swift */; };
|
||||
D681E4D5246E2BC30053414F /* UnmuteConversationActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D4246E2BC30053414F /* UnmuteConversationActivity.swift */; };
|
||||
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D6246E32290053414F /* StatusActivityItemSource.swift */; };
|
||||
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; };
|
||||
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; };
|
||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; };
|
||||
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */; };
|
||||
|
@ -180,6 +185,7 @@
|
|||
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 */; };
|
||||
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 */; };
|
||||
|
@ -228,6 +234,8 @@
|
|||
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */; };
|
||||
D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D862139E62700CB5196 /* LargeImageViewController.swift */; };
|
||||
D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D882139E6EC00CB5196 /* AttachmentView.swift */; };
|
||||
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */; };
|
||||
D6CA6A94249FADE700AD45C1 /* GalleryPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA6A93249FADE700AD45C1 /* GalleryPlayerViewController.swift */; };
|
||||
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */; };
|
||||
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D6D4DDD6212518A200E1C4BB /* Assets.xcassets */; };
|
||||
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6D4DDD8212518A200E1C4BB /* LaunchScreen.storyboard */; };
|
||||
|
@ -243,6 +251,8 @@
|
|||
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; };
|
||||
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */; };
|
||||
D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F1F84C2193B56E00F5FE67 /* Cache.swift */; };
|
||||
D6F2E965249E8BFD005846BB /* CrashReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */; };
|
||||
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */; };
|
||||
D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */; };
|
||||
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
@ -467,6 +477,11 @@
|
|||
D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusReplyView.swift; sourceTree = "<group>"; };
|
||||
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposingPrefsView.swift; sourceTree = "<group>"; };
|
||||
D68015412401A74600D6103B /* MediaPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPrefsView.swift; sourceTree = "<group>"; };
|
||||
D681A299249AD62D0085E54E /* LargeImageContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageContentView.swift; sourceTree = "<group>"; };
|
||||
D681E4D2246E2AFF0053414F /* MuteConversationActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteConversationActivity.swift; sourceTree = "<group>"; };
|
||||
D681E4D4246E2BC30053414F /* UnmuteConversationActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnmuteConversationActivity.swift; sourceTree = "<group>"; };
|
||||
D681E4D6246E32290053414F /* StatusActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityItemSource.swift; sourceTree = "<group>"; };
|
||||
D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountActivityItemSource.swift; sourceTree = "<group>"; };
|
||||
D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = "<group>"; };
|
||||
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = "<group>"; };
|
||||
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedNavigationViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -523,6 +538,8 @@
|
|||
D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentsContainerView.swift; sourceTree = "<group>"; };
|
||||
D6C94D862139E62700CB5196 /* LargeImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageViewController.swift; sourceTree = "<group>"; };
|
||||
D6C94D882139E6EC00CB5196 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = "<group>"; };
|
||||
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionHelper.swift; sourceTree = "<group>"; };
|
||||
D6CA6A93249FADE700AD45C1 /* GalleryPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryPlayerViewController.swift; sourceTree = "<group>"; };
|
||||
D6D4DDCC212518A000E1C4BB /* Tusker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Tusker.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
D6D4DDD6212518A200E1C4BB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
|
@ -544,6 +561,8 @@
|
|||
D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = "<group>"; };
|
||||
D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISceneSession+MastodonController.swift"; sourceTree = "<group>"; };
|
||||
D6F1F84C2193B56E00F5FE67 /* Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = "<group>"; };
|
||||
D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReporterViewController.swift; sourceTree = "<group>"; };
|
||||
D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CrashReporterViewController.xib; sourceTree = "<group>"; };
|
||||
D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = "<group>"; };
|
||||
D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = "<group>"; };
|
||||
D6F98BD523AE951F008A4DAC /* Swifter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Swifter.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -572,6 +591,7 @@
|
|||
D61099C02144B0CC00432DC2 /* Pachyderm.framework in Frameworks */,
|
||||
D6B0539F23BD2BA300A066FA /* SheetController in Frameworks */,
|
||||
D65A37F321472F300087646E /* SwiftSoup.framework in Frameworks */,
|
||||
D69CCBBF249E6EFD000AF167 /* CrashReporter in Frameworks */,
|
||||
D6BC874521961F73006163F1 /* Gifu.framework in Frameworks */,
|
||||
0461A3902163CBAE00C0A807 /* Cache.framework in Frameworks */,
|
||||
);
|
||||
|
@ -602,6 +622,7 @@
|
|||
04D14BAE22B34A2800642648 /* GalleryViewController.swift */,
|
||||
D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */,
|
||||
D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */,
|
||||
D6CA6A93249FADE700AD45C1 /* GalleryPlayerViewController.swift */,
|
||||
);
|
||||
path = "Attachment Gallery";
|
||||
sourceTree = "<group>";
|
||||
|
@ -773,6 +794,8 @@
|
|||
D627943823A553B600D38C68 /* UnbookmarkStatusActivity.swift */,
|
||||
D64BC18723C1640A000D0238 /* PinStatusActivity.swift */,
|
||||
D64BC18923C16487000D0238 /* UnpinStatusActivity.swift */,
|
||||
D681E4D2246E2AFF0053414F /* MuteConversationActivity.swift */,
|
||||
D681E4D4246E2BC30053414F /* UnmuteConversationActivity.swift */,
|
||||
);
|
||||
path = "Status Activities";
|
||||
sourceTree = "<group>";
|
||||
|
@ -839,6 +862,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
D6C693FA2162FE5D007D6A6D /* Utilities */,
|
||||
D6F2E960249E772F005846BB /* Crash Reporter */,
|
||||
D641C782213DD7F0004B4513 /* Main */,
|
||||
D641C783213DD7FE004B4513 /* Onboarding */,
|
||||
D641C781213DD7DD004B4513 /* Timeline */,
|
||||
|
@ -932,6 +956,7 @@
|
|||
D646C954213B364600269FB5 /* Transitions */,
|
||||
D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */,
|
||||
D6C94D862139E62700CB5196 /* LargeImageViewController.swift */,
|
||||
D681A299249AD62D0085E54E /* LargeImageContentView.swift */,
|
||||
041160FE22B442870030A9B7 /* LoadingLargeImageViewController.swift */,
|
||||
);
|
||||
path = "Large Image";
|
||||
|
@ -1131,6 +1156,8 @@
|
|||
D6AEBB3F2321640F00E5038B /* Activities */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D681E4D6246E32290053414F /* StatusActivityItemSource.swift */,
|
||||
D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */,
|
||||
D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */,
|
||||
D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */,
|
||||
D64BC19123C271D9000D0238 /* MastodonActivity.swift */,
|
||||
|
@ -1267,6 +1294,7 @@
|
|||
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */,
|
||||
D64D8CA82463B494006B0BAA /* CachedDictionary.swift */,
|
||||
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
|
||||
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
|
||||
D6F1F84E2193B9BE00F5FE67 /* Caching */,
|
||||
D6757A7A2157E00100721E32 /* XCallbackURL */,
|
||||
D62D241E217AA46B005076CC /* Shortcuts */,
|
||||
|
@ -1318,6 +1346,15 @@
|
|||
path = Caching;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D6F2E960249E772F005846BB /* Crash Reporter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */,
|
||||
D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */,
|
||||
);
|
||||
path = "Crash Reporter";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D6F953F121251A2F00CF0F2B /* Controllers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1395,6 +1432,7 @@
|
|||
name = Tusker;
|
||||
packageProductDependencies = (
|
||||
D6B0539E23BD2BA300A066FA /* SheetController */,
|
||||
D69CCBBE249E6EFD000AF167 /* CrashReporter */,
|
||||
);
|
||||
productName = Tusker;
|
||||
productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */;
|
||||
|
@ -1490,6 +1528,7 @@
|
|||
mainGroup = D6D4DDC3212518A000E1C4BB;
|
||||
packageReferences = (
|
||||
D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */,
|
||||
D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */,
|
||||
);
|
||||
productRefGroup = D6D4DDCD212518A000E1C4BB /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -1548,6 +1587,7 @@
|
|||
D63F9C66241C4CC3004C03CF /* AddAttachmentTableViewCell.xib in Resources */,
|
||||
D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */,
|
||||
D6A5FAF1217B7E05003DB2D9 /* ComposeViewController.xib in Resources */,
|
||||
D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -1690,11 +1730,14 @@
|
|||
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
|
||||
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */,
|
||||
D620483623D38075008A63EF /* ContentTextView.swift in Sources */,
|
||||
D6F2E965249E8BFD005846BB /* CrashReporterViewController.swift in Sources */,
|
||||
D6CA6A94249FADE700AD45C1 /* GalleryPlayerViewController.swift in Sources */,
|
||||
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
|
||||
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
|
||||
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */,
|
||||
D60309B52419D4F100A465FF /* ComposeAttachmentsViewController.swift in Sources */,
|
||||
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
|
||||
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */,
|
||||
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */,
|
||||
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */,
|
||||
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */,
|
||||
|
@ -1705,6 +1748,7 @@
|
|||
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */,
|
||||
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */,
|
||||
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
|
||||
D681E4D5246E2BC30053414F /* UnmuteConversationActivity.swift in Sources */,
|
||||
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */,
|
||||
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
|
||||
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
|
||||
|
@ -1744,6 +1788,7 @@
|
|||
D627943E23A564D400D38C68 /* ExploreViewController.swift in Sources */,
|
||||
D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */,
|
||||
D6434EB3215B1856001A919A /* XCBRequest.swift in Sources */,
|
||||
D681E4D3246E2AFF0053414F /* MuteConversationActivity.swift in Sources */,
|
||||
D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */,
|
||||
D64BC18623C1253A000D0238 /* AssetPreviewViewController.swift in Sources */,
|
||||
D663626221360B1900C9CBA2 /* Preferences.swift in Sources */,
|
||||
|
@ -1756,6 +1801,7 @@
|
|||
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
|
||||
D67895C0246870DE00D4CD9E /* LocalAccountAvatarView.swift in Sources */,
|
||||
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
|
||||
D681A29A249AD62D0085E54E /* LargeImageContentView.swift in Sources */,
|
||||
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */,
|
||||
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */,
|
||||
D63F9C68241C4F79004C03CF /* AddAttachmentTableViewCell.swift in Sources */,
|
||||
|
@ -1773,6 +1819,7 @@
|
|||
D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */,
|
||||
D6DFC6A0242C4CCC00ACC392 /* WeakArray.swift in Sources */,
|
||||
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */,
|
||||
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */,
|
||||
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */,
|
||||
D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */,
|
||||
D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */,
|
||||
|
@ -1792,6 +1839,7 @@
|
|||
0427037C22B316B9000D31B6 /* SilentActionPrefs.swift in Sources */,
|
||||
D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */,
|
||||
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */,
|
||||
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */,
|
||||
D626493F23C101C500612E6E /* AlbumAssetCollectionViewController.swift in Sources */,
|
||||
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */,
|
||||
D6757A7E2157E02600721E32 /* XCBRequestSpec.swift in Sources */,
|
||||
|
@ -2094,7 +2142,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 5;
|
||||
CURRENT_PROJECT_VERSION = 6;
|
||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
|
@ -2119,7 +2167,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 5;
|
||||
CURRENT_PROJECT_VERSION = 6;
|
||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
|
@ -2278,6 +2326,14 @@
|
|||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/microsoft/plcrashreporter";
|
||||
requirement = {
|
||||
kind = upToNextMinorVersion;
|
||||
minimumVersion = 1.7.0;
|
||||
};
|
||||
};
|
||||
D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://git.shadowfacts.net/shadowfacts/SheetController.git";
|
||||
|
@ -2289,6 +2345,11 @@
|
|||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
D69CCBBE249E6EFD000AF167 /* CrashReporter */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D69CCBBD249E6EFD000AF167 /* XCRemoteSwiftPackageReference "plcrashreporter" */;
|
||||
productName = CrashReporter;
|
||||
};
|
||||
D6B0539E23BD2BA300A066FA /* SheetController */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D6B0539D23BD2BA300A066FA /* XCRemoteSwiftPackageReference "SheetController" */;
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "SheetController",
|
||||
"repositoryURL": "https://git.shadowfacts.net/shadowfacts/SheetController.git",
|
||||
"package": "PLCrashReporter",
|
||||
"repositoryURL": "https://github.com/microsoft/plcrashreporter",
|
||||
"state": {
|
||||
"branch": "master",
|
||||
"revision": "6926446c4e15eb7f4513c4c00df9279553b330be",
|
||||
"version": null
|
||||
"branch": null,
|
||||
"revision": "4637a7854de2cc5c354d46fb931d74bdbc2c043e",
|
||||
"version": "1.7.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class AccountActivity: MastodonActivity {
|
||||
|
||||
|
@ -15,17 +14,17 @@ class AccountActivity: MastodonActivity {
|
|||
return .action
|
||||
}
|
||||
|
||||
var account: Account?
|
||||
var account: AccountMO?
|
||||
|
||||
override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
|
||||
for case is Account in activityItems {
|
||||
for case is AccountMO in activityItems {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override func prepare(withActivityItems activityItems: [Any]) {
|
||||
for case let account as Account in activityItems {
|
||||
for case let account as AccountMO in activityItems {
|
||||
self.account = account
|
||||
return
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
//
|
||||
// AccountActivityItemSource.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 5/14/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import LinkPresentation
|
||||
|
||||
class AccountActivityItemSource: NSObject, UIActivityItemSource {
|
||||
let account: AccountMO
|
||||
|
||||
init(_ account: AccountMO) {
|
||||
self.account = account
|
||||
}
|
||||
|
||||
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
|
||||
return account
|
||||
}
|
||||
|
||||
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
|
||||
return account
|
||||
}
|
||||
|
||||
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
|
||||
let metadata = LPLinkMetadata()
|
||||
metadata.originalURL = account.url
|
||||
metadata.url = account.url
|
||||
metadata.title = "\(account.displayName) (@\(account.username)@\(account.url.host!)"
|
||||
if let data = ImageCache.avatars.get(account.avatar),
|
||||
let image = UIImage(data: data) {
|
||||
metadata.iconProvider = NSItemProvider(object: image)
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
|
||||
}
|
|
@ -26,7 +26,7 @@ class BookmarkStatusActivity: StatusActivity {
|
|||
override func perform() {
|
||||
guard let status = status else { return }
|
||||
|
||||
let request = Status.bookmark(status)
|
||||
let request = Status.bookmark(status.id)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case let .success(status, _) = response {
|
||||
self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// MuteConversationActivity.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 5/14/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class MuteConversationActivity: StatusActivity {
|
||||
override var activityType: UIActivity.ActivityType? {
|
||||
return .muteConversation
|
||||
}
|
||||
|
||||
override var activityTitle: String? {
|
||||
return NSLocalizedString("Mute Conversation", comment: "mute conversation activity title")
|
||||
}
|
||||
|
||||
override var activityImage: UIImage? {
|
||||
return UIImage(systemName: "speaker.slash")
|
||||
}
|
||||
|
||||
override func perform() {
|
||||
guard let status = status else { return }
|
||||
|
||||
let request = Status.muteConversation(status.id)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case let .success(status, _) = response {
|
||||
self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||
} else {
|
||||
// todo: display error message
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -25,7 +25,7 @@ class PinStatusActivity: StatusActivity {
|
|||
override func perform() {
|
||||
guard let status = status else { return }
|
||||
|
||||
let request = Status.pin(status)
|
||||
let request = Status.pin(status.id)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case let .success(status, _) = response {
|
||||
self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class StatusActivity: MastodonActivity {
|
||||
|
||||
|
@ -15,17 +14,17 @@ class StatusActivity: MastodonActivity {
|
|||
return .action
|
||||
}
|
||||
|
||||
var status: Status?
|
||||
var status: StatusMO?
|
||||
|
||||
override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
|
||||
for case is Status in activityItems {
|
||||
for case is StatusMO in activityItems {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override func prepare(withActivityItems activityItems: [Any]) {
|
||||
for case let status as Status in activityItems {
|
||||
for case let status as StatusMO in activityItems {
|
||||
self.status = status
|
||||
return
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// UnmuteConversationActivity.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 5/14/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class UnmuteConversationActivity: StatusActivity {
|
||||
override var activityType: UIActivity.ActivityType? {
|
||||
return .unmuteConversation
|
||||
}
|
||||
|
||||
override var activityTitle: String? {
|
||||
return NSLocalizedString("Unmute Conversation", comment: "unmute conversation activity title")
|
||||
}
|
||||
|
||||
override var activityImage: UIImage? {
|
||||
return UIImage(systemName: "speaker")
|
||||
}
|
||||
|
||||
override func perform() {
|
||||
guard let status = status else { return }
|
||||
|
||||
let request = Status.unmuteConversation(status.id)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case let .success(status, _) = response {
|
||||
self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||
} else {
|
||||
// todo: display error message
|
||||
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -25,7 +25,7 @@ class UnpinStatusActivity: StatusActivity {
|
|||
override func perform() {
|
||||
guard let status = status else { return }
|
||||
|
||||
let request = Status.unpin(status)
|
||||
let request = Status.unpin(status.id)
|
||||
mastodonController.run(request) { (response) in
|
||||
if case let .success(status, _) = response {
|
||||
self.mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false)
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// StatusActivityItemSource.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 5/14/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import LinkPresentation
|
||||
import SwiftSoup
|
||||
|
||||
class StatusActivityItemSource: NSObject, UIActivityItemSource {
|
||||
let status: StatusMO
|
||||
|
||||
init(_ status: StatusMO) {
|
||||
self.status = status
|
||||
}
|
||||
|
||||
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
|
||||
return status
|
||||
}
|
||||
|
||||
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
|
||||
return status
|
||||
}
|
||||
|
||||
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
|
||||
let metadata = LPLinkMetadata()
|
||||
metadata.originalURL = status.url!
|
||||
metadata.url = status.url!
|
||||
let doc = try! SwiftSoup.parse(status.content)
|
||||
let content = try! doc.text()
|
||||
metadata.title = "\(status.account.displayName): \"\(content)\""
|
||||
if let data = ImageCache.avatars.get(status.account.avatar),
|
||||
let image = UIImage(data: data) {
|
||||
metadata.iconProvider = NSItemProvider(object: image)
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
|
||||
}
|
|
@ -22,5 +22,7 @@ extension UIActivity.ActivityType {
|
|||
static let unbookmarkStatus = UIActivity.ActivityType("\(Bundle.main.bundleIdentifier!).unbookmark_status")
|
||||
static let pinStatus = UIActivity.ActivityType("\(Bundle.main.bundleIdentifier!).pin_status")
|
||||
static let unpinStatus = UIActivity.ActivityType("\(Bundle.main.bundleIdentifier!).unpin_status")
|
||||
static let muteConversation = UIActivity.ActivityType("\(Bundle.main.bundleIdentifier!).mute_conversation")
|
||||
static let unmuteConversation = UIActivity.ActivityType("\(Bundle.main.bundleIdentifier!).unmute_conversation")
|
||||
|
||||
}
|
||||
|
|
|
@ -7,13 +7,41 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import CrashReporter
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
static private(set) var crashReporter: PLCrashReporter!
|
||||
static var pendingCrashReport: PLCrashReport?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
#if !DEBUG
|
||||
setupCrashReporter()
|
||||
#endif
|
||||
|
||||
AppShortcutItem.createItems(for: application)
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
AudioSessionHelper.disable()
|
||||
AudioSessionHelper.setDefault()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
private func setupCrashReporter() {
|
||||
let config = PLCrashReporterConfig(signalHandlerType: .BSD, symbolicationStrategy: .all)
|
||||
AppDelegate.crashReporter = PLCrashReporter(configuration: config)
|
||||
|
||||
if AppDelegate.crashReporter.hasPendingCrashReport() {
|
||||
let data = try! AppDelegate.crashReporter.loadPendingCrashReportDataAndReturnError()
|
||||
AppDelegate.crashReporter.purgePendingCrashReport()
|
||||
let report = try! PLCrashReport(data: data)
|
||||
|
||||
AppDelegate.pendingCrashReport = report
|
||||
}
|
||||
|
||||
AppDelegate.crashReporter.enable()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// AudioSessionHelper.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 6/21/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
struct AudioSessionHelper {
|
||||
static func enable() {
|
||||
try? AVAudioSession.sharedInstance().setActive(true, options: [])
|
||||
}
|
||||
|
||||
static func disable() {
|
||||
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
||||
}
|
||||
|
||||
static func setDefault() {
|
||||
try? AVAudioSession.sharedInstance().setCategory(.playback, options: .mixWithOthers)
|
||||
}
|
||||
|
||||
static func setVideoPlayback() {
|
||||
try? AVAudioSession.sharedInstance().setCategory(.playback, options: [])
|
||||
}
|
||||
}
|
|
@ -18,7 +18,7 @@ class ImageCache {
|
|||
|
||||
let cache: Cache<Data>
|
||||
|
||||
var requests = [URL: RequestGroup]()
|
||||
private var groups = [URL: RequestGroup]()
|
||||
|
||||
init(name: String, memoryExpiry expiry: Expiry) {
|
||||
let storage = MemoryStorage<Data>(config: MemoryConfig(expiry: expiry))
|
||||
|
@ -43,14 +43,18 @@ class ImageCache {
|
|||
completion?(data)
|
||||
return nil
|
||||
} else {
|
||||
if let completion = completion, let group = requests[url] {
|
||||
if let completion = completion, let group = groups[url] {
|
||||
return group.addCallback(completion)
|
||||
} else {
|
||||
let group = RequestGroup(url: url)
|
||||
let request = group.addCallback(completion)
|
||||
group.run { (data) in
|
||||
try? self.cache.setObject(data, forKey: key)
|
||||
let group = RequestGroup(url: url) { (data) in
|
||||
if let data = data {
|
||||
try? self.cache.setObject(data, forKey: key)
|
||||
}
|
||||
self.groups.removeValue(forKey: url)
|
||||
}
|
||||
groups[url] = group
|
||||
let request = group.addCallback(completion)
|
||||
group.run()
|
||||
return request
|
||||
}
|
||||
}
|
||||
|
@ -61,29 +65,30 @@ class ImageCache {
|
|||
}
|
||||
|
||||
func cancelWithoutCallback(_ url: URL) {
|
||||
requests[url]?.cancelWithoutCallback()
|
||||
groups[url]?.cancelWithoutCallback()
|
||||
}
|
||||
|
||||
class RequestGroup {
|
||||
private class RequestGroup {
|
||||
let url: URL
|
||||
private let onFinished: (Data?) -> Void
|
||||
private var task: URLSessionDataTask?
|
||||
private var requests = [Request]()
|
||||
|
||||
init(url: URL) {
|
||||
init(url: URL, onFinished: @escaping (Data?) -> Void) {
|
||||
self.url = url
|
||||
self.onFinished = onFinished
|
||||
}
|
||||
|
||||
deinit {
|
||||
task?.cancel()
|
||||
}
|
||||
|
||||
func run(cache: @escaping (Data) -> Void) {
|
||||
func run() {
|
||||
task = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
|
||||
guard error == nil, let data = data else {
|
||||
self.complete(with: nil)
|
||||
return
|
||||
}
|
||||
cache(data)
|
||||
self.complete(with: data)
|
||||
})
|
||||
task!.resume()
|
||||
|
@ -123,11 +128,12 @@ class ImageCache {
|
|||
callback(data)
|
||||
}
|
||||
}
|
||||
self.onFinished(data)
|
||||
}
|
||||
}
|
||||
|
||||
class Request {
|
||||
weak var group: RequestGroup?
|
||||
private weak var group: RequestGroup?
|
||||
private(set) var callback: ((Data?) -> Void)?
|
||||
private(set) var cancelled: Bool = false
|
||||
|
||||
|
|
|
@ -37,4 +37,17 @@ extension Status.Visibility {
|
|||
}
|
||||
}
|
||||
|
||||
var unfilledImageName: String {
|
||||
switch self {
|
||||
case .public:
|
||||
return "globe"
|
||||
case .unlisted:
|
||||
return "lock.open"
|
||||
case .private:
|
||||
return "lock"
|
||||
case .direct:
|
||||
return "envelope"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -41,14 +41,18 @@ class Preferences: Codable, ObservableObject {
|
|||
self.showRepliesInProfiles = try container.decode(Bool.self, forKey: .showRepliesInProfiles)
|
||||
self.avatarStyle = try container.decode(AvatarStyle.self, forKey: .avatarStyle)
|
||||
self.hideCustomEmojiInUsernames = try container.decode(Bool.self, forKey: .hideCustomEmojiInUsernames)
|
||||
self.showIsStatusReplyIcon = try container.decode(Bool.self, forKey: .showIsStatusReplyIcon)
|
||||
self.alwaysShowStatusVisibilityIcon = try container.decode(Bool.self, forKey: .alwaysShowStatusVisibilityIcon)
|
||||
|
||||
self.defaultPostVisibility = try container.decode(Status.Visibility.self, forKey: .defaultPostVisibility)
|
||||
self.automaticallySaveDrafts = try container.decode(Bool.self, forKey: .automaticallySaveDrafts)
|
||||
self.requireAttachmentDescriptions = try container.decode(Bool.self, forKey: .requireAttachmentDescriptions)
|
||||
self.contentWarningCopyMode = try container.decode(ContentWarningCopyMode.self, forKey: .contentWarningCopyMode)
|
||||
self.mentionReblogger = try container.decode(Bool.self, forKey: .mentionReblogger)
|
||||
|
||||
self.blurAllMedia = try container.decode(Bool.self, forKey: .blurAllMedia)
|
||||
self.automaticallyPlayGifs = try container.decode(Bool.self, forKey: .automaticallyPlayGifs)
|
||||
|
||||
self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps)
|
||||
self.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari)
|
||||
self.inAppSafariAutomaticReaderMode = try container.decode(Bool.self, forKey: .inAppSafariAutomaticReaderMode)
|
||||
|
@ -67,14 +71,18 @@ class Preferences: Codable, ObservableObject {
|
|||
try container.encode(showRepliesInProfiles, forKey: .showRepliesInProfiles)
|
||||
try container.encode(avatarStyle, forKey: .avatarStyle)
|
||||
try container.encode(hideCustomEmojiInUsernames, forKey: .hideCustomEmojiInUsernames)
|
||||
try container.encode(showIsStatusReplyIcon, forKey: .showIsStatusReplyIcon)
|
||||
try container.encode(alwaysShowStatusVisibilityIcon, forKey: .alwaysShowStatusVisibilityIcon)
|
||||
|
||||
try container.encode(defaultPostVisibility, forKey: .defaultPostVisibility)
|
||||
try container.encode(automaticallySaveDrafts, forKey: .automaticallySaveDrafts)
|
||||
try container.encode(requireAttachmentDescriptions, forKey: .requireAttachmentDescriptions)
|
||||
try container.encode(contentWarningCopyMode, forKey: .contentWarningCopyMode)
|
||||
try container.encode(mentionReblogger, forKey: .mentionReblogger)
|
||||
|
||||
try container.encode(blurAllMedia, forKey: .blurAllMedia)
|
||||
try container.encode(automaticallyPlayGifs, forKey: .automaticallyPlayGifs)
|
||||
|
||||
try container.encode(openLinksInApps, forKey: .openLinksInApps)
|
||||
try container.encode(useInAppSafari, forKey: .useInAppSafari)
|
||||
try container.encode(inAppSafariAutomaticReaderMode, forKey: .inAppSafariAutomaticReaderMode)
|
||||
|
@ -86,29 +94,35 @@ class Preferences: Codable, ObservableObject {
|
|||
try container.encode(statusContentType, forKey: .statusContentType)
|
||||
}
|
||||
|
||||
// MARK: - Appearance
|
||||
// MARK: Appearance
|
||||
@Published var theme = UIUserInterfaceStyle.unspecified
|
||||
@Published var showRepliesInProfiles = false
|
||||
@Published var avatarStyle = AvatarStyle.roundRect
|
||||
@Published var hideCustomEmojiInUsernames = false
|
||||
@Published var showIsStatusReplyIcon = false
|
||||
@Published var alwaysShowStatusVisibilityIcon = false
|
||||
|
||||
// MARK: - Behavior
|
||||
// MARK: Composing
|
||||
@Published var defaultPostVisibility = Status.Visibility.public
|
||||
@Published var automaticallySaveDrafts = true
|
||||
@Published var requireAttachmentDescriptions = false
|
||||
@Published var contentWarningCopyMode = ContentWarningCopyMode.asIs
|
||||
@Published var mentionReblogger = false
|
||||
|
||||
// MARK: Media
|
||||
@Published var blurAllMedia = false
|
||||
@Published var automaticallyPlayGifs = true
|
||||
|
||||
// MARK: Behavior
|
||||
@Published var openLinksInApps = true
|
||||
@Published var useInAppSafari = true
|
||||
@Published var inAppSafariAutomaticReaderMode = false
|
||||
|
||||
// MARK: - Digital Wellness
|
||||
// MARK: Digital Wellness
|
||||
@Published var showFavoriteAndReblogCounts = true
|
||||
@Published var defaultNotificationsMode = NotificationsMode.allNotifications
|
||||
|
||||
// MARK: - Advanced
|
||||
// MARK: Advanced
|
||||
@Published var silentActions: [String: Permission] = [:]
|
||||
@Published var statusContentType: StatusContentType = .plain
|
||||
|
||||
|
@ -117,14 +131,18 @@ class Preferences: Codable, ObservableObject {
|
|||
case showRepliesInProfiles
|
||||
case avatarStyle
|
||||
case hideCustomEmojiInUsernames
|
||||
case showIsStatusReplyIcon
|
||||
case alwaysShowStatusVisibilityIcon
|
||||
|
||||
case defaultPostVisibility
|
||||
case automaticallySaveDrafts
|
||||
case requireAttachmentDescriptions
|
||||
case contentWarningCopyMode
|
||||
case mentionReblogger
|
||||
|
||||
case blurAllMedia
|
||||
case automaticallyPlayGifs
|
||||
|
||||
case openLinksInApps
|
||||
case useInAppSafari
|
||||
case inAppSafariAutomaticReaderMode
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
import CrashReporter
|
||||
import MessageUI
|
||||
|
||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
|
||||
|
@ -21,17 +23,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
|
||||
window = UIWindow(windowScene: windowScene)
|
||||
|
||||
if LocalData.shared.onboardingComplete {
|
||||
if session.mastodonController == nil {
|
||||
let account = LocalData.shared.getMostRecentAccount()!
|
||||
session.mastodonController = MastodonController.getForAccount(account)
|
||||
}
|
||||
|
||||
showAppUI()
|
||||
if let report = AppDelegate.pendingCrashReport {
|
||||
AppDelegate.pendingCrashReport = nil
|
||||
handlePendingCrashReport(report, session: session)
|
||||
} else {
|
||||
showOnboardingUI()
|
||||
showAppOrOnboardingUI(session: session)
|
||||
}
|
||||
|
||||
|
||||
window!.makeKeyAndVisible()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(themePrefChanged), name: .themePreferenceChanged, object: nil)
|
||||
|
@ -113,6 +111,32 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
try! scene.session.mastodonController?.persistentContainer.viewContext.save()
|
||||
}
|
||||
|
||||
private func handlePendingCrashReport(_ report: PLCrashReport, session: UISceneSession) {
|
||||
#if !DEBUG
|
||||
guard MFMailComposeViewController.canSendMail() else {
|
||||
print("Cannot send email")
|
||||
showAppOrOnboardingUI(session: session)
|
||||
return
|
||||
}
|
||||
|
||||
window!.rootViewController = CrashReporterViewController.create(report: report)
|
||||
#endif
|
||||
}
|
||||
|
||||
func showAppOrOnboardingUI(session: UISceneSession? = nil) {
|
||||
let session = session ?? window!.windowScene!.session
|
||||
if LocalData.shared.onboardingComplete {
|
||||
if session.mastodonController == nil {
|
||||
let account = LocalData.shared.getMostRecentAccount()!
|
||||
session.mastodonController = MastodonController.getForAccount(account)
|
||||
}
|
||||
|
||||
showAppUI()
|
||||
} else {
|
||||
showOnboardingUI()
|
||||
}
|
||||
}
|
||||
|
||||
func activateAccount(_ account: LocalData.UserAccountInfo) {
|
||||
LocalData.shared.setMostRecentAccount(account)
|
||||
window!.windowScene!.session.mastodonController = MastodonController.getForAccount(account)
|
||||
|
@ -154,3 +178,11 @@ extension SceneDelegate: OnboardingViewControllerDelegate {
|
|||
activateAccount(account)
|
||||
}
|
||||
}
|
||||
|
||||
extension SceneDelegate: MFMailComposeViewControllerDelegate {
|
||||
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
|
||||
controller.dismiss(animated: true) {
|
||||
self.showAppOrOnboardingUI()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// GalleryPlayerViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 6/21/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AVKit
|
||||
|
||||
class GalleryPlayerViewController: AVPlayerViewController {
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
// starting while audio is already playing from another app often takes nearly a second,
|
||||
// so do it on a background thread as to not block the UI
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
AudioSessionHelper.enable()
|
||||
AudioSessionHelper.setVideoPlayback()
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
// starting often takes around half a second,
|
||||
// so do it on a background thread as to not block the UI
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
AudioSessionHelper.setDefault()
|
||||
AudioSessionHelper.disable()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -28,8 +28,8 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
|
|||
|
||||
var animationSourceView: UIImageView? { sourceViews[currentIndex] }
|
||||
var animationImage: UIImage? {
|
||||
if let page = pages[currentIndex] as? LoadingLargeImageViewController,
|
||||
let image = page.largeImageVC?.image {
|
||||
if let page = pages[currentIndex] as? LargeImageAnimatableViewController,
|
||||
let image = page.animationImage {
|
||||
return image
|
||||
} else {
|
||||
return animationSourceView?.image
|
||||
|
@ -65,18 +65,29 @@ class GalleryViewController: UIPageViewController, UIPageViewControllerDataSourc
|
|||
self.sourceViews = WeakArray(sourceViews)
|
||||
self.startIndex = startIndex
|
||||
|
||||
self.pages = attachments.map {
|
||||
switch $0.kind {
|
||||
self.pages = attachments.enumerated().map { (index, attachment) in
|
||||
switch attachment.kind {
|
||||
case .image:
|
||||
let vc = LoadingLargeImageViewController(attachment: $0)
|
||||
let vc = LoadingLargeImageViewController(attachment: attachment)
|
||||
vc.shrinkGestureEnabled = false
|
||||
return vc
|
||||
case .video, .audio:
|
||||
let vc = AVPlayerViewController()
|
||||
vc.player = AVPlayer(url: $0.url)
|
||||
let vc = GalleryPlayerViewController()
|
||||
vc.player = AVPlayer(url: attachment.url)
|
||||
return vc
|
||||
case .gifv:
|
||||
return GifvAttachmentViewController(attachment: $0)
|
||||
// Passing the source view to the LargeImageGifvContentView is a crappy workaround for not
|
||||
// having the video size directly inside the content view. This will break when there
|
||||
// are more than 4 attachments and there is a gifv at index >= 3 (the More... button will show
|
||||
// in place of the fourth attachment, so there aren't source views for the attachments at index >= 3).
|
||||
// Really, what should happen is the LargeImageGifvContentView should get the size of the video from
|
||||
// the AVFoundation instead of the source view.
|
||||
// This isn't a priority as only Mastodon converts gifs to gifvs, and Mastodon (in its default configuration,
|
||||
// I don't know about forks) doesn't allow more than four attachments, meaning there will always be a source view.
|
||||
let gifvContentView = LargeImageGifvContentView(attachment: attachment, source: sourceViews[index]!)
|
||||
let vc = LargeImageViewController(contentView: gifvContentView, description: attachment.description, sourceView: nil)
|
||||
vc.shrinkGestureEnabled = false
|
||||
return vc
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@ class BookmarksTableViewController: EnhancedTableViewController {
|
|||
|
||||
let mastodonController: MastodonController
|
||||
|
||||
private var loaded = false
|
||||
|
||||
var statuses: [(id: String, state: StatusState)] = []
|
||||
|
||||
var newer: RequestRange?
|
||||
|
@ -41,21 +43,30 @@ class BookmarksTableViewController: EnhancedTableViewController {
|
|||
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: statusCell)
|
||||
|
||||
tableView.prefetchDataSource = self
|
||||
|
||||
userActivity = UserActivityManager.bookmarksActivity()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
let request = Client.getBookmarks()
|
||||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(statuses, pagination) = response else { fatalError() }
|
||||
self.mastodonController.persistentContainer.addAll(statuses: statuses)
|
||||
self.statuses.append(contentsOf: statuses.map { ($0.id, .unknown) })
|
||||
self.newer = pagination?.newer
|
||||
self.older = pagination?.older
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.tableView.reloadData()
|
||||
if !loaded {
|
||||
loaded = true
|
||||
|
||||
let request = Client.getBookmarks()
|
||||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(statuses, pagination) = response else { fatalError() }
|
||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||
self.statuses.append(contentsOf: statuses.map { ($0.id, .unknown) })
|
||||
self.newer = pagination?.newer
|
||||
self.older = pagination?.older
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userActivity = UserActivityManager.bookmarksActivity()
|
||||
}
|
||||
|
||||
// MARK: - Table view data source
|
||||
|
@ -87,15 +98,16 @@ class BookmarksTableViewController: EnhancedTableViewController {
|
|||
mastodonController.run(request) { (response) in
|
||||
guard case let .success(newStatuses, pagination) = response else { fatalError() }
|
||||
self.older = pagination?.older
|
||||
self.mastodonController.persistentContainer.addAll(statuses: newStatuses)
|
||||
let newIndexPaths = (self.statuses.count..<(self.statuses.count + newStatuses.count)).map {
|
||||
IndexPath(row: $0, section: 0)
|
||||
}
|
||||
self.statuses.append(contentsOf: newStatuses.map { ($0.id, .unknown) })
|
||||
|
||||
DispatchQueue.main.async {
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertRows(at: newIndexPaths, with: .automatic)
|
||||
self.mastodonController.persistentContainer.addAll(statuses: newStatuses) {
|
||||
let newIndexPaths = (self.statuses.count..<(self.statuses.count + newStatuses.count)).map {
|
||||
IndexPath(row: $0, section: 0)
|
||||
}
|
||||
self.statuses.append(contentsOf: newStatuses.map { ($0.id, .unknown) })
|
||||
|
||||
DispatchQueue.main.async {
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertRows(at: newIndexPaths, with: .automatic)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import UIKit
|
|||
import Pachyderm
|
||||
import MobileCoreServices
|
||||
import PencilKit
|
||||
import Photos
|
||||
|
||||
protocol ComposeAttachmentsViewControllerDelegate: class {
|
||||
func composeSelectedAttachmentsDidChange()
|
||||
|
@ -314,6 +315,15 @@ class ComposeAttachmentsViewController: UITableViewController {
|
|||
actions.append(UIAction(title: "Edit Drawing", image: UIImage(systemName: "hand.draw"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
|
||||
self.presentComposeDrawingViewController(editingAttachmentAt: indexPath.row)
|
||||
}))
|
||||
case .asset(_), .image(_):
|
||||
if attachment.data.type == .image,
|
||||
let cell = tableView.cellForRow(at: indexPath) as? ComposeAttachmentTableViewCell {
|
||||
|
||||
let title = NSLocalizedString("Recognize Text", comment: "recognize image attachment text menu item title")
|
||||
actions.append(UIAction(title: title, image: UIImage(systemName: "doc.text.viewfinder"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
|
||||
cell.recognizeTextFromImage()
|
||||
}))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
@ -364,18 +374,23 @@ class ComposeAttachmentsViewController: UITableViewController {
|
|||
// MARK: Interaction
|
||||
|
||||
func addAttachmentPressed() {
|
||||
if traitCollection.horizontalSizeClass == .compact {
|
||||
let sheetContainer = AssetPickerSheetContainerViewController()
|
||||
sheetContainer.assetPicker.assetPickerDelegate = self
|
||||
present(sheetContainer, animated: true)
|
||||
} else {
|
||||
let picker = AssetPickerViewController()
|
||||
picker.assetPickerDelegate = self
|
||||
picker.overrideUserInterfaceStyle = .dark
|
||||
picker.modalPresentationStyle = .popover
|
||||
present(picker, animated: true)
|
||||
if let presentationController = picker.presentationController as? UIPopoverPresentationController {
|
||||
presentationController.sourceView = tableView.cellForRow(at: IndexPath(row: 0, section: 1))
|
||||
PHPhotoLibrary.requestAuthorization { (status) in
|
||||
guard status == .authorized else { return }
|
||||
DispatchQueue.main.async {
|
||||
if self.traitCollection.horizontalSizeClass == .compact {
|
||||
let sheetContainer = AssetPickerSheetContainerViewController()
|
||||
sheetContainer.assetPicker.assetPickerDelegate = self
|
||||
self.present(sheetContainer, animated: true)
|
||||
} else {
|
||||
let picker = AssetPickerViewController()
|
||||
picker.assetPickerDelegate = self
|
||||
picker.overrideUserInterfaceStyle = .dark
|
||||
picker.modalPresentationStyle = .popover
|
||||
self.present(picker, animated: true)
|
||||
if let presentationController = picker.presentationController as? UIPopoverPresentationController {
|
||||
presentationController.sourceView = self.tableView.cellForRow(at: IndexPath(row: 0, section: 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -497,6 +512,10 @@ extension ComposeAttachmentsViewController: AssetPickerViewControllerDelegate {
|
|||
}
|
||||
|
||||
extension ComposeAttachmentsViewController: ComposeAttachmentTableViewCellDelegate {
|
||||
func composeAttachment(_ cell: ComposeAttachmentTableViewCell, present viewController: UIViewController, animated: Bool) {
|
||||
self.present(viewController, animated: animated)
|
||||
}
|
||||
|
||||
func removeAttachment(_ cell: ComposeAttachmentTableViewCell) {
|
||||
guard let indexPath = tableView.indexPath(for: cell) else { return }
|
||||
attachments.remove(at: indexPath.row)
|
||||
|
@ -512,6 +531,12 @@ extension ComposeAttachmentsViewController: ComposeAttachmentTableViewCellDelega
|
|||
func attachmentDescriptionChanged(_ cell: ComposeAttachmentTableViewCell) {
|
||||
delegate?.composeRequiresAttachmentDescriptionsDidChange()
|
||||
}
|
||||
|
||||
func composeAttachmentDescriptionHeightChanged(_ cell: ComposeAttachmentTableViewCell) {
|
||||
tableView.performBatchUpdates(nil) { (_) in
|
||||
self.updateHeightConstraint()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeAttachmentsViewController: ComposeDrawingViewControllerDelegate {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<?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">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16097" 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="16082.1"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
|
@ -163,7 +163,7 @@
|
|||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="Tq7-6P-hMT" secondAttribute="trailing" id="GeN-8q-weq"/>
|
||||
<constraint firstItem="Heg-g4-sYM" firstAttribute="bottom" secondItem="6Z0-Vy-hMX" secondAttribute="bottom" id="Hf3-Cc-mVX"/>
|
||||
<constraint firstAttribute="bottom" secondItem="6Z0-Vy-hMX" secondAttribute="bottom" id="Hf3-Cc-mVX"/>
|
||||
<constraint firstItem="Tq7-6P-hMT" firstAttribute="top" secondItem="Heg-g4-sYM" secondAttribute="top" id="LgA-xu-VGE"/>
|
||||
<constraint firstItem="Tq7-6P-hMT" firstAttribute="leading" secondItem="7XG-Dk-OGm" secondAttribute="leading" id="agM-ZO-c3E"/>
|
||||
<constraint firstItem="Heg-g4-sYM" firstAttribute="trailing" secondItem="6Z0-Vy-hMX" secondAttribute="trailing" id="hjY-W6-wTQ"/>
|
||||
|
|
|
@ -127,6 +127,7 @@ class ConversationTableViewController: EnhancedTableViewController {
|
|||
} else {
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() }
|
||||
cell.showStatusAutomatically = showStatusesAutomatically
|
||||
cell.showReplyIndicator = false
|
||||
cell.delegate = self
|
||||
cell.updateUI(statusID: id, state: state)
|
||||
return cell
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
//
|
||||
// CrashReporterViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 6/20/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CrashReporter
|
||||
import MessageUI
|
||||
|
||||
class CrashReporterViewController: UIViewController {
|
||||
|
||||
private let report: PLCrashReport
|
||||
private var reportText: String!
|
||||
|
||||
private var reportFilename: String {
|
||||
let timestamp = ISO8601DateFormatter().string(from: report.systemInfo.timestamp)
|
||||
return "Tusker-crash-\(timestamp).crash"
|
||||
}
|
||||
|
||||
@IBOutlet weak var crashReportTextView: UITextView!
|
||||
@IBOutlet weak var sendReportButton: UIButton!
|
||||
|
||||
static func create(report: PLCrashReport) -> UINavigationController {
|
||||
let nav = UINavigationController(rootViewController: CrashReporterViewController(report: report))
|
||||
nav.navigationBar.prefersLargeTitles = true
|
||||
return nav
|
||||
}
|
||||
|
||||
private init(report: PLCrashReport){
|
||||
self.report = report
|
||||
|
||||
super.init(nibName: "CrashReporterViewController", bundle: .main)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
navigationItem.title = NSLocalizedString("Crash Detected", comment: "crash reporter title")
|
||||
navigationItem.largeTitleDisplayMode = .always
|
||||
|
||||
crashReportTextView.font = .monospacedSystemFont(ofSize: 14, weight: .regular)
|
||||
|
||||
reportText = PLCrashReportTextFormatter.stringValue(for: report, with: PLCrashReportTextFormatiOS)!
|
||||
let info = "Tusker has detected that it crashed the last time it was running. You can email the report to the developer or skip sending and continue to the app. You may review the report below before sending.\n\nIf you choose to send the report, please include any additional details about what you were doing prior to the crash that may be pertinent.\n\n"
|
||||
let attributed = NSMutableAttributedString()
|
||||
attributed.append(NSAttributedString(string: info, attributes: [
|
||||
NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17),
|
||||
NSAttributedString.Key.foregroundColor: UIColor.label
|
||||
]))
|
||||
attributed.append(NSAttributedString(string: reportText, attributes: [
|
||||
NSAttributedString.Key.font: UIFont.monospacedSystemFont(ofSize: 14, weight: .regular),
|
||||
NSAttributedString.Key.foregroundColor: UIColor.label
|
||||
]))
|
||||
crashReportTextView.attributedText = attributed
|
||||
|
||||
sendReportButton.layer.cornerRadius = 12.5
|
||||
sendReportButton.layer.masksToBounds = true
|
||||
|
||||
sendReportButton.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(sendReportButtonLongPressed)))
|
||||
}
|
||||
|
||||
private func updateSendReportButtonColor(lightened: Bool, animate: Bool) {
|
||||
let color: UIColor
|
||||
if lightened {
|
||||
var hue: CGFloat = 0, saturation: CGFloat = 0, brightness: CGFloat = 0, alpha: CGFloat = 0
|
||||
UIColor.systemBlue.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)
|
||||
color = UIColor(hue: hue, saturation: 0.85 * saturation, brightness: brightness, alpha: alpha)
|
||||
} else {
|
||||
color = .systemBlue
|
||||
}
|
||||
if animate {
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.sendReportButton.backgroundColor = color
|
||||
}
|
||||
} else {
|
||||
sendReportButton.backgroundColor = color
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func sendReportTouchDown(_ sender: Any) {
|
||||
updateSendReportButtonColor(lightened: true, animate: false)
|
||||
}
|
||||
|
||||
@IBAction func sendReportButtonTouchDragExit(_ sender: Any) {
|
||||
updateSendReportButtonColor(lightened: false, animate: true)
|
||||
}
|
||||
|
||||
@IBAction func sendReportButtonTouchDragEnter(_ sender: Any) {
|
||||
updateSendReportButtonColor(lightened: true, animate: true)
|
||||
}
|
||||
|
||||
@IBAction func sendReportTouchUpInside(_ sender: Any) {
|
||||
updateSendReportButtonColor(lightened: false, animate: true)
|
||||
|
||||
let composeVC = MFMailComposeViewController()
|
||||
composeVC.mailComposeDelegate = self
|
||||
composeVC.setToRecipients(["me@shadowfacts.net"])
|
||||
composeVC.setSubject("Tusker Crash Report")
|
||||
|
||||
let data = reportText.data(using: .utf8)!
|
||||
composeVC.addAttachmentData(data, mimeType: "text/plain", fileName: reportFilename)
|
||||
|
||||
self.present(composeVC, animated: true)
|
||||
}
|
||||
|
||||
@objc func sendReportButtonLongPressed() {
|
||||
let dir = FileManager.default.temporaryDirectory
|
||||
let url = dir.appendingPathComponent(reportFilename)
|
||||
try! reportText.data(using: .utf8)!.write(to: url)
|
||||
let activityController = UIActivityViewController(activityItems: [url], applicationActivities: nil)
|
||||
present(activityController, animated: true)
|
||||
}
|
||||
|
||||
@IBAction func cancelPressed(_ sender: Any) {
|
||||
(view.window!.windowScene!.delegate as! SceneDelegate).showAppOrOnboardingUI()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension CrashReporterViewController: MFMailComposeViewControllerDelegate {
|
||||
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
|
||||
controller.dismiss(animated: true) {
|
||||
(self.view.window!.windowScene!.delegate as! SceneDelegate).showAppOrOnboardingUI()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16097" 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="16087"/>
|
||||
<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" customClass="CrashReporterViewController" customModule="Tusker" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="crashReportTextView" destination="hxN-7J-Usc" id="TGd-yq-Ds5"/>
|
||||
<outlet property="sendReportButton" destination="Ofm-5l-nAp" id="6xM-hz-uvw"/>
|
||||
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
|
||||
</connections>
|
||||
</placeholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="a8U-KI-8PM">
|
||||
<rect key="frame" x="0.0" y="44" width="414" height="818"/>
|
||||
<subviews>
|
||||
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" verticalHuggingPriority="249" translatesAutoresizingMaskIntoConstraints="NO" id="uQy-Yw-Dba">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="722"/>
|
||||
<subviews>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" verticalHuggingPriority="249" scrollEnabled="NO" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="hxN-7J-Usc">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="166.5"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<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" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="LRh-7Z-mV1" firstAttribute="trailing" secondItem="hxN-7J-Usc" secondAttribute="trailing" id="1GJ-Nk-pIj"/>
|
||||
<constraint firstItem="hxN-7J-Usc" firstAttribute="leading" secondItem="LRh-7Z-mV1" secondAttribute="leading" id="dtj-bw-No1"/>
|
||||
<constraint firstItem="hxN-7J-Usc" firstAttribute="top" secondItem="LRh-7Z-mV1" secondAttribute="top" id="nvk-aw-YqH"/>
|
||||
<constraint firstItem="LRh-7Z-mV1" firstAttribute="bottom" secondItem="hxN-7J-Usc" secondAttribute="bottom" id="xAU-OS-1Zy"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="contentLayoutGuide" id="LRh-7Z-mV1"/>
|
||||
<viewLayoutGuide key="frameLayoutGuide" id="Rgd-t7-8QN"/>
|
||||
</scrollView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Ofm-5l-nAp">
|
||||
<rect key="frame" x="52" y="730" width="310.5" height="50"/>
|
||||
<color key="backgroundColor" systemColor="systemBlueColor" red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="50" id="jHf-W0-qQn"/>
|
||||
</constraints>
|
||||
<state key="normal" title="Send Crash Report">
|
||||
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="sendReportButtonTouchDragEnter:" destination="-1" eventType="touchDragEnter" id="jSF-RZ-NsG"/>
|
||||
<action selector="sendReportButtonTouchDragExit:" destination="-1" eventType="touchDragExit" id="CrB-QL-bN1"/>
|
||||
<action selector="sendReportTouchDown:" destination="-1" eventType="touchDown" id="P5K-4n-tO1"/>
|
||||
<action selector="sendReportTouchUpInside:" destination="-1" eventType="touchUpInside" id="ggd-fm-Orq"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="JiJ-Ng-jOz">
|
||||
<rect key="frame" x="169" y="788" width="76" height="30"/>
|
||||
<state key="normal" title="Don't Send"/>
|
||||
<connections>
|
||||
<action selector="cancelPressed:" destination="-1" eventType="touchUpInside" id="o4R-0Q-STS"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="uQy-Yw-Dba" firstAttribute="width" secondItem="i5M-Pr-FkT" secondAttribute="width" id="AX2-9e-cO0"/>
|
||||
<constraint firstItem="fnl-2z-Ty3" firstAttribute="bottom" secondItem="a8U-KI-8PM" secondAttribute="bottom" id="Ec3-Px-dSW"/>
|
||||
<constraint firstItem="hxN-7J-Usc" firstAttribute="width" secondItem="i5M-Pr-FkT" secondAttribute="width" id="Evf-Yz-Iui"/>
|
||||
<constraint firstItem="a8U-KI-8PM" firstAttribute="leading" secondItem="fnl-2z-Ty3" secondAttribute="leading" id="NDN-Jl-viT"/>
|
||||
<constraint firstItem="a8U-KI-8PM" firstAttribute="top" secondItem="fnl-2z-Ty3" secondAttribute="top" id="O8Y-sl-55I"/>
|
||||
<constraint firstItem="a8U-KI-8PM" firstAttribute="trailing" secondItem="fnl-2z-Ty3" secondAttribute="trailing" id="f59-qB-5T7"/>
|
||||
<constraint firstItem="Ofm-5l-nAp" firstAttribute="width" secondItem="i5M-Pr-FkT" secondAttribute="width" multiplier="0.75" id="ueo-xb-Tfm"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="fnl-2z-Ty3"/>
|
||||
<point key="canvasLocation" x="133" y="154"/>
|
||||
</view>
|
||||
</objects>
|
||||
</document>
|
|
@ -0,0 +1,84 @@
|
|||
//
|
||||
// LargeImageContentView.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 6/17/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Gifu
|
||||
import Pachyderm
|
||||
import AVFoundation
|
||||
|
||||
protocol LargeImageContentView {
|
||||
var animationImage: UIImage? { get }
|
||||
var animationGifData: Data? { get }
|
||||
var activityItemsForSharing: [Any] { get }
|
||||
}
|
||||
|
||||
class LargeImageImageContentView: UIImageView, GIFAnimatable, LargeImageContentView {
|
||||
lazy var animator: Animator? = {
|
||||
return Animator(withDelegate: self)
|
||||
}()
|
||||
|
||||
var animationImage: UIImage? { image! }
|
||||
let animationGifData: Data?
|
||||
|
||||
var activityItemsForSharing: [Any] {
|
||||
[image!]
|
||||
}
|
||||
|
||||
init(image: UIImage, gifData: Data?) {
|
||||
self.animationGifData = gifData
|
||||
|
||||
super.init(image: image)
|
||||
|
||||
contentMode = .scaleAspectFit
|
||||
|
||||
if let data = gifData {
|
||||
self.animate(withGIFData: data)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override public func display(_ layer: CALayer) {
|
||||
updateImageIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
class LargeImageGifvContentView: GifvAttachmentView, LargeImageContentView {
|
||||
private(set) var animationImage: UIImage?
|
||||
var animationGifData: Data? { nil }
|
||||
var activityItemsForSharing: [Any] {
|
||||
// todo: what should we share for gifvs?
|
||||
// some SO posts indicate that just sharing a URL to the video should work, but that may need to be a local URL?
|
||||
[]
|
||||
}
|
||||
|
||||
private let asset: AVURLAsset
|
||||
|
||||
// The content view needs to supply an intrinsicContentSize for the LargeImageViewController to handle layout/scrolling/zooming correctly
|
||||
override var intrinsicContentSize: CGSize {
|
||||
// This is a really sucky workaround for the fact that in the content view, we don't have access to the size of the underlying video.
|
||||
// There's probably some way of getting this from the AVPlayer/AVAsset directly
|
||||
animationImage?.size ?? CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
|
||||
}
|
||||
|
||||
init(attachment: Attachment, source: UIImageView) {
|
||||
precondition(attachment.kind == .gifv)
|
||||
|
||||
self.asset = AVURLAsset(url: attachment.url)
|
||||
|
||||
super.init(asset: asset, gravity: .resizeAspect)
|
||||
|
||||
self.animationImage = source.image
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
|
@ -7,22 +7,17 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import Gifu
|
||||
|
||||
class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeImageAnimatableViewController {
|
||||
|
||||
typealias ContentView = UIView & LargeImageContentView
|
||||
|
||||
weak var animationSourceView: UIImageView?
|
||||
var animationImage: UIImage? { image ?? animationSourceView?.image }
|
||||
var animationGifData: Data? { gifData }
|
||||
var animationImage: UIImage? { contentView.animationImage }
|
||||
var animationGifData: Data? { contentView.animationGifData }
|
||||
var dismissInteractionController: LargeImageInteractionController?
|
||||
|
||||
@IBOutlet weak var scrollView: UIScrollView!
|
||||
@IBOutlet weak var imageView: GIFImageView!
|
||||
@IBOutlet weak var imageViewLeadingConstraint: NSLayoutConstraint!
|
||||
@IBOutlet weak var imageViewTrailingConstraint: NSLayoutConstraint!
|
||||
@IBOutlet weak var imageViewTopConstraint: NSLayoutConstraint!
|
||||
@IBOutlet weak var imageViewBottomConstraint: NSLayoutConstraint!
|
||||
|
||||
@IBOutlet weak var topControlsView: UIView!
|
||||
@IBOutlet weak var topControlsHeightConstraint: NSLayoutConstraint!
|
||||
@IBOutlet weak var shareButton: UIButton!
|
||||
|
@ -35,8 +30,10 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
|||
@IBOutlet weak var bottomControlsView: UIView!
|
||||
@IBOutlet weak var descriptionLabel: UILabel!
|
||||
|
||||
var image: UIImage?
|
||||
var gifData: Data?
|
||||
var contentView: ContentView
|
||||
var contentViewLeadingConstraint: NSLayoutConstraint!
|
||||
var contentViewTopConstraint: NSLayoutConstraint!
|
||||
|
||||
var imageDescription: String?
|
||||
|
||||
var initialControlsVisible = true
|
||||
|
@ -57,10 +54,11 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
|||
return !controlsVisible
|
||||
}
|
||||
|
||||
init(image: UIImage, description: String?, sourceView: UIImageView?) {
|
||||
self.image = image
|
||||
init(contentView: ContentView, description: String?, sourceView: UIImageView?) {
|
||||
self.imageDescription = description
|
||||
self.animationSourceView = sourceView
|
||||
|
||||
self.contentView = contentView
|
||||
|
||||
super.init(nibName: "LargeImageViewController", bundle: nil)
|
||||
|
||||
|
@ -74,16 +72,20 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
scrollView.addSubview(contentView)
|
||||
contentViewLeadingConstraint = contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor)
|
||||
contentViewTopConstraint = contentView.topAnchor.constraint(equalTo: scrollView.topAnchor)
|
||||
NSLayoutConstraint.activate([
|
||||
contentViewLeadingConstraint,
|
||||
contentViewTopConstraint,
|
||||
])
|
||||
|
||||
setControlsVisible(initialControlsVisible, animated: false)
|
||||
|
||||
imageView.image = image
|
||||
if let gifData = gifData {
|
||||
imageView.animate(withGIFData: gifData)
|
||||
}
|
||||
|
||||
shareButton.isEnabled = !contentView.activityItemsForSharing.isEmpty
|
||||
|
||||
scrollView.delegate = self
|
||||
imageView.bounds = CGRect(origin: .zero, size: imageView.image!.size)
|
||||
|
||||
|
||||
if let imageDescription = imageDescription {
|
||||
descriptionLabel.text = imageDescription
|
||||
} else {
|
||||
|
@ -99,23 +101,24 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
|||
doubleTap.numberOfTapsRequired = 2
|
||||
view.addGestureRecognizer(doubleTap)
|
||||
}
|
||||
|
||||
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
// todo: does this need to be in viewDidLayoutSubviews?
|
||||
// limit the image height to the safe area height, so the image doesn't overlap the top controls
|
||||
// while zoomed all the way out
|
||||
let maxHeight = view.bounds.height - view.safeAreaInsets.top - view.safeAreaInsets.bottom
|
||||
let heightScale = maxHeight / imageView.bounds.height
|
||||
let widthScale = view.bounds.width / imageView.bounds.width
|
||||
let heightScale = maxHeight / contentView.intrinsicContentSize.height
|
||||
let widthScale = view.bounds.width / contentView.intrinsicContentSize.width
|
||||
let minScale = min(widthScale, heightScale)
|
||||
scrollView.minimumZoomScale = minScale
|
||||
scrollView.zoomScale = minScale
|
||||
scrollView.maximumZoomScale = minScale >= 1 ? minScale + 2 : 2
|
||||
|
||||
|
||||
centerImage()
|
||||
|
||||
// todo: does this need to be in viewDidLayoutSubviews?
|
||||
if view.safeAreaInsets.top == 44 {
|
||||
// running on iPhone X style notched device
|
||||
let notchWidth: CGFloat = 209
|
||||
|
@ -147,7 +150,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
|||
}
|
||||
|
||||
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
||||
return imageView
|
||||
return contentView
|
||||
}
|
||||
|
||||
func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
||||
|
@ -163,18 +166,18 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
|||
}
|
||||
|
||||
func centerImage() {
|
||||
let yOffset = max(0, (view.bounds.size.height - imageView.frame.height) / 2)
|
||||
imageViewTopConstraint.constant = yOffset
|
||||
let yOffset = max(0, (view.bounds.size.height - contentView.frame.height) / 2)
|
||||
contentViewTopConstraint.constant = yOffset
|
||||
|
||||
let xOffset = max(0, (view.bounds.size.width - imageView.frame.width) / 2)
|
||||
imageViewLeadingConstraint.constant = xOffset
|
||||
let xOffset = max(0, (view.bounds.size.width - contentView.frame.width) / 2)
|
||||
contentViewLeadingConstraint.constant = xOffset
|
||||
}
|
||||
|
||||
func zoomRectFor(scale: CGFloat, center: CGPoint) -> CGRect {
|
||||
var zoomRect = CGRect.zero
|
||||
zoomRect.size.width = imageView.frame.width / scale
|
||||
zoomRect.size.height = imageView.frame.height / scale
|
||||
let newCenter = scrollView.convert(center, to: imageView)
|
||||
zoomRect.size.width = contentView.frame.width / scale
|
||||
zoomRect.size.height = contentView.frame.height / scale
|
||||
let newCenter = scrollView.convert(center, to: contentView)
|
||||
zoomRect.origin.x = newCenter.x - (zoomRect.width / 2)
|
||||
zoomRect.origin.y = newCenter.y - (zoomRect.height / 2)
|
||||
return zoomRect
|
||||
|
@ -225,8 +228,8 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
|||
}
|
||||
|
||||
@IBAction func sharePressed(_ sender: Any) {
|
||||
guard let image = image else { return }
|
||||
let activityVC = UIActivityViewController(activityItems: [image], applicationActivities: nil)
|
||||
let activityVC = UIActivityViewController(activityItems: contentView.activityItemsForSharing, applicationActivities: nil)
|
||||
activityVC.popoverPresentationController?.sourceView = shareButton
|
||||
present(activityVC, animated: true)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14845" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16097" 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="14799.2"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
|
@ -14,9 +14,6 @@
|
|||
<outlet property="closeButtonTopConstraint" destination="ImD-2H-0XK" id="DUe-b1-a2N"/>
|
||||
<outlet property="closeButtonTrailingConstraint" destination="JFe-ig-3Ic" id="cWO-Rr-y3F"/>
|
||||
<outlet property="descriptionLabel" destination="eo5-fc-RV8" id="vrW-RJ-y5k"/>
|
||||
<outlet property="imageView" destination="qcn-1t-3sS" id="Q01-G2-y1c"/>
|
||||
<outlet property="imageViewLeadingConstraint" destination="bI3-V8-M70" id="nIe-xI-E9u"/>
|
||||
<outlet property="imageViewTopConstraint" destination="tfL-hp-2I2" id="EDV-RO-pTe"/>
|
||||
<outlet property="scrollView" destination="Skj-xq-AgQ" id="TFb-zF-m1b"/>
|
||||
<outlet property="shareButton" destination="vhp-0u-Q0S" id="JZS-K9-4w9"/>
|
||||
<outlet property="shareButtonLeadingConstraint" destination="MJx-2r-p0k" id="Dn5-Eg-Pid"/>
|
||||
|
@ -31,24 +28,14 @@
|
|||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" ambiguous="YES" minimumZoomScale="0.25" maximumZoomScale="2" translatesAutoresizingMaskIntoConstraints="NO" id="Skj-xq-AgQ">
|
||||
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" minimumZoomScale="0.25" maximumZoomScale="2" translatesAutoresizingMaskIntoConstraints="NO" id="Skj-xq-AgQ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<subviews>
|
||||
<imageView contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="qcn-1t-3sS" customClass="GIFImageView" customModule="Gifu">
|
||||
<rect key="frame" x="0.0" y="-10" width="375" height="647"/>
|
||||
<gestureRecognizers/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<gestureRecognizers/>
|
||||
<constraints>
|
||||
<constraint firstItem="qcn-1t-3sS" firstAttribute="leading" secondItem="Skj-xq-AgQ" secondAttribute="leading" id="bI3-V8-M70"/>
|
||||
<constraint firstItem="qcn-1t-3sS" firstAttribute="top" secondItem="Skj-xq-AgQ" secondAttribute="top" id="tfL-hp-2I2"/>
|
||||
</constraints>
|
||||
</scrollView>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="kHo-B9-R7a">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="36"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="vhp-0u-Q0S">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="vhp-0u-Q0S">
|
||||
<rect key="frame" x="16" y="16" width="20" height="20"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="20" id="4tF-oL-qXT"/>
|
||||
|
@ -60,7 +47,7 @@
|
|||
<action selector="sharePressed:" destination="-1" eventType="touchUpInside" id="7Oz-zv-m2t"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="pnA-ne-k0v">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="pnA-ne-k0v">
|
||||
<rect key="frame" x="339" y="16" width="20" height="20"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="20" id="eg0-hN-rda"/>
|
||||
|
@ -119,7 +106,7 @@
|
|||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="square.and.arrow.up" catalog="system" width="56" height="64"/>
|
||||
<image name="xmark" catalog="system" width="64" height="56"/>
|
||||
<image name="square.and.arrow.up" catalog="system" width="115" height="128"/>
|
||||
<image name="xmark" catalog="system" width="128" height="113"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
|
|
@ -36,8 +36,8 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
|||
var shrinkGestureEnabled = true
|
||||
|
||||
weak var animationSourceView: UIImageView?
|
||||
var animationImage: UIImage? { largeImageVC?.image ?? animationSourceView?.image }
|
||||
var animationGifData: Data? { largeImageVC?.gifData }
|
||||
var animationImage: UIImage? { largeImageVC?.animationImage ?? animationSourceView?.image }
|
||||
var animationGifData: Data? { largeImageVC?.animationGifData }
|
||||
var dismissInteractionController: LargeImageInteractionController?
|
||||
|
||||
override var prefersStatusBarHidden: Bool {
|
||||
|
@ -108,12 +108,12 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
|||
|
||||
func createLargeImage(data: Data) {
|
||||
guard let image = UIImage(data: data) else { return }
|
||||
largeImageVC = LargeImageViewController(image: image, description: imageDescription, sourceView: animationSourceView)
|
||||
let gifData = url.pathExtension == "gif" ? data : nil
|
||||
let imageView = LargeImageImageContentView(image: image, gifData: gifData)
|
||||
|
||||
largeImageVC = LargeImageViewController(contentView: imageView, description: imageDescription, sourceView: animationSourceView)
|
||||
largeImageVC!.initialControlsVisible = initialControlsVisible
|
||||
largeImageVC!.shrinkGestureEnabled = false
|
||||
if url.pathExtension == "gif" {
|
||||
largeImageVC!.gifData = data
|
||||
}
|
||||
embedChild(largeImageVC!)
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,8 @@ class NotificationsTableViewController: EnhancedTableViewController {
|
|||
|
||||
let excludedTypes: [Pachyderm.Notification.Kind]
|
||||
let groupTypes = [Notification.Kind.favourite, .reblog, .follow]
|
||||
|
||||
private var loaded = false
|
||||
|
||||
var groups: [NotificationGroup] = []
|
||||
|
||||
|
@ -54,21 +56,29 @@ class NotificationsTableViewController: EnhancedTableViewController {
|
|||
tableView.register(UINib(nibName: "BasicTableViewCell", bundle: .main), forCellReuseIdentifier: unknownCell)
|
||||
|
||||
tableView.prefetchDataSource = self
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
let request = Client.getNotifications(excludeTypes: excludedTypes)
|
||||
mastodonController.run(request) { result in
|
||||
guard case let .success(notifications, pagination) = result else { fatalError() }
|
||||
if !loaded {
|
||||
loaded = true
|
||||
|
||||
let groups = NotificationGroup.createGroups(notifications: notifications, only: self.groupTypes)
|
||||
|
||||
self.groups.append(contentsOf: groups)
|
||||
|
||||
self.newer = pagination?.newer
|
||||
self.older = pagination?.older
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(notifications: notifications) {
|
||||
DispatchQueue.main.async {
|
||||
self.tableView.reloadData()
|
||||
let request = Client.getNotifications(excludeTypes: excludedTypes)
|
||||
mastodonController.run(request) { result in
|
||||
guard case let .success(notifications, pagination) = result else { fatalError() }
|
||||
|
||||
let groups = NotificationGroup.createGroups(notifications: notifications, only: self.groupTypes)
|
||||
|
||||
self.groups.append(contentsOf: groups)
|
||||
|
||||
self.newer = pagination?.newer
|
||||
self.older = pagination?.older
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(notifications: notifications) {
|
||||
DispatchQueue.main.async {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,12 @@ struct AppearancePrefsView : View {
|
|||
Toggle(isOn: $preferences.hideCustomEmojiInUsernames) {
|
||||
Text("Hide Custom Emoji in Usernames")
|
||||
}
|
||||
Toggle(isOn: $preferences.showIsStatusReplyIcon) {
|
||||
Text("Show Status Reply Icons")
|
||||
}
|
||||
Toggle(isOn: $preferences.alwaysShowStatusVisibilityIcon) {
|
||||
Text("Always Show Status Visibility Icons")
|
||||
}
|
||||
}
|
||||
.listStyle(GroupedListStyle())
|
||||
.navigationBarTitle(Text("Appearance"))
|
||||
|
|
|
@ -14,36 +14,16 @@ class ProfileTableViewController: EnhancedTableViewController {
|
|||
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
var accountID: String! {
|
||||
didSet {
|
||||
if shouldLoadOnAccountIDSet {
|
||||
DispatchQueue.main.async {
|
||||
self.updateAccountUI()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var accountID: String!
|
||||
|
||||
var pinnedStatuses: [(id: String, state: StatusState)] = [] {
|
||||
didSet {
|
||||
DispatchQueue.main.async {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
var timelineSegments: [[(id: String, state: StatusState)]] = [] {
|
||||
didSet {
|
||||
DispatchQueue.main.async {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
var pinnedStatuses: [(id: String, state: StatusState)] = []
|
||||
var timelineSegments: [[(id: String, state: StatusState)]] = []
|
||||
|
||||
var older: RequestRange?
|
||||
var newer: RequestRange?
|
||||
|
||||
var shouldLoadOnAccountIDSet = false
|
||||
var loadingVC: LoadingViewController? = nil
|
||||
private var loadingVC: LoadingViewController? = nil
|
||||
private var loaded = false
|
||||
|
||||
init(accountID: String?, mastodonController: MastodonController) {
|
||||
self.mastodonController = mastodonController
|
||||
|
@ -80,7 +60,22 @@ class ProfileTableViewController: EnhancedTableViewController {
|
|||
|
||||
tableView.prefetchDataSource = self
|
||||
|
||||
if let accountID = accountID {
|
||||
if accountID == nil {
|
||||
loadingVC = LoadingViewController()
|
||||
embedChild(loadingVC!)
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
if !loaded, let accountID = accountID {
|
||||
loaded = true
|
||||
loadingVC?.removeViewAndController()
|
||||
loadingVC = nil
|
||||
|
||||
if mastodonController.persistentContainer.account(for: accountID) != nil {
|
||||
updateAccountUI()
|
||||
} else {
|
||||
|
@ -106,18 +101,10 @@ class ProfileTableViewController: EnhancedTableViewController {
|
|||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loadingVC = LoadingViewController()
|
||||
embedChild(loadingVC!)
|
||||
shouldLoadOnAccountIDSet = true
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
||||
}
|
||||
|
||||
func updateAccountUI() {
|
||||
loadingVC?.removeViewAndController()
|
||||
|
||||
updateUIForPreferences()
|
||||
|
||||
getStatuses(onlyPinned: true) { (response) in
|
||||
|
@ -125,6 +112,12 @@ class ProfileTableViewController: EnhancedTableViewController {
|
|||
|
||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||
self.pinnedStatuses = statuses.map { ($0.id, .unknown) }
|
||||
let indexPaths = (0..<statuses.count).map { IndexPath(row: $0, section: 1) }
|
||||
DispatchQueue.main.async {
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertRows(at: indexPaths, with: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,6 +129,12 @@ class ProfileTableViewController: EnhancedTableViewController {
|
|||
|
||||
self.older = pagination?.older
|
||||
self.newer = pagination?.newer
|
||||
|
||||
DispatchQueue.main.async {
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertSections(IndexSet(integer: 2), with: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -210,9 +209,16 @@ class ProfileTableViewController: EnhancedTableViewController {
|
|||
guard case let .success(newStatuses, pagination) = response else { fatalError() }
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(statuses: newStatuses) {
|
||||
self.timelineSegments[indexPath.section - 2].append(contentsOf: newStatuses.map { ($0.id, .unknown) })
|
||||
|
||||
self.older = pagination?.older
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let start = self.timelineSegments[indexPath.section - 2].count
|
||||
let indexPaths = (0..<newStatuses.count).map { IndexPath(row: start + $0, section: indexPath.section) }
|
||||
self.timelineSegments[indexPath.section - 2].append(contentsOf: newStatuses.map { ($0.id, .unknown) })
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertRows(at: indexPaths, with: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -237,13 +243,17 @@ class ProfileTableViewController: EnhancedTableViewController {
|
|||
guard case let .success(newStatuses, pagination) = response else { fatalError() }
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(statuses: newStatuses) {
|
||||
self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0)
|
||||
|
||||
if let newer = pagination?.newer {
|
||||
self.newer = newer
|
||||
}
|
||||
|
||||
let indexPaths = (0..<newStatuses.count).map { IndexPath(row: $0, section: 2) }
|
||||
DispatchQueue.main.async {
|
||||
self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0)
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.insertRows(at: indexPaths, with: .none)
|
||||
}
|
||||
|
||||
self.refreshControl?.endRefreshing()
|
||||
}
|
||||
}
|
||||
|
@ -263,7 +273,12 @@ class ProfileTableViewController: EnhancedTableViewController {
|
|||
}
|
||||
pinnedStatuses.append((status.id, state))
|
||||
}
|
||||
self.pinnedStatuses = pinnedStatuses
|
||||
DispatchQueue.main.async {
|
||||
self.pinnedStatuses = pinnedStatuses
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.reloadSections(IndexSet(integer: 1), with: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -288,19 +303,27 @@ extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate {
|
|||
func showMoreOptions(cell: ProfileHeaderTableViewCell) {
|
||||
let account = mastodonController.persistentContainer.account(for: accountID)!
|
||||
|
||||
let request = Client.getRelationships(accounts: [account.id])
|
||||
mastodonController.run(request) { (response) in
|
||||
var customActivities: [UIActivity] = [OpenInSafariActivity()]
|
||||
if case let .success(results, _) = response, let relationship = results.first {
|
||||
let toggleFollowActivity = relationship.following ? UnfollowAccountActivity() : FollowAccountActivity()
|
||||
customActivities.insert(toggleFollowActivity, at: 0)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let activityController = UIActivityViewController(activityItems: [account.url, account], applicationActivities: customActivities)
|
||||
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(viewController: self, url: account.url)
|
||||
activityController.popoverPresentationController?.sourceView = cell.moreButtonVisualEffectView
|
||||
self.present(activityController, animated: true)
|
||||
func showActivityController(activities: [UIActivity]) {
|
||||
let activityController = UIActivityViewController(activityItems: [account.url, AccountActivityItemSource(account)], applicationActivities: activities)
|
||||
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(viewController: self, url: account.url)
|
||||
activityController.popoverPresentationController?.sourceView = cell.moreButtonVisualEffectView
|
||||
self.present(activityController, animated: true)
|
||||
}
|
||||
|
||||
if account.id == mastodonController.account.id {
|
||||
showActivityController(activities: [OpenInSafariActivity()])
|
||||
} else {
|
||||
let request = Client.getRelationships(accounts: [account.id])
|
||||
mastodonController.run(request) { (response) in
|
||||
var customActivities: [UIActivity] = [OpenInSafariActivity()]
|
||||
if case let .success(results, _) = response, let relationship = results.first {
|
||||
let toggleFollowActivity = relationship.following ? UnfollowAccountActivity() : FollowAccountActivity()
|
||||
customActivities.insert(toggleFollowActivity, at: 0)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
showActivityController(activities: customActivities)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ class TimelineTableViewController: EnhancedTableViewController {
|
|||
var timeline: Timeline!
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
private var loaded = false
|
||||
|
||||
var timelineSegments: [[(id: String, state: StatusState)]] = []
|
||||
|
||||
var newer: RequestRange?
|
||||
|
@ -63,11 +65,18 @@ class TimelineTableViewController: EnhancedTableViewController {
|
|||
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: nil), forCellReuseIdentifier: "statusCell")
|
||||
|
||||
tableView.prefetchDataSource = self
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
loadInitialStatuses()
|
||||
}
|
||||
|
||||
func loadInitialStatuses() {
|
||||
guard !loaded else { return }
|
||||
loaded = true
|
||||
|
||||
let request = Client.getStatuses(timeline: timeline)
|
||||
mastodonController.run(request) { response in
|
||||
guard case let .success(statuses, pagination) = response else { fatalError() }
|
||||
|
@ -99,6 +108,7 @@ class TimelineTableViewController: EnhancedTableViewController {
|
|||
|
||||
let (id, state) = timelineSegments[indexPath.section][indexPath.row]
|
||||
cell.delegate = self
|
||||
|
||||
cell.updateUI(statusID: id, state: state)
|
||||
|
||||
return cell
|
||||
|
|
|
@ -36,14 +36,6 @@ protocol TuskerNavigationDelegate: class {
|
|||
|
||||
func reply(to statusID: String, mentioningAcct: String?)
|
||||
|
||||
func largeImage(_ image: UIImage, description: String?, sourceView: UIImageView) -> LargeImageViewController
|
||||
|
||||
func largeImage(gifData: Data, description: String?, sourceView: UIImageView) -> LargeImageViewController
|
||||
|
||||
func showLargeImage(_ image: UIImage, description: String?, animatingFrom sourceView: UIImageView)
|
||||
|
||||
func showLargeImage(gifData: Data, description: String?, animatingFrom sourceView: UIImageView)
|
||||
|
||||
func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController
|
||||
|
||||
func showLoadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView)
|
||||
|
@ -150,27 +142,6 @@ extension TuskerNavigationDelegate where Self: UIViewController {
|
|||
vc.presentationController?.delegate = compose
|
||||
present(vc, animated: true)
|
||||
}
|
||||
|
||||
func largeImage(_ image: UIImage, description: String?, sourceView: UIImageView) -> LargeImageViewController {
|
||||
let vc = LargeImageViewController(image: image, description: description, sourceView: sourceView)
|
||||
vc.transitioningDelegate = self
|
||||
return vc
|
||||
}
|
||||
|
||||
func largeImage(gifData: Data, description: String?, sourceView: UIImageView) -> LargeImageViewController {
|
||||
let vc = LargeImageViewController(image: UIImage(data: gifData)!, description: description, sourceView: sourceView)
|
||||
vc.transitioningDelegate = self
|
||||
vc.gifData = gifData
|
||||
return vc
|
||||
}
|
||||
|
||||
func showLargeImage(_ image: UIImage, description: String?, animatingFrom sourceView: UIImageView) {
|
||||
present(largeImage(image, description: description, sourceView: sourceView), animated: true)
|
||||
}
|
||||
|
||||
func showLargeImage(gifData: Data, description: String?, animatingFrom sourceView: UIImageView) {
|
||||
present(largeImage(gifData: gifData, description: description, sourceView: sourceView), animated: true)
|
||||
}
|
||||
|
||||
func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController {
|
||||
let vc = LoadingLargeImageViewController(url: url, cache: cache, imageDescription: description)
|
||||
|
@ -205,24 +176,33 @@ extension TuskerNavigationDelegate where Self: UIViewController {
|
|||
private func moreOptions(forStatus statusID: String) -> UIActivityViewController {
|
||||
guard let status = apiController.persistentContainer.status(for: statusID) else { fatalError("Missing cached status \(statusID)") }
|
||||
guard let url = status.url else { fatalError("Missing url for status \(statusID)") }
|
||||
var customActivites: [UIActivity] = [OpenInSafariActivity()]
|
||||
|
||||
let bookmarked = status.bookmarked ?? false
|
||||
customActivites.insert(bookmarked ? UnbookmarkStatusActivity() : BookmarkStatusActivity(), at: 0)
|
||||
|
||||
var customActivites: [UIActivity] = [
|
||||
OpenInSafariActivity(),
|
||||
(status.bookmarked ?? false) ? UnbookmarkStatusActivity() : BookmarkStatusActivity(),
|
||||
status.muted ? UnmuteConversationActivity() : MuteConversationActivity(),
|
||||
]
|
||||
|
||||
if apiController.account != nil, status.account.id == apiController.account.id {
|
||||
let pinned = status.pinned ?? false
|
||||
customActivites.insert(pinned ? UnpinStatusActivity() : PinStatusActivity(), at: 1)
|
||||
}
|
||||
|
||||
let activityController = UIActivityViewController(activityItems: [url, status], applicationActivities: customActivites)
|
||||
let activityController = UIActivityViewController(activityItems: [url, StatusActivityItemSource(status)], applicationActivities: customActivites)
|
||||
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(viewController: self, url: url)
|
||||
return activityController
|
||||
}
|
||||
|
||||
private func moreOptions(forAccount accountID: String) -> UIActivityViewController {
|
||||
guard let account = apiController.persistentContainer.account(for: accountID) else { fatalError("Missing cached account \(accountID)") }
|
||||
return moreOptions(forURL: account.url)
|
||||
|
||||
let customActivities: [UIActivity] = [
|
||||
OpenInSafariActivity(),
|
||||
]
|
||||
|
||||
let activityController = UIActivityViewController(activityItems: [account.url, AccountActivityItemSource(account)], applicationActivities: customActivities)
|
||||
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(viewController: self, url: account.url)
|
||||
return activityController
|
||||
}
|
||||
|
||||
func showMoreOptions(forStatus statusID: String, sourceView: UIView?) {
|
||||
|
|
|
@ -9,10 +9,13 @@
|
|||
import UIKit
|
||||
import Photos
|
||||
import AVFoundation
|
||||
import Vision
|
||||
|
||||
protocol ComposeAttachmentTableViewCellDelegate: class {
|
||||
func composeAttachment(_ cell: ComposeAttachmentTableViewCell, present viewController: UIViewController, animated: Bool)
|
||||
func removeAttachment(_ cell: ComposeAttachmentTableViewCell)
|
||||
func attachmentDescriptionChanged(_ cell: ComposeAttachmentTableViewCell)
|
||||
func composeAttachmentDescriptionHeightChanged(_ cell: ComposeAttachmentTableViewCell)
|
||||
}
|
||||
|
||||
class ComposeAttachmentTableViewCell: UITableViewCell {
|
||||
|
@ -21,11 +24,30 @@ class ComposeAttachmentTableViewCell: UITableViewCell {
|
|||
|
||||
@IBOutlet weak var assetImageView: UIImageView!
|
||||
@IBOutlet weak var descriptionTextView: UITextView!
|
||||
@IBOutlet weak var descriptionTextViewHeightConstraint: NSLayoutConstraint!
|
||||
@IBOutlet weak var descriptionPlaceholderLabel: UILabel!
|
||||
@IBOutlet weak var removeButton: UIButton!
|
||||
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
|
||||
|
||||
var attachment: CompositionAttachment!
|
||||
|
||||
var state: State = .allowEntry {
|
||||
didSet {
|
||||
switch state {
|
||||
case .allowEntry:
|
||||
descriptionTextView.isEditable = true
|
||||
updateDescriptionPlaceholderLabel()
|
||||
activityIndicator.stopAnimating()
|
||||
case .recognizingText:
|
||||
descriptionTextView.isEditable = false
|
||||
descriptionPlaceholderLabel.isHidden = true
|
||||
activityIndicator.startAnimating()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var textRecognitionRequest: VNRecognizeTextRequest?
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
|
@ -74,21 +96,81 @@ class ComposeAttachmentTableViewCell: UITableViewCell {
|
|||
removeButton.isEnabled = enabled
|
||||
}
|
||||
|
||||
func recognizeTextFromImage() {
|
||||
precondition(attachment.data.type == .image)
|
||||
state = .recognizingText
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
self.attachment.data.getData { (data, mimeType) in
|
||||
let handler = VNImageRequestHandler(data: data, options: [:])
|
||||
let request = VNRecognizeTextRequest { (request, error) in
|
||||
DispatchQueue.main.async {
|
||||
self.state = .allowEntry
|
||||
|
||||
if let results = request.results as? [VNRecognizedTextObservation] {
|
||||
var text = ""
|
||||
for observation in results {
|
||||
let result = observation.topCandidates(1).first!
|
||||
text.append(result.string)
|
||||
text.append("\n")
|
||||
}
|
||||
self.descriptionTextView.text = text
|
||||
self.textViewDidChange(self.descriptionTextView)
|
||||
}
|
||||
}
|
||||
}
|
||||
request.recognitionLevel = .accurate
|
||||
request.usesLanguageCorrection = true
|
||||
self.textRecognitionRequest = request
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
do {
|
||||
try handler.perform([request])
|
||||
} catch {
|
||||
// The perform call throws an error with code 1 if the request is cancelled, which we don't want to show an alert for.
|
||||
guard (error as NSError).code != 1 else { return }
|
||||
DispatchQueue.main.async {
|
||||
self.state = .allowEntry
|
||||
let title = NSLocalizedString("Text Recognition Failed", comment: "text recognition error alert title")
|
||||
let message = error.localizedDescription
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
self.delegate?.composeAttachment(self, present: alert, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
assetImageView.image = nil
|
||||
descriptionTextViewHeightConstraint.constant = 80
|
||||
}
|
||||
|
||||
@IBAction func removeButtonPressed(_ sender: Any) {
|
||||
textRecognitionRequest?.cancel()
|
||||
delegate?.removeAttachment(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ComposeAttachmentTableViewCell {
|
||||
enum State {
|
||||
case allowEntry, recognizingText
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeAttachmentTableViewCell: UITextViewDelegate {
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
attachment.attachmentDescription = textView.text
|
||||
updateDescriptionPlaceholderLabel()
|
||||
delegate?.attachmentDescriptionChanged(self)
|
||||
let smallestSize = textView.sizeThatFits(CGSize(width: textView.bounds.width, height: .greatestFiniteMagnitude))
|
||||
let old = descriptionTextViewHeightConstraint.constant
|
||||
descriptionTextViewHeightConstraint.constant = max(80, smallestSize.height)
|
||||
if old != descriptionTextViewHeightConstraint.constant {
|
||||
delegate?.composeAttachmentDescriptionHeightChanged(self)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<?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">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16097" 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"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
|
@ -36,11 +36,14 @@
|
|||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="cwP-Eh-5dJ">
|
||||
<rect key="frame" x="84" y="0.0" width="194" height="80"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="80" id="6aZ-w8-j9n"/>
|
||||
</constraints>
|
||||
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Lvf-I9-aV3">
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Lvf-I9-aV3">
|
||||
<rect key="frame" x="282" y="29" width="22" height="22"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="22" id="aIh-Ym-ARv"/>
|
||||
|
@ -57,6 +60,9 @@
|
|||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="80" id="jWo-An-3h6"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="medium" translatesAutoresizingMaskIntoConstraints="NO" id="Kzy-5r-UW8">
|
||||
<rect key="frame" x="179" y="38" width="20" height="20"/>
|
||||
</activityIndicatorView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="xRe-ec-Coh" secondAttribute="bottom" constant="8" id="DOS-Wv-G3s"/>
|
||||
|
@ -64,15 +70,19 @@
|
|||
<constraint firstItem="h6T-x4-yzl" firstAttribute="trailing" secondItem="cwP-Eh-5dJ" secondAttribute="trailing" constant="4" id="KN2-Ve-3B2"/>
|
||||
<constraint firstItem="h6T-x4-yzl" firstAttribute="top" secondItem="cwP-Eh-5dJ" secondAttribute="top" constant="8" id="P3B-Jo-XMs"/>
|
||||
<constraint firstItem="h6T-x4-yzl" firstAttribute="leading" secondItem="cwP-Eh-5dJ" secondAttribute="leading" constant="4" id="UjP-Gs-ZjO"/>
|
||||
<constraint firstItem="Kzy-5r-UW8" firstAttribute="centerX" secondItem="cwP-Eh-5dJ" secondAttribute="centerX" id="czP-Ia-Ddc"/>
|
||||
<constraint firstItem="Kzy-5r-UW8" firstAttribute="centerY" secondItem="cwP-Eh-5dJ" secondAttribute="centerY" id="eel-xx-aFq"/>
|
||||
<constraint firstItem="xRe-ec-Coh" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="8" id="gRN-PV-gm6"/>
|
||||
<constraint firstAttribute="trailing" secondItem="xRe-ec-Coh" secondAttribute="trailing" constant="8" id="tyE-HK-4qb"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||
<connections>
|
||||
<outlet property="activityIndicator" destination="Kzy-5r-UW8" id="lmy-NY-Owu"/>
|
||||
<outlet property="assetImageView" destination="GLY-o8-47z" id="hZH-ur-m4z"/>
|
||||
<outlet property="descriptionPlaceholderLabel" destination="h6T-x4-yzl" id="jBe-R0-Sfn"/>
|
||||
<outlet property="descriptionTextView" destination="cwP-Eh-5dJ" id="pxJ-zF-GKC"/>
|
||||
<outlet property="descriptionTextViewHeightConstraint" destination="6aZ-w8-j9n" id="ees-sT-Trc"/>
|
||||
<outlet property="removeButton" destination="Lvf-I9-aV3" id="3qk-Zr-je1"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="107" y="181"/>
|
||||
|
|
|
@ -19,8 +19,8 @@ class GifvAttachmentView: UIView {
|
|||
layer as! AVPlayerLayer
|
||||
}
|
||||
|
||||
private let item: AVPlayerItem
|
||||
private let player: AVPlayer
|
||||
let item: AVPlayerItem
|
||||
let player: AVPlayer
|
||||
|
||||
init(asset: AVAsset, gravity: AVLayerVideoGravity) {
|
||||
item = AVPlayerItem(asset: asset)
|
||||
|
@ -30,8 +30,9 @@ class GifvAttachmentView: UIView {
|
|||
|
||||
playerLayer.player = player
|
||||
playerLayer.videoGravity = gravity
|
||||
player.isMuted = true
|
||||
player.play()
|
||||
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(restartItem), name: .AVPlayerItemDidPlayToEndTime, object: item)
|
||||
}
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ class ComposeStatusReplyView: UIView {
|
|||
displayNameLabel.updateForAccountDisplayName(account: status.account)
|
||||
usernameLabel.text = "@\(status.account.acct)"
|
||||
statusContentTextView.overrideMastodonController = mastodonController
|
||||
statusContentTextView.statusID = status.id
|
||||
statusContentTextView.setTextFrom(status: status)
|
||||
|
||||
avatarRequest = ImageCache.avatars.get(status.account.avatar) { [weak self] (data) in
|
||||
guard let self = self, let data = data else { return }
|
||||
|
|
|
@ -28,7 +28,7 @@ class ProfileHeaderTableViewCell: UITableViewCell {
|
|||
@IBOutlet weak var noteTextView: StatusContentTextView!
|
||||
@IBOutlet weak var fieldsStackView: UIStackView!
|
||||
@IBOutlet weak var fieldNamesStackView: UIStackView!
|
||||
@IBOutlet weak var fieldValuesStack: UIStackView!
|
||||
@IBOutlet weak var fieldValuesStackView: UIStackView!
|
||||
@IBOutlet weak var moreButtonVisualEffectView: UIVisualEffectView!
|
||||
|
||||
var accountID: String!
|
||||
|
@ -102,13 +102,16 @@ class ProfileHeaderTableViewCell: UITableViewCell {
|
|||
|
||||
fieldsStackView.isHidden = account.fields.isEmpty
|
||||
|
||||
fieldsStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
fieldNamesStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
fieldValuesStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
for field in account.fields {
|
||||
let nameLabel = UILabel()
|
||||
nameLabel.text = field.name
|
||||
nameLabel.font = .boldSystemFont(ofSize: 17)
|
||||
nameLabel.textAlignment = .right
|
||||
nameLabel.numberOfLines = 0
|
||||
nameLabel.lineBreakMode = .byWordWrapping
|
||||
nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
fieldNamesStackView.addArrangedSubview(nameLabel)
|
||||
|
||||
let valueTextView = ContentTextView()
|
||||
|
@ -119,7 +122,10 @@ class ProfileHeaderTableViewCell: UITableViewCell {
|
|||
valueTextView.textAlignment = .left
|
||||
valueTextView.awakeFromNib()
|
||||
valueTextView.navigationDelegate = delegate
|
||||
fieldValuesStack.addArrangedSubview(valueTextView)
|
||||
valueTextView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
fieldValuesStackView.addArrangedSubview(valueTextView)
|
||||
|
||||
nameLabel.heightAnchor.constraint(equalTo: valueTextView.heightAnchor).isActive = true
|
||||
}
|
||||
|
||||
if accountUpdater == nil {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<?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">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16097" 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="16082.1"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
|
@ -78,13 +78,17 @@
|
|||
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="sHU-GU-klv">
|
||||
<rect key="frame" x="16" y="238" width="343" height="50"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillEqually" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="pV2-Mz-54W">
|
||||
<rect key="frame" x="0.0" y="0.0" width="147" height="50"/>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="pV2-Mz-54W">
|
||||
<rect key="frame" x="0.0" y="0.0" width="167.5" height="50"/>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillEqually" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="oza-9d-8v4">
|
||||
<rect key="frame" x="155" y="0.0" width="188" height="50"/>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="oza-9d-8v4">
|
||||
<rect key="frame" x="175.5" y="0.0" width="167.5" height="50"/>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="oza-9d-8v4" firstAttribute="width" relation="lessThanOrEqual" secondItem="pV2-Mz-54W" secondAttribute="width" multiplier="2" id="LHm-6k-LyV"/>
|
||||
<constraint firstItem="oza-9d-8v4" firstAttribute="width" relation="greaterThanOrEqual" secondItem="pV2-Mz-54W" secondAttribute="width" multiplier="0.5" id="Zbr-l3-Lff"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="mQY-XN-PfZ">
|
||||
<rect key="frame" x="335" y="110" width="32" height="32"/>
|
||||
|
@ -161,7 +165,7 @@
|
|||
<outlet property="avatarImageView" destination="tH8-sR-DHC" id="6ll-yL-g1o"/>
|
||||
<outlet property="displayNameLabel" destination="LjK-72-Bez" id="nIU-ey-H1C"/>
|
||||
<outlet property="fieldNamesStackView" destination="pV2-Mz-54W" id="xfG-60-K0s"/>
|
||||
<outlet property="fieldValuesStack" destination="oza-9d-8v4" id="UIS-KM-5fR"/>
|
||||
<outlet property="fieldValuesStackView" destination="oza-9d-8v4" id="UIS-KM-5fR"/>
|
||||
<outlet property="fieldsStackView" destination="sHU-GU-klv" id="Gli-Gf-Ubh"/>
|
||||
<outlet property="followsYouLabel" destination="a32-1a-xXZ" id="phY-0L-NnN"/>
|
||||
<outlet property="headerImageView" destination="Fw7-OL-iy5" id="6sv-E5-D73"/>
|
||||
|
|
|
@ -27,6 +27,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
@IBOutlet weak var avatarImageView: UIImageView!
|
||||
@IBOutlet weak var displayNameLabel: EmojiLabel!
|
||||
@IBOutlet weak var usernameLabel: UILabel!
|
||||
@IBOutlet weak var visibilityImageView: UIImageView!
|
||||
@IBOutlet weak var contentWarningLabel: EmojiLabel!
|
||||
@IBOutlet weak var collapseButton: UIButton!
|
||||
@IBOutlet weak var contentTextView: StatusContentTextView!
|
||||
|
@ -86,7 +87,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
accessibilityElements = [displayNameLabel!, contentWarningLabel!, collapseButton!, contentTextView!, attachmentsView!]
|
||||
attachmentsView.isAccessibilityElement = true
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||
}
|
||||
|
||||
open func createObserversIfNecessary() {
|
||||
|
@ -125,8 +126,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
let account = status.account
|
||||
self.accountID = account.id
|
||||
updateUI(account: account)
|
||||
|
||||
updateUIForPreferences()
|
||||
updateUIForPreferences(account: account)
|
||||
|
||||
attachmentsView.updateUI(status: status)
|
||||
attachmentsView.isAccessibilityElement = status.attachments.count > 0
|
||||
|
@ -134,7 +134,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
|
||||
updateStatusState(status: status)
|
||||
|
||||
contentTextView.statusID = statusID
|
||||
contentTextView.setTextFrom(status: status)
|
||||
|
||||
contentWarningLabel.text = status.spoilerText
|
||||
contentWarningLabel.isHidden = status.spoilerText.isEmpty
|
||||
|
@ -142,6 +142,18 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
contentWarningLabel.setEmojis(status.emojis, identifier: statusID)
|
||||
}
|
||||
|
||||
let reblogDisabled: Bool
|
||||
switch mastodonController.instance.instanceType {
|
||||
case .mastodon:
|
||||
reblogDisabled = status.visibility == .private || status.visibility == .direct
|
||||
case .pleroma:
|
||||
// Pleroma allows 'Boost to original audience' for your own private posts
|
||||
reblogDisabled = status.visibility == .direct || (status.visibility == .private && status.account.id != mastodonController.account.id)
|
||||
}
|
||||
reblogButton.isEnabled = !reblogDisabled
|
||||
|
||||
updateStatusIconsForPreferences(status)
|
||||
|
||||
if state.unknown {
|
||||
collapsible = !status.spoilerText.isEmpty
|
||||
var shouldCollapse = collapsible
|
||||
|
@ -191,13 +203,35 @@ class BaseStatusTableViewCell: UITableViewCell {
|
|||
}
|
||||
}
|
||||
|
||||
@objc func updateUIForPreferences() {
|
||||
guard let mastodonController = mastodonController, let account = mastodonController.persistentContainer.account(for: accountID) else { return }
|
||||
@objc func preferencesChanged() {
|
||||
guard let mastodonController = mastodonController,
|
||||
let account = mastodonController.persistentContainer.account(for: accountID),
|
||||
let status = mastodonController.persistentContainer.status(for: statusID) else { return }
|
||||
updateUIForPreferences(account: account)
|
||||
updateStatusIconsForPreferences(status)
|
||||
}
|
||||
|
||||
func updateUIForPreferences(account: AccountMO) {
|
||||
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
|
||||
displayNameLabel.updateForAccountDisplayName(account: account)
|
||||
attachmentsView.contentHidden = Preferences.shared.blurAllMedia || (mastodonController.persistentContainer.status(for: statusID)?.sensitive ?? false)
|
||||
}
|
||||
|
||||
func updateStatusIconsForPreferences(_ status: StatusMO) {
|
||||
visibilityImageView.isHidden = !Preferences.shared.alwaysShowStatusVisibilityIcon
|
||||
if Preferences.shared.alwaysShowStatusVisibilityIcon {
|
||||
visibilityImageView.image = UIImage(systemName: status.visibility.unfilledImageName)
|
||||
visibilityImageView.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "status visibility indicator accessibility label"), status.visibility.displayName)
|
||||
}
|
||||
let reblogButtonImage: UIImage
|
||||
if Preferences.shared.alwaysShowStatusVisibilityIcon || reblogButton.isEnabled {
|
||||
reblogButtonImage = UIImage(systemName: "repeat")!
|
||||
} else {
|
||||
reblogButtonImage = UIImage(systemName: status.visibility.imageName)!
|
||||
}
|
||||
reblogButton.setImage(reblogButtonImage, for: .normal)
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
|
|
|
@ -63,9 +63,9 @@ class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
|
|||
profileAccessibilityElement.accessibilityLabel = account.displayNameWithoutCustomEmoji
|
||||
}
|
||||
|
||||
@objc override func updateUIForPreferences() {
|
||||
super.updateUIForPreferences()
|
||||
|
||||
override func updateUIForPreferences(account: AccountMO) {
|
||||
super.updateUIForPreferences(account: account)
|
||||
|
||||
favoriteAndReblogCountStackView.isHidden = !Preferences.shared.showFavoriteAndReblogCounts
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16097" 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="16086"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
|
@ -27,7 +27,7 @@
|
|||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" contentMode="left" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lZY-2e-17d" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="58" y="0.0" width="277" height="29"/>
|
||||
<rect key="frame" x="58" y="0.0" width="255" height="29"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="24"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
|
@ -38,18 +38,29 @@
|
|||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="globe" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="3Qu-IO-5wt">
|
||||
<rect key="frame" x="321" y="1" width="22" height="20"/>
|
||||
<color key="tintColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="22" id="Kqh-qI-dSa"/>
|
||||
<constraint firstAttribute="width" constant="22" id="QY1-tL-QHr"/>
|
||||
</constraints>
|
||||
<preferredSymbolConfiguration key="preferredSymbolConfiguration" weight="thin"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="SWg-Ka-QyP" secondAttribute="trailing" id="4g6-BT-eW4"/>
|
||||
<constraint firstItem="lZY-2e-17d" firstAttribute="top" secondItem="Cnd-Fj-B7l" secondAttribute="top" id="8fU-y9-K5Z"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="lZY-2e-17d" secondAttribute="trailing" id="AAJ-pd-omx"/>
|
||||
<constraint firstItem="lZY-2e-17d" firstAttribute="leading" secondItem="mB9-HO-1vf" secondAttribute="trailing" constant="8" id="Aqj-co-Szp"/>
|
||||
<constraint firstItem="3Qu-IO-5wt" firstAttribute="leading" secondItem="lZY-2e-17d" secondAttribute="trailing" constant="8" id="MS8-zq-SWT"/>
|
||||
<constraint firstAttribute="trailing" secondItem="3Qu-IO-5wt" secondAttribute="trailing" id="NWa-lL-aLk"/>
|
||||
<constraint firstItem="mB9-HO-1vf" firstAttribute="top" secondItem="Cnd-Fj-B7l" secondAttribute="top" id="R7P-rD-Gbm"/>
|
||||
<constraint firstAttribute="bottom" secondItem="mB9-HO-1vf" secondAttribute="bottom" id="Wd0-Qh-idS"/>
|
||||
<constraint firstItem="mB9-HO-1vf" firstAttribute="leading" secondItem="Cnd-Fj-B7l" secondAttribute="leading" id="bxq-Fs-1aH"/>
|
||||
<constraint firstItem="SWg-Ka-QyP" firstAttribute="leading" secondItem="mB9-HO-1vf" secondAttribute="trailing" constant="8" id="e45-gE-myI"/>
|
||||
<constraint firstItem="SWg-Ka-QyP" firstAttribute="top" secondItem="lZY-2e-17d" secondAttribute="bottom" id="lvX-1b-8cN"/>
|
||||
<constraint firstItem="3Qu-IO-5wt" firstAttribute="top" secondItem="Cnd-Fj-B7l" secondAttribute="top" id="pPU-WS-Y6B"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" verticalCompressionResistancePriority="751" text="Content Warning" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cwQ-mR-L1b" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
||||
|
@ -83,10 +94,10 @@
|
|||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="IF9-9U-Gk0" customClass="AttachmentsContainerView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="184" width="343" height="0.0"/>
|
||||
<rect key="frame" x="0.0" y="184" width="343" height="193"/>
|
||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" priority="999" constant="200" id="UMv-Bk-ZyY"/>
|
||||
<constraint firstAttribute="width" secondItem="IF9-9U-Gk0" secondAttribute="height" multiplier="16:9" id="5oh-eK-J5d"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ejU-sO-Og5">
|
||||
|
@ -219,6 +230,7 @@
|
|||
<outlet property="totalFavoritesButton" destination="yyj-Bs-Vjq" id="4pV-Qi-Z2X"/>
|
||||
<outlet property="totalReblogsButton" destination="dem-vG-cPB" id="i9E-Qn-d76"/>
|
||||
<outlet property="usernameLabel" destination="SWg-Ka-QyP" id="h2I-g4-AD9"/>
|
||||
<outlet property="visibilityImageView" destination="3Qu-IO-5wt" id="sFB-ni-FcZ"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="40.799999999999997" y="-122.78860569715144"/>
|
||||
</view>
|
||||
|
@ -227,6 +239,7 @@
|
|||
<image name="arrowshape.turn.up.left.fill" catalog="system" width="128" height="106"/>
|
||||
<image name="chevron.down" catalog="system" width="128" height="72"/>
|
||||
<image name="ellipsis" catalog="system" width="128" height="37"/>
|
||||
<image name="globe" catalog="system" width="128" height="121"/>
|
||||
<image name="repeat" catalog="system" width="128" height="99"/>
|
||||
<image name="star.fill" catalog="system" width="128" height="116"/>
|
||||
</resources>
|
||||
|
|
|
@ -22,11 +22,13 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
|||
@IBOutlet weak var reblogLabel: EmojiLabel!
|
||||
@IBOutlet weak var timestampLabel: UILabel!
|
||||
@IBOutlet weak var pinImageView: UIImageView!
|
||||
@IBOutlet weak var replyImageView: UIImageView!
|
||||
|
||||
var reblogStatusID: String?
|
||||
var rebloggerID: String?
|
||||
|
||||
var showPinned: Bool = false
|
||||
var showPinned = false
|
||||
var showReplyIndicator = true
|
||||
|
||||
var updateTimestampWorkItem: DispatchWorkItem?
|
||||
|
||||
|
@ -66,9 +68,11 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
|||
if let rebloggedStatus = status.reblog {
|
||||
reblogStatusID = statusID
|
||||
rebloggerID = status.account.id
|
||||
reblogLabel.isHidden = false
|
||||
updateRebloggerLabel(reblogger: status.account)
|
||||
|
||||
status = rebloggedStatus
|
||||
realStatusID = rebloggedStatus.id
|
||||
reblogLabel.isHidden = false
|
||||
} else {
|
||||
reblogStatusID = nil
|
||||
rebloggerID = nil
|
||||
|
@ -80,13 +84,14 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
|||
|
||||
updateTimestamp()
|
||||
|
||||
let pinned = status.pinned ?? false
|
||||
pinImageView.isHidden = !(pinned && showPinned)
|
||||
timestampLabel.isHidden = !pinImageView.isHidden
|
||||
let pinned = showPinned && (status.pinned ?? false)
|
||||
timestampLabel.isHidden = pinned
|
||||
pinImageView.isHidden = !pinned
|
||||
}
|
||||
|
||||
@objc override func updateUIForPreferences() {
|
||||
super.updateUIForPreferences()
|
||||
@objc override func preferencesChanged() {
|
||||
super.preferencesChanged()
|
||||
|
||||
if let rebloggerID = rebloggerID,
|
||||
let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) {
|
||||
updateRebloggerLabel(reblogger: reblogger)
|
||||
|
@ -103,6 +108,12 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
|||
}
|
||||
}
|
||||
|
||||
override func updateStatusIconsForPreferences(_ status: StatusMO) {
|
||||
super.updateStatusIconsForPreferences(status)
|
||||
|
||||
replyImageView.isHidden = !Preferences.shared.showIsStatusReplyIcon || !showReplyIndicator || status.inReplyToID == nil
|
||||
}
|
||||
|
||||
func updateTimestamp() {
|
||||
// if the mastodonController is nil (i.e. the delegate is nil), then the screen this cell was a part of has been deallocated
|
||||
// so we bail out immediately, since there's nothing to update
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16097" 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="16086"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
|
@ -23,7 +23,7 @@
|
|||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" verticalHuggingPriority="249" translatesAutoresizingMaskIntoConstraints="NO" id="ve3-Y1-NQH">
|
||||
<rect key="frame" x="0.0" y="28.5" width="343" height="165.5"/>
|
||||
<rect key="frame" x="0.0" y="28.5" width="343" height="195.5"/>
|
||||
<subviews>
|
||||
<imageView contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="QMP-j2-HLn">
|
||||
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
|
||||
|
@ -37,9 +37,9 @@
|
|||
</constraints>
|
||||
</imageView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="751" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="gIY-Wp-RSk">
|
||||
<rect key="frame" x="58" y="0.0" width="277" height="165.5"/>
|
||||
<rect key="frame" x="58" y="0.0" width="277" height="195.5"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="3Sm-P0-ySf">
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="3Sm-P0-ySf">
|
||||
<rect key="frame" x="0.0" y="0.0" width="277" height="20.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" horizontalCompressionResistancePriority="749" verticalCompressionResistancePriority="752" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="gll-xe-FSr" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
||||
|
@ -53,7 +53,7 @@
|
|||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="249" verticalHuggingPriority="252" horizontalCompressionResistancePriority="748" verticalCompressionResistancePriority="752" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="j89-zc-SFa">
|
||||
<rect key="frame" x="115" y="0.0" width="129.5" height="20.5"/>
|
||||
<rect key="frame" x="111" y="0.0" width="137.5" height="20.5"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<accessibilityTraits key="traits" staticText="YES" notEnabled="YES"/>
|
||||
</accessibility>
|
||||
|
@ -62,8 +62,8 @@
|
|||
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView hidden="YES" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="pin.fill" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="LRh-Cc-1br">
|
||||
<rect key="frame" x="248.5" y="-0.5" width="0.0" height="22"/>
|
||||
<imageView hidden="YES" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="pin.fill" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="wtt-8G-Ua1">
|
||||
<rect key="frame" x="250.5" y="-0.5" width="0.0" height="22"/>
|
||||
<color key="tintColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<accessibility key="accessibilityConfiguration" label="Pinned Status"/>
|
||||
</imageView>
|
||||
|
@ -106,73 +106,96 @@
|
|||
</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" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
<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="171.5" width="277" height="156"/>
|
||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="nbq-yr-2mA" secondAttribute="height" multiplier="16:9" id="Rvt-zs-fkd"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" distribution="equalSpacing" alignment="bottom" translatesAutoresizingMaskIntoConstraints="NO" id="Zlb-yt-NTw">
|
||||
<rect key="frame" x="0.0" y="173.5" width="277" height="22"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="leading" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="rKF-yF-KIa">
|
||||
<rect key="frame" x="0.0" y="0.0" width="21" height="22"/>
|
||||
<accessibility key="accessibilityConfiguration" label="Reply"/>
|
||||
<state key="normal" image="arrowshape.turn.up.left.fill" catalog="system"/>
|
||||
<connections>
|
||||
<action selector="replyPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="ybz-3W-jAa"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="x0t-TR-jJ4">
|
||||
<rect key="frame" x="85" y="0.0" width="22" height="22"/>
|
||||
<accessibility key="accessibilityConfiguration" label="Favorite"/>
|
||||
<state key="normal" image="star.fill" catalog="system"/>
|
||||
<connections>
|
||||
<action selector="favoritePressed" destination="iN0-l3-epB" eventType="touchUpInside" id="8Q8-Rz-k02"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6tW-z8-Qh9">
|
||||
<rect key="frame" x="171.5" y="0.0" width="22.5" height="22"/>
|
||||
<accessibility key="accessibilityConfiguration" label="Reblog"/>
|
||||
<state key="normal" image="repeat" catalog="system"/>
|
||||
<connections>
|
||||
<action selector="reblogPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="Wa2-ZA-TBo"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="982-J4-NGl">
|
||||
<rect key="frame" x="258" y="0.0" width="19" height="22"/>
|
||||
<accessibility key="accessibilityConfiguration" label="More Actions"/>
|
||||
<state key="normal" image="ellipsis" catalog="system"/>
|
||||
<connections>
|
||||
<action selector="morePressed" destination="iN0-l3-epB" eventType="touchUpInside" id="WT4-fi-usq"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="oie-wK-IpU">
|
||||
<rect key="frame" x="0.5" y="54" width="49.5" height="22"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bubble.left.and.bubble.right" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="KdQ-Zn-IhD">
|
||||
<rect key="frame" x="0.0" y="1" width="25.5" height="21.5"/>
|
||||
<color key="tintColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<accessibility key="accessibilityConfiguration" label="Is a reply"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="22" id="x0C-Qo-YVA"/>
|
||||
</constraints>
|
||||
<preferredSymbolConfiguration key="preferredSymbolConfiguration" weight="thin"/>
|
||||
</imageView>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="globe" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="LRh-Cc-1br">
|
||||
<rect key="frame" x="30.5" y="1" width="19" height="20"/>
|
||||
<color key="tintColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="22" id="3Mk-NN-6fY"/>
|
||||
</constraints>
|
||||
<preferredSymbolConfiguration key="preferredSymbolConfiguration" weight="thin"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="gIY-Wp-RSk" firstAttribute="leading" secondItem="QMP-j2-HLn" secondAttribute="trailing" constant="8" id="0Tm-v7-Ts4"/>
|
||||
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="8" id="2Ao-Gj-fY3"/>
|
||||
<constraint firstItem="oie-wK-IpU" firstAttribute="top" secondItem="QMP-j2-HLn" secondAttribute="bottom" constant="4" id="7Mp-WS-FhY"/>
|
||||
<constraint firstItem="QMP-j2-HLn" firstAttribute="top" secondItem="ve3-Y1-NQH" secondAttribute="top" id="PC4-Bi-QXm"/>
|
||||
<constraint firstItem="oie-wK-IpU" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="ve3-Y1-NQH" secondAttribute="leading" id="QKi-ny-jOJ"/>
|
||||
<constraint firstItem="gIY-Wp-RSk" firstAttribute="top" secondItem="QMP-j2-HLn" secondAttribute="top" id="fEd-wN-kuQ"/>
|
||||
<constraint firstItem="gIY-Wp-RSk" firstAttribute="leading" secondItem="oie-wK-IpU" secondAttribute="trailing" constant="8" id="fqd-p6-oGe"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="gIY-Wp-RSk" secondAttribute="trailing" id="hKk-kO-wFT"/>
|
||||
<constraint firstAttribute="bottom" secondItem="gIY-Wp-RSk" secondAttribute="bottom" id="kRU-Ct-CIg"/>
|
||||
<constraint firstItem="QMP-j2-HLn" firstAttribute="leading" secondItem="ve3-Y1-NQH" secondAttribute="leading" id="zeW-tQ-uJl"/>
|
||||
</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="198" width="343" height="0.0"/>
|
||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" priority="999" constant="200" id="J42-49-2MU"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" distribution="equalSpacing" alignment="bottom" translatesAutoresizingMaskIntoConstraints="NO" id="Zlb-yt-NTw">
|
||||
<rect key="frame" x="0.0" y="202" width="343" height="22"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="leading" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="rKF-yF-KIa">
|
||||
<rect key="frame" x="0.0" y="0.0" width="21" height="22"/>
|
||||
<accessibility key="accessibilityConfiguration" label="Reply"/>
|
||||
<state key="normal" image="arrowshape.turn.up.left.fill" catalog="system"/>
|
||||
<connections>
|
||||
<action selector="replyPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="ybz-3W-jAa"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="x0t-TR-jJ4">
|
||||
<rect key="frame" x="107" y="0.0" width="22" height="22"/>
|
||||
<accessibility key="accessibilityConfiguration" label="Favorite"/>
|
||||
<state key="normal" image="star.fill" catalog="system"/>
|
||||
<connections>
|
||||
<action selector="favoritePressed" destination="iN0-l3-epB" eventType="touchUpInside" id="8Q8-Rz-k02"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6tW-z8-Qh9">
|
||||
<rect key="frame" x="215.5" y="0.0" width="22.5" height="22"/>
|
||||
<accessibility key="accessibilityConfiguration" label="Reblog"/>
|
||||
<state key="normal" image="repeat" catalog="system"/>
|
||||
<connections>
|
||||
<action selector="reblogPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="Wa2-ZA-TBo"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="982-J4-NGl">
|
||||
<rect key="frame" x="324" y="0.0" width="19" height="22"/>
|
||||
<accessibility key="accessibilityConfiguration" label="More Actions"/>
|
||||
<state key="normal" image="ellipsis" catalog="system"/>
|
||||
<connections>
|
||||
<action selector="morePressed" destination="iN0-l3-epB" eventType="touchUpInside" id="WT4-fi-usq"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="nbq-yr-2mA" firstAttribute="width" secondItem="yNh-ac-v6c" secondAttribute="width" id="JCZ-x5-Xa2"/>
|
||||
<constraint firstItem="Zlb-yt-NTw" firstAttribute="width" secondItem="yNh-ac-v6c" secondAttribute="width" id="wxD-pe-Udd"/>
|
||||
<constraint firstItem="ve3-Y1-NQH" firstAttribute="width" secondItem="yNh-ac-v6c" secondAttribute="width" id="xN6-cs-Tnn"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
|
@ -195,20 +218,24 @@
|
|||
<outlet property="displayNameLabel" destination="gll-xe-FSr" id="vVS-WM-Wqx"/>
|
||||
<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="LRh-Cc-1br" id="9jn-0V-PdJ"/>
|
||||
<outlet property="pinImageView" destination="wtt-8G-Ua1" id="mE8-oe-m1l"/>
|
||||
<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"/>
|
||||
<outlet property="replyImageView" destination="KdQ-Zn-IhD" id="jqs-FK-K1N"/>
|
||||
<outlet property="timestampLabel" destination="35d-EA-ReR" id="Ny2-nV-nqP"/>
|
||||
<outlet property="usernameLabel" destination="j89-zc-SFa" id="bXX-FZ-fCp"/>
|
||||
<outlet property="visibilityImageView" destination="LRh-Cc-1br" id="pxm-JK-jAz"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="29.600000000000001" y="79.160419790104953"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="arrowshape.turn.up.left.fill" catalog="system" width="128" height="106"/>
|
||||
<image name="bubble.left.and.bubble.right" catalog="system" width="128" height="96"/>
|
||||
<image name="chevron.down" catalog="system" width="128" height="72"/>
|
||||
<image name="ellipsis" catalog="system" width="128" height="37"/>
|
||||
<image name="globe" catalog="system" width="128" height="121"/>
|
||||
<image name="pin.fill" catalog="system" width="119" height="128"/>
|
||||
<image name="repeat" catalog="system" width="128" height="99"/>
|
||||
<image name="star.fill" catalog="system" width="128" height="116"/>
|
||||
|
|
|
@ -11,16 +11,12 @@ import Pachyderm
|
|||
|
||||
class StatusContentTextView: ContentTextView {
|
||||
|
||||
var statusID: String? {
|
||||
didSet {
|
||||
guard let statusID = statusID else { return }
|
||||
guard let mastodonController = mastodonController,
|
||||
let status = mastodonController.persistentContainer.status(for: statusID) else {
|
||||
fatalError("Can't set StatusContentTextView text without cached status for \(statusID)")
|
||||
}
|
||||
setTextFromHtml(status.content)
|
||||
setEmojis(status.emojis)
|
||||
}
|
||||
private var statusID: String?
|
||||
|
||||
func setTextFrom(status: StatusMO) {
|
||||
statusID = status.id
|
||||
setTextFromHtml(status.content)
|
||||
setEmojis(status.emojis)
|
||||
}
|
||||
|
||||
override func getMention(for url: URL, text: String) -> Mention? {
|
||||
|
|
Loading…
Reference in New Issue