Compare commits

...

1284 Commits

Author SHA1 Message Date
Shadowfacts 2ee34acbad Fix remove attachment menu item not being marked destructive 2023-01-24 15:02:11 -05:00
Shadowfacts 6eee97759e Add context menu action to remove pinned timeline
Closes #334
2023-01-24 10:19:04 -05:00
Shadowfacts f88bf552af Reuse client ID/secret when trying to sign in to the same account again
Workaround for mastodon.social signins being flaky
2023-01-23 17:43:41 -05:00
Shadowfacts d2c7664073 Add profile suggestions to Explore on iPad 2023-01-23 17:10:26 -05:00
Shadowfacts e91249a876 Detect Misskey links properly 2023-01-23 16:59:24 -05:00
Shadowfacts 1eab964c0b Parse HTML in trending link card descriptions 2023-01-23 15:15:43 -05:00
Shadowfacts 2933ac491b Fix Open in Safari action not working 2023-01-23 10:35:23 -05:00
Shadowfacts 2958d2b1ac Change TrendingLinkCardCollectionViewCell to use CachedImageView 2023-01-22 18:21:58 -05:00
Shadowfacts 3262fe002b Add hover interaction to trending link cards 2023-01-22 17:37:41 -05:00
Shadowfacts 521e5ad5fc Make trend history view respond to preferred content size category 2023-01-22 17:23:22 -05:00
Shadowfacts 2b651b0bc4 Fix trending hashtag cells not adjusting to dynamic type 2023-01-22 17:23:19 -05:00
Shadowfacts 99b3532e64 Add description to trending link cards, fix not responding to dynamic type 2023-01-22 17:23:19 -05:00
Shadowfacts 2ea8e9cf1e Fix preview action on iPad Explore screen not working 2023-01-22 15:44:36 -05:00
Shadowfacts e8b7446117 Fix split view expand breaking when transferring trending statuses/hashtags/links VCs 2023-01-22 14:01:44 -05:00
Shadowfacts a47b9c0c75 Move trending statuses to Explore on iPad
See #171
2023-01-22 13:57:37 -05:00
Shadowfacts a75862b5cc Mask trending link card previews with same corner radius as cells 2023-01-22 12:08:22 -05:00
Shadowfacts 0738683ee3 Add search scopes
Closes #328
2023-01-22 11:41:38 -05:00
Shadowfacts 155f4036f9 Handle authentication required error for instance timelines 2023-01-22 11:18:43 -05:00
Shadowfacts 8181090763 Bump build number and update changelog 2023-01-21 23:01:55 -05:00
Shadowfacts 6328627a97 Fix extra spacing above content in conversation main status 2023-01-21 20:27:20 -05:00
Shadowfacts c6043d60ee Fix crash when inserting present items in empty timeline 2023-01-21 16:31:52 -05:00
Shadowfacts dd6813c058 Bump build number and update changelog 2023-01-21 15:31:35 -05:00
Shadowfacts 2229b332e0 Try to resolve statuses from links that match known patterns 2023-01-21 14:03:21 -05:00
Shadowfacts 63ed3b6e10 Add loading indicator to conversation screen 2023-01-21 13:17:11 -05:00
Shadowfacts ccd1672e72 Show highlight on expand thread cell selection 2023-01-21 13:14:16 -05:00
Shadowfacts addcc2dacc Rewrite conversation screen to use UICollectionView 2023-01-21 11:26:51 -05:00
Shadowfacts a49e9f2c1f Bump build number and update changelog 2023-01-21 11:24:19 -05:00
Shadowfacts b1421767dd Fix tapping expand thread cell not working 2023-01-20 14:17:15 -05:00
Shadowfacts 8ee916411e Further card tweaks 2023-01-20 13:58:40 -05:00
Shadowfacts 9d845bf6c1 Show loading indicator when restoring timeline state 2023-01-20 13:47:14 -05:00
Shadowfacts 9a2c24942a Fix SegmentedPageViewController next sub-page shortcut not working 2023-01-20 11:38:31 -05:00
Shadowfacts cca2a03b2f When routing the SplitNav responder chain through the root VC, go as deep into it as possible
Makes keyboard shortcuts from, e.g., TimelineVC accessible when the root is TimelinesPageVC

See #302
2023-01-20 11:34:44 -05:00
Shadowfacts 1a64bfcef8 Disallow keyboard focus in sidebar
Makes keyboard shortcuts from the split VC's primary content available

See #302
2023-01-20 11:33:28 -05:00
Shadowfacts 907810d98a Make link preview cards larger 2023-01-20 11:22:28 -05:00
Shadowfacts 23a4999196 Complete asynchronous swipe actions immediately
Fixes crash when the user things the action has failed and taps it
again, which results in an invalid completion handler later being called
2023-01-20 10:53:30 -05:00
Shadowfacts 3e0feba273 Fix collapse button disappearing when navigating away 2023-01-20 10:51:56 -05:00
Shadowfacts 468a559127 Fix crash when TimelinePosition's center status ID isn't in the list of IDs 2023-01-19 21:46:57 -05:00
Shadowfacts c03fc86300 Bump build number 2023-01-19 14:38:35 -05:00
Shadowfacts a33be0b556 Remove unused background audio mode 2023-01-19 13:13:08 -05:00
Shadowfacts 6aee926f00 Fix table views being too far inset on iPhone 2023-01-19 13:13:01 -05:00
Shadowfacts 13640be91d Bump build number and update changelog 2023-01-19 13:08:05 -05:00
Shadowfacts 5123cf20c3 Rename Delete Status -> Delete Post 2023-01-18 15:05:12 -05:00
Shadowfacts bf739b9f41 Add pagination to status actions account list 2023-01-18 15:02:56 -05:00
Shadowfacts 4211806b5f Add followers/following screen
Closes #323
2023-01-18 15:02:56 -05:00
Shadowfacts 88aada8d35 Add follower/ing counts to profile header 2023-01-18 14:02:23 -05:00
Shadowfacts 5623cedab3 Fix conversation reloading on appear 2023-01-18 13:59:42 -05:00
Shadowfacts ccfc8331fb Fix avatars not un-grayscaling on timeline 2023-01-18 11:37:15 -05:00
Shadowfacts 10803408cd Post status deleted notifications when load fails with not found 2023-01-17 20:04:48 -05:00
Shadowfacts fb7a7db6e8 Handle deleted statuses in status action account list 2023-01-17 20:02:03 -05:00
Shadowfacts 78cd1313fe Fix new conversation VC not responding to status bar taps 2023-01-17 19:36:12 -05:00
Shadowfacts db1bbf7148 Add delete status action 2023-01-17 19:32:50 -05:00
Shadowfacts 5f19adf2d0 Only show report action for other people's posts 2023-01-17 19:15:54 -05:00
Shadowfacts 6f006adbc1 Show better message when opening conv for deleted status
Also split conversation loading out into separate view controller
2023-01-17 19:15:54 -05:00
Shadowfacts 39bff06897 Fix profile header buttons not adjusting height for dynamic type size
Closes #317
2023-01-17 11:51:14 -05:00
Shadowfacts 68682ee291 Maybe fix race condition between iCloud sync and state restoration 2023-01-17 10:50:36 -05:00
Shadowfacts 5029b26b40 Bump build number and update changelog 2023-01-17 10:50:14 -05:00
Shadowfacts 907cf08400 Fix expand thread cell not adjusting to accent color pref 2023-01-16 17:54:56 -05:00
Shadowfacts e85d194e5f Make table and collection view focusable 2023-01-16 17:54:56 -05:00
Shadowfacts cfeb87d2ba Fix status collection cells being too far inset 2023-01-16 17:54:56 -05:00
Shadowfacts e4f3735c9f Don't use UIPageViewController for SegmentedPageViewController 2023-01-16 17:54:56 -05:00
Shadowfacts baa9dfe0f1 More logging 2023-01-16 15:51:03 -05:00
Shadowfacts 5e73439e7b Fix statuses being inset too much on iPhones 2023-01-16 14:21:42 -05:00
Shadowfacts 4b2776ee81 Fix conversation non-main status collapse button not adjusting to accent color preference 2023-01-16 11:54:09 -05:00
Shadowfacts 566df3e285 Bump build number and update changelog 2023-01-16 11:53:10 -05:00
Shadowfacts 0653d695d9 Fix various things not adjusting to accent color preference
Closes #325
2023-01-16 11:24:42 -05:00
Shadowfacts 4811747790 Fix crash when resuming search user activity in unloaded explore VC 2023-01-16 10:58:45 -05:00
Shadowfacts ed2519848c Prevent all pinned timelines from being removed 2023-01-16 10:55:32 -05:00
Shadowfacts b1374b12a3 More error reporting tweaks 2023-01-15 15:01:16 -05:00
Shadowfacts c5a25eecf1 Fix row separators not being inset to readable content width 2023-01-15 12:45:41 -05:00
Shadowfacts a4dbf3ddbb Add New List action to Add to List menu 2023-01-15 11:49:20 -05:00
Shadowfacts be3a61ebc7 Fix Send Report button not adapting to accent color 2023-01-15 11:48:16 -05:00
Shadowfacts ababa4b428 Add more logging around state restoration crash 2023-01-15 11:30:34 -05:00
Shadowfacts d75c2558ca Capture strong references in ToggleFollowHashtagService retry actions 2023-01-15 10:33:06 -05:00
Shadowfacts ac0dedfd3d Bump build number and update changelog 2023-01-15 10:30:17 -05:00
Shadowfacts 37563b6afd Fix @Published property being changed on background thread 2023-01-14 11:32:31 -05:00
Shadowfacts 937afc0dfd Add accent color preference 2023-01-14 11:32:31 -05:00
Shadowfacts 94c34e03dd Add reporting accounts and statuses 2023-01-14 11:03:39 -05:00
Shadowfacts 1ad556f9cf Fix crash when displaying poll finished notification 2023-01-13 15:27:48 -05:00
Shadowfacts 019f7d6d6a Fix crash if preferences change while there are cells that don't have statuses 2023-01-13 15:26:26 -05:00
Shadowfacts b4384d11f5 Delete Relationship when Account is deleted
Prevents errors when accessing dangling relationships w/o accounts
2023-01-13 10:31:51 -05:00
Shadowfacts 2ed8d22899 Fix crash when trying to restore activity for non-pinned timeline 2023-01-09 12:39:35 -04:00
Shadowfacts cce6413e2b Fix crash when trying to load deleted statuses for restoration 2023-01-08 17:56:21 -04:00
Shadowfacts 8fb0fb66e3 Start playing video attachments immediately on appear 2023-01-06 21:43:27 -04:00
Shadowfacts abe2bbdfd4 Bump build number and update changelog 2023-01-06 21:18:04 -04:00
Shadowfacts 1d9efc7fb5 Include status code in automatic mastodon error reports 2023-01-03 11:31:24 -05:00
Shadowfacts b17b7b7a24 Fix crash when inserting present items when there are no existing items 2023-01-02 17:18:30 -05:00
Shadowfacts 18d7917756 Add subjects for activity item sources 2023-01-02 17:16:31 -05:00
Shadowfacts cc401fce8c Allow sharing gifv attachments, improve share sheet behavior for images 2023-01-02 16:59:55 -05:00
Shadowfacts a5fc35d0b1 More tweaks to automatic error reporting 2023-01-02 15:14:28 -05:00
Shadowfacts acd48a6db4 When logging out, remove the scene's active account rather than the most-recently activated one, as they may not be the same 2023-01-02 11:41:47 -05:00
Shadowfacts b45d3fb80a Use WebURL for status URLs 2023-01-02 11:36:06 -05:00
Shadowfacts 3ea1ad5622 Bump build number and update changelog 2023-01-01 15:28:55 -05:00
Shadowfacts 5898da3234 Maybe fix race condition when account is loaded as profile statuses VC is dealloc'd 2023-01-01 15:27:25 -05:00
Shadowfacts 9dd966f639 Fix duplicate saved instances not being uniqued correctly 2023-01-01 15:27:25 -05:00
Shadowfacts 48662ef1f3 Bump build number and update changelog 2023-01-01 15:12:21 -05:00
Shadowfacts 854d48e54e Unique saved hashtag/instance items
This may happen when migrating to iCloud, if the same hashtag is saved
on multiple devices.
2023-01-01 14:49:04 -05:00
Shadowfacts d4c560d7fc Add createdAt to AccountPreferences and TimelinePosition to guard against race conditions when creating/migrating 2023-01-01 12:58:44 -05:00
Shadowfacts 91b7ce3008 Add pointer interaction to ToastView 2023-01-01 12:35:40 -05:00
Shadowfacts 4dca231a06 Add loading animation while syncing timeline position 2023-01-01 12:25:44 -05:00
Shadowfacts b81c83a250 Add iCloud env entitlement and ITSAppUsesNonExemptEncryption 2022-12-31 16:58:39 -05:00
Shadowfacts f9e619d9e7 Deduplicate updated timeline positions when handling remote changes 2022-12-31 16:58:20 -05:00
Shadowfacts ae7962ae50 Better Sentry messages 2022-12-31 16:57:43 -05:00
Shadowfacts 5027660b52 Maybe fix crash when restoring unloaded statuses due to race condition 2022-12-31 16:57:13 -05:00
Shadowfacts 358d81b5cf Fix crash when accessing SegmentedPageViewController before it's loaded 2022-12-31 16:46:00 -05:00
Shadowfacts 79b9108a8f Add CloudKit status indicator to advanced prefs 2022-12-31 11:24:42 -05:00
Shadowfacts 5ab22e742b Automatically report errors displayed to the user 2022-12-29 17:30:39 -05:00
Shadowfacts 4f655bb80a Change collection view deselect on appear to happen alongside nav pop 2022-12-28 15:01:21 -05:00
Shadowfacts e4f1309e2d Make everything follow the readable width 2022-12-26 12:22:17 -05:00
Shadowfacts bb40894778 Ensure all statuses are cached before returning 2022-12-26 12:09:57 -05:00
Shadowfacts 24b3fa1e3f Guard against race condition when loading card image 2022-12-26 11:27:58 -05:00
Shadowfacts 16cd045588 Show individual attachments uncropped inline in statuses 2022-12-25 14:13:59 -05:00
Shadowfacts 15a7cd5f65 Fix not being able to tap attachments in the timeline 2022-12-25 10:27:19 -05:00
Shadowfacts e676075d5b Fix spacing on toolbar when visibility and local-only items visible 2022-12-25 10:03:07 -05:00
Shadowfacts 967bff063b Tweak iCloud timeline sync 2022-12-25 09:59:35 -05:00
Shadowfacts 3cba0bce34 Update pinned timelines when changed remotely 2022-12-24 12:20:13 -05:00
Shadowfacts 60b182ac18 Sync timeline position using iCloud 2022-12-23 16:37:42 -05:00
Shadowfacts 619878ac85 Don't show Hide Reblogs/Replies prefs in Preferences, only in Customize Timelines 2022-12-23 16:37:42 -05:00
Shadowfacts 169f1a0191 Add haptic feedback to profile follow button 2022-12-23 11:19:37 -05:00
Shadowfacts fa31c28e92 Fix relationship change breaking header layout because the collection view wasn't resizing the cell 2022-12-22 18:51:55 -05:00
Shadowfacts f815d4e2e4 Replace VisualEffectImageButton with ProfileHeaderButton 2022-12-22 18:47:53 -05:00
Shadowfacts a3e5b29cfc Fix crash inserting present items when currentItems includes posts from since-unfollowed users 2022-12-22 17:57:17 -05:00
Shadowfacts 46cecde014 Add more prominent follow button to profile pages 2022-12-22 17:26:50 -05:00
Shadowfacts 86143c5887 Add window titles to main and compose scenes 2022-12-22 15:02:49 -05:00
Shadowfacts 0a1dc423d4 Fix compose attachment list buttons not using accent color on macOS 2022-12-22 14:54:41 -05:00
Shadowfacts 1cb0f1ae56 Fix non-mention notifications showing in Mentions tab on Pleroma
e0d97cd2a8 introduced a regression on Pleroma, because specifying the
allowed types of notifications in the Masto API was only added in 3.5
2022-12-22 14:41:56 -05:00
Shadowfacts 9f86158bb7 Add About screen 2022-12-22 13:59:39 -05:00
Shadowfacts 231b0ea830 Add Acknowledgements page 2022-12-21 11:59:40 -05:00
Shadowfacts 4dc108f782 Add pinned timeline customization 2022-12-20 23:37:12 -05:00
Shadowfacts 795146cde4 Cache lists in CoreData 2022-12-20 15:13:18 -05:00
Shadowfacts 975be17d13 Avoid doing unnecessary work for filtered statuses 2022-12-20 11:32:20 -05:00
Shadowfacts 32be76ebee Update UI in responds to remote changes of saved hashtags/instances 2022-12-19 13:56:46 -05:00
Shadowfacts d13b517128 Sync saved hashtags and instances over iCloud
Closes #160
2022-12-19 10:58:14 -05:00
Shadowfacts e0d97cd2a8 Fix unknown notifications appearing in the Mentions tab 2022-12-18 11:33:49 -05:00
Shadowfacts 8b718ce50b Only allow continuous scroll gestures to dismiss gallery 2022-12-17 17:55:05 -05:00
Shadowfacts ce708e2d16 Hide reblogs and hide replies filters
Closes #202
2022-12-17 13:40:15 -05:00
Shadowfacts 01467574d0 Don't show reblog swipe action when reblogging is forbidden
Closes #313
2022-12-17 13:09:33 -05:00
Shadowfacts 97a2278634 Fix previewing link in conversation main status activating link
Closes #311
2022-12-17 13:05:50 -05:00
Shadowfacts 4b2a263889 Better accessibility label for conversation toggle collapse button 2022-12-14 22:05:17 -05:00
Shadowfacts 1f37a5e7eb Bump build number and update changelog 2022-12-14 22:04:48 -05:00
Shadowfacts 77c9fac3ce Fix preferences not checking current account correctly when multiple scenes open 2022-12-14 21:27:50 -05:00
Shadowfacts a13d5d5a82 Fix crash when activating account in My Profile scene 2022-12-14 21:24:54 -05:00
Shadowfacts 23e4541eb7 Don't reload list timeline if edit screen is closed without making changes 2022-12-14 21:00:36 -05:00
Shadowfacts d4b9f71fd3 Remove old, unused code 2022-12-14 20:54:41 -05:00
Shadowfacts a9edeaf5b9 Apply filters to Trending Posts 2022-12-14 20:52:44 -05:00
Shadowfacts 1f6074e539 Fix monospace fonts not adjusting for Dynamic Type 2022-12-14 20:07:16 -05:00
Shadowfacts df7b62e14b Use KVO to invalidate LazilyDecoding properties 2022-12-14 19:46:02 -05:00
Shadowfacts cacc8a51cc Remove unused code 2022-12-14 10:15:15 -05:00
Shadowfacts 89ca0629b3 Move bundle ID prefix to xcconfig 2022-12-14 10:04:25 -05:00
Shadowfacts 360db07ef2 Fix URLs getting pasted as broken attachments
Closes #309
2022-12-14 09:47:17 -05:00
Shadowfacts f55a870964 Move development team setting to xcconfig
Closes #308
2022-12-13 23:58:44 -05:00
Shadowfacts 5ee140cdab Bump build number and update changelog 2022-12-13 21:26:28 -05:00
Shadowfacts ff4dff1147 Fix status icons flashing blue during expand/collapse
Closes #209
2022-12-13 20:56:08 -05:00
Shadowfacts ba1eed7a85 Add pointer effect to custom alert actions
Closes #306
2022-12-13 20:36:18 -05:00
Shadowfacts 0c9f6e02bd Fix controls reappearing when swiping between pages in gallery 2022-12-13 14:14:13 -05:00
Shadowfacts 565d17970f Make attachment description scrollable beyond a certain height
Closes #168
2022-12-13 14:07:16 -05:00
Shadowfacts dc3c2d027c Prevent statuses which are in the persisted timeline state from being pruned 2022-12-13 13:31:34 -05:00
Shadowfacts ba2c34fdd6 Persist timeline state using CoreData, rather than NSUserActivity
This allows persisting state for all the primary timelines, and across
all accounts.

Closes #297
Closes #293
2022-12-13 13:31:34 -05:00
Shadowfacts 3691c3f483 Actually encode the swipe action prefs 2022-12-12 23:09:18 -05:00
Shadowfacts 9c103103e8 Fix ToastableViewController automatic scroll view detection not handling collection views 2022-12-12 22:57:33 -05:00
Shadowfacts 382d8ef2c8 Fix Trending Posts appearing to reload forever 2022-12-12 22:51:50 -05:00
Shadowfacts 2891f47cb3 Fix statuses from the wrong timeline being restored into Home (again) 2022-12-12 22:47:16 -05:00
Shadowfacts 3c80ec8b43 Allow saving or following hashtag from Add screen 2022-12-12 22:06:55 -05:00
Shadowfacts 478ba3db28 Include followed hashtags in Explore and sidebar 2022-12-12 22:02:07 -05:00
Shadowfacts f96cd1b5e2 Copy showStatusesAutomatically when selecting conversation expand thread item
Closes #303
2022-12-12 21:06:05 -05:00
Shadowfacts 7f4ab57a1d Fix <li> bullets/numbers appearing black in dark mode
Closes #304
2022-12-12 21:00:12 -05:00
Shadowfacts 8caf93bf0a Add ScrollingSegmentedControl, and home/notifs/profiles to use it 2022-12-12 20:57:38 -05:00
Shadowfacts 9c4b68b09e Reorganize gestures 2022-12-12 20:56:14 -05:00
Shadowfacts b49e8d0279 Move Pachyderm to Packages folder 2022-12-11 14:25:25 -05:00
Shadowfacts 71a57e9859 Fix images copied from Safari pasting as URLs
Closes #301
2022-12-11 12:54:25 -05:00
Shadowfacts 081ef16e5e Fix My Profile item in sidebar not updating when avatar style changes
Closes #298
2022-12-10 19:41:45 -05:00
Shadowfacts b3ec259ce9 Fix status bar scroll to top not working in single-column navigation on iPad
Closes #296
2022-12-10 19:40:05 -05:00
Shadowfacts 4f48514d1a Actually only restore existing statuses 2022-12-08 20:15:12 -05:00
Shadowfacts f96acd33f2 Tweak timeline status VO labels to only include attachment text when not blurred 2022-12-06 22:29:03 -05:00
Shadowfacts cde061c77a Fix custom emoji not being stripped from usernames in VoiceOver labels 2022-12-06 22:26:08 -05:00
Shadowfacts a79b3cfd70 Fix gallery controls not being accessible, fix escape gesture not working
Closes #292
2022-12-06 22:21:59 -05:00
Shadowfacts 9a35f96c75 VoiceOver: Include attachment descriptions in timeline statuses
Closes #291
2022-12-06 22:14:23 -05:00
Shadowfacts 60767c6a7e Profile Directory screen VoiceOver improvements
Add label to filter button (and change icon to match other filters)

Make each profile a single accessibility element
2022-12-06 21:54:17 -05:00
Shadowfacts 57668886b2 Fix crash when scrolling through Local/Federated timeline with VoiceOver
It seems that the accessibility scroll mechanism does something like:
1. Find the next IndexPath to focus
2. Scroll to make it visible
3. Focus that cell

But because the timeline description cell is removed during the scroll,
the IndexPath that the accessibility system wants to focus becomes
invalid between steps 2 and 3, causing a crash when trying to focus it.

As a workaround, only remove the timeline description _item_ rather than
the header section so that section indices aren't affected.

Closes #290
2022-12-06 21:46:32 -05:00
Shadowfacts ffb5c76f7c Add preference to never blur attachments 2022-12-06 21:12:58 -05:00
Shadowfacts 00e8dd6345 Fix crash when previeiwng non-HTTP(S) link 2022-12-06 10:58:13 -05:00
Shadowfacts 7904462920 Fix serializing the nodeinfo version instead of the software version in breadcrumb 2022-12-05 22:24:33 -05:00
Shadowfacts 13d649bace Bump build number and update changelog 2022-12-05 22:24:10 -05:00
Shadowfacts bebe563e8f Further tweak persistent store migration 2022-12-05 19:32:59 -05:00
Shadowfacts 4be2258882 Fix saving expired filters not reenabling them
Closes #289
2022-12-05 19:01:32 -05:00
Shadowfacts 40ff8d0a2a VoiceOver: improve description of gap cell, add actions to specify direction 2022-12-05 18:43:32 -05:00
Shadowfacts 0dcb7e71c4 Also perform jump to present check when the timeline VC reappears onscreen 2022-12-05 18:27:23 -05:00
Shadowfacts 08878f2fb9 Re-add tusker:// scheme
Apparently it was accidentally removed in d661870401

Closes #287
2022-12-05 17:28:28 -05:00
Shadowfacts 3ea7e1057b Add preference to disable timeline state restoration 2022-12-05 17:24:01 -05:00
Shadowfacts fc8fcb76fd Fix crash when TimelineViewController tries to apply snapshot while not visible 2022-12-05 17:17:34 -05:00
Shadowfacts eac2a9b19f Move VoiceOver Jump to Present action to timeline pages segmented control 2022-12-05 17:13:45 -05:00
Shadowfacts 0ce57d1308 More fiddling with how Jump to Present works
Now, when loading present items, they're inserted into the data source
immediately along with a gap. If the user taps Jump to Present, then a
new snapshot _with only the present items_ will be applied (which allows
infinite scrolling to work properly when they scroll back down) and the
view scrolled-to-top. Tapping Go Back, then, applies the original
snapshot (i.e., the current one from when Jump to Present was tapped)
and restores the scroll position.
2022-12-05 17:09:11 -05:00
Shadowfacts 97dec0f9d2 Add accessibility hint for segmented controls 2022-12-05 16:25:16 -05:00
Shadowfacts b64c748b73 Add Jump to Present VoiceOver action
Closes #288
2022-12-04 22:06:04 -05:00
Shadowfacts 77ab2c3753 Fix Trending Posts reloading on every appearance 2022-12-04 22:03:48 -05:00
Shadowfacts b90262bfd0 Tweak fav/reblog counts pref text 2022-12-04 19:50:15 -05:00
Shadowfacts 581f4b24bd Add Sentry breadcrumb for instance software/version 2022-12-04 18:26:06 -05:00
Shadowfacts 5f3d9da9f8 Only try to restore statuses that exist in the cache
This could result in discontinuities in the restored timeline, but I'm
not sure there's anything better we could do.
2022-12-04 17:34:28 -05:00
Shadowfacts 41775e5d19 Actually migrate to new persistent store locations 2022-12-04 17:33:09 -05:00
Shadowfacts 044d34d20f Bump build number and update changelog 2022-12-04 15:40:00 -05:00
Shadowfacts f1b1732e5c Fix filter HTML to attributed string conversion optimization not being applied 🤦‍♂️ 2022-12-04 15:36:26 -05:00
Shadowfacts 1da2b17a76 Fix dynamic type not applying to timeline status content 2022-12-04 15:35:54 -05:00
Shadowfacts e49725e06d Bump build number and update changelog 2022-12-04 14:57:22 -05:00
Shadowfacts 669404d6f8 Copy local-only status from replied-to post
Closes #280
2022-12-04 14:03:12 -05:00
Shadowfacts 2e21742264 Add Cmd+Enter keyboard shortcut for sending post
Closes #283
2022-12-04 14:01:09 -05:00
Shadowfacts 7763d08816 VoiceOver: Fix not being able to select account from conversation main status cell 2022-12-04 13:51:05 -05:00
Shadowfacts 726be85223 VoiceOver: Fix profile relationship label not being read 2022-12-04 13:51:05 -05:00
Shadowfacts 19bf6cbf18 VoiceOver: Add show profile rotor action to timeline statuses
Closes #285
2022-12-04 13:51:05 -05:00
Shadowfacts df07fa85d5 Fix unsatisfiable constraints warning for ZeroHeightCollectionViewCell 2022-12-04 12:17:31 -05:00
Shadowfacts e3e55de55b Fix hide filter action not working on profiles 2022-12-04 12:11:52 -05:00
Shadowfacts 54857a3bf3 Avoid converting HTML to attributed string twice when displaying a status cell for the first time
Now, when Filterer performs the conversion, the status cell can reuse
the attributed string.
2022-12-04 12:08:22 -05:00
Shadowfacts b28f616e85 Don't apply expired filters 2022-12-04 11:55:46 -05:00
Shadowfacts 97c7104dbc Don't update constraints in StatusContentContainer.setCollapsed unless the state actually changes 2022-12-04 11:14:19 -05:00
Shadowfacts 6501343f24 Reapply filters on when they change 2022-12-04 10:54:02 -05:00
Shadowfacts fabe339215 VoiceOver: Indicate filtered posts, make double tapping expand them 2022-12-03 23:20:19 -05:00
Shadowfacts e1886509d3 Filter statuses on profiles 2022-12-03 23:11:09 -05:00
Shadowfacts 8ad48784d9 Fix V2 filter actions not saving 2022-12-03 23:11:09 -05:00
Shadowfacts 75e9c9f986 Fix home/list filters not applying to lists 2022-12-03 23:11:09 -05:00
Shadowfacts a17afe247c Better filter cell and animation for showing filtered post 2022-12-03 23:11:09 -05:00
Shadowfacts 81abcfcf7b Timeline filtering! 2022-12-03 22:16:43 -05:00
Shadowfacts 7e5d8675c2 Extract HTML to attributed string converter to separate helper 2022-12-03 18:58:19 -05:00
Shadowfacts cde3109203 Rename StatusState to CollapseState 2022-12-03 18:21:49 -05:00
Shadowfacts fcf95ba8c1 Filters view UI tweaks 2022-12-03 15:22:10 -05:00
Shadowfacts f71804f094 Extract filter create/update/delete logic into separate services 2022-12-03 14:40:12 -05:00
Shadowfacts 83ca7f1321 Creating filters UI 2022-12-03 14:40:12 -05:00
Shadowfacts 16a1e4008b V2 filters API, CoreData, and editing UI 2022-12-03 12:29:11 -05:00
Shadowfacts 518a8eba0a Start doing filters UI 2022-12-02 22:03:28 -05:00
Shadowfacts 8d56a6450e Fix mute account time not being 1 week 2022-12-02 21:39:05 -05:00
Shadowfacts 8896bfbc59 Consistent "OK" capitalization 2022-12-02 18:06:15 -05:00
Shadowfacts 4ca57f8c76 Better case-insensitive sorting for lists 2022-12-01 18:26:48 -05:00
Shadowfacts c9fa11cc3b Fetch filters and store in CoreData 2022-11-30 22:16:33 -05:00
Shadowfacts 0247c50650 Fix invalid names being used for persistent store 2022-11-30 21:35:52 -05:00
Shadowfacts eca06cb14a Fix too much space on profile header view above description 2022-11-30 21:13:48 -05:00
Shadowfacts c07e2cfdd8 Add more possibilities to relationship label on profile header 2022-11-30 17:05:18 -05:00
Shadowfacts db7615d26f Fix Edit List Accounts search field being jammed in the corner on iPad 2022-11-30 16:53:11 -05:00
Shadowfacts 2f0acad866 Return to previous item when the selected list/hashtag/instance is removed from the sidebar 2022-11-30 16:47:06 -05:00
Shadowfacts a2b3fc0628 Fix saved/followed hashtag lookups being case-sensitive 2022-11-30 16:46:18 -05:00
Shadowfacts e005b70071 Fix creating list on iPad not showing Edit List screen immediately 2022-11-30 16:34:12 -05:00
Shadowfacts b515664db3 Fix creating list on iPad overwriting previous item navigation stack 2022-11-30 16:34:05 -05:00
Shadowfacts 948eff1f7e Workaround for crash when pressing Cmd+1/2/... on macOS
See #253

The actions won't work, but it's better than crashing :/
2022-11-29 23:19:19 -05:00
Shadowfacts f1a39c2faa Add follow/unfollow hashtag actions 2022-11-29 23:14:36 -05:00
Shadowfacts ab8e498cee Refactor menu actions to allow presenting from menu bar items 2022-11-29 23:14:36 -05:00
Shadowfacts c6da754875 Indicate when a followed hashtag caused a post to appear in the home timeline 2022-11-29 23:14:36 -05:00
Shadowfacts 97d5b955a0 Store followed hashtags
The followed hashtags may not load until after the timeline request
completes, and we want to be able to show the hashtag indicator (or at
least make a best effort attempt) immediately.
2022-11-29 23:14:36 -05:00
Shadowfacts 80f9800fd6 Completely replace all items when jumping to present 2022-11-29 20:53:00 -05:00
Shadowfacts 0485400c1f Tweak how InstanceFeatures is updated 2022-11-29 20:52:39 -05:00
Shadowfacts 811aac35d7 Fix timeline statuses not getting deselected when entering split nav
Closes #275
2022-11-29 10:29:40 -05:00
Shadowfacts a77b090435 Fix mute screen layout on iPad
Closes #276
2022-11-29 10:23:00 -05:00
Shadowfacts 21874b0966 Organize expanded custom emoji picker by category
Closes #223
2022-11-28 22:13:06 -05:00
Shadowfacts 08c63a2f84 Add indicator for locked profiles 2022-11-28 21:53:45 -05:00
Shadowfacts 97f00e9d6f Indicate pending follow requests, feedback on successful async menu actions
Closes #265
2022-11-28 21:41:56 -05:00
Shadowfacts a97a7e0aea Fix attachments disappearing from status cells in certain circumstances 2022-11-28 20:40:24 -05:00
Shadowfacts cf870916c9 Fix links in conversation main status not being activatable with VoiceOver
Closes #272
2022-11-28 19:14:08 -05:00
Shadowfacts 7297566060 Fix some swipe actions getting called off the main thread 2022-11-28 19:14:08 -05:00
Shadowfacts 4f28fec62a Add links/mentions/hashtag to VoiceOver rotor in timelines
Closes #231
2022-11-28 19:14:08 -05:00
Shadowfacts c01bc4d840 Compose screen VoiceOver improvements 2022-11-28 18:40:35 -05:00
Shadowfacts ea6698a2d8 State restoration for non-home timeline pages 2022-11-28 16:33:19 -05:00
Shadowfacts 1e950b5ccb State restoration for presented and edited drafts
Closes #270
2022-11-28 16:09:29 -05:00
Shadowfacts 3e5a3c81b5 Add cache size info to Advanced prefs 2022-11-28 14:05:35 -05:00
Shadowfacts a5506aeab6 Add more tracing for notifications missing statuses
See #274
2022-11-27 21:54:58 -05:00
Shadowfacts 23b76a7276 Better crash messages for sidebar collapse/expand failures 2022-11-27 21:46:21 -05:00
Shadowfacts d8f503351b Limit edit list accounts search to accounts the user follows 2022-11-27 21:44:17 -05:00
Shadowfacts d5887f1f02 Add post edited notifications
Closes #238
2022-11-27 11:50:14 -05:00
Shadowfacts e04cdd16d6 Add preferences for status cell swipe actions
Closes #249
2022-11-26 20:26:26 -05:00
Shadowfacts c256fb4cbd When refreshing timeline, hide activity indicator as soon as loadNewer completes 2022-11-26 17:33:58 -05:00
Shadowfacts 21299c8eb8 Fix error when refreshing timeline with no items 2022-11-26 17:33:07 -05:00
Shadowfacts 527706154a Fix long status table view cells not getting collapsed 2022-11-26 17:28:55 -05:00
Shadowfacts 07c86b6949 Fix gifv attachments not being centered
Closes #271
2022-11-25 13:20:31 -05:00
Shadowfacts 92cf938e99 Fix cells not being deselected in account list and status action account list 2022-11-24 12:30:56 -05:00
Shadowfacts f23d3dfa3f Bump build number and update changelog 2022-11-24 12:24:38 -05:00
Shadowfacts 23f9e200dc Fix potential crash when trying to save timeline state 2022-11-24 12:14:19 -05:00
Shadowfacts 366834e2e4 Tweak timeline state restoration to maintain scroll position of center item 2022-11-24 11:05:56 -05:00
Shadowfacts d409d26478 Fix pressing CW button in Compose not toggling field visibility
Bring back the wrapper view, turn's out it was load bearing. We need to
be able to observe both the ui state and the draft object, while also
updating the observed draft object when the ui state's draft changes,
and this seems like the most straightforward way of doing that.
2022-11-23 14:07:03 -05:00
Shadowfacts 76fc73de95 Bump build number and update changelog 2022-11-23 12:25:27 -05:00
Shadowfacts 40800f964d Fix jump to present not scrolling all the way to the top 2022-11-23 11:58:52 -05:00
Shadowfacts 9f7d16a70e Don't show duplicate actions in status cell more actions menu 2022-11-23 11:47:00 -05:00
Shadowfacts c2cb0a0c5a Timeline state restoration 2022-11-23 11:35:25 -05:00
Shadowfacts 272f35417b Rewrite account list VC using UICollectionView 2022-11-22 15:38:40 -05:00
Shadowfacts 848c3dd950 Rewrite status action account list to use UICollectionView 2022-11-22 15:29:17 -05:00
Shadowfacts dfeb39b31f Fix selecting draft not working
Closes #263
2022-11-22 14:00:41 -05:00
Shadowfacts bab5226f2a Fix albums in asset picker not being sorted by name 2022-11-22 13:57:56 -05:00
Shadowfacts 88cfbfb1f3 Improve reblog indicator on statuses
Closes #225
2022-11-22 11:48:59 -05:00
Shadowfacts 49f1d6339f Fix crash when toggling collapse in Trending Posts
Closes #262
2022-11-22 11:47:57 -05:00
Shadowfacts 3e7cb443fa Correct post content type warning
Hometown does not support formatting
2022-11-22 11:39:47 -05:00
Shadowfacts b5c8a38b9b Add preference for using twitter-style keyboard 2022-11-22 11:06:21 -05:00
Shadowfacts ab19922530 Indicate verified profile links
Closes #241
2022-11-22 11:00:52 -05:00
Shadowfacts 45c844b065 Separate Shared Albums section in asset picker
Closes #244
2022-11-21 23:21:21 -05:00
Shadowfacts 47b838a386 Change timeline gap-filling to do a proper job of maintaining the bottom-relative scroll position 2022-11-21 22:47:44 -05:00
Shadowfacts 276691efbf Embiggen gallery share/close buttons
Closes #257
2022-11-20 21:37:57 -05:00
Shadowfacts 0a8d50cc27 Fix double-tap to zoom in gallery not working
Closes #256
2022-11-20 15:48:29 -05:00
Shadowfacts 11e81acbc1 Fix toasts not adjusting font for Dynamic Type 2022-11-20 14:15:21 -05:00
Shadowfacts fb2c9b341c Fix custom alert action icon getting squished when Dynamic Type is on
Closes #254
2022-11-20 14:12:00 -05:00
Shadowfacts 810ae71832 Make poll options in Compose reorderable with drag/drop 2022-11-20 14:06:45 -05:00
Shadowfacts 001a73af3c Workaround for profile header changing size when statuses are loaded in the background
Closes #250
2022-11-20 13:57:51 -05:00
Shadowfacts c8375b742a Make more actions button on profiles more prominent 2022-11-19 14:29:21 -05:00
Shadowfacts 9feef054fc Fix list timeline VC presenting edit screen repeatedly 2022-11-19 14:22:26 -05:00
Shadowfacts bf87ae7a7d Add Add to List menu action to accounts
Closes #247
2022-11-19 14:22:26 -05:00
Shadowfacts f8de6f9e10 Fix follow/block/mute actions showing up on user's own account 2022-11-19 14:10:19 -05:00
Shadowfacts ab47fa776e Store lists on MastodonController 2022-11-19 14:08:39 -05:00
Shadowfacts 7178473f34 Fix compose toolbar being hidden by software keyboard on iPadOS 15
Closes #252
2022-11-19 13:35:34 -05:00
Shadowfacts c8319d8af2 Remove old and debug code 2022-11-19 13:11:29 -05:00
Shadowfacts 9ff1452c68 Show jump to present toast if necessary when scene re-appears 2022-11-19 13:09:37 -05:00
Shadowfacts ce534c4a05 Actual gap cell implementation 2022-11-19 11:15:14 -05:00
Shadowfacts 0fddf94292 Timeline jump to present 2022-11-18 20:49:15 -05:00
Shadowfacts 8276e99d27 Timeline gaps and gap filling 2022-11-18 17:29:55 -05:00
Shadowfacts a5ad8e43b1 Disable attachment colorspace conversion on Mastodon v4 2022-11-15 21:45:42 -05:00
Shadowfacts ce7ce3ac92 Fix crash when requests race with own account
If the notifications/etc load first, and the table view cells are
created, mastodonController.account may still be nil
2022-11-14 21:38:24 -05:00
Shadowfacts 99a1c76cb1 Clean up instance type/feature detection
Add akkoma detection
2022-11-14 21:17:08 -05:00
Shadowfacts 603e989879 Fix error when server responds with rich cards 2022-11-14 19:39:18 -05:00
Shadowfacts dd82283341 Bump build number and update changelog 2022-11-13 18:40:40 -05:00
Shadowfacts af2d9e7eb8 Fix pleroma version detection 2022-11-13 18:24:46 -05:00
Shadowfacts 06ad46e639 Fix confirm reblog alert not adjusting to Dynamic Type
Closes #246
2022-11-13 17:15:06 -05:00
Shadowfacts 71f97d41c4 Fix certain instance features not being detected properly 2022-11-13 17:08:15 -05:00
Shadowfacts df131f32c6 Fix reblog visibility dropdown displaying even when unsupported 2022-11-13 17:07:57 -05:00
Shadowfacts 77dece36d0 Fix Hometown versions not being parsed correctly 2022-11-13 17:05:08 -05:00
Shadowfacts 1a767ff910 Fix crash when opening My Profile on iPad 2022-11-13 14:30:00 -05:00
Shadowfacts 220c8050b1 Re-add pointer effects to Compose toolbar buttons 2022-11-13 14:15:44 -05:00
Shadowfacts d4fa9c96e8 Add context menu action to delete draft 2022-11-13 14:03:51 -05:00
Shadowfacts 22b5d62ba1 Make GIF attachments animate in the Compose screen 2022-11-13 14:01:54 -05:00
Shadowfacts b9bdd29986 Fix GIFs dragged from Finder posting as static images
Closes #239
2022-11-13 13:46:19 -05:00
Shadowfacts f848bbf7c4 Remove unneeded ComposeContainerView 2022-11-12 22:59:11 -05:00
Shadowfacts 0fe9edfdbc Fix crash when opening Drafts screen on macOS 2022-11-12 22:59:11 -05:00
Shadowfacts 6d2830cf78 Rewrite Compose toolbar with SwiftUI
Fixes buttons not being accessible with VoiceOver
Fixes content overflowing on small devices

Closes #232
Closes #218
2022-11-12 22:59:11 -05:00
Shadowfacts 7294ff6e1a Status VoiceOver improvements
Closes #229
Closes #230
2022-11-12 15:17:30 -05:00
Shadowfacts 3fd62552b3 Hide redundant info from VoiceOver in mute screen 2022-11-12 14:45:30 -05:00
Shadowfacts fa5abc27f7 Make profile fields view VoiceOver accessible 2022-11-12 14:43:47 -05:00
Shadowfacts ccc47e204d Fix InstanceFeatures not correctly using pleroma version 2022-11-12 14:34:57 -05:00
Shadowfacts bf3f735062 Focus CW field immediately when CW enabled, move focus to main text view when return key pressed
Closes #226
2022-11-12 14:16:05 -05:00
Shadowfacts de0198946e Fix keyboard reappearing after pressing Post button on Compose screen 2022-11-12 13:52:36 -05:00
Shadowfacts 072a77b58e Cleanup previewing actions code 2022-11-11 23:35:30 -05:00
Shadowfacts eb7fe22863 Add mute action to profiles
Closes #201
2022-11-11 23:35:30 -05:00
Shadowfacts f1511039ef Add domain block action to profiles 2022-11-11 22:44:58 -05:00
Shadowfacts 5c479e3bf0 Convert wide-gamut images to sRGB before uploading 2022-11-11 21:02:38 -05:00
Shadowfacts 0413f326a0 Add block action to accounts
Closes #208
2022-11-11 19:09:34 -05:00
Shadowfacts 9d1c3f1410 Fix error when decoding notification that has a status field but is null 2022-11-11 18:48:58 -05:00
Shadowfacts 802a0ac9ba Fix scope selector in Profile Directory being flipped 2022-11-11 18:30:09 -05:00
Shadowfacts 9da986e3b8 Tweak heuristic for showing profile fields in single column 2022-11-11 18:26:59 -05:00
Shadowfacts e6a5b899be Add context menu action for deleting lists on iPad 2022-11-11 18:20:16 -05:00
Shadowfacts 60bf3b2e33 Fix potential crash when deleting list 2022-11-11 18:16:44 -05:00
Shadowfacts b465838b71 Fix renaming list not updating UI
Closes #213
2022-11-11 18:08:44 -05:00
Shadowfacts 21bd716844 Fix crash when creating list fails
Closes #212
2022-11-11 17:54:25 -05:00
Shadowfacts 523fb91b21 Add scope to search following accounts when editing list
Also fixes crash when loading or editing list

Closes #216
Closes #221
2022-11-11 17:33:48 -05:00
Shadowfacts d8bf770902 Instance selector tweaks
Closes #234
Closes #237
2022-11-10 17:05:51 -05:00
Shadowfacts 10aa32d9cc Don't use UIPageViewController for profiles
Closes #228
2022-11-10 17:00:46 -05:00
Shadowfacts 7474969969 Workaround for AVPlayerViewController controls not respecting safe area
Closes #176
2022-11-09 21:46:52 -05:00
Shadowfacts 319b5458fc Fix refreshing not loading initial when previous attempt failed
Closes #214
2022-11-09 19:15:08 -05:00
Shadowfacts f7304a011c Fix images not being cached
Fixes #219
2022-11-09 18:56:59 -05:00
Shadowfacts 94dc5d3177 Fix not being able to tap links in profile fields
Closes #211
2022-11-09 18:51:27 -05:00
Shadowfacts 6d692c2730 Rewrite Drafts screen with SwiftUI 2022-11-09 18:18:31 -05:00
Shadowfacts d0f8691560 Fix draft cells become untappably small 2022-11-09 17:20:56 -05:00
Shadowfacts 9a43ab5a13 Fix caret not scrolling into view when focusing compose text views
Closes #233
2022-11-09 17:18:17 -05:00
Shadowfacts 01124b76a3 Add Duckable package, make Compose screen duckable 2022-11-08 22:17:01 -05:00
Shadowfacts 7600954f4b Refactor ComposeView to use a single List for everything 2022-11-07 22:58:01 -05:00
Shadowfacts 5a5c67e445 Try to prevent pruning accounts that still have statuses referencing them 2022-11-07 18:47:46 -05:00
Shadowfacts 68c3affacf Bump build number and update changelog 2022-11-05 18:31:22 -04:00
Shadowfacts e40f4faa8e Rewrite TrendingStatusesViewController to use collection view 2022-11-05 15:13:20 -04:00
Shadowfacts b56c6c37ec Fix crash when ProfileHeaderView tries to create observers after ProfileVC is deinit'd
Can happen if the network is slow and the user closes the profile screen before the header loads
2022-11-05 14:42:40 -04:00
Shadowfacts 999118798c Fix inserting pinned items that already exist when refreshing profile 2022-11-05 14:38:08 -04:00
Shadowfacts 84cf755332 Fix drawing VC background flickering in dark mode
Closes #199
2022-11-05 14:29:45 -04:00
Shadowfacts 5bd7c0ad2b Add preference to prevent blurring media behind CW
Closes #203
2022-11-05 13:20:55 -04:00
Shadowfacts 7fe06d42ce Consider content height, not just char count, when collapsing posts
Closes #205
2022-11-05 13:11:36 -04:00
Shadowfacts 20986ba3f0 Add preference for default reply visibility
Closes #207
2022-11-05 12:20:30 -04:00
Shadowfacts 97a95c435e Improve performance when displaying posts with many custom emojis
Closes #204
2022-11-05 11:00:14 -04:00
Shadowfacts b9555cf7dd Dynamic type support in assorted places 2022-11-04 22:32:40 -04:00
Shadowfacts 590b9f0bcc Dynamic type support on notifications screen 2022-11-04 22:32:34 -04:00
Shadowfacts ca2ceaea56 Remove now-unused confirm load more table view cell 2022-11-04 22:32:34 -04:00
Shadowfacts 96d8a79d42 Dynamic type support in Explore screen 2022-11-04 21:47:42 -04:00
Shadowfacts 11233f7d25 Dyanmic type support in profile header view 2022-11-04 21:39:47 -04:00
Shadowfacts a991e0f429 Dynamic Type support in status cells 2022-11-04 16:52:37 -04:00
Shadowfacts bfdce07d81 Fix compose reply avatar being wrongly aligned for 1-line statuses 2022-11-03 19:14:52 -04:00
Shadowfacts f5953655c5 Set merge policy on managed object contexts and maybe fix some CoreData errors? 2022-11-03 18:56:06 -04:00
Shadowfacts 6bc4993d81 Fix favorite/reblog menu actions not working 2022-11-03 18:48:39 -04:00
Shadowfacts 68646c4b4d Fix objc associated objects not working in release builds 2022-11-03 18:37:32 -04:00
Shadowfacts 38b0d57118 Improve CoreData error reporting 2022-11-03 10:27:45 -04:00
Shadowfacts b38c24b347 Bump build number and update changelog 2022-11-02 23:48:53 -04:00
Shadowfacts a6d51cee3c More fiddling with the sentry script 2022-11-02 23:47:14 -04:00
Shadowfacts 7bdbd9f71a Handle task cancellation in MastodonController.run 2022-11-02 23:00:29 -04:00
Shadowfacts b47876dc3d Fix retain cycle due to account follow action workaround 2022-11-02 22:59:44 -04:00
Shadowfacts 4644475bc7 Fix crashes when ProfileStatusesVC doesn't finish loading until ProfileVC is deinit'd 2022-11-02 22:53:07 -04:00
Shadowfacts 16ba292afa Remove debug print 2022-11-02 22:34:40 -04:00
Shadowfacts c7f3bac330 Add sterner warning about post content type 2022-11-02 22:06:08 -04:00
Shadowfacts abb8352c92 Fix ImageCache.get completion not being called when image isn't loaded 2022-11-02 22:06:08 -04:00
Shadowfacts 59d866aa23 Ditch custom image request grouping, rely on URLSession's 2022-11-02 22:06:08 -04:00
Shadowfacts ba032412eb Fix timeline reloading every time VC appears
Caused by changes to TimelineLikeController required to let list
timelines reload from scratch
2022-11-02 22:06:07 -04:00
Shadowfacts 5de0c034f4 Remove old TimelineTableViewController 2022-11-01 21:11:13 -04:00
Shadowfacts b1d83f2746 Switch hashtag/instance/list timelines to use new collection view impl 2022-11-01 21:10:41 -04:00
Shadowfacts 658c08010d Re-add undo scroll-to-top to timelines/profiles 2022-11-01 20:49:07 -04:00
Shadowfacts 6a5753fac8 Fix crash when tapping Load More button with Disable Infinite Scrolling 2022-10-31 17:45:36 -04:00
Shadowfacts 8da89986df Fix find instance VC requiring double dismiss 2022-10-31 17:39:57 -04:00
Shadowfacts c7e39cb041 Use short descriptions in instance selector when available 2022-10-31 17:35:50 -04:00
Shadowfacts b755607895 Fix crash when TimelineStatusTableViewCell outlives its containing VC 2022-10-31 17:33:33 -04:00
Shadowfacts 508eef8c07 Nothing to see here 2022-10-31 17:33:33 -04:00
Shadowfacts a18dfc38af Fix crash when refreshing profile before it has loaded 2022-10-31 17:33:33 -04:00
Shadowfacts 95f9fad673 Tweak Sentry config 2022-10-31 17:33:33 -04:00
Shadowfacts 4857b507b1 Send CoreData saving errors to Sentry 2022-10-31 12:26:09 -04:00
Shadowfacts bca7bd3586 Tweak sentry upload script and fix using dist build config in debug 2022-10-31 12:25:54 -04:00
Shadowfacts 9978e392a2 Bump build number and update changelog 2022-10-31 12:25:37 -04:00
Shadowfacts cc33cf18f2 Workaround for follow menu item never resolving on macOS
See #198
2022-10-30 18:54:14 -04:00
Shadowfacts c5921bc4cb Add option to disable automatic crash reporting 2022-10-30 18:17:53 -04:00
Shadowfacts 91450ced7c Use Sentry for crash reporting 2022-10-30 17:10:58 -04:00
Shadowfacts 5afd9e83eb Shhh 2022-10-30 14:47:36 -04:00
Shadowfacts d05275020f Tweak timeline status cell spacing 2022-10-29 21:18:01 -04:00
Shadowfacts c420c236d9 Whoops 2022-10-29 21:06:27 -04:00
Shadowfacts d5433e9b91 Fix crash when opening profile view controller with uncached account
E.g., by tapping a mention in a status
2022-10-29 18:55:13 -04:00
Shadowfacts cbbe9ec11f Fix crash in profile due to accessing data source before it exists
This could happen if an account is updated in the background while a
profile is on screen and the user has not visited all of the tabs.
2022-10-29 18:40:41 -04:00
Shadowfacts 0e06d47687 Fix status collapse changes not animating on profiles 2022-10-29 18:27:24 -04:00
Shadowfacts c907b7257a Bump build number and update changelog 2022-10-29 18:27:12 -04:00
Shadowfacts 10239d14c9 Fix selected segment not updating on profiles when switching tabs with keyboard shortcuts 2022-10-29 15:08:03 -04:00
Shadowfacts 2344275ff9 Enable blurhash in debug
Capping the size at 32x32 means this is fast enough even in un-optimized builds
2022-10-29 14:19:43 -04:00
Shadowfacts e0ffa1d9c5 Cap blurhash image size at 32x32 2022-10-29 14:19:43 -04:00
Shadowfacts 77a6654ff2 Fix crash when generating blurhash image for AttachmentView that hasn't been laid out
It was passing a negative size into the blurhash decoder, which is invalid

Instead, cap the size at 32x32 (letting the image view scale it up when rendering)
2022-10-29 14:19:43 -04:00
Shadowfacts 43aee0ec67 Add pointer interaction to avatar in timeline status cell 2022-10-29 14:19:43 -04:00
Shadowfacts d95ba82e5b Improve pointer interaction on new status cell action buttons
Closes #195
2022-10-29 14:19:43 -04:00
Shadowfacts b6d8232951 Fix replies appearing multiple times in drafts 2022-10-29 14:19:43 -04:00
Shadowfacts bb9cef55ea Don't remove persistent data when clearing cache 2022-10-29 14:19:43 -04:00
Shadowfacts 67718d8fe4 Fix wrong logs getting sent with crash reports 2022-10-29 14:19:43 -04:00
Shadowfacts 71a2029752 Switch everything to new profile view controller 2022-10-28 21:38:56 -04:00
Shadowfacts 6bb1f3b7dc Finish converting profiles to collection views 2022-10-28 21:31:18 -04:00
Shadowfacts 2469d285bc Initial implementation of profile switching with collection views 2022-10-28 19:17:33 -04:00
Shadowfacts 5f410213e2 Start converting profile statuses to collection view 2022-10-28 19:17:33 -04:00
Shadowfacts bb3e1b44b1 Hide live text controls when other gallery controls are hidden
Closes #189
2022-10-28 19:16:00 -04:00
Shadowfacts 868df25417 Disable pruning offscreen rows in new timelines
I don't think this is actually necessary, the system should kill us
often enough that the amount of items in the data source doesn't become
unmanageable.

Making modifications to the data source in viewDidDisappear was causing
the collection view's contentOffset to change to be scrolled to top
(roughly) when the view became visible again.

Disabling it also fixes several issues caused by updating the data
source even when there were no changes.

Closes #193
Closes #192
Closes #187
Closes #186
2022-10-28 19:05:07 -04:00
Shadowfacts 2801f65e67 Fix reblog labels in new cells not being tappable
Closes #197
2022-10-28 18:48:30 -04:00
Shadowfacts cccde29e6c Fix crash when long-pressing Send Report button on iPad
Closes #190
2022-10-27 23:11:21 -04:00
Shadowfacts aa0629d202 Don't dismiss issue reporter when email is cancelled
Closes #191
2022-10-27 23:10:00 -04:00
Shadowfacts ba209fa4d2 Protect DiskCache.fileStates with a lock
Closes #194
2022-10-27 23:06:50 -04:00
Shadowfacts d224f47b8c Fix long content warnings getting truncated in new status cells
Closes #185
2022-10-11 17:04:31 -04:00
Shadowfacts ffb0ceba20 Remove old XCB code 2022-10-11 10:10:55 -04:00
Shadowfacts 22022f5ef6 Bump build number and update changelog 2022-10-10 19:04:26 -04:00
Shadowfacts 1ac72bc363 Fix collection view cells not deselecting in split nav controller on iPad 2022-10-10 18:58:07 -04:00
Shadowfacts dcc8f38f3d Fix key commands not working inside split nav controller on iPad
Fixes #179
2022-10-10 18:58:07 -04:00
Shadowfacts 8cf217d2ba Fix crash when trying to prune rows before statuses have loaded 2022-10-10 16:21:08 -04:00
Shadowfacts 7d66117fab Fix mentions from Misskey opening browser instead of profile view 2022-10-10 14:31:26 -04:00
Shadowfacts 9c0c1f87f8 Fix links/mentions/hashtags in timeline statuses not being tappable 2022-10-10 14:26:47 -04:00
Shadowfacts 7a2d8e78eb Attempt the third at making debug logging work in TestFlight 2022-10-10 14:25:25 -04:00
Shadowfacts c15a5fc90f Fix reblog statuses being selected in timeline 2022-10-10 14:23:27 -04:00
Shadowfacts 212ce69ffd Log when status unexpectedly doesn't have URL 2022-10-10 14:21:12 -04:00
Shadowfacts 7470b053c6 Bump build number and update changelog 2022-10-09 22:02:17 -04:00
Shadowfacts d1b4b39e86 Fix MultiThreadDictionary crash on iOS 15 due to using existential types
See #178
2022-10-09 21:53:58 -04:00
Shadowfacts b43f0d5bd9 Bump build number and update changelog 2022-10-09 20:53:45 -04:00
Shadowfacts 035034430e Fix crash when hovering with the cursor over certain text views
Closes #183
2022-10-09 20:49:08 -04:00
Shadowfacts a703b7cc0a Prune offscreen rows on new timeline 2022-10-09 20:11:00 -04:00
Shadowfacts e78bec8409 Fix sensitive attachments not being hidden in new timeline 2022-10-09 19:15:41 -04:00
Shadowfacts 412e4a4dc5 Fix public timeline descriptions not working
Closes #182
2022-10-09 19:11:34 -04:00
Shadowfacts 81e10326d3 Add logging to persistent store 2022-10-09 17:09:55 -04:00
Shadowfacts 20f88ef161 Fix debug logs not working
Apparently only values in Info.plist do substitution
2022-10-09 16:46:40 -04:00
Shadowfacts bce0f8ef18 Bump build number and update changelog 2022-10-09 14:46:48 -04:00
Shadowfacts d661870401 Include log data in issue/crash reports 2022-10-09 14:26:44 -04:00
Shadowfacts afa1a733f4 Remove old XCB docs 2022-10-09 13:53:14 -04:00
Shadowfacts 1b186725ce Re-add timeline context menus 2022-10-08 23:47:42 -04:00
Shadowfacts 164a8e26c4 Fix not being able to press attachments in new status cells 2022-10-08 19:10:21 -04:00
Shadowfacts cadcc1a92a Don't navigate to profile when tapping name stack in timeline status
Otherwise it's too difficult to open short posts
2022-10-08 16:53:48 -04:00
Shadowfacts bcb3c24027 Fix context menu presentation animation getting clipped in new status cells 2022-10-08 16:53:48 -04:00
Shadowfacts fd6a4ba41c Fix update timestamp work item firing too frequently
A reconfiguration would schedule a new work item without cancelling the
old one, resulting in the timestamp updating multiple times in quick
succession (noticeable for statuses <60s old).
2022-10-08 16:53:48 -04:00
Shadowfacts 3ab82b2dbb Fix attachments/cards flickering in new cells on reconfiguration 2022-10-08 16:53:48 -04:00
Shadowfacts 1ed218d5e3 Fix new status cells not showing meta indicators or reblog button visibility 2022-10-08 16:53:48 -04:00
Shadowfacts 0fee770411 Fix crash when displaying new status cells with polls 2022-10-08 15:12:17 -04:00
Shadowfacts 5b116c0d4e More logging! 2022-10-08 15:12:10 -04:00
Shadowfacts b7a4f7e30f Make tapping content warning label toggle expand/collapse 2022-10-08 15:03:50 -04:00
Shadowfacts ba1300b1b7 Re-add status cell dragging 2022-10-08 15:01:23 -04:00
Shadowfacts 817ef0c2cc New timeline key commands 2022-10-08 14:53:21 -04:00
Shadowfacts 18ee621489 Status cell swipe actions 2022-10-08 14:33:07 -04:00
Shadowfacts ddf5094acf Only show collapse button on collapsible statuses 2022-10-08 13:21:01 -04:00
Shadowfacts 133921848d Extract favoriting/reblogging to separate services
Allows displaying error popups and retrying
2022-10-08 13:19:32 -04:00
Shadowfacts 46db70d58b Fix building in release mode
When handleEvent dispatches to the other methods, it crashes the compiler
during an optimization pass. Seems to be related to:
https://github.com/apple/swift/issues/61350
2022-10-08 11:45:02 -04:00
Shadowfacts 21958eb77f Merge branch 'develop' into collection-timelines 2022-10-08 11:01:19 -04:00
Shadowfacts b30f149dc9 Use mutex on iOS 15 instead of os_unfair_lock
See #178
2022-10-08 10:57:59 -04:00
Shadowfacts 9b83566482 Fix TuskerTests not compiling 2022-10-08 10:55:55 -04:00
Shadowfacts b688631937 Update status cells on status changes 2022-10-06 22:36:55 -04:00
Shadowfacts 4d654358d7 Extract a bunch of common stuff to StatusCollectionViewCell protocol 2022-10-05 23:19:30 -04:00
Shadowfacts 24e90de672 Status cell interaction 2022-10-05 22:28:10 -04:00
Shadowfacts 780e8b09b7 Status cell UI 2022-10-05 21:39:58 -04:00
Shadowfacts 2196663d94 Make StatusContentContainer play nice with hiding subviews 2022-10-04 22:48:42 -04:00
Shadowfacts 7085ac01cb Timeline status collection view cell collapsing 2022-10-04 00:02:41 -04:00
Shadowfacts 81671d73c7 Start converting timeline status to UICollectionViewCell 2022-10-04 00:01:16 -04:00
Shadowfacts a38c89a17f Re-add public timeline descriptions 2022-10-01 15:32:06 -04:00
Shadowfacts 253fb8d27d Extract more things to TimelineLikeCollectionViewController 2022-10-01 15:08:51 -04:00
Shadowfacts a682c8f5cc Extract a bunch of timeline view controller stuff to separate protocol 2022-09-24 11:39:12 -04:00
Shadowfacts d18a4b3c42 Fixing loadInitial happening multiple times 2022-09-24 11:31:52 -04:00
Shadowfacts 426b31d46c Initial TimelineLikeController + TimelineViewController implementation 2022-09-24 10:49:06 -04:00
Shadowfacts 5c09b1910f Cleanup/reorganize some things 2022-09-19 22:52:52 -04:00
Shadowfacts fe72d8faec Remove x-callback-url support
Closes #1
2022-09-19 22:44:27 -04:00
Shadowfacts b560bcd8dc Prevent loading indicator from potentially being added multiple times
Not sure how this could happen, but it's caused 1 crash in the wild so w/e
2022-09-19 22:35:27 -04:00
Shadowfacts 85ced7ff5f Bump build number and update changelog 2022-09-19 15:05:10 -04:00
Shadowfacts 5ac76ef9c4 Revert "Maybe fix timeline discontinuities"
This reverts commit 43b4976ed7.

That commit reintroduced #166
2022-09-18 22:37:18 -04:00
Shadowfacts 123a512d3c Bump build number and update changelog 2022-09-18 22:14:54 -04:00
Shadowfacts d141ed7d03 Enable reblog with visibility on Pleroma 2022-09-18 22:01:57 -04:00
Shadowfacts 95e120afd6 Fix large image controls not being hidden on iPhone 14 Pro 2022-09-18 11:30:50 -04:00
Shadowfacts ca8a214cf6 Add reblog with visibility menu to reblog confirmation alert 2022-09-18 11:28:33 -04:00
Shadowfacts 7161861d36 Add API param for reblog visibility 2022-09-18 11:28:33 -04:00
Shadowfacts c6c8f63e39 Fix compose reply view not working after ContentTextView refactor, use named CoordinateSpace for calculating scroll offset in reply avatar view 2022-09-18 11:28:33 -04:00
Shadowfacts e9962997a6 Show preview of status in reblog confirmation alert
Closes #121
2022-09-17 20:27:36 -04:00
Shadowfacts f2ab1778c5 Replace expanded emoji picker with SwiftUI 2022-09-15 21:49:50 -04:00
Shadowfacts 0f71d61b88 Fix crash when there are duplicate emojis
Closes #164
2022-09-15 21:10:52 -04:00
Shadowfacts 80c4fcce82 Use AnyAccount instead of EitherAccount for compose autocomplete 2022-09-15 21:05:18 -04:00
Shadowfacts 8f8d50efbd Bring back StatusProtocol 2022-09-15 21:04:53 -04:00
Shadowfacts 43b4976ed7 Maybe fix timeline discontinuities
See #174
2022-09-15 20:54:28 -04:00
Shadowfacts ff3681627b Fix reblog status cell not showing selection background in spacer
Closes #175
2022-09-15 20:45:45 -04:00
Shadowfacts 35d21fb725 Switch to stable, hash-based account IDs
#160
2022-09-12 23:05:35 -04:00
Shadowfacts bbfb3b0a7a Add loading indicator to DiffableTimelineLikeTableViewController 2022-09-12 22:05:19 -04:00
Shadowfacts 8b78a5e7ad Don't parent background managed object contexts to view context
Otherwise, certain operations require the background contexts to
interact with the view context, which can block the main thread from
accessing the view context (potentially causing hitches if the view
context access is in a critical path, like cell fetching).
2022-09-11 23:00:51 -04:00
Shadowfacts 66c17006d1 Fix poll votes displaying random number
i have no idea where the number was coming from
2022-09-11 22:35:09 -04:00
Shadowfacts 8a911f238b Fix emojis getting set without setting emoji identifier 2022-09-11 22:20:46 -04:00
Shadowfacts 77c44c323f Use os_unfair_lock for MultiThreadDictionary instead of DispatchQueue 2022-09-11 22:20:46 -04:00
Shadowfacts c2d1fe45d8 Update for iPhone 14 series 2022-09-07 18:43:46 -04:00
Shadowfacts 24591cee05 Improve account switching animation 2022-08-01 21:29:24 -04:00
Shadowfacts 50dd785ef8 ContentTextView cleanup 2022-07-31 19:39:14 -04:00
Shadowfacts af2e95ea39 Fix apparent crash when tapping tab bar item of selected tab 2022-07-11 15:07:11 -04:00
Shadowfacts 4fa1bd7268 Fix crash due to nested navigation controllers 2022-07-11 14:59:01 -04:00
Shadowfacts ea07e6aef6 Simplify timeline status cell layout, fix due to missing constraint
Fixes crash when re-showing timeline actions after being hidden
2022-07-11 14:42:49 -04:00
Shadowfacts 5e7a1e5974 Bump build number and update changelog 2022-07-09 12:05:17 -04:00
Shadowfacts 9b3cc61dcb Update WebURL to version with IDNA support
Closes #163
2022-07-09 11:45:27 -04:00
Shadowfacts 0c37b99a68 i don't even remember 2022-07-09 11:26:37 -04:00
Shadowfacts f96d1d780c Enable data detectors on main status text view
Tapping detected items doesn't work because it conflicts with our tap
gesture recognizer, but long pressing does
2022-07-09 11:25:23 -04:00
Shadowfacts 5a5364ad3b Use iOS 16 API for disabling compose attachment list scrolling 2022-07-09 11:02:01 -04:00
Shadowfacts 5b70c713b2 Two column navigation on iPad 2022-07-06 17:47:40 -04:00
Shadowfacts efb96eddf3 Fix compiling for Catalyst 2022-07-02 11:33:15 -07:00
Shadowfacts 5cb25c8c1f Move trending hashtags/links to Explore tab on iPad 2022-06-30 19:53:40 -07:00
Shadowfacts 700cc2c67c temp env var 2022-06-30 19:24:49 -07:00
Shadowfacts a9e0bffe5f Bump deployment target to iOS 15 2022-06-30 19:04:08 -07:00
Shadowfacts 512e0e9053 Fix passing invalid points to CoreGraphics when building trend history graph 2022-06-30 18:15:13 -07:00
Shadowfacts b842389449 Convert trending hashtags to collection view 2022-06-30 18:15:13 -07:00
Shadowfacts cc10a13785 TextKit 2, baby 2022-06-29 00:12:45 -07:00
Shadowfacts f9c3ad5921 Bring back interactive keyboard dismissal on compose screen 2022-06-28 17:30:04 -07:00
Shadowfacts 0960699699 Fix building for iOS 14 2022-06-28 17:29:46 -07:00
Shadowfacts c6e06fe9f3 Use SwiftUI for sheet presentation detents on iOS 16 2022-06-28 17:29:46 -07:00
Shadowfacts 10f6a68065 Use new-style self-sizing cells on iOS 16 2022-06-28 17:29:46 -07:00
Shadowfacts 037b717e60 Include filename extension for attachments
Fixes posting attachments on pleroma resulting in them served as
application/octet-stream, even though we're sending the mime type as well
2022-06-28 17:29:46 -07:00
Shadowfacts 9fa352d4f8 Fix retain cycle in DiffableTimelineLikeTableViewController 2022-06-28 17:29:46 -07:00
Shadowfacts 73345bb927 Always used stacked search field in instance selector 2022-06-28 17:29:46 -07:00
Shadowfacts f5385b0a1d Use context menu for filter/sort on profile directory 2022-06-28 17:29:46 -07:00
Shadowfacts 46fbbdc99a Always use stacked search bar placement on iPadOS 16 2022-06-10 23:44:52 -04:00
Shadowfacts 6ef8c92d09 Update to recommended Xcode settings 2022-06-10 23:44:52 -04:00
Shadowfacts 08b7cf013b Use browser-style navigation bars on iPad 2022-06-10 23:44:52 -04:00
Shadowfacts f702df2f15 Add context menu action for deleting draft so it's accessible by cursor 2022-06-10 23:44:52 -04:00
Shadowfacts 92efee6f46 Fix crash when loading older/newer notifications on Pixelfed
Damn Pixelfed returning nonsensical pagination links

Closes #166
2022-06-10 23:44:52 -04:00
Shadowfacts facf039f97 Live text in gallery view 2022-06-10 23:44:52 -04:00
Shadowfacts d7f35cd1e4 Bring back interactive keyboard dismissal on Compose screen 2022-06-10 23:44:52 -04:00
Shadowfacts 332637e0d9 Add edit menu actions 2022-06-10 23:44:52 -04:00
Shadowfacts 6d6fd3d49d Maybe fix crash in sceneDidEnterBackground 2022-06-10 23:44:52 -04:00
Shadowfacts b4675a97c7 Add missing awaits due to changed overload resolution 2022-06-10 23:44:52 -04:00
Shadowfacts 02e3417c27 Full size attachment previews on Compose screen (iOS 16)
Closes #110
2022-06-10 23:44:44 -04:00
Shadowfacts f5ac2616ad Disable unnecessary UIAppearance hacks on iOS 16 2022-06-07 09:42:33 -04:00
Shadowfacts 01bb37b0f6 Fix warning 2022-06-06 23:58:43 -04:00
Shadowfacts a4d43889ce Fix crash when opening conversations in new windows 2022-06-06 23:00:57 -04:00
Shadowfacts 4991da1622 Add favorite/reblog menu actions on iOS 16 2022-06-06 22:58:14 -04:00
Shadowfacts f106cc78bb Fall back to Foundation URL if WebURL parsing fails
WebURL doesn't support Unicode domains/IDNA
2022-05-17 11:57:59 -04:00
Shadowfacts 2617d22819 Show notifications for other people's posts
Closes #161
2022-05-17 10:36:33 -04:00
Shadowfacts dbdf1d39bd Bump build number and update changelog 2022-05-17 10:31:56 -04:00
Shadowfacts 54ff3893a6 Slightly improve ActionNotificationGroupTableViewCell layout 2022-05-17 10:19:04 -04:00
Shadowfacts 0168c05259 More detailed error message for decoding hashtag urls 2022-05-17 10:18:28 -04:00
Shadowfacts 65e75afa8b Fix using -[NSObject description] instead of attachmentDescription field 2022-05-16 22:53:27 -04:00
Shadowfacts 90809811c1 Clean up ActionNotificationGroupTableViewCell avatar fetching code 2022-05-16 22:52:04 -04:00
Shadowfacts 0f6e9c97cc Bump build number and update changelog 2022-05-15 17:40:01 -04:00
Shadowfacts 98516e3802 Fix multiple lines of emojis (e.g., wordle) getting smushed together 2022-05-15 15:42:48 -04:00
Shadowfacts 68b03838a2 Fix saved hashtags sorting being case-sensitive 2022-05-15 10:37:38 -04:00
Shadowfacts 1f0025b101 Fix Send Message action not working on iPad/Mac 2022-05-15 10:34:39 -04:00
Shadowfacts b46f007f64 Fix Cmd+N shortcut for Compose not working on Mac (Catalyst or Designed
for iPad)
2022-05-15 10:34:24 -04:00
Shadowfacts ecab33bdce Better generics for LazilyDecoding 2022-05-13 17:33:07 -04:00
Shadowfacts cc0da2ec54 Fix user activities not continuing when passed at launch
Fix crash when continuing user activities on iPad
2022-05-13 17:10:18 -04:00
Shadowfacts a2868739c2 Fix crash when poll voting fails 2022-05-13 10:00:11 -04:00
Shadowfacts 2f75510889 Disable transparent nav bar in conversation vc 2022-05-11 19:15:56 -04:00
Shadowfacts 46332cd1b9 Jump to statuses below parent when expanding subthread in conversation 2022-05-11 19:12:28 -04:00
Shadowfacts 21e9ca990d Use async/await for conversation loading 2022-05-11 19:10:38 -04:00
Shadowfacts 1a02319894 Fix using old style for show all statuses bar button item when showing a
conversation that initially expands all statuses
2022-05-11 11:33:18 -04:00
Shadowfacts 4a95ccccdb Show expand thread indicator when there are additional replies to an
intermediate post in thread authored by a single person
2022-05-11 11:20:01 -04:00
Shadowfacts d3187ce2c4 Move saved instances and hashtags to CoreData 2022-05-10 22:58:30 -04:00
Shadowfacts ed0643c4ad Change explore swipe action titles 2022-05-10 22:58:30 -04:00
Shadowfacts 1e2947ceba Fix crash when accept/reject follow request fails 2022-05-10 22:58:30 -04:00
Shadowfacts ddcb13dd28 Fix notifications sometimes getting deleted in group merging
Closes #156
2022-05-10 22:58:25 -04:00
Shadowfacts c71bf3ba23 Fix displaying toasts from non-main queue 2022-05-09 15:55:35 -04:00
Shadowfacts 3e5c441b24 Fix crash when refreshing polls 2022-05-09 15:54:27 -04:00
Shadowfacts 0b6c16b0a6 Fix newly created statuses/accounts not having lastFetchedAt set
awakeFromFetch is only called on existing objects
2022-05-06 10:24:50 -04:00
Shadowfacts 5f566724bb Fix compose CW field overflowing 2022-05-03 20:14:55 -04:00
Shadowfacts 4a89ae3cfe Don't cache state of follow menu action
Fixes #151
2022-05-02 17:59:03 -04:00
Shadowfacts 56a0518c80 Add toast error messages to menu actions 2022-05-01 23:06:59 -04:00
Shadowfacts bf8a294676 Split MenuActionProvider from MenuPreviewProvider 2022-05-01 23:05:23 -04:00
Shadowfacts c069712c22 Don't include Open in Tusker on Catalyst 2022-05-01 21:50:16 -04:00
Shadowfacts d04957ba41 Remove reference counting system
Delete statuses/accounts that haven't been fetched in a week
2022-05-01 21:50:16 -04:00
Shadowfacts 8cc08cf4c0 Fix crash when displaying polls on Catalyst in Optimize for Mac
Closes #152
2022-05-01 21:50:11 -04:00
Shadowfacts 1b917f6bed Fix saved hashtags not getting persisted 2022-05-01 12:05:38 -04:00
Shadowfacts 514e569bd5 Fast account switching on iPad 2022-05-01 11:53:12 -04:00
Shadowfacts a22059a1a1 Show current user avatar in sidebar 2022-04-30 13:05:20 -04:00
Shadowfacts 2cfefc9432 Add "Add Account" placeholder to fast account switcher 2022-04-30 11:46:14 -04:00
Shadowfacts 2f7c7bae5e Extract status posting to separate class, convert to async/await 2022-04-30 11:11:22 -04:00
Shadowfacts 3f04d74dd6 Better error messages when exporting video fails 2022-04-27 23:33:29 -04:00
Shadowfacts 4dd8c1d692 Add subtitles to visibility context menu items
Closes #155
2022-04-27 23:21:08 -04:00
Shadowfacts eb9a5aeb42 Perform grouping with existing notifications when refreshing
Closes #88
2022-04-26 22:57:46 -04:00
Shadowfacts 7465abe0a9 Fix crash when loading account 2022-04-26 22:11:19 -04:00
Shadowfacts 20dab7c77a Handle missing account emojis on pixelfed instances 2022-04-26 17:50:23 -04:00
Shadowfacts 4e105e0fbc Fix table view cell gesture blocking toast long-press
Fixes #149
2022-04-26 13:29:22 -04:00
Shadowfacts d2f1d78aa2 Fix crash when preferences are changed before own account is loaded 2022-04-25 18:53:51 -04:00
Shadowfacts 360f52d0cf Don't crash when saving persistent store fails 2022-04-25 18:51:16 -04:00
Shadowfacts 8c888906c9 Bump build number and update changelog 2022-04-25 16:30:52 -04:00
Shadowfacts d611aeb035 Change selector names because apparently App Store Connect thinks the old ones are SPI now 2022-04-25 16:30:44 -04:00
Shadowfacts 0e888d35eb Revert "Fix refreshing skipping items"
This reverts commit 77007dcea0.
2022-04-17 11:44:35 -04:00
Shadowfacts 98bb230817 Fix crash when disabling hide status actions in timeline 2022-04-09 15:05:49 -04:00
Shadowfacts 3d6d9b2a91 Fix crash due to empty html element 2022-04-09 15:05:39 -04:00
Shadowfacts bc9a700383 Improve expanded emoji picker layout on iPad 2022-04-09 12:14:37 -04:00
Shadowfacts 62c7a30bbc Add emoji picker button to compose
Closes #144
2022-04-09 12:14:19 -04:00
Shadowfacts abf6ff8115 Unify compose screen input accessory toolbars 2022-04-09 11:42:32 -04:00
Shadowfacts a718721537 Fix crash if getting pending crash report fails 2022-04-08 18:45:09 -04:00
Shadowfacts 4f99d3c6e1 Add preference to disable status action buttons in timelines
Closes #145
2022-04-08 18:42:15 -04:00
Shadowfacts a2fc1652d1 Enable sidebar toggle button and gesture
Closes #146
2022-04-08 17:47:02 -04:00
Shadowfacts 77007dcea0 Fix refreshing skipping items
Closes #147
2022-04-08 17:09:14 -04:00
Shadowfacts dc818524b2 Bump build number and update changelog 2022-04-06 22:05:13 -04:00
Shadowfacts d1ba1105b5 Fix Pachyderm not depending on WebURLFoundationExtras 2022-04-06 21:50:07 -04:00
Shadowfacts 89a9bfba47 Fix crash when refreshing while logged in to a Pixelfed account
Closes #142
2022-04-06 21:48:04 -04:00
Shadowfacts 2798a199aa Fix some pixelfed hashtags not being decodable 2022-04-06 21:34:40 -04:00
Shadowfacts 3d0402c1e0 Fix potential deadlock when infinite scrolling is disabled
Fixes crash when used with Pixelfed
2022-04-04 09:59:45 -04:00
Shadowfacts af0c9c92b6 Fix warning when a post appears in both the pinned and regular sections of a profile 2022-04-02 20:34:31 -04:00
Shadowfacts 0a7709526f Bump build number and update changelog 2022-04-02 20:24:02 -04:00
Shadowfacts 9ec821f6b3 Nix the xcworkspace, convert Pachyderm to a Swift package
Closes #138
2022-04-02 19:28:10 -04:00
Shadowfacts 5c4474dc87 Only show Trending Posts/Links on new enough Mastodon versions 2022-04-02 13:18:14 -04:00
Shadowfacts 829ecf06da Add Trending Posts/Links to sidebar 2022-04-02 12:03:11 -04:00
Shadowfacts cb2bb215d3 Change sidebar Discover section to be collapsible 2022-04-02 12:03:11 -04:00
Shadowfacts 916c6fba0d Fix Send Message action not setting visibility to direct 2022-04-02 12:03:11 -04:00
Shadowfacts 8473f32781 Add Trending Links 2022-04-02 12:03:11 -04:00
Shadowfacts 240ccf23a4 Add Trending Posts 2022-04-02 12:03:11 -04:00
Shadowfacts e49859e5ea Add preference to disable Discover 2022-04-02 12:03:11 -04:00
Shadowfacts c6d158a8a3 Don't display error message on login cancellation 2022-04-01 21:00:46 -04:00
Shadowfacts 7e90fe2401 Fix all profile statuses appearing as pinned on PixelFed 2022-04-01 21:00:46 -04:00
Shadowfacts cab78a4aa4 Remove unnecessary IssueReporterDelegate 2022-03-30 09:58:50 -04:00
Shadowfacts 7da139be4d Redact request paths in error reporter 2022-03-29 22:37:39 -04:00
Shadowfacts 2444783edf Add error reporter to Client.Error toast on long-press 2022-03-29 22:37:26 -04:00
Shadowfacts 727615a818 Fix crash when providing account actions before own account is loaded 2022-03-29 12:52:14 -04:00
Shadowfacts 6e3089f025 Use WebURL for parsing links in HTML 2022-03-29 12:40:16 -04:00
Shadowfacts e09b0ff4e3 Fix crash when AccountTableViewCell is cached by a
SearchResultsTableViewController that has since decremented the
reference count of the cell's account
2022-03-29 12:34:54 -04:00
Shadowfacts 830eea5e95 Fix crash when attempting to prune offscreen rows without content sections 2022-03-29 12:20:32 -04:00
Shadowfacts 705fbbe343 Fix deadlock when loading assets after requesting authorization 2022-03-29 12:07:57 -04:00
Shadowfacts 12bcf52764 Improve error reporting for onboarding, use async/await 2022-03-29 11:58:11 -04:00
Shadowfacts f31c909517 Fix a race condition when refreshing My Profile before initial load is complete 2022-03-28 23:02:32 -04:00
Shadowfacts 781c37fbae Fix crash when refreshing My Profile
Closes #140
2022-03-28 22:23:33 -04:00
Shadowfacts 930ec7ccff Handle gotosocial gif attachments 2022-02-16 22:12:56 -05:00
Shadowfacts de93d6e171 Make Account.avatar optional for gotosocial 2022-02-16 22:12:47 -05:00
Shadowfacts 80c79ded3b Bump build number and update changelog, fix building weburl 2022-02-16 22:11:24 -05:00
Shadowfacts 126b0ae90a Extend disk cache expiry times
The cache keys are URLs, and Mastodon changes the url if the a new image is uploaded for avatar/header
2022-02-06 14:36:01 -05:00
Shadowfacts d6a847bfcc Use background image preparation apis on iOS 15
Closes #128
2022-02-06 10:24:48 -05:00
Shadowfacts 9b33059089 Fix crash when ProfileHeaderView leaks 2022-02-06 10:20:06 -05:00
Shadowfacts 804fdb439d Fix offscreen row pruning removing all rows from profile statuses 2022-02-06 10:19:38 -05:00
Shadowfacts 6ba5f70615 Fix pinned statuses from foreign instances not showing on Mastodon 2022-02-03 23:16:31 -05:00
Shadowfacts 54c01be7ff Use WebURL for more lenient parsing of external URLs
Fixes #136
2022-02-03 23:11:29 -05:00
Shadowfacts 6e964ff601 Profile directory can have a little shadow, as a treat 2022-01-25 21:34:41 -05:00
Shadowfacts 73d33ae730 Fix pleroma not being detected 2022-01-25 21:34:41 -05:00
Shadowfacts 434d975767 Fix crash when ownInstanceLoaded callback is called multiple times 2022-01-25 21:34:41 -05:00
Shadowfacts 41a31c23b7 Allow posting local-only from Glitch instances
See #130
2022-01-24 22:49:51 -05:00
Shadowfacts 02461ad46c Support local only posts on Hometown
Closes #130
2022-01-23 23:45:46 -05:00
Shadowfacts 072e68e97b Add nodeinfo request and InstanceFeatures 2022-01-23 23:26:49 -05:00
Shadowfacts 6879acbe02 Add local-only post icon 2022-01-23 23:22:34 -05:00
Shadowfacts ace503ad3d Use username on compose screen when there is no display name 2022-01-23 11:06:23 -05:00
Shadowfacts e12a82b476 Show local only posts on hometown instances
#130
2022-01-23 10:58:36 -05:00
Shadowfacts 51cb7c3edf Store local only post data 2022-01-23 10:57:32 -05:00
Shadowfacts 2198e2bf3e Allow development against local instances with self-signed certificates 2022-01-23 10:56:36 -05:00
Shadowfacts 6138fc7748 Add select more photos option to asset picker 2022-01-23 10:55:07 -05:00
Shadowfacts dc1eb3d6f0 Remove old code 2022-01-21 11:13:47 -05:00
Shadowfacts fa1482a152 Fix crash when fetching attachment data fails 2022-01-21 11:10:03 -05:00
Shadowfacts e65ed3e773 Fix crash when ProfileHeaderView leaks 2022-01-21 11:09:55 -05:00
Shadowfacts eca7f31e82 Use stringsdict for favorites/reblogs count 2021-11-25 12:38:05 -05:00
Shadowfacts 2b22180191 Remove TimelineLikeTableViewController
Everything now uses DiffableTimelineLike
2021-11-25 12:29:35 -05:00
Shadowfacts 654b5d9c59 Convert ProfileStatusesViewController to DiffableTimelineLike 2021-11-25 12:27:59 -05:00
Shadowfacts 777d1f378c Fix hashtag history view background being opaque 2021-11-24 15:15:34 -05:00
Shadowfacts 3b132ab4dc Enable context menus and drag and drop for trending hashtags 2021-11-24 15:12:25 -05:00
Shadowfacts d1083116e0 Use a single disptach queue for attachment/card thumbnails 2021-11-24 15:02:35 -05:00
Shadowfacts 7b79cec0ed Remove old comments 2021-11-22 23:41:06 -05:00
Shadowfacts 50cbbb86fc Fix instance selector activity indicator background color 2021-11-22 23:23:52 -05:00
Shadowfacts 5a914ea5a3 Don't show Mute action when not applicable to status 2021-11-22 23:23:19 -05:00
Shadowfacts ca5ac8b826 Fix crash due to leaked ProfileHeaderView not having a
mastodonController
2021-11-22 21:38:00 -05:00
Shadowfacts 2b50609e5c Fix animating poll configuration button size change when selected option
changes
2021-11-20 11:37:09 -05:00
Shadowfacts 57cb0614a9 Fix keyboard getting dismissed when scrolling autocomplete suggestions
Presentation controller takes care of dismissing keyboard when swipe
down in main scroll view starts
2021-11-20 11:28:37 -05:00
Shadowfacts eccb1043db Bump build number and update changelog 2021-11-13 22:40:26 -05:00
Shadowfacts 9768097488 Match gif playback progress through animation
Closes #8
2021-11-13 14:52:02 -05:00
Shadowfacts f5e9f71586 Use link replacement length from instance config if available 2021-11-11 13:44:24 -05:00
Shadowfacts 9f8b14d180 Replace Gifu with CGImageAnimation
Closes #44
2021-11-11 13:26:11 -05:00
Shadowfacts 10a3cbbe9c Improve padding on multi-line poll options 2021-11-10 17:25:13 -05:00
Shadowfacts b917120f17 Fix crash when conversation loading fails 2021-11-10 17:25:05 -05:00
Shadowfacts 30ef9cc6d0 Extract compose image into separate view 2021-11-10 16:57:27 -05:00
Shadowfacts 948c792e5d Fix crash when leaving timeline VC that was showing timeline description message and doesn't have any statuses 2021-11-07 23:22:48 -05:00
Shadowfacts 2df703ab71 Add haptic feedback to header view tab switcher to match home/notifications 2021-11-07 18:22:21 -05:00
Shadowfacts 1ec85ca095 Use video thumbnails from API when possible 2021-11-07 15:10:18 -05:00
Shadowfacts 5a26739b78 Remove old compilation condition 2021-11-07 14:35:14 -05:00
Shadowfacts 36a78f1a3c Improve emoji loading behavior
Use transparent placeholders to prevent wrong initial layout when some
or all emojis aren't cached.
2021-11-07 14:23:56 -05:00
Shadowfacts 1c0291b1dd Unify emoji replacement code 2021-11-07 13:11:49 -05:00
Shadowfacts e7d9e3780e Remove non-required app icon 2021-10-28 20:28:21 -04:00
Shadowfacts 83d4af2303
Fix interactive gallery dismiss going wrong direction when gesture starts out very slow 2021-09-21 23:46:22 -04:00
Shadowfacts 7c5076d01a
Fix dismissing gallery presented by modally-presented VC removing the
gallery's presenting VC from the view hierarchy

Closes #132
2021-09-21 23:30:38 -04:00
Shadowfacts e61823b78f Update LIVC comments for iPhones 13 2021-09-19 12:43:38 -04:00
Shadowfacts 4d52ac4d34
Support new Mastodon instance configuration 2021-09-12 16:32:23 -04:00
Shadowfacts aced0a63c9
Bump build number and update changelog 2021-08-15 22:43:32 -04:00
Shadowfacts 1e54235ff5 Hide public timeline description when user begins scrolling rather than
after cell moves offscreen

Fixes description getting dismissed prematurely on iOS 14 and hitching
when the cell moves offscreen
2021-08-15 22:29:14 -04:00
Shadowfacts e6e5554edf Fix fast account switcher animation weirdness when 1 account only 2021-08-15 19:29:26 -04:00
Shadowfacts 9026f487ec Convert notifications to use DiffableTimelineLikeTableViewController 2021-08-15 19:25:29 -04:00
Shadowfacts c0097ba752 Fix potential race condition with DiffableTimelineLikeTableViewController 2021-08-15 18:44:23 -04:00
Shadowfacts f109253bba Show toast when there are no new posts 2021-08-15 18:27:30 -04:00
Shadowfacts 1fda4248ec Add activity indicator to instance selector 2021-08-15 11:02:19 -04:00
Shadowfacts 7781c5252b Display toast on load errors 2021-08-15 10:37:37 -04:00
Shadowfacts 7f4bf52050 Add toast system 2021-08-15 10:37:20 -04:00
Shadowfacts ba0d179de5 Fix AccountSwtichingContainerViewController not sending sceneDidEnterBackground to children 2021-08-15 10:37:04 -04:00
Shadowfacts 71b6f1bdf0 Alphabetize things in Xcode 2021-08-14 18:27:22 -04:00
Shadowfacts 09ec4a920c
Fix retain cycle in ProfileViewController 2021-08-14 10:25:32 -04:00
Shadowfacts 7edf0fdb93
Fix crash when replying to post with preformatted text 2021-08-12 21:03:11 -04:00
Shadowfacts 99e06441f0
Fix crash when getting account relationship fails
UIDeferredMenuElement completion handler should only be called from the
main thread
2021-08-12 19:41:00 -04:00
Shadowfacts 85e1e131f6
Fix crash when fetching recommended instances fails 2021-08-12 19:36:28 -04:00
Shadowfacts 1d79918a94
Fix crash when refreshing before anything is loaded 2021-08-08 10:26:51 -04:00
Shadowfacts 340d13b1fa
Fix crash when reloading list timelines 2021-08-08 10:19:18 -04:00
Shadowfacts cf1000a4df
Fix loadOlder being called excessively on public timelines 2021-08-08 10:09:38 -04:00
Shadowfacts b781b56efd
Add public timeline descriptions 2021-08-08 10:09:28 -04:00
Shadowfacts 10a8a85bfc
Enable object lifetime optimization 2021-08-07 11:06:07 -04:00
Shadowfacts 6d8a014cc7 Bump build number and update changelog 2021-06-27 19:02:51 -04:00
Shadowfacts 60c88ded5e Require iOS 15 for Disable Infinite Scrolling 2021-06-27 17:17:39 -04:00
Shadowfacts 1e7a6af0bf Fix TimelineTableVC item hash including status state
Fixes crash when refreshing on iOS 14
2021-06-27 15:52:22 -04:00
Shadowfacts f8b79ef34f Fix app extension build number 2021-06-27 10:37:03 -04:00
Shadowfacts 4cf56685b5 Disable profile screen compose button when logged out 2021-06-27 10:31:02 -04:00
Shadowfacts fdcd2aa540 Add Open in New Window context menu action to sidebar items 2021-06-27 10:30:53 -04:00
Shadowfacts 667d30a710 Fix crash when editing accounts in a list
Closes #127
2021-06-26 18:54:59 -04:00
Shadowfacts b0f23e46ba Let Xcode update the stupid package name 2021-06-26 18:52:12 -04:00
Shadowfacts 9b30b48016 Bump build number and update changelog 2021-06-26 18:28:38 -04:00
Shadowfacts bd49683e13 Fix not being able to select assets on iOS 15 beta 2 2021-06-26 17:18:04 -04:00
Shadowfacts c22945b1e7 Use sheetPresentationController property 2021-06-26 17:02:17 -04:00
Shadowfacts 0a16a2e261 Fix potential data races 2021-06-26 16:51:54 -04:00
Shadowfacts b95819cada Fix crash when switching accounts 2021-06-26 16:42:56 -04:00
Shadowfacts dc1ea1bed9 Fix timeline momentum scrolling stopping due to adding footer section 2021-06-26 15:54:10 -04:00
Shadowfacts 5f9fe505d5 Add pref to disable infinite scrolling on timelines
Closes #125
2021-06-25 23:28:43 -04:00
Shadowfacts 5b8e97287e Convert TimelineTableViewController to use DiffableTimelineLikeTableViewController 2021-06-20 22:27:38 -04:00
Shadowfacts 49572c1fec Add DiffableTimelineLikeTableViewController 2021-06-20 22:27:29 -04:00
Shadowfacts ebb0770198 Add context menu action to remove attachments in Compose 2021-06-18 11:32:17 -04:00
Shadowfacts 27e05cc72d Enable focus loop debugging in debug 2021-06-12 22:17:59 -04:00
Shadowfacts 4ca48a5f50 Add iOS 15 compilation condition 2021-06-12 22:17:41 -04:00
Shadowfacts 230bd50661 Disable selection of presenting sidebar items on focus 2021-06-12 22:17:09 -04:00
Shadowfacts 4f2f8d517f Don't initiate table view cell drag while user is selecting poll options 2021-06-12 19:22:51 -04:00
Shadowfacts 130da9d4cc Improve status collapse animation
Use an additional label with no content and no height to absorb the
extra space creating during collapse when the content text view
disappears immediately.
2021-06-12 11:39:15 -04:00
Shadowfacts 472b9aa5e2 Fixes for large image animations on devices with square screns 2021-06-12 11:26:44 -04:00
Shadowfacts 3413dff8f9 Present compose screen in new window on iOS 15 and iPad/Mac 2021-06-11 10:50:31 -04:00
Shadowfacts 66e8fce488 Fix crash when conversation VC tries to restore from unloaded status 2021-06-11 10:19:59 -04:00
Shadowfacts aa2d333f4a Disable transparent nav bar on page view controllers 2021-06-10 10:55:09 -04:00
Shadowfacts c8a45d8eef Add Open in New Window menu item to profiles 2021-06-10 10:52:27 -04:00
Shadowfacts 40f5be28f6 Cleanup un/follow menu action 2021-06-10 10:36:02 -04:00
Shadowfacts 7c9287543c Fix crash due to PencilKit undo manager not being available until viewDidAppear 2021-06-10 10:33:24 -04:00
Shadowfacts 2a05b6d326 Add pointer hover effects to compose poll buttons 2021-06-09 19:18:54 -04:00
Shadowfacts 2499d25432 Use built-in sheet for asset picker on iOS 15 2021-06-09 19:12:10 -04:00
Shadowfacts 9417872790 Don't show Reply action in menu button on statuses 2021-06-09 17:10:44 -04:00
Shadowfacts c02a1bbf74 Make Pin status action title clearer 2021-06-09 17:10:13 -04:00
Shadowfacts 0a894b219a Allow Open in New Window action on iPadOS 2021-06-09 17:09:59 -04:00
Shadowfacts 22803668d2 Remove ellipsis from Share menu item title 2021-06-09 17:09:45 -04:00
Shadowfacts 2f6d1cb069 Use plain list style for Compose attachments 2021-06-09 17:08:59 -04:00
Shadowfacts 8889261b6b Fix compose reply avatar scroll effect not working on iOS 15 2021-06-09 11:01:11 -04:00
Shadowfacts 91f1a5195c Use visibility bar button item selection state instead of changing icon 2021-06-08 15:00:48 -04:00
Shadowfacts 1a5b958b1a Hide compose progress bar while there is no progress
On iOS 15, the progress bar displays a little bit of progress even at 0
2021-06-08 14:54:42 -04:00
Shadowfacts d667f6362c Use UniformTypeIdentifiers framework for everything 2021-06-07 20:08:46 -04:00
Shadowfacts ef1db466b9
Fix VoiceOver reading profile field names/values in incorrect order 2021-06-06 22:35:15 -04:00
Shadowfacts 0566f0ddfa
Fix More button in profile header not being VoiceOver accessible 2021-06-06 22:35:03 -04:00
Shadowfacts f54d4d757f
Make status attachments VoiceOver accessible 2021-06-06 22:31:11 -04:00
Shadowfacts fbc5d6eed9
Make timeline status cells single accessibility elements 2021-06-06 22:16:44 -04:00
Shadowfacts 2c4d2ce551
Make polls in statuses accessible 2021-06-06 22:11:29 -04:00
Shadowfacts bbe260bc9e
Construct PKToolPicker ourselves 2021-06-06 21:33:17 -04:00
Shadowfacts 2fe19a5abe
Add fast account switching indicator to tab bar item 2021-06-06 18:30:46 -04:00
Shadowfacts feacf576d7
Allow draging accounts in Preferences into new scenes 2021-06-06 14:55:18 -04:00
Shadowfacts ceb58f1d92
Add state restoration for current account in main scene 2021-06-06 14:55:04 -04:00
Shadowfacts 806591f5b7
Remove old framework from Xcode project 2021-05-24 19:30:20 -04:00
Shadowfacts 18ce21c2c6
Add Open in Tusker action extension 2021-05-24 19:30:11 -04:00
Shadowfacts 47fb0ea868
Update PLCrashReporter 2021-05-22 13:45:18 -04:00
Shadowfacts ffe6450b26
Xcode recommendations, use AnyObject instead of class in protocol requirements 2021-05-22 13:44:58 -04:00
Shadowfacts b51c1c03cb
Fix poll option percentages getting cut off
Closes #120
2021-05-22 11:44:50 -04:00
Shadowfacts e745d78d67
Fix polls not being collapsed inside CW
Closes #119
2021-05-22 11:30:56 -04:00
Shadowfacts 4c9d5e8465
Fix nav bar on iPad search screen hiding 2021-05-22 11:25:12 -04:00
Shadowfacts 9ec7177bfa
Fix crash when searching fails 2021-05-22 11:22:01 -04:00
Shadowfacts 421881d461
Remove dead code 2021-05-13 22:42:26 -04:00
Shadowfacts c78f152670
Animate attachment rows in when picking assets 2021-05-13 22:34:26 -04:00
Shadowfacts dabcae0905
Fix being unable to commit previewed profile from timeline status 2021-05-13 22:25:28 -04:00
Shadowfacts e7e141bd1e
Bump build number and update changelog 2021-05-09 21:58:28 -04:00
Shadowfacts 8386e9d3c6
Fix crash when decoding responses from Mastodon 3.4.0rc1 due to
differing date formats
2021-05-09 21:49:16 -04:00
Shadowfacts 21e4828a72
Fix crash when notifications fail to load 2021-05-09 21:48:59 -04:00
Shadowfacts 9ab95dfc43
Bump build number and update changelog 2021-05-06 22:28:34 -04:00
Shadowfacts c34ce758dd
Fix Home sidebar item getting deselected immediately on load 2021-05-06 22:02:27 -04:00
Shadowfacts 2c9f00d19f
Fix compose poll durations not being set/persisted 2021-05-06 21:52:16 -04:00
Shadowfacts f7127b84d8
Show vote percentages on completed polls 2021-05-06 21:41:41 -04:00
Shadowfacts fdb21cd1fb
Add Refresh Poll option 2021-05-05 17:51:11 -04:00
Shadowfacts 9f0c1eece8
Add haptic feedback to poll voting 2021-05-05 17:46:41 -04:00
Shadowfacts e18a09f4ac
Don't show Voted button for polls that you authored 2021-05-03 23:18:15 -04:00
Shadowfacts 005001b081
Add authoring polls
Closes #48
2021-05-03 23:12:59 -04:00
Shadowfacts 90f17693f1
Fix compose autocomplete suggestions not displaying
c737354ed3 was overzealous
2021-05-01 19:18:00 -04:00
Shadowfacts 698b045f86
Add poll finished notifications 2021-04-28 21:47:38 -04:00
Shadowfacts 654f84363a
Fix polls displaying incorrectly in dark mode 2021-04-28 20:52:57 -04:00
Shadowfacts 4dd510f3af
Only attach profile context menu interaction to correct views in statuses 2021-04-28 19:11:41 -04:00
Shadowfacts 1c36dfcc5f
Add displaying and voting on polls in statuses 2021-04-28 19:00:17 -04:00
Shadowfacts b0bd27db31
Fix crash when tapping non-HTTP(S) links with In-App Safari enabled 2021-04-25 12:58:51 -04:00
Shadowfacts daa1a9eef7
Fix potential crash when collapsing w/o selected sidebar item 2021-04-25 12:39:45 -04:00
Shadowfacts c737354ed3 Fix cursor movement not working in compose text fields when emoji added
Removes workaround introduced in 8c4ef3caa6. This is no longer necessary
and autocorrect works fine without it since at least iOS 14.4.

Closes #118
2021-04-05 18:31:03 -04:00
Shadowfacts 8ea15d3bab Add preference for requiring confirmation before reblogging 2021-04-05 18:31:00 -04:00
Shadowfacts 13a4221fce Add own-instance API request retrying 2021-04-04 15:11:29 -04:00
Shadowfacts a896573a5e Show assets immediately after granting permissions 2021-04-04 15:04:32 -04:00
Shadowfacts edd89450aa Fail gracefully when fetching statuses in timeline controller 2021-04-04 14:43:51 -04:00
Shadowfacts 5f5ef8fcea Fix potential crash when large image loading fails 2021-04-04 14:05:00 -04:00
Shadowfacts a3b59c990b Fix compile issue on Xcode 12.4 2021-04-04 14:04:44 -04:00
Shadowfacts 1e7bfac13c Bump build number and update changelog 2021-02-15 10:56:36 -05:00
Shadowfacts 6e92633793 Fix crash when adding pinned statuses section during refresh 2021-02-08 18:24:44 -05:00
Shadowfacts e4ff632dcb Fix conversation main status being selectable 2021-02-07 20:16:08 -05:00
Shadowfacts b0ebef2cfd Only show Trending Hashtags and Profile Directory on Mastodon 2021-02-07 19:52:59 -05:00
Shadowfacts bbb8707cb7 Add Profile Directory 2021-02-07 19:39:22 -05:00
Shadowfacts 6a927e4092 Enable drag & drop on account list screen 2021-02-07 11:34:04 -05:00
Shadowfacts 13cdb5d8c7 Add Trending Hashtags to sidebar 2021-02-07 10:45:36 -05:00
Shadowfacts 9f0883d0cb Fix sidebar item getting deselected on add list/instance/hashtag 2021-02-07 10:43:54 -05:00
Shadowfacts eba2e17479 Fix wrong content mode for profile header avatar image 2021-02-06 22:30:28 -05:00
Shadowfacts 5d1c95621b Fix VisualEffectImageButton retain cycle
Button had a menu which had an action which had a closure which strongly
referenced the sourceView which was the button itself.
2021-02-06 15:31:13 -05:00
Shadowfacts 02ba45fa34 Fix crash when opening & closing Preferences after changing account
The old ProfileHeaderView was being leaked, and it was still listening
to the preferencesDidChange notification, but crashing because its
delegate (and therefore MastodonController) had been dealloc'd.
2021-02-06 15:29:35 -05:00
Shadowfacts 9d5c004ec4 Add Trending Hashtags screen 2021-02-06 14:54:35 -05:00
Shadowfacts 37e90229c2 Fix crash when editing list 2021-02-06 14:35:34 -05:00
Shadowfacts 73aceda97f Convert Explore screen to use list-style collection view 2021-02-06 13:48:31 -05:00
Shadowfacts 669d55500a Remove unused pre-iOS 14 code 2021-02-06 13:47:45 -05:00
Shadowfacts f44d127110 Bump deployment target to iOS 14.1 2021-02-05 23:46:31 -05:00
Shadowfacts bcc023a127 Show threads on Conversation screen 2021-01-31 17:42:29 -05:00
Shadowfacts 122cce3bc7 Disable blurhashes in debug builds 2021-01-30 14:15:17 -05:00
Shadowfacts 949162bcab Fix fast account switching animating in wrong direction 2021-01-28 23:20:38 -05:00
Shadowfacts 4ed862120c Add trending hashtags to add saved hashtag controller 2021-01-28 23:20:25 -05:00
Shadowfacts f9411d706b Bump build number and update changelog 2021-01-20 20:55:50 -05:00
Shadowfacts 8f61b0b9a6 Remove old imports 2021-01-20 18:52:16 -05:00
Shadowfacts cdffda5593 Fix crash when profile screen disappears 2021-01-20 18:41:24 -05:00
Shadowfacts d1c45a87e6 Fix low resolution avatars being shown on profile 2021-01-20 18:31:30 -05:00
Shadowfacts 2761c05a01 Remove Cache library 2021-01-20 18:31:14 -05:00
Shadowfacts e7800249af Avoid loading cached data into memory when prefetching 2021-01-18 14:50:56 -05:00
Shadowfacts 2e88b266d9 Prefetch on a background queue to avoid blocking main queue with
CoreData lookups
2021-01-18 14:29:32 -05:00
Shadowfacts 0b008489f7 Add CachingDiskStorage 2021-01-18 14:17:20 -05:00
Shadowfacts de67327f6d Fix ImageCache kicking off extra requests when a completion block was
not provided
2021-01-18 13:46:07 -05:00
Shadowfacts 04a6fe807e Cache scaled images 2021-01-17 13:27:30 -05:00
Shadowfacts 6dee0957ea Remove in-memory caches of most original image data 2021-01-17 11:45:04 -05:00
Shadowfacts c12d2db258 Cache UIImage objects to avoid re-decoding images unnecessarily 2021-01-17 11:28:50 -05:00
Shadowfacts 27b39b79e6 Fix refreshes after no-result refresh not working
Fixes #117
2021-01-13 00:16:33 -05:00
Shadowfacts d7aa3f1617 Fix crash when updating timestamp of removed status 2021-01-12 22:17:30 -05:00
Shadowfacts 69c2faf0e1 Fix crash when user refreshes profile before initial pinned statuses request completes 2021-01-12 22:17:01 -05:00
Shadowfacts 678ed4959b Fix crash upon split view expansion before sidebar VC is loaded 2021-01-12 22:16:20 -05:00
Shadowfacts 0bdcda1b23 Fix secondary windows not respecting theme preference 2021-01-06 19:20:14 -05:00
Shadowfacts 74a30d27e8 Hide keyboard before dismissing windows 2021-01-06 19:16:57 -05:00
Shadowfacts f0e2bb8db6 Fix crash while cancelling prefetching of rows 2020-12-31 23:20:53 -05:00
Shadowfacts 3fdeb51353 Bump build number and update changelog 2020-12-31 11:13:47 -05:00
Shadowfacts a7b2a7df71 Don't try to re-replace emojis unnecessarily 2020-12-29 11:56:40 -05:00
Shadowfacts 41403c84f9 Don't allocate a new Set every time timeAgo is computed 2020-12-29 11:54:02 -05:00
Shadowfacts e67f6b2ad8 Avoid redundant database lookups 2020-12-25 12:01:35 -05:00
Shadowfacts 4ac3292183 Allow dragging attachments between Compose windows 2020-12-14 22:46:56 -05:00
Shadowfacts d3c13ee1e6 Fix own account/instance not being loaded if the only active scene was non-main 2020-12-14 22:35:34 -05:00
Shadowfacts 1b44117891 Don't unnecessarily load views when transferring navigation stacks 2020-12-14 22:24:48 -05:00
Shadowfacts c7b708e62b Fix crash when sidebar collapses 2020-12-14 22:23:22 -05:00
Shadowfacts 56b51f944d Add drag and drop spring loading to tab bar & sidebar 2020-12-14 22:16:16 -05:00
Shadowfacts 30297c2390 Add multi-window drag and drop to all the things 2020-12-14 18:44:48 -05:00
Shadowfacts 522c9b2b03 Add multi-window support and auxiliary windows 2020-12-13 22:37:37 -05:00
Shadowfacts 67a029180e
Don't construct unnecessary view controllers in MainSplitViewController 2020-11-15 19:04:46 -05:00
Shadowfacts dfad8740eb
Extract common functionality into TimelineLikeTableViewController 2020-11-15 15:48:49 -05:00
Shadowfacts b45dc19811
Re-enable split view controller on iPhone 2020-11-14 22:48:09 -05:00
Shadowfacts 80c0d08ec6
Tweak MenuController 2020-11-14 22:28:52 -05:00
Shadowfacts 2b5ab90cd8
Add key commands for prev/next sub tab 2020-11-14 22:26:02 -05:00
Shadowfacts 0303c9af9d
Use correct sidebar style on Catalyst 2020-11-14 12:23:13 -05:00
Shadowfacts 1e59f663e5
Add sidebar item key commands 2020-11-14 12:15:49 -05:00
Shadowfacts 72217cde51
Add compose key command 2020-11-14 11:55:19 -05:00
Shadowfacts 4bccbe254b
Add text formatting key commands 2020-11-14 11:47:20 -05:00
Shadowfacts 9e15a84006
Add refresh key command 2020-11-14 11:22:47 -05:00
Shadowfacts c19b7ec2c6
Fix crash when logging in for the first time 2020-11-14 10:45:36 -05:00
Shadowfacts 59c00b01dc
Fix not being able to compile for Catalyst 2020-11-14 10:45:27 -05:00
Shadowfacts 75d26e613b
Add account switching animation 2020-11-11 15:28:17 -05:00
Shadowfacts 904ff4eecf
Fix crash when decoding emojis with spaces in URLs 2020-11-11 12:45:13 -05:00
Shadowfacts 0249207dcc
Fix LazilyDecoding not handling top-level optionals 2020-11-11 12:44:57 -05:00
Shadowfacts 366378f267
Scroll attachment description views to ensure caret is always visible 2020-11-11 12:44:39 -05:00
Shadowfacts 80cca7673a
Tweak compose text view scrolling behavior 2020-11-11 12:14:36 -05:00
Shadowfacts fc888b168c
Add fast account switching on iPhone 2020-11-09 19:39:42 -05:00
Shadowfacts 348c306858
Add tapping CW to expand/collapse status
Expand status collapse button tap area to cover stack view spacing
2020-11-03 15:58:08 -05:00
Shadowfacts 0a11d2de47
Fix playing gifs from a background thread 2020-11-03 15:49:30 -05:00
Shadowfacts 4ac76ab672
Add opposite collapse keywords preference 2020-11-03 15:39:02 -05:00
Shadowfacts eb4e6e32f7
Add Grayscale Images preference 2020-11-01 13:59:58 -05:00
Shadowfacts 89b35fab6d
Move pruning of offscreen rows to when the VC disappears, instead of
during scrolling

Prevents race when removing and adding cells in the willDisplay table
view delegate method.
2020-10-26 22:55:58 -04:00
Shadowfacts d638ff513b
Fix crash when using show timeline Siri Shortcut 2020-10-26 22:13:48 -04:00
Shadowfacts 93828830a9
Fix logging in to PixelFed instances
See #68
2020-10-25 23:07:41 -04:00
Shadowfacts 39b244384b
Show link cards on statuses 2020-10-25 16:05:28 -04:00
Shadowfacts 80b3585b71
Fix stautses on My Profile not appearing until scroll 2020-10-25 11:19:37 -04:00
Shadowfacts 5d9f4b8ea8
Bump build number and update changelog 2020-10-24 16:00:11 -04:00
Shadowfacts 16b02edf87
Ensure the cursor remains visible when composing posts 2020-10-24 15:46:24 -04:00
Shadowfacts b8f169d0cd
Fix broken layout on Compose screen when replying to certain posts
Closes #115
2020-10-24 11:34:49 -04:00
Shadowfacts 62a9535394
Fix crash when ending dictation in Compose CW field
Closes #116
2020-10-24 11:26:29 -04:00
Shadowfacts 8c4ef3caa6
Fix system autocomplete not working in Compose post body and CW 2020-10-24 11:20:43 -04:00
Shadowfacts e763d48bf3
Bump build number and update changelog 2020-10-22 23:18:15 -04:00
Shadowfacts f841854c5f
Fix crash logging into instances whose domain does not match the Instance uri field 2020-10-21 21:47:01 -04:00
Shadowfacts 1c871a12a1
Bump build number and update changelog 2020-10-21 18:08:20 -04:00
Shadowfacts 8a528936b8
Fix crash when tapping My Profile tab too quickly after app launch 2020-10-19 18:41:38 -04:00
Shadowfacts 744329dca2
Upload photos taken from UIImagePickerController as JPEGs instead of PNGs 2020-10-19 18:33:10 -04:00
Shadowfacts 45ac40b125
Fix broken Compose layout when replying to long statuses 2020-10-18 16:31:41 -04:00
Shadowfacts 2426989161
Fix unsatisfiable constraints in timeline status action buttons
UIStackView internal constraints all have a required priority, so adding
the image constrain in TimelineStatusTableViewCell.awakeFromNib caused
an unsatisfiable constraint. Fixed by replicating the UISV constraints
manually, with the constrain on the leading edge of the first button
being made a placeholder.
2020-10-18 14:42:17 -04:00
Shadowfacts 1439c8b162
Fix unsatisfiable constraints on attachment container view
The stack view hiding constraint sets the height to 0 with a priority of
999.999, so the priority 1000 aspect ratio constraint was causing an
error and making the container view still have a height. Setting the
priority to 999 resolves the issue.
2020-10-18 13:50:52 -04:00
Shadowfacts 5125cc3397
Show custom emojis in display names in follow/favorite/reblog
notifications
2020-10-18 12:22:12 -04:00
Shadowfacts 9b949af390
Add complete emoji list to Compose emoji autocomplete 2020-10-18 11:17:58 -04:00
Shadowfacts 3ff9fdabdb
Use MultiThreadedDictionary for ImageCache request groups
Prevents a crash due a race condition if multiple requets complete
simultaneously and attempt to modify the dictionary
2020-10-18 11:03:56 -04:00
Shadowfacts a805da9faa
Enable picture-in-picture playback for video attachments 2020-10-17 12:56:13 -04:00
Shadowfacts e0acb0f04a
Don't search for unnecessary data 2020-10-16 19:14:29 -04:00
Shadowfacts 5414f2329c
Fix race condition causing My Profile tab bar image to not be set 2020-10-14 19:34:30 -04:00
Shadowfacts 08045dd1e9
Prioritize followed/following accounts in mention suggestions 2020-10-14 19:28:32 -04:00
Shadowfacts 288f855e2f
Support positing large image VC controls in iPhone 12/Pro/Mini ears 2020-10-13 21:12:21 -04:00
Shadowfacts 7883b04618
Fix autocomplete bar cutting off scroll view contents 2020-10-12 22:12:35 -04:00
Shadowfacts 0687c040a0
Prevent inserting extra whitespace when autocompleting 2020-10-12 22:03:50 -04:00
Shadowfacts 58c6d508ec
Prevent caret from changing position on auto complete 2020-10-12 19:39:50 -04:00
Shadowfacts ae272582ac
Autocomplete custom emojis in CW field 2020-10-12 19:17:57 -04:00
Shadowfacts 1a4517c43a
Cache account relationships in CoreData 2020-10-12 18:20:57 -04:00
Shadowfacts 2cfc0cf28a
Add Compose screen mention, hashtag, emoji completion
Closes #10
2020-10-11 22:14:45 -04:00
Shadowfacts cf63384dce
Why use many DispatchQueue.main.async when few do trick? 2020-09-25 11:31:53 -04:00
Shadowfacts 733d50b642
Strip U+FFFC from status bodies when posting
Fixes #112
2020-09-23 17:20:45 -04:00
Shadowfacts 0e60e74a8a
Fix being able to rotate into landscape on iOS 14 2020-09-21 18:45:52 -04:00
Shadowfacts fd0054addf
Correctly round corners of My Profile tab icon 2020-09-21 18:42:06 -04:00
Shadowfacts 576e4aa90d
Add conversation screen title 2020-09-21 18:24:02 -04:00
Shadowfacts ea3de4cdda
Fix wrong icon in context menu action 2020-09-21 18:18:55 -04:00
Shadowfacts 83c7609df5
Fix crash when using app icon shortcuts 2020-09-21 18:11:28 -04:00
Shadowfacts 809584cc54
Fix crash when opening Compose screen before account/instance is loaded
Prevents when opening the Compose screen with poor network connectivity
2020-09-21 18:04:08 -04:00
Shadowfacts 9b85090884
Add debug environment variable to disable image caching 2020-09-21 18:03:51 -04:00
Shadowfacts 6965a4c374
Remove no longer necessary iOS 13.4 availability checks 2020-09-20 11:34:46 -04:00
Shadowfacts b6c0c02028
Remove no longer necessary conditional compilation directives 2020-09-20 11:34:09 -04:00
Shadowfacts 42f9d19ee9
Fix memory leak in attachment container view 2020-09-20 11:28:18 -04:00
Shadowfacts b80a61cc95
WeakArray improvements 2020-09-20 11:27:14 -04:00
Shadowfacts 0d972d987c
Remove old Compose screen cold 2020-09-20 10:46:51 -04:00
Shadowfacts 3e33c8e6f9
Minimize file system requests during image cache lookup 2020-09-17 21:53:51 -04:00
Shadowfacts 3822d536c8
Reduce redundant status database lookups when updating cell UI 2020-09-17 21:53:47 -04:00
Shadowfacts 5906c374ba
Fix not being able to tap text view placeholders on Compose screen
Fixes #111
2020-09-17 18:40:02 -04:00
Shadowfacts ee90b20f7f
Add swipe to remove accounts in Preferences 2020-09-16 22:21:12 -04:00
Shadowfacts 14e8c11f02
Bump build number and update changelog 2020-09-16 19:19:40 -04:00
Shadowfacts 9d9ea565f1
Fix crash opening Preferences with deleted accounts 2020-09-16 17:52:00 -04:00
Shadowfacts 99db842411
Fix content warning not being copied when replying 2020-09-16 17:41:27 -04:00
Shadowfacts 184fe49c0f
Fix visibility not being copied when replying 2020-09-16 17:32:01 -04:00
Shadowfacts 4719342a06
Bump build number and update changelog 2020-09-15 22:22:20 -04:00
Shadowfacts 6df5f7fb08
Add preferences for auto-expanding CW'd posts and disabling long post
collapsing

See #105
2020-09-15 21:37:08 -04:00
Shadowfacts 02135aa0de
Use inset list style for preferences on iOS 14 2020-09-15 20:48:53 -04:00
Shadowfacts be5a4c03a6
Fix attachments not being posted in the correct order. 2020-09-14 23:29:31 -04:00
Shadowfacts 2c1ba7926e
Support JSON request bodies 2020-09-14 23:25:26 -04:00
Shadowfacts 911e66a159
Allow more browsing of instance public timelines
Closes #74
2020-09-13 15:51:08 -04:00
Shadowfacts ab4bcfa50f
Fix profile screen title not being set 2020-09-13 15:34:45 -04:00
Shadowfacts b94bfca406
Fix crash tapping attachments on instance public timelines 2020-09-13 13:55:33 -04:00
Shadowfacts 7999ecafd0
Update SheetController 2020-09-13 13:27:52 -04:00
Shadowfacts 1c6e464a4c
Start Compose screen tests 2020-09-13 13:19:56 -04:00
Shadowfacts acd01a81cc
More UI tests for onboarding/my profile 2020-09-12 22:16:58 -04:00
Shadowfacts 8ac3deb55a
Remove old file 2020-09-12 22:04:41 -04:00
Shadowfacts 5e9cc430c6
Use cross fade transitions for displaying gallery and asset picker if
Reduce Motion/Prefer Cross Fade is enabled

Closes #108
2020-09-12 13:25:59 -04:00
Shadowfacts 0b6ef6517b
Fix gallery action buttons not being centered in device "ears" on iPhone
XR and 11
2020-09-12 12:01:16 -04:00
Shadowfacts 34a01094f7
Fix gallery expand animation description not starting at correct
position

Safe are insets weren't being taken into account when hiding the
controls, because the toVC had not yet been added to the container view
and thus didn't have anything to receive insets from.
2020-09-12 12:01:16 -04:00
Shadowfacts 95b215c6b5
Add Clear Image Cache option to Advanced prefs 2020-09-12 12:01:16 -04:00
Shadowfacts e21dceb3b3
Tweak gallery spring animation parameters 2020-09-12 12:01:16 -04:00
Shadowfacts 9534f19262
Show BlurHash previews of attachments 2020-09-12 12:01:08 -04:00
Shadowfacts e44ae29775
Improve asset picker opening animation 2020-09-10 23:24:24 -04:00
Shadowfacts a5b30c4243
Update PLCrashReporter 2020-09-10 23:24:14 -04:00
Shadowfacts 479ca23e00
Tweak follow request notification cells 2020-09-10 22:54:01 -04:00
Shadowfacts 5b03e0cf12
Fix follow notifications not showing names for users without explicit
display names
2020-09-09 18:45:38 -04:00
Shadowfacts 7c4bbfd730
Improve compose posting error messages 2020-09-09 18:33:59 -04:00
Shadowfacts e19a6528ad
Improve gallery expand animation
Use spring timing, slide in top/bottom controls
2020-09-08 23:41:15 -04:00
Shadowfacts f5110c773a
Tweak default font sizes 2020-09-07 18:49:25 -04:00
Shadowfacts fe1db72f19
Fix save draft sheet showing even when draft had no content 2020-09-07 17:15:18 -04:00
Shadowfacts b4ddb8f533
Fix safe area on Compose screen not including keyboard on iOS 13 2020-09-07 17:05:50 -04:00
Shadowfacts 9a4ddfea3f
Fix Compose reply scroll effect not working on iOS 13 2020-09-07 16:56:06 -04:00
Shadowfacts dd8a196630
Show custom emoji in display names on Compose screen 2020-09-07 15:22:06 -04:00
Shadowfacts 3da7aacb35
Fix visiblity context menu in main text view accessory not updating 2020-09-07 14:46:17 -04:00
Shadowfacts 39c8162931
Prevent attempting to add an attachment when the possibility would be
invalid
2020-09-07 14:44:56 -04:00
Shadowfacts fe95cb9e1a
Replace Draw Something context menu item with dedicated button
Fixes add attachment button not working on iOS 13. Adding a context menu
to a Button inside a List on iOS 13 prevents the button from ever
recognizing taps.
2020-09-07 14:41:31 -04:00
Shadowfacts ec2d510be2
Fix crash when opening Compose screen on iOS 13 2020-09-06 23:27:43 -04:00
Shadowfacts 262aadf807
Fix very bad performance when laying out Compose reply view
Using a non-scrolling UITextView wrapped in SwiftUI combined with the
old hack of fixing its layout by passing the view controller's width
down to the wrapped view caused very slow layouts, resulting in
significant lag when typing into the main text view of the compose
screen.
2020-09-06 22:47:02 -04:00
Shadowfacts 9dce94c014
Fix acounts not updating locally
Fix reblogged statuses potentially not updating
2020-09-06 16:03:03 -04:00
Shadowfacts d008b882cb
Use context menu for visibility on iOS 14 2020-08-31 23:07:41 -04:00
Shadowfacts 3d13df87f0
Add pointer interaction to main status favorites/reblogs buttons 2020-08-31 21:40:18 -04:00
Shadowfacts f0582739cc
Re-add Compose button to Profile screen
Add menu with Direct Message option
2020-08-31 21:39:36 -04:00
Shadowfacts 4c82b1a341
Rewrite Compose screen in SwiftUI 2020-08-31 19:28:50 -04:00
Shadowfacts b55a96d649
Fix crash when resizing split view on iPad if Explore search controller
hadn't been created yet
2020-08-30 19:57:32 -04:00
Shadowfacts 77ac8cbe40
Bump deployment target to iOS 13.4 2020-08-30 19:28:11 -04:00
Shadowfacts e026c9a6c6
Bump build number and update changelog 2020-08-17 19:06:56 -04:00
Shadowfacts 3937dde2bf
Fix crash when selecting attachments on iOS 13 2020-08-17 18:52:54 -04:00
Shadowfacts 95ebca04d2
Disable automatic GIF playback in low-power mode 2020-08-16 19:14:32 -04:00
Shadowfacts 0986fa285e
Fix crash due to leaked table view cell 2020-08-16 15:07:59 -04:00
Shadowfacts 1cd3e6adf9
Show custom emoji in profile field names 2020-08-16 15:07:55 -04:00
Shadowfacts 722b81dad9
Group appearance prefs into sections 2020-08-16 14:58:10 -04:00
Shadowfacts 059f7307b3
Let system uppercase section headers 2020-08-16 14:58:02 -04:00
Shadowfacts ee20c95a5d
Prevent link activation when outside character 2020-08-16 14:52:08 -04:00
Shadowfacts be81ffb61f
Allow display names to shrink to fit available width 2020-08-16 14:49:44 -04:00
Shadowfacts 08e0c3769f
Make link preview background opaque 2020-08-16 14:45:01 -04:00
Shadowfacts 6d7c9fd553
Make tap targets on status action buttons larger 2020-08-16 14:41:30 -04:00
Shadowfacts 9b04b75949
Prevent potential race condition when loading additional statuses 2020-08-16 10:29:31 -04:00
Shadowfacts 273b74ddfb
Bump build number and update changelog 2020-08-15 22:10:44 -04:00
Shadowfacts ae055f1ffd
Remove debug code 2020-08-15 18:00:47 -04:00
Shadowfacts eef9b96a1a
Fix crash when showing profile for uncached account 2020-08-15 18:00:18 -04:00
Shadowfacts 29aed65b99
Fix crash if profile header view outlives VC 2020-08-15 17:59:14 -04:00
Shadowfacts 090746f292
Disallow opening universal links from Open in Safari context menu action 2020-08-15 17:48:58 -04:00
Shadowfacts af300a3559
Remove unused TuskerNavigationDelegate customization points 2020-08-15 17:47:33 -04:00
Shadowfacts 79eb23ef5d
Remove unused preference 2020-08-15 17:43:31 -04:00
Shadowfacts 60565f9625
Fix crash if status table view cell outlives VC 2020-08-15 17:37:56 -04:00
Shadowfacts 70bedf17a8
Set app category 2020-08-15 17:36:23 -04:00
Shadowfacts 392e51eb3e
Remove unnecessary prefernces change notification 2020-08-15 17:31:24 -04:00
Shadowfacts 86d5a73c85
Change menu item order
Open in Safari should be the closest to the user's finger when tapping a
menu button
2020-08-15 17:20:09 -04:00
Shadowfacts eaefa366b7
Fix displaying images on iOS 14 2020-08-15 17:03:02 -04:00
Shadowfacts 79b23127e9
Fix crash on refreshing 2020-08-15 14:15:38 -04:00
Shadowfacts f9b85c87b4
Fix crash on launch due to unloaded sidebar VC 2020-08-15 13:55:47 -04:00
Shadowfacts 260bedcf10
Fix retain cycle between status cells and menu actions 2020-07-07 23:23:39 -04:00
Shadowfacts fe09c5e522
Switch asset picker to use diffable data sources 2020-07-06 18:16:18 -04:00
Shadowfacts 985d30a401 Add background to image descriptions so they're visible against light backgrounds
Closes #102
2020-07-06 17:48:19 -04:00
Shadowfacts 794594805c Prevent needlessly prefetching non-image attachments 2020-07-06 00:00:55 -04:00
Shadowfacts 1c708732f2 Exclude iOS 14-specific code from compilation on Xcode 11 to allow building for TestFlight 2020-07-06 00:00:51 -04:00
Shadowfacts db30471011 Fix not being able to refresh timelines 2020-07-05 16:30:16 -04:00
Shadowfacts 2825345c7e Add switching between Posts, Posts and Replies, and Media pages of user profiles
Closes #103
2020-07-05 16:17:56 -04:00
Shadowfacts f3d01c47c3 Merge branch 'develop-xcode-12' into ios-14 2020-07-04 11:21:00 -04:00
Shadowfacts caab5e357a Fix crash loading audio attachment uploaded on Mastodon
Closes #104
2020-07-03 22:13:49 -04:00
Shadowfacts 2916d7a72d Add tapping the active tab bar item to scroll to top
Closes #106
2020-07-03 19:36:52 -04:00
Shadowfacts d190636fbd Fix Preferences button not appearing (again) 2020-07-03 19:36:08 -04:00
Shadowfacts 4e4701ead5 Use SwiftSoup from SPM instead of Git submodule 2020-07-03 19:09:58 -04:00
Shadowfacts b07efc150c Use App Group for user defaults 2020-07-03 18:54:21 -04:00
Shadowfacts 19fa12391d Fix Preferences button not appearing 2020-07-03 18:53:19 -04:00
Shadowfacts c55ea2e005 More link context menu preview tweaks 2020-07-03 18:52:35 -04:00
Shadowfacts 47dc00ab8f Fix sometimes broken masking of text view link preview animations 2020-07-03 18:52:23 -04:00
Shadowfacts fdcdbced38 Limit context menu previews in ContentTextView to link's text line rects 2020-07-03 18:50:37 -04:00
Shadowfacts e70a84274e Fix showing instance public timeline 2020-07-03 18:50:22 -04:00
Shadowfacts 641ab765a7 Fix crash when displaying search results 2020-07-03 18:50:05 -04:00
Shadowfacts 986fc5b833 Prevent crash when displaying accounts with no pinned statuses 2020-07-03 18:49:55 -04:00
Shadowfacts cf5b97d9c8 Fix crash showing custom instance on iOS 14 2020-07-03 18:49:28 -04:00
Shadowfacts 7f0fd119c5 Use App Group for user defaults 2020-07-03 18:45:37 -04:00
Shadowfacts b2c7735256 Fix Preferences button not appearing 2020-07-03 18:44:38 -04:00
Shadowfacts 1d815d6cd6 More link context menu preview tweaks 2020-07-03 17:01:52 -04:00
Shadowfacts f86d3a0ed1 Fix sometimes broken masking of text view link preview animations 2020-07-01 00:01:36 -04:00
Shadowfacts 864fd77ecc Sync active tab and navigation stack between split view/tab bar controllers 2020-06-29 22:21:03 -04:00
Shadowfacts 78da04162f Fix missing file from project.pbxproj 2020-06-29 21:47:11 -04:00
Shadowfacts 40a742139b Fix menu state getting out of sync with bookmarked/muted state 2020-06-27 13:13:04 -04:00
Shadowfacts 8bbc572fa7 Replace more with share button for timeline status swipe actions 2020-06-27 10:47:31 -04:00
Shadowfacts 2a8e970738 Use context menus as primary actions for 'More Actions' buttons on >= iOS 14 2020-06-27 00:22:14 -04:00
Shadowfacts 3abb5972b9 Limit context menu previews in ContentTextView to link's text line rects 2020-06-25 10:42:46 -04:00
Shadowfacts 0c06d91f6b Fix showing instance public timeline 2020-06-24 16:41:01 -04:00
Shadowfacts 6cf6db6a8d Add sidebar on iPadOS 14 2020-06-24 16:40:45 -04:00
Shadowfacts fb11e36467 Fix crash when displaying search results 2020-06-24 15:42:56 -04:00
Shadowfacts 0fa87e9177 Prevent crash when displaying accounts with no pinned statuses 2020-06-23 22:21:50 -04:00
Shadowfacts 5cb84e271a Prefer ephemeral sessions in ASWebAuthneticationSession 2020-06-23 21:35:14 -04:00
Shadowfacts 50f1a9a7de Change ComposeDrawingViewController to use drawingPolicy on iOS 14 2020-06-23 19:33:14 -04:00
Shadowfacts 154fc7cd02 Fix ASWebAuthenticationSession usage in Catalyst 2020-06-23 19:32:30 -04:00
Shadowfacts 01d765fa45 Enable Catalyst 2020-06-23 19:32:04 -04:00
Shadowfacts 04aad1252a Use SwiftSoup from SPM instead of Git submodule 2020-06-23 19:31:32 -04:00
Shadowfacts 43779e42df Fix crash showing custom instance on iOS 14 2020-06-23 19:27:34 -04:00
Shadowfacts a5a2cd147e
Fix attachment blur view missing corner radius 2020-06-22 21:03:08 -04:00
Shadowfacts 0e91fc239d
Fix missing anchor for Compose screen visibility popover 2020-06-22 09:53:20 -04:00
Shadowfacts 0e5aab75df
Bump build number 2020-06-21 19:32:47 -04:00
Shadowfacts c715d11fc2
Add CHANGELOG.md 2020-06-21 19:32:08 -04:00
Shadowfacts 8010e86711
Change attachment views to be 16:9 2020-06-21 16:01:34 -04:00
Shadowfacts a41d27f18c
Move status action buttons back below attachments 2020-06-21 16:01:34 -04:00
Shadowfacts 083add273b
Prevent audio from other apps pausing when showing gifv attachments
Fixes #101
2020-06-21 16:01:29 -04:00
Shadowfacts 64365bdf2b
Fix compose attachments being cut off at the bottom of the safe area 2020-06-21 10:31:40 -04:00
Shadowfacts 6adcad63b3
Add crash report helper 2020-06-20 23:11:35 -04:00
Shadowfacts 393a134648
Don't show Follow activity for user's own account 2020-06-19 23:00:59 -04:00
Shadowfacts ba3e9e7491
Fix compose attachment description text view not expanding to fit text 2020-06-19 19:46:08 -04:00
Shadowfacts 920f926b48
Add text recognition image description for image attachments 2020-06-19 19:14:24 -04:00
Shadowfacts 6e27399e10
Fix loading additional statuses on profiles not working
This was a regression introduced in
d27bddb2ca which removed the didSet
handlers which called reloadData on the pinnedStatuses/timelineSegments
property without adding the appropriate insertRows calls where they were
modified.
2020-06-18 22:39:04 -04:00
Shadowfacts c3c19b1994
Fix pin image still showing on statuses after cell reuse 2020-06-18 22:23:19 -04:00
Shadowfacts 1f40cc9928
Show controls/description for gifv attachments
See #98
2020-06-17 23:33:48 -04:00
Shadowfacts 66020b7847
Add preference for always showing status visiblity icon 2020-06-17 18:00:13 -04:00
Shadowfacts 00bf99334f
Add preference for status reply icons 2020-06-17 17:45:34 -04:00
Shadowfacts 3aef7d4d93
Organize Preferences.swift 2020-06-17 17:40:36 -04:00
Shadowfacts a901af6be9
Merge branch 'private-beta' into develop 2020-06-16 23:19:33 -04:00
Shadowfacts b623e348c2 Fix crash when opening compose screen before initial network requests completed 2020-06-16 23:13:46 -04:00
Shadowfacts 056346cee9
Add reply indicator to statuses in timelines 2020-06-16 23:06:36 -04:00
Shadowfacts 30c04b49e7
Add visibility indicator to statuses 2020-06-16 23:00:39 -04:00
Shadowfacts 848022ec6e
Disable reblog button for private posts 2020-06-16 22:47:30 -04:00
Shadowfacts 39e847bda8
Fix reblog label showing incorrect account 2020-06-16 22:47:04 -04:00
Shadowfacts 5d751cd994
Prevent redundant status database lookups 2020-06-15 23:22:45 -04:00
Shadowfacts d27bddb2ca Fix profile header image not showing up on first load
The issue occurred because the profile header would kick off a request
upon loading, then the profile table would request the initial set of
statuses shortly thereafter which would result in reloadData being called
which would cancel the request without removing the group, so the request
generated by the newly-reloaded header cell would attach a callback to
the cancelled request, resulting in the header image never displaying.
2020-06-15 22:34:42 -04:00
Shadowfacts 36326e4469
Make network requests in viewWillAppear instead of viewDidLoad 2020-06-15 19:41:51 -04:00
Shadowfacts 6b7904ed52
Improve profile field layout 2020-06-15 19:02:09 -04:00
Shadowfacts 61c6d63c67
Fix profile fields not displaying
Closes #96
2020-06-15 18:36:04 -04:00
Shadowfacts c0316f55ef
Fix crash when sharing large image on iPad 2020-06-15 18:29:04 -04:00
Shadowfacts 803ba50f53
Add pointer interaction to remove attachment, large image share/dismiss buttons 2020-06-15 18:26:56 -04:00
Shadowfacts 5d0c59e863
Prompt for Photos access before showing asset picker 2020-06-15 18:15:05 -04:00
Shadowfacts c7b4d00da7
Fix race condition loading bookmarks 2020-06-15 18:02:07 -04:00
Shadowfacts f2a8b91769
Provide metadata to UIActivityViewController
Closes #56
2020-05-14 22:43:56 -04:00
Shadowfacts ce464dfb9f
Add mute/unmute conversation status activities
Closes #70
2020-05-14 22:43:47 -04:00
Shadowfacts d4bf289716
Fx more actions not workign 2020-05-14 22:43:37 -04:00
Shadowfacts cf48e4e973
Bump build number 2020-05-13 21:21:57 -04:00
Shadowfacts 2eaeaf3277
Fix previewing gifv attacments 2020-05-13 21:20:22 -04:00
Shadowfacts d396eb0823
Change background CoreData context to be a child of the main context so
that updates on the background context propogate up to the view context
on save
2020-05-13 19:49:35 -04:00
Shadowfacts 35a510e8ed
Add cache reset button to Advanced Preferences 2020-05-13 18:58:11 -04:00
Shadowfacts 0582812563
Remove strong references to MastodonController 2020-05-13 18:57:04 -04:00
Shadowfacts e581f384e4
Fix account descriptions being squashed in the follows list 2020-05-12 22:24:51 -04:00
Shadowfacts c42a48ee12
Fix header images not displaying 2020-05-12 22:05:57 -04:00
Shadowfacts 1c9b1b9ac3
Add support (sort of) for gifv attachments
See #98
2020-05-12 21:46:08 -04:00
Shadowfacts 82ad3b9fc4
Add reference counting for accounts
Closes #97
2020-05-11 22:03:17 -04:00
Shadowfacts 0a89dd3041
Don't double update accounts
Adding a status to the cache will also cache the status' account
2020-05-11 18:27:54 -04:00
Shadowfacts 40863ef130
Fix crash when opening more options for status in instance public timeline 2020-05-11 17:58:43 -04:00
Shadowfacts cd78287a87
Fix crash when viewing instance public timelines
Use a CoreData in-memory store for public timelines.
2020-05-11 17:57:50 -04:00
Shadowfacts 04496aca1d
Apply avatar style to local account avatar images 2020-05-10 19:30:19 -04:00
Shadowfacts 5a098df931
Fix crash when searching 2020-05-10 15:47:50 -04:00
Shadowfacts 9812d4aff2
Prevent double-decrementing reference count for conversation main status 2020-05-10 15:08:45 -04:00
Shadowfacts f4f2a5546c
Prevent race in status action account list 2020-05-10 15:04:22 -04:00
Shadowfacts b220948e2b
Only initialize NSManagedObjectModel once
Prevents CoreData warnings when switching accounts and constructing a
second MastodonCachePersistentStore
2020-05-10 14:54:43 -04:00
Shadowfacts 866edc472d
Show avatar and instance domain in account list in Preferences 2020-05-10 14:54:20 -04:00
Shadowfacts 88e4f52b5d
Fix crash when adding account
Adding a UserData.LocalAccountInfo with a nil username while the
PreferencesView is on screen will cause a crash, since it triggers a
Combine publish upon which the PreferencesView expects to be able to
display the username of all accounts.
2020-05-10 14:41:07 -04:00
Shadowfacts 98529ca5af
Remove notifications from the bottom when scrolling up notifications list 2020-05-10 12:56:03 -04:00
Shadowfacts 6d8c5f632c
Fix scroll-to-top sometimes not scrolling all the way to the top 2020-05-10 12:56:01 -04:00
Shadowfacts 4fdafa893e
Add drawing attachments using PencilKit 2020-05-09 22:14:48 -04:00
Shadowfacts 9f75106706
Fix crash when opening statuses in Safari 2020-05-09 13:31:07 -04:00
Shadowfacts bbd7d82620
Fix test in ContentTextView not being de-selectable 2020-05-07 21:46:59 -04:00
Shadowfacts 02088b1f55
Remove MastodonCache 🎉 2020-05-06 23:29:57 -04:00
Shadowfacts 1e41c8fa17
Remove MastodonCache usgae from XCBActions 2020-05-06 23:05:15 -04:00
Shadowfacts ebbfc7a132
Fix race condition on loading notifications 2020-05-06 19:32:32 -04:00
Shadowfacts aa625a41f5
Merge branch 'develop' into coredata 2020-05-06 19:18:58 -04:00
Shadowfacts 7fb92c9ce3
Prevent avatars in action notification group cell from overflowing 2020-05-06 19:18:47 -04:00
Shadowfacts 90bc9b91de
Add AccountProtocol and StatusProtocol
Provides a single interfaces for API and CoreData statuses and accounts
2020-05-06 18:40:12 -04:00
Shadowfacts d6c506488b
Replace a bunch of MastodonCache uses with CoreData 2020-05-02 19:52:35 -04:00
Shadowfacts 5786c24846
Fix statuses/accounts updating 2020-05-02 12:45:28 -04:00
Shadowfacts 2cba168804
Fix account cells using old cache 2020-04-27 19:33:36 -04:00
Shadowfacts 49d00bb1b0
Fix swipe actions not showing up 2020-04-27 19:32:16 -04:00
Shadowfacts ee5e049355
Use CoreData for bookmarks and search results 2020-04-27 19:25:41 -04:00
Shadowfacts f53474ac90
Use CoreData for notifications screen 2020-04-27 19:20:09 -04:00
Shadowfacts fa1daa682f
Convert profile VC to use CoreData objects
Does not yet remove old statuses when scrolling up, like timeline VC
2020-04-13 22:51:21 -04:00
Shadowfacts 030bee1948
Convert conversation VC to use CoreData models 2020-04-13 22:51:15 -04:00
Shadowfacts ed37b16463
Start adding CoreData-based "reference" counting for statuses
Prune old statuses that aren't likely to be shown again when scrolling
in timeline table view
2020-04-12 23:08:33 -04:00
Shadowfacts 2c8ba878b7
Start converting UI to use CoreData backed objects instead of API
objects directly
2020-04-12 12:54:27 -04:00
Shadowfacts a0e95d4577
Remove unnecessary attachment decoding code
For some reason, creating a URL from a string decoded from the container
was producing URL objects that could not be round-tripped through
PropertyListEncoder/Decoder. Decoding a URL directly from the container
works correctly.
2020-04-12 12:52:51 -04:00
Shadowfacts 465aedd43f
Make account info username optional
Onboarding view controller needs to set the account info object on the
mastodon controller before calling getOwnAccount since getOwnAccount
will upsert the user's account into the persistent container, which
requires the account info to exist to create a unique-per-account
identifier.
2020-04-12 11:14:10 -04:00
Shadowfacts 102fe6ed91
Convert API objects to CoreData models and save them 2020-04-11 22:23:31 -04:00
Shadowfacts 7deb4fc5b4
Add LazilyDecoding for CoreData embedded objects 2020-04-11 15:35:00 -04:00
Shadowfacts 2a419eb87c
Add basic Status/Account CoreData model 2020-04-11 15:32:25 -04:00
Shadowfacts fcab6818b0
Hide large image source view during expand/shrink animation 2020-03-25 23:10:48 -04:00
Shadowfacts 80cf1850dd
Add trackpad/magic mouse support for navigation controller interactive push gesture 2020-03-25 22:29:32 -04:00
Shadowfacts e612964464
Allow scrolling w/ trackpad/magic mouse to dismiss gallery 2020-03-25 22:12:26 -04:00
Shadowfacts 49a437583e
Fix incorrect large image size during expand/shrink animation in some
cases
2020-03-25 22:09:00 -04:00
Shadowfacts 8a513186aa
Add pointer interactions status buttons and profile header more button 2020-03-24 23:02:40 -04:00
Shadowfacts d9517047d7
Fix previewing video/audio attachments 2020-03-20 22:48:28 -04:00
Shadowfacts bef3388fe8
Move attachment view corner radius to individual views
Masking the container makes context menu interactions look weird
2020-03-20 22:34:50 -04:00
Shadowfacts 2e8241d734
Move attachment context menu interaction to AttachmentView 2020-03-20 22:28:23 -04:00
Shadowfacts c9c001d403
Improve attachment previewing
- Set correct preview size
- Don't show controls
2020-03-20 22:13:04 -04:00
Shadowfacts 4ce8de280e
Bump build number 2020-03-17 21:58:14 -04:00
Shadowfacts 4018d39312
Fix double gestures in attachments gallery 2020-03-17 21:56:29 -04:00
Shadowfacts ae416bb604
Prevent crash if BaseStatusTableViewCell is leaked
If prefernces change and the the view controller the cell belongs to is dealloced, the
mastodonController will be nil, previously causing a crash.
2020-03-17 21:44:06 -04:00
Shadowfacts 5e9caf9179
Use LoadingLargeImageViewController for account avatar/header
Prevents crash when tapping unloaded avatar/header images
2020-03-17 21:42:09 -04:00
Shadowfacts 3bbbb05083
Rename AttachmentsViewController to LoadingLargeImageViewController and
make non-specific to attachments
2020-03-17 21:24:15 -04:00
Shadowfacts bd3e74c611
Remove unnecessary XIB 2020-03-17 21:07:44 -04:00
Shadowfacts 2e8c416e04
Merge gallery and large image animations 2020-03-17 21:05:45 -04:00
Shadowfacts 955f9e5916
Fix attachment descriptions not being set correctly 2020-03-17 21:03:29 -04:00
Shadowfacts 17f15db32d
Don't round bottom corners of asset picker
Corner radius doesn't match that used on 2019 iPad Pro, so rounding the
bottom corners results in the view controller beneath the asset picker
showing through in some split-screen configurations
2020-03-16 20:50:16 -04:00
Shadowfacts 1a11dd2a69
Present asset picker as popover in regular horizontal size class 2020-03-16 20:45:51 -04:00
Shadowfacts b5fa0bceab
Fix pasting using compose app shortcut while app isn't running 2020-03-16 19:09:25 -04:00
Shadowfacts c224d11417
Allow pasting and drag/dropping video attachments on compose screen 2020-03-16 19:05:58 -04:00
Shadowfacts bebf47f05c
Prevent incompatible items from being pasted on compose screen 2020-03-16 17:31:43 -04:00
Shadowfacts e76b719c6a
Add context menu previews to explore VC 2020-03-15 23:54:04 -04:00
Shadowfacts 478c7b7a23
Fix crash when long-presing add attachment button 2020-03-15 22:59:43 -04:00
Shadowfacts e3cc0df283
Remove unnecessary URL escaping 2020-03-15 21:09:11 -04:00
Shadowfacts 9ed05de3ee
Add compose attachments preview 2020-03-15 14:25:02 -04:00
Shadowfacts 64f41ea2b7
Fix crash when updating timeline status cell timestamp 2020-03-15 12:17:19 -04:00
Shadowfacts 9af4118dfc
Show truncated note in account cell 2020-03-15 11:56:41 -04:00
Shadowfacts 64a8f6d733
Reorganize code 2020-03-15 11:43:41 -04:00
Shadowfacts ca76568c79
Remove old code 2020-03-15 11:40:28 -04:00
Shadowfacts 18e91feb00
Fix requires attachment descriptions preference not working 2020-03-15 11:39:35 -04:00
Shadowfacts c5d2e9af68
Fix preferences/drafts not saving on iPad in some circumstances 2020-03-15 11:26:30 -04:00
Shadowfacts 0691c3b9d6
Fix asset preview size 2020-03-14 23:32:54 -04:00
Shadowfacts 1ccb450477
Support dragging and dropping attachments in the compose view controller
Allos dragging in attachments from other apps and drag/dropping with the
compose VC to reorder attachments
2020-03-14 20:08:36 -04:00
Shadowfacts 7117ce6320
Support pasting images to create attachments
Closes #91
2020-03-14 16:46:50 -04:00
Shadowfacts 34dccf1f37
Extract compose attachments into separate VC 2020-03-14 15:47:15 -04:00
Shadowfacts a3303dc8fb
Use same order for status and account preview actions 2020-03-11 22:54:38 -04:00
Shadowfacts d15fa2199e
Fix attachments container more view not beign removed on cell reuse
Closes #92
2020-03-11 22:49:53 -04:00
Shadowfacts fadddeda7f
Fix crash when deleting draft
Closes #94
2020-03-11 22:18:31 -04:00
Shadowfacts b232bec80f
Show custom emojis in content warnings
Closes #95
2020-03-11 21:56:35 -04:00
Shadowfacts 1b19a13b05
Decode status cards 2020-03-04 21:14:58 -05:00
Shadowfacts cd5b4c1145
Remove old code 2020-03-02 22:31:37 -05:00
Shadowfacts b61418e062
Bump build 2020-03-02 19:45:14 -05:00
Shadowfacts c7746d3084
Add unknown notification fallback
Closes #90
2020-03-02 19:44:10 -05:00
Shadowfacts 315ea39682
Fix crash in silent action prefs 2020-03-02 19:44:10 -05:00
Shadowfacts 44fbbd6a80
Revert "Fix custom emojis in display namesnot showing in conversation main"
This reverts commit 73da828e7cec09bcfbe65295bbd2f02e3b719ff6.

Fixes #89
2020-03-02 19:44:10 -05:00
Shadowfacts fa4b5d3542
Fix custom emojis not being shown in display names when scrolling
quickly

If the emojiIdentifier didn't change, the emojis wouldn't be re-added
even after the text had been reset.
2020-03-02 19:44:10 -05:00
Shadowfacts de02c73957
Fix custom emojis in display namesnot showing in conversation main
statuses

Caused by the cell updating it's UI multiple times in quick succession.
As a workaround, prevent the main cell from being reloaded.
2020-03-02 19:44:10 -05:00
Shadowfacts 2cebb6bd7d
Show custom emojis in display names (where possible) 2020-03-02 19:44:09 -05:00
Shadowfacts 53707593a6
Show custom emojis in display names (where possible) 2020-03-01 19:40:32 -05:00
Shadowfacts 244659c262
Fix intermittent crash
If a status in a conversation view controller creates a work item to
update the timestamp in 1 minute, but the view controller is deinit'd
before that time elapses, the mastodonController instance will be nil,
resulting in a crash.

The DispatchWorkItems's are cancelled by the respective cell deinit
methods. But if the work item has already begun, cancelling it has no
effect, potentially leading to a crash in the conditions described above
are true. Using a weak reference to self fixes this.

Additionally, don't unnecessarily recreate the work items every time.
They don't capture any local variables, only self, so nothing changes.
2020-03-01 18:33:44 -05:00
Shadowfacts d4ca39761e
Change version, disable UI test web server temporarily 2020-03-01 18:23:10 -05:00
Shadowfacts f87944b47e
Add app icon 2020-03-01 13:11:09 -05:00
Shadowfacts af821081b0
Temporary fix for crash that occurs when switching accounts immediately
after adding a new one
2020-02-29 17:36:54 -05:00
Shadowfacts 804636dcbb
Don't show warning when loading draft on top of for empty statuses
Closes #87
2020-02-28 19:50:04 -05:00
Shadowfacts 5bed38f661
Show gallery instead of large image when previewing status attachments
Fixes crash when attempting to preview audio/video attachments
2020-02-28 19:47:38 -05:00
Shadowfacts 56de0ab359
Update profile header to always reflect most recently cached data 2020-02-28 19:47:31 -05:00
Shadowfacts 387623a309
Remove old code 2020-02-28 19:24:14 -05:00
Shadowfacts 70bca052c4
Tweak notification grouping
Notifications that are of the same type but are separated by a groupable
notification of a different type are now considered groupable. For
example:

favorite 1 (status 1)
reblog 1 (status 1)
favorite 2 (status 1)
reblog 2 (status 1)
mention 1
reblog 3 (status 1)

will be grouped into:

favorite 1, 2 (status 1)
reblog 1, 2 (status 1)
mention 1
reblog 3 (status 1)
2020-02-28 19:21:39 -05:00
Shadowfacts d9bae42f81
Prevent empty drafts from being saved 2020-02-22 15:43:17 -05:00
Shadowfacts a814ee37cc
Update SheetController
Fixes image picker losing velocity during dismiss animation
2020-02-22 15:29:42 -05:00
Shadowfacts 1a8e84f5fa
Reorganize behavior preferences 2020-02-22 13:19:31 -05:00
Shadowfacts 1f56823a17
Add preference to disable gif animation in timelines 2020-02-22 13:12:28 -05:00
Shadowfacts 65d57df949
Add interacting pushing to navigation controllers
Allows people to move forward in the navigation stack after popping
(making popping a non-destructive action).
2020-02-19 22:07:12 -05:00
Shadowfacts 8be7480755
Change bundle identifier and signing account 2020-02-08 17:54:04 -05:00
Shadowfacts d7953470e3
Add rudimentary support for audio attachments
Closes #7
2020-01-26 18:50:45 -05:00
Shadowfacts 8c7bebcce8
Fix large image controls being positioned incorrectly on notched-devices
when opening via a context menu preview.

The constraints for the top controls were being set only once, but when
showing a large image in the contxt menu preview window, the top inset
safe area is 0 and didn't become 44 (the value for notched devices)
until the preview was expanded.

Fixes #86
2020-01-26 18:28:46 -05:00
Shadowfacts 20c602f911
Disable row insertion animations 2020-01-26 18:23:18 -05:00
Shadowfacts f2e08e96f3
Improve large image/gallery animation handling when images aren't loaded 2020-01-25 22:29:12 -05:00
Shadowfacts d1913d7e69
Change white background when loading attachment to black 2020-01-25 22:09:00 -05:00
Shadowfacts f9a62ec3f3
Fix crash when tapping unloaded image attachment 2020-01-25 22:03:21 -05:00
Shadowfacts 26c99a1a35
Improve scroll perform when loading new rows into table views
Instead of reloading the whole table view, only insert the rows for
statuses/notifications that were added.
2020-01-25 11:11:48 -05:00
Shadowfacts 6757031dcb
Hide context menus and swipe actions on instance public timelines 2020-01-25 10:44:31 -05:00
Shadowfacts 7c207efa07
Tweak More swipe action to be in-line with system appearance 2020-01-25 10:44:12 -05:00
Shadowfacts 81256b7a96
Only show local posts on public instance timelines 2020-01-25 10:37:22 -05:00
Shadowfacts 5a6c12c5a7 Fix missing context menu actions on follow notifications for only one person 2020-01-25 10:30:04 -05:00
Shadowfacts d6ae51c02f Improve ImageCache loading
Keep track of the number of requests and only cancel the underlying
URLSessionTask if there are no concrete requsts remaining.

Closes #81
2020-01-25 10:30:04 -05:00
Shadowfacts 3220436893
Merge branch 'multiple-accounts' 2020-01-23 22:36:42 -05:00
Shadowfacts 62408fc4bd
Merge branch 'master' into multiple-accounts 2020-01-22 22:30:30 -05:00
Shadowfacts 1d169bec67
Fix statuses showing incorrect timestamps when switching accounts 2020-01-22 22:30:05 -05:00
Shadowfacts 4abda02b76
Only show drafts from current account 2020-01-22 22:27:58 -05:00
Shadowfacts e9db3fa0ac
Collapse whitespace according to CSS spec after converting HTML to
attributed string

Fixes #27
2020-01-21 21:28:14 -05:00
Shadowfacts 59277ec64f
Change drafts to store which accounts was used to create them
When loading a draft:

If the draft was created from a different account than the current one:
    If the draft was in reply to a status, don't allow it to be loaded.
    If the draft was not in reply to a status, prompt the user whether
    or not to load the draft.
If the draft was in reply to a different status than the current one:
    Prompt the user whether or not to load the draft.
Otherwise, load the draft.

Draft replies created from other accounts can't be loaded from different
accounts because the status for inReplyToID of the draft will have a
different instance-local ID if the two accounts are on different
instances.

See #16
2020-01-20 15:26:25 -05:00
Shadowfacts 08086f1b18
Fix compose reply view missing MastodonController instance 2020-01-20 15:25:23 -05:00
Shadowfacts 12b6623113
Merge branch 'master' into multiple-accounts 2020-01-20 12:16:11 -05:00
Shadowfacts cece8825ad
Fix decoding Account.moved on Mastodon 2020-01-20 12:10:10 -05:00
Shadowfacts f9ffb240ef
Fix decoding Hashtag.History on Mastodon 2020-01-20 12:07:30 -05:00
Shadowfacts 79f44c9b58
Change recommended instance selector to store categories as strings
instead of enum

Additional categories can be added, which would cause a crash when
decoding.
As the category isn't used for anything, storing it as an enum value is
not necessary.
2020-01-20 11:56:43 -05:00
Shadowfacts ff97b0f76d
Change saved hashtags/instances to be per-account
See #16
2020-01-20 11:48:47 -05:00
Shadowfacts 26f1aafa15
Unify SavedHashtagsManager and SavedInstancesManager 2020-01-20 11:20:39 -05:00
Shadowfacts c99a724bf3
Fix instance public timelines crashing
The instance timeline controller needs to store a strong reference to
the instance-specific MastodonController since the timeline VC only
holds a weak reference, and unlike normal screens, the scene session
doesn't hold onto the MastodonController for other instances.
2020-01-20 11:18:55 -05:00
Shadowfacts f7421d83ef
Add preference to mention reblogger when replying to a reblogged status 2020-01-19 23:48:36 -05:00
Shadowfacts 7934bc15ac
Split Composing prefs into Composing and Replying 2020-01-19 23:23:31 -05:00
Shadowfacts 3aa5aa1bc0
Fix weird crashes when switching accounts 2020-01-19 23:16:36 -05:00
Shadowfacts ee252c02e9
Fix retain cycle in timeline cell cache observers
The use an unowned reference to self because when the cell is deinit'd,
the Combine observers will be cancelled.
2020-01-19 23:14:51 -05:00
Shadowfacts 2f630f2f8f
Fix retain cycle between MastodonController/MastodonCache
The cache should only store a weak reference to the controller, so that
when the controller is deinit'd the cache is as well.
2020-01-19 23:14:13 -05:00
Shadowfacts 8eb6f6f573
Fix retain cycle in timestamp updating code
The timestamp update work item shouldn't retain a reference to the cell.
It can be unowned because when the cell is deinit'd, the work item will
be cancelled.
2020-01-19 23:10:52 -05:00
Shadowfacts 32e89f2c16
Fix retain cycles with TuskerNavigationDelegate
TuskerNavigationDelegate is now class-bound and only weak references to
it are stored.
2020-01-19 23:02:07 -05:00
Shadowfacts c45dd99088
Clean up account switching code 2020-01-19 11:52:06 -05:00
Shadowfacts 863867c522
Add logging in to additional accounts and switching accounts via Preferences
See #16
2020-01-18 22:45:06 -05:00
Shadowfacts 08c84688cf
Change recommended instance selector to store categories as strings
instead of enum

Additional categories can be added, which would cause a crash when
decoding.
As the category isn't used for anything, storing it as an enum value is
not necessary.
2020-01-18 22:42:20 -05:00
Shadowfacts 66fe861442
Merge branch 'master' into multiple-accounts 2020-01-18 19:33:01 -05:00
Shadowfacts 11f9642cba
Actually fix link interaction 2020-01-18 19:32:39 -05:00
Shadowfacts 6421d4dc12
Merge branch 'master' into multiple-accounts 2020-01-18 18:56:36 -05:00
Shadowfacts 38085eee37
Fix not being able to collapse/expand statuses
Instead of simply returning the content text view from hitTest(_:with:),
we need to call the super method so that the system still performs its
own checks.
2020-01-18 18:38:00 -05:00
Shadowfacts e19364abdf
Fix content text view text color in dark mode 2020-01-18 18:21:01 -05:00
Shadowfacts fa358a3e97
Remove xtra padding from content text view 2020-01-18 16:27:18 -05:00
Shadowfacts 5d86b35672
Enable text selection in conversation main status 2020-01-18 16:18:32 -05:00
Shadowfacts 784c71342d
Fix preformatted text not being displayed correctly 2020-01-18 16:05:44 -05:00
Shadowfacts b5a41badcc
Replace content labels with text views
UITextView uses TextKit internally, unlike UILabel, so no additional
code is needed to keep the TextKit and view representations of the text
in sync since they are one and the same. This means that detecting which
character was tapped in a content text view is much more accurate, which
means link handling is substantially imrpoved.

Fixes #20
2020-01-18 16:05:00 -05:00
Shadowfacts 23de131290
Add preference to require attachment descriptions before posting
Closes #76
2020-01-17 21:55:21 -05:00
Shadowfacts 8178a1f339
Fix crash when tapping more actions buttons on iPad
Fixes #78
2020-01-17 21:29:53 -05:00
Shadowfacts 53702a8324
Add pinned status refreshing
Closes #82
2020-01-17 21:13:17 -05:00
Shadowfacts bb86e1aafd
Allow rotaion in the attachment gallery
Closes #77
2020-01-07 22:19:38 -05:00
Shadowfacts db4312ee34
Fix refreshing multiple times with no new data not working
When the requested range has no results, no pagination data is returned,
so the existing `newer` request range is replaced with nil. As there
was no new data, the existing request range is still correct and should
not be replaced.

Fixes #75
2020-01-07 21:54:19 -05:00
Shadowfacts 3928b2e88a
Store an array of logged-in accounts internally, get the active
MastodonController from the current UIScene

See #16
2020-01-07 21:29:15 -05:00
Shadowfacts 8dba15ca17
Switch to scene-based lifecycle events
See #16
2020-01-07 18:39:19 -05:00
Shadowfacts ec2062ad42
Fix not being able to sign into Mastodon instances not in the
recommended list
2020-01-06 22:14:17 -05:00
Shadowfacts 1e066ac28e
Add installation instructions to readme 2020-01-06 20:35:11 -05:00
Shadowfacts 29e0128a55
Fix broken file paths 2020-01-06 20:27:58 -05:00
Shadowfacts 0255483f97
Make MastodonCache specific to each API controller
See #16
2020-01-05 19:54:28 -05:00
Shadowfacts e3be424f5a
Fix instance public timelines not loading 2020-01-05 19:45:12 -05:00
Shadowfacts 2bdcb9b7f8
Replace global shared MastodonController instance with (mostly)
dependency injection

The places still using the .shared property are cases where there is no
view controller from which to (easily) get the appropriate instance,
such as user activity and X-Callback-URL handling. These uses will need
to be revisited once there are multiple MastodonControllers.

See #16
2020-01-05 15:25:07 -05:00
Shadowfacts a18bcac8b8
Pachyderm: Change Client request methods to be static, like all other
models

Tusker: Add run method to MastodonController and no-longer expose API
client object
2020-01-05 14:00:39 -05:00
Shadowfacts 787dc9f24f
Fix crash decoding statuses sent from certain applications
If an application provides its URL as an empty string, decoding it would
cause throw an error because Foundation's URL class does not accept
empty strings. Instead, during parsing, consume the invalid URL and
replace it with a `nil` app URL.
2020-01-04 23:13:55 -05:00
Shadowfacts ad09e36907
Show follow requsts in notifications w/ accept/reject buttons
Closes #64
2020-01-04 23:13:23 -05:00
Shadowfacts c31916d67e
Remove references to old images 2020-01-04 23:04:45 -05:00
Shadowfacts 05cc3fd6d9
Add pinning/unpinning statuses
Closes #69
2020-01-04 19:31:35 -05:00
Shadowfacts b70256c525
Remove old icons 2020-01-04 19:22:16 -05:00
Shadowfacts 3ecbb1895c
Replace GMImagePicker with custom asset picker based on SheetController
Fixes #23
Closes #50
2020-01-04 16:25:15 -05:00
Shadowfacts b2956b6118
Convert HEIC images to JPEG before upload
Fixes #60
2019-12-31 16:41:56 -05:00
Shadowfacts 6ef643e374
Fix custom instances not showing up when typed into the instance
selector search field

With Combine Cancellables are automatically cancelled on deinit, so the
instance selector needs to hold on to a reference to pipeline
cancellable for its lifetime, otherwise it's cancelled immediately after
creation.

Closes #59
2019-12-31 11:45:34 -05:00
Shadowfacts 18c3c3c434
More UI testing setup and API mocks 2019-12-31 11:40:56 -05:00
Shadowfacts b9e359ba83
Fix custom instance domains not being parsed correctly 2019-12-31 00:13:09 -05:00
Shadowfacts edb86fc503
Add theme toggle separate from system appearance
Closes #67
2019-12-31 00:12:18 -05:00
Shadowfacts 49f58cf955
Initial UI testing setup 2019-12-30 16:00:14 -05:00
Shadowfacts 377b5f5c85
Add ability to save and view instance public timelines 2019-12-19 22:55:41 -05:00
Shadowfacts f92a2acc97
Show list edit screen immediately upon creation 2019-12-19 21:37:47 -05:00
Shadowfacts ae6a0513e4
Add local saved hashtags
Closes #66
2019-12-19 21:20:29 -05:00
Shadowfacts 6831ab5385
Pachyderm: Fix request bodies form parameters not being percent-escaped
Fixes #65
2019-12-18 21:59:08 -05:00
Shadowfacts afc2bfcf6b
Add list editing 2019-12-17 22:56:53 -05:00
Shadowfacts 76a7c5bdf8
Add list deletion 2019-12-17 21:40:08 -05:00
Shadowfacts 19f15d8fa9
Add list creation 2019-12-17 21:18:32 -05:00
Shadowfacts 270cbc2a6c
Add lists to Explore tab
Closes #63
2019-12-17 21:10:30 -05:00
Shadowfacts 036791bd39
Replace Search tab with Explore tab
- Search controller (functionally the same, presents results on top of
explore menu)
- Add bookmarks screen

See #63
2019-12-17 00:22:25 -05:00
Shadowfacts 382decd7da
Fix search section titles 2019-12-16 22:23:12 -05:00
Shadowfacts 05d79d5d03
Use same nav delegate more options for context menu share sheet 2019-12-14 13:36:05 -05:00
Shadowfacts 4c0607af79
Add (un)bookmarking to status more options 2019-12-14 12:40:50 -05:00
Shadowfacts eb6cfba9aa
Fix tablel view cells being re-selected on aborted nav swipe back 2019-12-14 11:59:31 -05:00
Shadowfacts c26657bafa
Use synchronized MastodonCache to prevent race condition crashes 2019-12-14 11:31:14 -05:00
Shadowfacts 0c78af7d4f
Store in reply to status in drafts 2019-12-14 11:30:35 -05:00
Shadowfacts 681cdb8bb5
Fix automatically created drafts not being deleted after successful post
The newly created draft needs to be set to the compose VC's currentDraft
so that it gets removed after the status is successfully created.

Also, save the drafts to disk after saving a draft so that crashes don't
cause draft loss.
2019-11-28 22:26:37 -05:00
Shadowfacts 06442b5629
Fix controls in large image/gallery not hiding/showing on zoom
Closes #58
2019-11-28 21:51:24 -05:00
Shadowfacts d5232c0b03
Fix content warning label always showing in conversation main status
When the conversation was opened, the status state of the main status
would already be known, so the CW label wasn't getting updated or
hidden/shown.
2019-11-28 21:22:13 -05:00
Shadowfacts 7140590ccf
Fix covnerstaion expand/collapse button not working on all statuses 2019-11-28 18:58:47 -05:00
Shadowfacts b47b08fa95
Store status collapse state in containing view controller
Also, copy the state between screens, so e.g. expanding a status in the
timeline and then opening that conversation already has that status
expanded.

This intentionally doesn't store the sensitive attachment visibility
state, since showing text when not necessary is less dangerous than for
images. (Possibly a preference for this in the future?)

Closes #55
2019-11-28 18:36:58 -05:00
Shadowfacts 24a1e7ceb9
Unify most of TimelineStatus and ConverastionMainStatus cell code
Closes #54
2019-11-19 12:08:11 -05:00
Shadowfacts 8fb3b211b6
Add button to conversation table view to collapse/expand all posts
This needs a refactor once StatusTableViewCell and
ConversationMainStatusTableViewCell are merged. See #54
2019-11-18 22:28:47 -05:00
Shadowfacts da6ff67a51
Add notification dismissal context menu actions
Closes #49

This is a workaround for UIKit's built-in suggested context menu actions
not working as expected, and should be replaced with the system thing if
it becomes possible.
2019-11-17 18:49:48 -05:00
Shadowfacts a92d9ddc6f
Automatically collapse long (> 500 chars) statuses
Closes #12
2019-11-17 18:36:19 -05:00
Shadowfacts eb8afdaab8
Change how pin icon is displayed on statuses 2019-11-17 15:28:58 -05:00
Shadowfacts d4fa2f36e3
Remove send message from profile share menu 2019-11-17 15:19:20 -05:00
Shadowfacts 4cfe5e0fa5
Change action notification line limit to 3 2019-11-17 15:16:58 -05:00
Shadowfacts 975fb23292
Possibly fix crash when reblogger account is cached 2019-11-17 14:46:14 -05:00
Shadowfacts 85812d774d
Fix crash when trying to open context menu for attachment that hasn't yet loaded 2019-11-17 14:45:38 -05:00
Shadowfacts 150adeb581
Use custom navigation controller for preferences to override
viewWillDisappear method and send preferences changed notification

Workaround for #36
2019-11-17 14:31:07 -05:00
Shadowfacts 81a5fce602
Add preference to always blur media 2019-11-17 12:52:42 -05:00
Shadowfacts 6ce96764f3
Use visual effect views for sensitive media hide button so the button is
visible regardless of the image color
2019-11-17 12:28:21 -05:00
Shadowfacts 42a0a8890c
Tweak profile header more button appearance 2019-11-17 11:33:49 -05:00
Shadowfacts 56d4a6690f
Fix crash when displaying posts with more than 4 attachments 2019-11-17 11:32:35 -05:00
Shadowfacts c91a7baaa6
Show pinned posts on profiles
Closes #53
2019-11-17 11:14:33 -05:00
Shadowfacts af65aa88e0
Don't use suggested actions in context menus 2019-11-17 10:19:50 -05:00
Shadowfacts c85836eda6
Add preferences for using in-app vs out of app Safari and using Reader Mode for in-app 2019-11-14 19:53:27 -05:00
Shadowfacts 5076aec54e
Use ObservedObject for binding to prefs in SwiftUI instead of custom property wrappers 2019-11-14 19:53:00 -05:00
Shadowfacts b4d41ac9b7
Fix crash in compose screen 2019-09-28 00:37:43 -04:00
Shadowfacts 1808aaa3e8
A bunch of VoiceOver/Voice Control stuff 2019-09-26 22:32:51 -04:00
Shadowfacts 5ebf651f76
Revert "Use correct ISO 8601 date decoder"
This reverts commit 5da357ee9c.
2019-09-26 18:39:48 -04:00
576 changed files with 50196 additions and 13692 deletions

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
Dist.xcconfig
Tusker.xcconfig
.DS_Store
MyPlayground.playground/

15
.gitmodules vendored
View File

@ -1,9 +1,6 @@
[submodule "SwiftSoup"]
path = SwiftSoup
url = git://github.com/scinfu/SwiftSoup.git
[submodule "Cache"]
path = Cache
url = git@github.com:hyperoslo/Cache.git
[submodule "Gifu"]
path = Gifu
url = git://github.com/kaishin/Gifu.git
[submodule "Embassy"]
path = Embassy
url = https://github.com/envoy/Embassy.git
[submodule "Ambassador"]
path = Ambassador
url = https://github.com/envoy/Ambassador.git

1
Ambassador Submodule

@ -0,0 +1 @@
Subproject commit 4fe264af51e0dd7228486c604750909e368241a7

View File

@ -1,91 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="210mm"
height="297mm"
viewBox="0 0 210 297"
version="1.1"
id="svg4592"
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
sodipodi:docname="Download.svg">
<defs
id="defs4586" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.979899"
inkscape:cx="166.13768"
inkscape:cy="136.01503"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="2560"
inkscape:window-height="1395"
inkscape:window-x="1920"
inkscape:window-y="1"
inkscape:window-maximized="1" />
<metadata
id="metadata4589">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.7641871;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5166"
width="100"
height="10"
x="-45"
y="302"
ry="4.9918189" />
<rect
transform="matrix(-0.70710679,-0.70710677,-0.70710679,0.70710677,0,0)"
ry="4.4665961"
y="199.22142"
x="-214.47757"
height="8.9331923"
width="66.99894"
id="rect5164"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.46821165;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
transform="matrix(0,1,1,0,0,0)"
ry="4.4665961"
y="0"
x="212"
height="8.9331923"
width="85"
id="rect5168"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.78008413;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.46821189;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5162"
width="66.99894"
height="8.9331923"
x="141.1559"
y="205.54411"
ry="4.4665961"
transform="matrix(-0.70710679,0.70710677,0.70710679,0.70710677,0,0)" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1,73 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="210mm"
height="297mm"
viewBox="0 0 210 297"
version="1.1"
id="svg1007"
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
sodipodi:docname="Favorite.svg">
<defs
id="defs1001" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.98994949"
inkscape:cx="293.05888"
inkscape:cy="341.92599"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="2560"
inkscape:window-height="1395"
inkscape:window-x="1920"
inkscape:window-y="1"
inkscape:window-maximized="1" />
<metadata
id="metadata1004">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
sodipodi:type="star"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.64583325;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
id="path1571"
sodipodi:sides="5"
sodipodi:cx="68.784126"
sodipodi:cy="238.46798"
sodipodi:r1="72.331841"
sodipodi:r2="30.379374"
sodipodi:arg1="0.94281504"
sodipodi:arg2="1.5711336"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="M 111.27998,297.00001 68.77388,268.84736 26.248805,296.97133 39.888461,247.84598 -6.6077817e-8,216.09302 50.935869,213.88454 68.80852,166.13615 l 17.840442,47.76043 50.934368,2.24284 -39.90987,31.72605 z"
inkscape:transform-center-x="-0.0075377331"
inkscape:transform-center-y="-6.8999101" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1,78 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="210mm"
height="297mm"
viewBox="0 0 210 297"
version="1.1"
id="svg5214"
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
sodipodi:docname="Link.svg">
<defs
id="defs5208" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="2.8"
inkscape:cx="166.9267"
inkscape:cy="23.876042"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="2560"
inkscape:window-height="1395"
inkscape:window-x="1920"
inkscape:window-y="1"
inkscape:window-maximized="1" />
<metadata
id="metadata5211">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<ellipse
cy="308.6665"
cx="68.333336"
id="circle5763"
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:9;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
rx="24.99999"
ry="25.000156" />
<circle
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:9;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path5218"
cx="28.333334"
cy="268.66666"
r="25" />
<rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:3.61034346;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5767"
width="65"
height="12.5"
x="206.42897"
y="164.74048"
ry="6.25"
transform="rotate(45)" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -1,84 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="80"
height="40"
viewBox="0 0 21.166666 10.583334"
version="1.1"
id="svg1623"
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
sodipodi:docname="More.svg">
<defs
id="defs1617" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6"
inkscape:cx="83.737701"
inkscape:cy="47.564201"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="2560"
inkscape:window-height="1395"
inkscape:window-x="1920"
inkscape:window-y="1"
inkscape:window-maximized="1"
units="px" />
<metadata
id="metadata1620">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-286.41665)">
<rect
style="fill:#000000;fill-opacity:0;stroke:none;stroke-width:2.73260474;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
id="rect2326"
width="21.166668"
height="10.583334"
x="6.9388939e-18"
y="286.41666" />
<circle
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15993595;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
id="path2187"
cx="2.6458333"
cy="291.70831"
r="2.6458333" />
<circle
cy="291.70831"
cx="-10.583333"
id="circle2189"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15993595;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
transform="scale(-1,1)"
r="2.6458333" />
<circle
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15993595;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
id="circle2191"
cx="18.520834"
cy="291.70831"
r="2.6458333" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -1,115 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="793.70081"
height="1122.5197"
viewBox="0 0 210 297"
version="1.1"
id="svg886"
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
sodipodi:docname="Reblog.svg"
enable-background="new">
<defs
id="defs880">
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter2010">
<feBlend
inkscape:collect="always"
mode="multiply"
in2="BackgroundImage"
id="feBlend2012" />
</filter>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="2.12"
inkscape:cx="426.6819"
inkscape:cy="222.48454"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="2560"
inkscape:window-height="1395"
inkscape:window-x="1920"
inkscape:window-y="1"
inkscape:window-maximized="1"
inkscape:lockguides="false"
inkscape:pagecheckerboard="false">
<sodipodi:guide
position="100.46678,110.57587"
orientation="0,1"
id="guide1767"
inkscape:locked="false" />
<sodipodi:guide
position="39.817226,99.415325"
orientation="1,0"
id="guide1773"
inkscape:locked="false" />
<sodipodi:guide
position="107.95499,89.359277"
orientation="0,1"
id="guide1987"
inkscape:locked="false" />
<sodipodi:guide
position="79.634452,79.589293"
orientation="-0.70563567,-0.70857484"
id="guide1999"
inkscape:locked="false" />
<sodipodi:guide
position="70.763562,8.6713087"
orientation="0,1"
id="guide904"
inkscape:locked="false" />
</sodipodi:namedview>
<metadata
id="metadata883">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="opacity:0.92000002;filter:url(#filter2010)">
<path
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.54638195;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
d="m 159.65664,296.99998 39.81721,-39.65184 h -29.05972 v -49.56596 h -0.005 c 0.003,0 0.005,-0.003 0.005,-0.005 V 186.4343 c 0,-0.003 -0.002,-0.005 -0.005,-0.005 h -54.66849 c -8e-4,0 -0.001,-0.003 -0.003,-0.003 H 75.53562 l 21.441585,21.35219 h 18.756475 c 7.9e-4,0 7.9e-4,0.003 0.002,0.003 h 33.16389 v 49.56617 h -29.05973 z"
id="path1592"
inkscape:connector-curvature="0" />
<path
inkscape:connector-curvature="0"
id="path1716"
d="M 39.817206,177.75866 0,217.4105 h 29.059724 v 49.56596 h 0.0047 c -0.0026,0 -0.0047,0.003 -0.0047,0.005 v 21.34288 c 0,0.003 0.0021,0.005 0.0047,0.005 h 54.668491 c 7.93e-4,0 0.0013,0.003 0.0026,0.003 H 123.93823 L 102.49665,266.97932 H 83.740169 c -7.94e-4,0 -7.94e-4,-0.003 -0.0016,-0.003 H 50.574691 V 217.4105 h 29.059724 z"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.54638195;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,78 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="793.70081"
height="1122.5197"
viewBox="0 0 210 297"
version="1.1"
id="svg8"
sodipodi:docname="Reply.svg"
inkscape:version="0.92.2 5c3e80d, 2017-08-06">
<defs
id="defs2">
<inkscape:path-effect
effect="powerstroke"
id="path-effect821"
is_visible="true"
offset_points="0,0.13229166"
sort_points="true"
interpolator_type="CubicBezierJohan"
interpolator_beta="0.2"
start_linecap_type="zerowidth"
linejoin_type="extrp_arc"
miter_limit="4"
end_linecap_type="zerowidth" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="134.7614"
inkscape:cy="56.711221"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:measure-start="46.669,115.435"
inkscape:measure-end="64.166,149.577"
inkscape:window-width="2560"
inkscape:window-height="1395"
inkscape:window-x="1920"
inkscape:window-y="1"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.26310158px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 0.20308372,275.78816 20.85866428,-18.46405 -4.77e-4,14.33185 c 0,0 41.110218,1.57695 31.919391,25.03042 2.746954,-16.26972 -31.82696,-14.6095 -31.82696,-14.6095 l -0.184082,14.75226 z"
id="path825"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

842
CHANGELOG.md Normal file
View File

@ -0,0 +1,842 @@
# Changelog
## 2023.2 (68)
Bugfixes:
- Fix crash when inserting present items in empty timeline
- Fix extra spacing above content in conversation main status
## 2023.2 (67)
Features/Improvements:
- Try to resolve remote statuses and show conversation screen when tapping status links
- Add loading indicator to conversation screen
- Improve collapse/expand animation on conversation screen
## 2023.2 (66)
Features/Improvements:
- Improve design of link preview card
- Show loading indicator during timeline state restoration
Bugfixes:
- iPadOS/macOS: Fix some keyboard shortcuts not working
- Fix crash when restoring timeline state
- Fix status collapse button disappearing when navigating away
- Fix crash when status swipe action takes too long to complete
- Fix tapping expand thread cell not working
## 2023.1 (64)
Features/Improvements:
- Add Delete Post action to statuses
- Add follower/following counts and lists to profiles
- Show better message when opening conversation for deleted status
- Add pagination for showing all accounts that favorited/reblogged a status
Bugfixes:
- Fix race condition causing crash when syncing timeline position from iCloud
- Fix profile header buttons not adjusting to Dynamic Type
- Don't show report button for your own posts
- Fix avatars on timeline not reverting from grayscale when turning off preference
## 2023.1 (63)
Bugfixes:
- Fix status cells being inset too much on iPhones
- Fix more things not adjusting to accent color preference
- Fix various views not being keyboard focusable
- Add more logging around state restoartion crash
## 2023.1 (62)
Features/Improvements:
- Add New List action in Add to List menu
Bugfixes:
- Fix crash when retrying follow hashtag
- Fix separators on timeline not being properly inset
- Fix various elements not adjusting to the accent color preference
- Prevent all pinned timelines from being removed, which would previously crash
- Fix crash when handling search activity
## 2023.1 (61)
Features/Improvements:
- Add report UI
- Add accent color preference
- Start playing videos immediately when gallery opens
Bugfixes:
- Fix crash when trying to load deleted status for state restoration/sync
- Fix crash when trying to restore state for non-pinned timeline
- Fix crash due to relationships being cached longer than their corresponding accounts
- Fix crash if preferences change when there are cells that haven't yet been displayed
- Fix crash when displaying poll finished notifications
## 2023.1 (60)
Features/Improvements:
- Allow sharing gifv attachments
- Add email subject when sharing status/account
Bugfixes:
- Fix crash when inserting present items after navigating away from and returning to timeline
- Fix error decoding statuses from certain instances
- When logging out, remove the scene's active account rather than the most recently activated
## 2022.1 (59)
This build is a hotfix for a crash when migrating saved instances to iCloud.
## 2022.1 (58)
Features/Improvements:
- Sync timeline positions via iCloud
- Add pinned timelines customization (synced via iCloud)
- Sync saved hashtags & instances via iCloud
- Add filters for hiding reblogs and replies from home timeline
- Show uncropped attachments in the timeline for posts that have only one attachment
- Add more prominent Follow button to profile pages
- Add About and Acknowledgements pages to Preferences
- Automatically report certain kinds of errors
- iPadOS/macOS: Add window titles to indicate which account is in use
- iPadOS: Limit content to readable width
- VoiceOver: Improve label for toggle collapse button in conversation view
Bugfixes:
- Fix attachments in timeline somtimes being untappable
- Fix previewing links in the main status in a conversation activating the link
- Don't show reblog swipe action when reblogging is forbidden
- Fix unknown notifications appearing in the Mentions view
- Fix crash when fetching present items in certain circumstances
- Fix relationship (following/blocked/etc.) change breaking profile header layout
- Fix crash when restored timeline state includes unloaded statuses
- macOS: Fix add attachment buttons not matching system accent color
## 2022.1 (53)
Features/Improvements:
- Apply filters to Trending Posts
- Don't reload List timelines if Edit screen is closed without changes
Bugfixes:
- Fix URLs getting pasted as broken attachments in the Compose screen
- Fix monspace fonts not adjusting to Dynamic Type setting
- Fix crash when activating account from Preferences when My Profile is opened in a separate window
- Fix Preferences showing wrong account as current when multiple windows with different accounts are open
## 2022.1 (52)
Features/Improvements:
- Save and restore position for all timelines and all accounts
- As a side effect of this change, the first time you launch with this update, the timeline position will be lost
- Add preference to never blur attachments
- New segmented control for timeline switcher that scrolls when there's not enough space
- Copy status expand all setting when viewing More Replies in a converstaion
- Include followed hashtags in the Explore screen and iPad sidebar
- Allow saving or following hashtag from Add Hashtag screen
- Scroll long attachment descriptions in the gallery
- VoiceOver: Improve Profile Directory
- VoiceOver: Include attachment descriptions in timeline items when not marked as sensitive
Bugfixes:
- Fix swipe actions preference not persisting
- Fix rich text list bullets/numbers appearing black in dark mode
- Fix crash when previewing non-HTTP(S) link
- Fix images from Safari pasting as URLs rather than attachments
- Fix Trending Posts appearing to reload forever
- Fix controls reappearing when swiping between pages in the attachments gallery
- Fix reblog confirmation alert actions missing hover effect
- Fix status reply/visibility/local icons flashing blue when expanding a status
- iPad: Fix My Profile item sidebar item not updating when avatar style changes
- iPad: Fix tapping status bar not scrolling to top in single-column navigation mode
- VoiceOver: Fix crash when scrolling through local/federated timeline
- VoiceOver: Fix escape gesture not working in attachment gallery
- VoiceOver: Fix custom emoji not being stripped from display names
## 2022.1 (51)
Features/Improvements:
- Clarify text for conversation main status favorite/reblog count preference
- Improve timeline refresh behavior
- Present posts are inserted automatically, creating a gap
- Tapping the Jump to Present bubble scrolls to the top and removes everything below the gap
- Add preference to disable timeline state restoration (Preferences -> Behavior)
- VoiceOver: Add Jump to Present action on the timeline selector segmented control
- VoiceOver: Add accessibility hint for segmented controls when using the group navigation mode
- VoiceOver: Improve description of timeline gap and add actions to load in a specific direction
Bugfixes:
- Fix crash due to change cache location on disk
- Fix potential crash when trying to restore statuses that aren't available
- Fix saving expired filters not re-enabling them
- Fix tusker:// URL scheme not working
- Fix Trending Posts reloading constantly
- Fix crash when timeline tries to refresh while in the background
## 2022.1 (50)
This is a hotfix for Dynamic Type not working in the timeline. The previous build's changelog is included below.
## 2022.1 (49)
The major new feature of this build is filters! Filters are editable by pressing the Filters button in the top-left corner of the Home tab. Filters are currently applied to timelines and profiles (filtering conversations and notifications will be added in a future build).
Features/Improvements:
- Filters
- Edit/create filters
- Apply filters to timelines and profiles
- Add preference to customize swipe actions on status cells (Preferences -> Appearance -> Leading/Trailing Swipe Actions)
- Show notifications for edited posts
- Add more details to the relationship label on profiles
- "Follows you", "You follow", "You follow each other", and "You block"
- Add state restoration for Federated and Local timelines
- Also fixes an issue where posts from other timelines would appear in the home timeline after a relaunch
- Add state restoration for Compose screen
- Organize expanded custom emoji picker by category
- Add indicator for locked profiles
- Add cache size info to Advanced preferences
- Indicate when a followed hashtag caused a post to appear in the home timeline
- Add hashtag follow/unfollow actions
- Completely replace timeline items after Jump to Present used
- Fixes infinite scroll not being usable after jumping
- iPad/Mac: Add Cmd+Return shortcut for sending post on the Compose screen
- VoiceOver: Hide redundant information on Compose screen
- VocieOver: Add links/mentions/hashtags to actions rotor on statuses
- VoiceOver: Add show profile action to timeline statuses
Bugfixes:
- Fix gifv attachments appearing off-center
- Fix long status on certain screens not getting collapsed
- Fix error when refreshing a timeline with no items
- Fix cells in account list and status action account list not being deselected when navigating back
- Limit search in Edit List screen to accounts the user follows
- Fix attachments disappearing from statuses on the Conversation screen
- Fix extra space gap appearing in profile headers below avatar
- Fix mute duration options not including 1 week
- Hometown: Fix local-only state not being copied from the replied-to post
- iPad: Fix timeline statuses not getting deselected when entering split navigation
- iPad: Fix Mute screen using pointless two-column layout
- iPad: Fix creating a list from the sidebar making the previous tab inaccessible
- iPad: Fix creating a list not showing the Edit List screen automatically
- iPad: Fix selected sidebar item becoming out-of-sync when deleting a list/hashtag/instance
- Mac: Workaround for pressing Cmd+1/2/... crashing
- This prevents the crash, but the actions remain unusable due to a macOS bug
- VoiceOver: Fix escape gesture not working on Compose screen
- VoiceOver: Fix links in conversation main status not being selectable
- VoiceOver: Fix profile relationship not being read
- VoiceOver: Fix not being able to select profile from conversation main status
Known Issues:
- Filters are not applied to notifications and conversations
## 2022.1 (48)
This build is a hotfix for the CW button in the Compose screen not working. The previous build's changelog is attached below.
Bugfixes:
- Fix pressing CW button in Compose not showing the content warning field
- Tweak timeline state restoration to try and maintain the scroll position of the middle item on screen, rather than the top one
## 2022.1 (46)
The headlining feature is state restoration and timeline gaps! When you re-open the app after it's been closed for a while, it will remember your position in the timeline and allow you to keep reading from there. It will also let you jump all the way to the present.
Features/Improvements:
- Timeline state restoration, timeline gaps, and jump-to-present
- Allow posting wide color gamut images on Mastodon 4
- Add Add to List menu action to profiles
- Improve More Actions button visibility on dark profile header images
- Make poll options in the Compose screen reorderable with drag & drop
- Embiggen Share/Close controls in the gallery to make them easier to tap
- Separate section for Shared Albums in the Compose attachment picker
- Indicate verified profile links with a checkmark and popover explaining what it means
- Add a preference for using the Twitter-style keyboard with @ and # (Preferences -> Composing -> Show @ and # on Keyboard)
- Improve reblog indicator in timeline
Bugfixes:
- Fix not being able to select an existing draft to edit it
- Fix double-tap to zoom in the gallery not working
- Fix crash when toggling collapsed posts in Trending Posts
- Fix albums in asset picker not being sorted by name
- Fix profile headers getting squished when statuses are loaded while the profile is offscreen
- Fix error loading posts when server returns rich cards
- Fix Akkoma instnaces not being detected as supporting Pleroma features
- Fix crash when launching the app in slow network conditions
- Fix lists not updating in the UI when renamed
- Fix follow/block/mute actions displaying on the user's own profile
- Fix Edit List screen being presented repeatedly when switching tabs back to Explore with a list open
- Fix reblog visibility icon getting squished in the reblog confirmation dialog when Dynamic Type is active
- Fix toasts not adjusting to Dynamic Type size
- Don't show duplicate reply/favorite/reblog actions in the status More Actions menu
- iPadOS 15: Fix toolbar in Compose window being obscured by the keyboard
## 2022.1 (45)
Features/Improvements:
- iPhone: Temporarily hide the Compose screen by swiping down to access the rest of the applies
- Add Block, Domain Block, and Mute actions to accounts
- Don't change scroll position when switch sections in the Profile screen
- Use URL keyboard in the instance selector and clarify that you can enter any domain
- iPad: Add context menu action for deleting lists in sidebar
- Tweak conditions in which profile fields are shown in a single column, rather than two
- Convert wide color gamut images to sRGB before uploading
- The Mastodon backend does not support wide-gamut images and does a poor job of conversion, so the conversion is performed locally
- Focus content warning field immediately when CW button is pressed
- Move focus to main text field when return key is pressed while editing the content warning
- Make GIF attachments animate on the Compose screen
- VoiceOver: Make profile fields accessible
- VoiceOver: Only read content warning and not content for CW'd posts
- VoiceOver: Expand collapsed posts when performing double-tap
- VoiceOver: Announce visibility of followers-only & direct posts
- VoiceOver: Make Compose toolbar accessible
Bugfixes:
- Fix tapping links in profile fields
- Fix crash when creating/editing list fails
- Fix renaming a list not updating it elsewhere in the UI
- Fix instance-local/everywhere scope selector in Profile Directory being flipped
- Fix context menu previews of attachments not working
- Fix caret not scrolling into view when opening Compose
- Fix cells in the Drafts list being too small to tap
- Fix refresh failing when initial load failed
- Fix video controls in the gallery being too close to the edge of the screen
- Fix error when decoding malformed notifications
- Fix reblog with visibility not being available on Hometown instances
- Fix visibility dropdown being shown in confirm reblog alert even when unavailable
- Fix confirm reblog alert not adjusting to Dynamic Type
- Fix layout issues with replies on Compose screen
- macOS: Fix GIFs dragged from Finder posting static images
Known Issues:
- Drag/drop to add attachments when composting a post does not work
## 2022.1 (44)
Features/Improvements:
- Dynamic Type support
- Improve performance when displaying statuses with large numbers of custom emojis
- Add preference for default reply visibility
- Add preference to turn off blurring media in posts with content warnings
Bugfixes:
- Fix drawing background flashing between black/white in dark mode
- Fix undo scroll-to-top not working in release builds
- Fix favorite and reblog menu actions not working
- Fix avatar in compose being wrongly aligned on short statuses
- Fix posts that are tall but have few characters not getting collapsed
- Fix crash when profile screen is closed for the profile loads
## 2022.1 (43)
Features/Improvements:
- Re-add undo scroll-to-top by tapping the status bar a second time
- Convert hashtag/list/instance timelines to use new timeline implementation
- Clarify warning on Post Content Type preference
Bugfixes:
- Fix crash when refreshing profile before it loaded
- Fix crash when tapping Load More when infinite scrolling is disabled
- Fix crash when profile screen is closed before loading finishes
- Fix having to tap Cancel twice to dismiss Find Instance screen
## 2022.1 (42)
Features/Improvements:
- Add automatic crash reporting
- Tweak spacing on timeline statuses
Bugfixes:
- Fix status collapse/expand not animating on profiles
- Fix crash when opening profile for unloaded account (e.g., by tapping mentions)
- macOS: Add workaround for Follow/Unfollow menu item never loading
## 2022.1 (41)
Features/Improvements:
- Rewrite profile screens to use new timeline implementation
- Disable Infinite Scrolling preference (Preferences -> Digital Wellness) now applies to profiles
- Improve behavior when switching tabs on profiles
- Improve pointer interaction on timeline status cells
Bugfixes:
- Fix crash when loading images in certain circumstances
- Fix gallery dismissal leaving status bar hidden and breaking future gallery dismisses
- Fix timeline scroll position changing after dismissing gallery
- Fix images flickering when switching back to the Home tab
- Fix crash reporter being dismissed when sending email is cancelled
- Fix crash when long pressing Send Report button in crash reporter on iPad
- Fix Live Text controls not hiding when other gallery controls are hidden
- Fix replies appearing multiple times in Drafts list
- Fix crash when displaying blur hash images on Pleroma
## 2022.1 (40)
Bugfixes:
- Fix selecting reblogged statuses in the timeline
- Fix links/mentions/hashtags in the timeline not being tappable
- Fix mentions from Misskey opening in the browser rather than the profile screen
- Fix crash when leaving timeline tab before it finished loading
- Fix status cells in the timeline not deselected when tapped in split navigation mode on iPad
- Fix keyboard shortcuts not working on iPad
## 2022.1 (39)
This is a(nother) hotfix for the previous build. Their changelogs are included below.
Bugfixes:
- Fix instance selector screen crashing on iOS 15
## 2022.1 (38)
This is a hotfix for the previous build. Its changelog is included below.
Bugfixes:
- Fix sensitive attachments not being hidden on the timeline
- Fix timeline descriptions appearing repeatedly
- iPadOS: Fix occasional crash when hovering over text
## 2022.1 (37)
This is the first build with the rewritten/rearchitected timeline screen. In future builds, this will roll out to the notifications and profile screens as well, but for now it's only used in the home tab. If you encounter crashes or errors, please report them. If you see a blue error bubble pop up, you can long-press it to send an error report.
Features/Improvements:
- Display error messages when favoriting/reblogging fails
Bugfixes:
- iOS 15: (hopefully) fix lock-related crash
- Fix crash when loading indicator is shown multiple times
Known Issues:
- Videos played from the timeline do not enter picture-in-picture mode when backgrounding the app
- Status expand/collapse animations on other screens do not match timelines
Other:
- X-Callback-URL support has been removed
## 2022.1 (36)
This build is a hotfix for a crash when refreshing on Pixelfed.
## 2022.1 (35)
Features/Improvements:
- Add loading indicator to timelines/notifications/profiles
- Show status preview in reblog confirmation dialog (Preferences -> Behavior -> Require confirmation Before Reblogging)
- Add reblogging with unlisted/private visibility (requires reblog confirmation to be enabled)
- Fix controls not hiding on iPhone 14 Pro
- Improve account switching animation
Bugfixes:
- Fix crash when resizing window on iPad
- Fix poll vote count displaying random number
- Fix crash when opening emoji picker on instances that have duplicate emojis
## 2022.1 (33)
Features/Improvements:
- Show notifications when subscribed to other people's posts
- Use context menu for filter/sort in Profile Directory
- Enable data detectors (flight numbers, addresses, shippment numbers, phone numbers, currency (iOS 16), and physical units (iOS 16)) for the main status in Conversation
- iPadOS: Two column navigation
- In potrait orientation with the sidebar hidden, or in landscape, Tusker uses two column navigation on iPad
- Selecting something will open a second column in which navigation takes place
- The second column can be closed from its top level
- You can drill down farther inside the second column
- Selecting a different item in the first column replaces the second column
- iPadOS: Move Trending Hashtags/Links to Explore screen (formerly Search)
- Trending Statuses and Profile Directory will also be moved in a future version
- iPadOS: Add context menu action for deleting drafts
- iOS 16: Add Live Text to images in the gallery view
- iOS 16: Show favorite/reblog context menu actions
- iOS 16: Show full size previews when long-pressing attachments on the Compose screen
- iOS 16: Show formatting actions in edit menu on Compose screen
Bugfixes:
- Fix attachments on Pleroma not being served as the correct content type
- Fix not being able to open some hashtags with non-ASCII characters
- Fix crash when leaving the app shortly after opening it
- Fix crash when loading notifications on Pixelfed
- Fix crash due to retain cycle when changing preferences
- iPadOS: Fix crash when opening a conversation in a new window
## 2022.1 (31)
Bugfixes:
- Fix not being able to post attachments with descriptions
- Fix potential crash when displaying certain notifications
- More detailed error message when decoding invalid URLs
## 2022.1 (30)
Features/Improvements:
- Add fast account switching on iPad
- Add "Add Account" option to fast account switcher
- Show "# more replies" indicator in conversation in more circumstances
- When refreshing notifications, new ones are grouped with existing notifications
- Add subtitles to explain post visibility options
- Improve error messages when posting a video fails
- Display error messages instead of crashing when certain actions fail
Bugfixes:
- Fix Shortcuts actions not working in some circumstances
- Fix CW field growing wider than the screen
- Fix saved hashtags not persisting
- Fix not being able to long-press error-message bubbles
- Fix follow context menu item not updating after following
- Fix not being able to log in to certain Pixelfed instances
- Fix crash when closing the app
- Fix crash when loading profile screen
- Fix crash when refreshing polls
- Fix crash when poll voting fails
- Fix crash when accepting/rejecting a follow request fails
- Fix saved hashtags being sorted with case-sensitivity
- Fix multiple lines of text with emojis getting squashed together
- iPad: Fix Shortcuts actions showing wrong window type
- Mac: Fix Cmd+N shortcut not opening Compose window
- iPad/Mac: Fix Send Message action not mentioning account
## 2022.1 (27)
Features/Improvements:
- Add emoji picker button to Compose screen toolbar
- Preference to disable reply/like/reblog/more buttons on statuses in the timeline
- iPad: Sidebar toggle button
Bugfixes:
- Fix crash when displaying malformed statuses
## 2022.1 (26)
This build contains hotfixes for several crashes that may occur when logged-in to a Pixelfed account.
## 2022.1 (25)
Features/Improvements:
- Improve error reporting for non-crash errors
- Long-press on the blue error bubble to send a report
- Improve error feedback during login process
- Add Trending Post and Trending Links on Mastodon 3.5
- Add Digital Wellness preference to disable Discover
- Basic support for GotoSocial
- Reduce app file size
Bugfixes:
- Fix all statues appearing as pinned on Pixelfed
- Fix crash when refreshing My Profile
- Fix My Profile never loading in some circumstances
- Fix crash the first time the attachment picker is opened
- Fix crash when closing certain screens
- Fix certain links in posts not being detected
## 2022.1 (24)
Features/Improvements:
- Local only posts (Glitch/Hometown)
- Show indicator for local only posts
- Add local only option to Compose screen
- Add extend selection button to asset picker when the limited library was used
- Improve profile directory UI
- Improve scrolling performance with large attachments on older devices
Bugfixes:
- Fix crash when closing Preferences
- Fix crash when posting attachment fails
- Fix scrolling through compose autocomplete suggestions dismissing keyboard
- Only show Mute action on your own posts
## 2021.1 (23)
Features/Improvements:
- Synchronize GIF playback through animations and in gallery
- Known Issue: This does not currently work with GIFVs used by Mastodon instances.
Bugfixes:
- Fix crash when a conversation fails to load
- Fix gallery dismissal breaking public timelines
- Fix gallery dismissal going in the wrong direction when the gesture was started slowly
## 2021.1 (22)
This is the first public beta build of Tusker, so if you're just joining us, welcome! Not too many new features this build, mostly bugfixes, so test everything and generally use the app.
Features/Improvements:
- Add timeline descriptions the first time you view federated/local
- Show messages when loading posts fails or when there are no newer posts
Bugfixes:
- Fix crash after editing lists
- Fix crash when refreshing before anything is loaded
- Fix crash when fetching recommended instances fails
- Fix crash when replying to posts with code formatting
- Fix crash when changing preferences after switching accounts
## 2021.1 (21)
This is a quick follow-up to the previous build with fixes for a couple major crashes. Unfortunately, due to a bug in iOS 14, the Disable Infinite Scrolling preference now requires the iOS 15 beta to use. It may return in a future build if I can find a workaround, but it's disabled in the meantime.
Features/Improvements:
- iPadOS 15: Add Open in New Window context menu action to sidebar items
Bugfixes:
- Fix crash when editing accounts in a list
- Fix crash when refreshing timeline on iOS 14
- Fix(ish) crash when opening collapsed status with Disable Infinite Scrolling active on iOS 15
## 2021.1 (20)
This is a big one! In addition to a bunch of fixes for anyone on the iOS 15 beta, there are a couple of big ticket features, including the Open in Tusker action extension and the Disable Infinite Scrolling preference.
Features/Improvements:
- Add Open in Tusker action extension
- Quickly search for any URL in Tusker
- In a share sheet, scroll to the bottom, tap "Edit Actions..." and turn on the "Open in Tusker" action
- Add Digital Wellness preference to disable infinite scrolling
- Add fast account switching indicator to My Profile tab
- Improve VoiceOver accessibility of polls and timeline statuses
- iPadOS: Create multiple main windows for different accounts by dragging from an account in Preferences
- iPadOS: Delete attachments on Compose screen by right-clicking and selecting Delete
- iPadOS 15: Add Open in New Window context menu action to most things
- iPadOS 15: Allow dragging the Compose sheet into a separate window
Bugfixes:
- Fix being unable to commit previewed account from timeline status
- Fix crash when searching fails
- Fix poll option percentages being cut off
- Fix polls not collapsing inside CWs
- Fix More button on profiles not being accessible with VoiceOver
- Fix VoiceOver reading profile fields in incorrect order
- Fix gallery animations jittering on devices with square screens (iPads, non-notched iPhones)
- Fix CW text jumping around post collapse animation
- iOS 15: Fix crash due when showing Draw Something screen in Compose
- iPadOS 14/iOS 15: Fix navigation bar turning transparent after opening the attachment gallery
- iPadOS 14/iOS 15: Fix drag-selecting poll options initiating a status cell drag interaction
- iPadOS: Fix crash when loading a previously-opened conversation window
- iPadOS 15: Fix showing Compose screen when keyboard focus moves through the sidebar
Known Issues:
- Disable Infinite Scrolling preference only affects timelines, not notifications or profiles
- iPadOS 15: The Compose sheet cannot be dismissed by swiping down
- iPadOS 15: Keyboard focus is stuck in the sidebar
## 2021.1 (19)
This is an emergency fix for Tusker breaking when connecting to Mastodon instances on 3.4.0rc1.
Bugfixes:
- Fix crash when connecting to Mastodon 3.4.0rc1
- Fix crash when loading notifications fails
## 2021.1 (18)
Polls! They're finally here. There will likely be another build in the next several weeks to polish some things off before WWDC, so if you've encountered any issues, now's the time to let me know :)
Features/Improvements:
- Show polls on posts
- Add authoring polls to Compose screen
- Add poll completed notifications
- Add preference for requiring confirmation before reblogging
Bugfixes:
- Fix cursor movement not working in Compse text field when an emoji was entered
- Fix several crashes related to network requests failing
- Show assets in attachment picker immediately after permissions is initially granted
- Fix crash when tapping non-HTTP(S) link with the In-App Safari preference enabled
Known Issues:
- Polls with between 23h30m and 24h left show as "0 weeks remaining"
## 2021.1 (17)
The main improvement this build is a complete overhaul of the Conversation screen, along with fixes for a few different crashes.
Features/Improvements:
- Group replies by thread on Conversation screen
- Adding Trending Hashtags and Profile Directory to Explore screen (Mastodon only)
Bugfixes:
- Fix crash when editing List members
- Fix crash when re-opening Preferences after switching accounts
- Fix crash when refreshing profiles in some circumstances
## 2021.1 (16)
This build fixes a number of crashes and significantly improves performance on older devices.
Features/Improvements:
- Significantly improve performance when scrolling through timelines
Bugfixes:
- Fix crash when timeline or profile went offscreen
- Fix crash when refreshing profile too quickly
- iPadOS: Fix secondary windows not respecting theme preference
- Fix refreshes breaking after a refresh which did not return new results
## 2020.1 (15)
There are a whole bunch of new features in this release, in addition to a slew of bugfixes. The big ticket items are multi-window support on iPadOS and fast account switching on iPhone (fast account switching will be supported on iPads in a future build).
Features/Improvements:
- Add fast account switching on iPhone
- Long-press on the My Profile tab and drag up to select another account
- Add account switching animation
- iPadOS: Add multiple windows
- Drag and drop statuses and accounts on timelines into auxiliary windows
- Drag and drop sidebar items into new windows
- Drag and drop Compose drafts into new windows
- Add key commands
- ⌘R to refresh timelines
- ⌘N to Compose a post
- ⌘B/⌘I text formatting on Compose screen
- ⇧⌘[/⇧⌘] to switch between sub-tabs
- iPadOS: ⌘1/⌘2/⌘3/⌘4/⌘5 to select sidebar items
- Show link cards on statuses
- Add Grayscale Images preference to Digital Wellness
- Add preference for applying opposite collapse behavior for specific keywords
Bugfixes:
- Fix statuses not appearing on My Profile until scrolling
- Fix crashes when scrolling through timelines
- Fix logging into PixelFed instances
- Interacting with PixelFed instances remains only partially supported
- Enlarge expand/collapse button tap area
- Fix cursor disappearing behind keyboard when typing attachment descriptions
- Fix crash when viewing custom emojis with spaces in their URLs
- Fix split view not working in landscape on Plus/Max iPhones
- Minor performance improvements
## 2020.1 (13)
This is another quick build to fix a couple of severe issues on the Compose screen.
Features/Improvements:
- When composing posts, ensure the cursor is always visible and does not scroll below the keyboard while typing
Bugfixes:
- Fix builtin iOS keyboard suggestions not working in text fields on the Compose screen
- Fix crash when ending dictation in the CW field
- Fix broken layout on the Compose when replying to certain posts
## 2020.1 (12)
This build is a hotfix for the issue of being unable to login to certain instances. The changelog for the previous build is included below.
## 2020.1 (11)
This release is primarily focused on bug fixes with the one key feature of autocomplete suggestions when typing in the Compose screen. It also fixes an issue on the various new sizes of iPhone 12, so if you're getting a new device, make sure to update.
Features/Improvements:
- Add autocompletion on Compose screen
- Autocomplete provides suggestions for @-mentions, hashtags, and emojis as you're typing in the post body
- Provides suggestions for emojis as you're typing in the CW field
- Type a colon and expand the emoji suggestions to view all custom emoji on your instance
- Hashtag suggestions prioritize trending and saved hashtags, in addition to searching all hashtags on your instance
- Account suggestions prioritize accounts that you follow or that follow you, as well as searching all accounts known to your instance
- Show custom emojis in users' display names in follow, favorite, and reblog notifications
- Enable picture-in-picture playback of video attachments
- iOS 14: Automatically enter picture-in-picture when closing the app while a video is playing
- Correctly positiong gallery controls on iPhone 12-family devices
- Round corners of the avatar on the My Profile tab icon
- Remove extraneous U+FFFC characters inserted by dictation when posting
- Add swipe to remove accounts in Preferences
Bugfixes:
- Fix not being able to tap placeholders in Compose
- Fix broken layout on Compose screen when replying to very long posts
- Fix crash when opening Compose or My Profile too quickly after launch
- Upload photos taken with the in-app camera as JPEGs instead of PNGs
- Fixes an issue where Mastodon would incorrectly believe the file size to be too large
- Fix crash when using home screen shortcuts
- Disable rotating into landscape on iPhone on iOS 14
- Fix assorted other crashes and memory leaks
## 2020.1 (10)
This build is a hotfix for a couple pressing issues. The changelog for the previous build is included below.
Bugfixes:
- Fix crash when opening Preferences while signed in with a deleted account
- Fix visibility and content warning not being copied when replying to a post
## 2020.1 (9)
The marquee feature of this build is the new and improved Compose screen. It's been rewritten to use SwiftUI, is significantly more resilient to data loss, and now shows the toolbar when the main text field is not focused. It also turns out Apple is surprise-releasing iOS 14 very soon (or possibly already has, depending when you're reading this). For those who were not already on the beta train, iOS 14 brings a number of new features including a sidebar on iPadOS and lots and lots of context menus (a home screen widget is coming Soon™).
Known Issues:
- Pasting images to create attachments when composing a post is not currently supported due to an iOS bug (#109)
- Full-size previews do not display in context menus for attachments on the Compose screen due to an iOS issue (#110)
Features/Improvements:
- Rewrite Compose screen using SwiftUI
- Prevent draft posts being lost if the app crahes or is killed by the system while composing
- Show toolbar while post content is not being edited
- Save post visibility in drafts
- Move Draw Something action out of the context menu
- iOS 14: Use context menus for setting post visibility
- Show BlurHash previews for attachments on Mastodon
- Add Expand All Content Warnings preference (Preferences -> Behavior)
- Add Collapse Long Posts preference (Preferences -> Behavior)
- Improve image gallery opening animation
- Use fade in/out animations for opening/closing gallery and attachment picker when the Reduce Motion system setting is enabled
- iOS 14: Also requires the "Prefer Cross Fade" setting be enabled
- Slightly reduce default status font sizes
- Add "Direct Message" context menu action to Compose button on profile screen
- Allow viewing attachments and navigating through posts/accounts on instance public timelines
Bugfixes:
- Fix errors when uploading attachments not displaying
- Fix attachments not posting in the correct, user-specified order
- Fix accounts displaying with outdated information (avatars, display names, etc.)
- Fix Compose not showing button on profile screen
- Fix navigation title not being set on profile screen
- Fix follow notifications not showing names for users without display names set
- iPadOS 14: Fix crash when resizing app in split view mode
## 2020.1 (8)
This is just an emergency build to fix crashes on iOS 13 when selecting attachments. The changelog of the previous build is included below.
Features/Improvements:
- Enlarge tap targets on status reply/favorite/reblog/more buttons
- Disable automatic GIF playback when Low Power Mode is enabled
- Show custom emoji in user profile field names
Bugfixes:
- Fix crash when attempting to add attachments on iOS 13
- Fix potential crashes
## 2020.1 (7)
This is the first update since WWDC and the introduction of iOS 14. As such, most of the focus has been on fixing iOS 14-specific problems. However, there are still a couple new features, both for those on the iOS 14 beta and those not.
Features/Improvements:
- Add toggle between Posts, Posts and Replies, and Media on user profiles
- Remove 'Show Replies in Profiles' preference
- Limit link preview animation to only link text
- Add additional context menu actions for statuses, accounts, and hashtags
- Add semi-translucent background to image descriptions, so they're legible against light images
- iPadOS 14: Add sidebar
- When using multitasking on iPad and switching in and out of "compact" mode, the active tab as well as the navigation history for all tabs will be transferred between the sidebar and tab bar modes.
- iOS 14: Use context menus on status/account '...' buttons
- iOS 14: Replace 'More' status swipe action with 'Share'
Bugfixes:
- Fix crash when attempting to change post visibility on iPad
- Fix attachment view corners not being rounded
- Fix crash when viewing instance public timelines
- Fix Preferences button not appearing on My Profile tab
- Fix tapping current tab bar item not scrolling to top
- Fix crash showing audio attachments on Mastodon
- Fix timeline refreshing forever
- Set app category (fixes usage not being categorized correctly under Screen Time)
- iOS 14: Fix crash when searching for instances
- iOS 14: Fix crash when displaying accounts with no pinned posts
- iOS 14: Fix crash when displaying search results
## 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

1
Cache

@ -1 +0,0 @@
Subproject commit 8c42c575cf28b2ff0e780c9728721e9a8891c92e

View File

@ -1,355 +0,0 @@
# X-Callback-URLs in Tusker
Tusker supports inter-app-communication using the [X-Callback-URL standard](http://x-callback-url.com/).
In short, requests are performed by opening the URL `tusker://x-callback-url/[request]` (where `[request]` is one of the requests listed below) with a variety of parameters.
## Callbacks
X-Callback-URLs support three types of callbacks: on success, on cancellation, and on error. Callbacks are specified as query parameters whose keys identify which callback (`x-success`, `x-cancel`, and `x-error`) and whose values are other URLs that should be opened to run the callback.
Data is passed to callbacks by adding additional query parameters to the callback URL. The `x-error` callback always returns a description of the error in the `error` parameter. Other data is provided depending on the request.
### JSON Responses
By default, callback data is included in URL query parameters of the callback URL. If the `json=true` parameter is provided, the response data will be encoded as JSON, converted to [Base64](https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding), and provided in the `response` query parameter of the callback.
## Silent Requests
Tusker X-Callback-URL requests can be performed silently, without user confirmation. Each source app requires user permission on the first attempted silent action.
To perform a silent request:
1. Provide the `silent=true` URL query parameter in the request.
2. Specify the `x-source` parameter. It must be a (human interpretable) name of the source application/service. If `x-source` is not specified, the error callback will be invoked with the error message:
```
Cannot perform silent action without source app, x-source parameter must be specified.
```
3. Depending on the current permission state of the source app, one of several things will happen:
1. If the permission is **undecided** (i.e. the user has neither accepted nor rejected the silent action request), an alert will be displayed notifying the user that the source app has requested permission to silently perform actions. After the user either accepts or rejects the request, execution will continue with that permission state.
2. **Accepted**: the request will be carried out silently and the appropriate callback executed.
3. **Rejected**: the request will be performed with the confirmation UI, as if the `silent` parameter had been false/unprovided.
The silent actions permission state of a given source app is not exposed in the callback.
## Other Notes
#### Instance-Local IDs
Instance-local IDs are provided for many responses and accept in place of URLs/URIs/qualified names in many requests. When possible, instance-local IDs should be preferred requests using them can often be performed faster because there's no need to perform a search query or make requests to remote instances.
#### Qualified Usernames
Qualified username refers to the domain-qualified identifier of an account. For example, `shadowfacts@social.shadowfacts.net`. They do not include a leading `@`.
#### Dates
Dates in responses are encoded as Unix timestamps.
## Requests
- [Accounts](#accounts)
- [`showAccount`](#showaccount)
- [`getCurrentUser`](#getcurrentuser)
- [`getAccount`](#getaccount)
- [`followUser`](#followuser)
- [Statuses](#statuses)
- [`showStatus`](#showstatus)
- [`getStatus`](#getstatus)
- [`postStatus`](#poststatus)
- [`favoriteStatus`](#favoritestatus)
- [`reblogStatus`](#reblogstatus)
- [Notifications](#notifications)
- [`getNotification`](#getnotification)
- [`getNotifications`](#getnotifications)
- [`dismissNotification`](#dismissnotification)
- [`dismissAllNotifications`](#dismissallnotifications)
- [Instances](#instances)
- [`getCurrentInstance`](#getcurrentinstance)
- [Misc](#misc)
- [`search`](#search)
### Accounts
#### `showAccount`
Presents the given account in Tusker.
##### Request
| Parameter (type) | Description | Optional |
| -------------------- | ------------------------------------------------------------ | -------- |
| `accountID` (string) | The instance-local ID of the account | Yes |
| `accountURL` (URL) | The URL of the remote account | Yes |
| `acct` (string) | The [qualified username](#qualifiedusernames) of the account | Yes |
##### Response
No data if successful.
#### `getCurrentUser`
Retrieves the currently logged-in user.
##### Request
No parameters.
##### Response:
| Parameter (type) | Description | Optional |
| ---------------------- | --------------------------------------------- | -------- |
| `username` (string) | The [qualified username](#qualifiedusernames) | No |
| `displayName` (string) | The display name | No |
| `locked` (bool) | Whether the user's account is locked | No |
| `followers` (int) | The number of followers the user has | No |
| `following` (int) | The number of accounts user is following | No |
| `url` (URL) | The URL of the user's account | No |
| `avatarURL` (URL) | The URL of the user's avatar image | No |
| `headerURL` (URL) | The URL of the user's header image | No |
#### `getAccount`
Retrieves the given account details. One of `accountID`, `accountURL`, or `acct` must be provided.
##### Request
| Parameter (type) | Description | Optional |
| -------------------- | ------------------------------------------------------------ | -------- |
| `accountID` (string) | The instance-local ID of the account | Yes |
| `accountURL` (URL) | The URL/URI of the account | Yes |
| `acct` (string) | The [qualified username](#qualifiedusernames) of the account | Yes |
##### Response
| Parameter (type) | Description | Optional |
| ---------------------- | ------------------------------------------- | -------- |
| `username` (string) | The qualified username | No |
| `displayName` (string) | The display name | No |
| `locked` (bool) | Whether the account is locked | No |
| `followers` (int) | The number of followers the account has | No |
| `following` (int) | The number of accounts account is following | No |
| `url` (URL) | The URL of the account | No |
| `avatarURL` (URL) | The URL of the account's avatar image | No |
| `headerURL` (URL) | The URL of the account's header image | No |
#### `followUser`
Follows the given account from the logged-in user's account. One of `accountID`, `accountURL`, or `acct` must be provided.
##### Request
| Parameter (type) | Description | Optional |
| -------------------- | ------------------------------------------------------------ | -------- |
| `accountID` (string) | The instance-local ID of the account | Yes |
| `accountURL` (URL) | The URL/URI of the account | Yes |
| `acct` (string) | The [qualified username](#qualifiedusernames) of the account | Yes |
##### Response
| Parameter (type) | Description | Optional |
| ---------------- | ------------------------------- | -------- |
| `url` (URL) | The URL of the followed account | No |
### Statuses
#### `showStatus`
Presents the given status in Tusker.
##### Request
| Parameter (type) | Description | Optional |
| ------------------- | ----------------------------------- | -------- |
| `statusID` (string) | The instance-local ID of the status | Yes |
| `statusURL` (URL) | The URL of a remote status | Yes |
##### Response
No data if successful.
#### `getStatus`
Retrieves the given status details. One of `statusID` or `statusURL` must be provided.
##### Request
| Parameter (type) | Description | Optional |
| ------------------- | ------------------------------------------------------------ | -------- |
| `statusID` (string) | The instance-local ID of the status | Yes |
| `statusURL` (URL) | The URL/URI of the status | Yes |
| `html` (bool) | Whether to return the content as HTML or plain-text only. Default: `false` (plain-text). | Yes |
##### Response
| Parameter (type) | Description | Optional |
| -------------------- | ------------------------------------------------------------ | -------- |
| `url` (URL) | The URL of the status | Yes |
| `uri` (string) | The URI of the status | No |
| `id` (string) | The instance-local ID of the status | |
| `account` (string) | The [qualified username](#qualifiedusernames) of the account that posted (or reblogged if `reblog` is present) the status | No |
| `inReplyTo` (string) | The instance-local ID of the status that this status is a reply to | Yes |
| `posted` (date) | The date the status was posted | No |
| `content` (string) | The content of the status (HTML if the `html` parameter was true, plain-text otherwise) | No |
| `reblog` (string) | The **instance-local** ID of the status that this is a reblog of. If not present, this status was not a reblog. | Yes |
#### `postStatus`
Posts a status from the logged-in user's account.
Can be performed silently.
##### Request
| Parameter (type) | Description | Optional |
| ------------------- | ------------------------------------------------------------ | -------- |
| `mentioning` (bool) | The [qualified username](#qualifiedusernames) to mention in the status | Yes |
| `text` (string) | The text to post/pre-fill the status text field with | Yes |
##### Response
| Parameter (type) | Description | Optional |
| -------------------- | ---------------------------- | -------- |
| `statusURL` (URL) | The URL of the posted status | Yes |
| `statusURI` (string) | The URI of the posted status | No |
#### `favoriteStatus`
Favorites the given status from the logged-in user's account. One of `statusID` or `statusURL` must be provided.
Can be performed silently.
##### Request
| Parameter (type) | Description | Optional |
| ------------------- | ----------------------------------- | -------- |
| `statusID` (string) | The instance-local ID of the status | Yes |
| `statusURL` (URL) | The URL/URI of a status | Yes |
##### Response
| Parameter (type) | Description | Optional |
| -------------------- | ------------------------------- | -------- |
| `statusURL` (URL) | The URL of the favorited status | Yes |
| `statusURI` (string) | The URI of the favorited status | No |
#### `reblogStatus`
Reblogs the given status from the logged-in user's account. One of `statusID` or `statusURL` must be provided.
Can be performed silently.
##### Request
| Parameter (type) | Description | Optional |
| ------------------- | ----------------------------------- | -------- |
| `statusID` (string) | The instance-local ID of the status | Yes |
| `statusURL` (URL) | The URL/URI of a status | Yes |
##### Response
| Parameter (type) | Description | Optional |
| -------------------- | ------------------------------- | -------- |
| `statusURL` (URL) | The URL of the reblogged status | Yes |
| `statusURI` (string) | The URI of the reblogged status | No |
### Notifications
#### `getNotification`
Retrieves the given notification details.
##### Request
| Parameter (type) | Description | Optional |
| ------------------------- | ----------------------------------------- | -------- |
| `notificationID` (string) | The instance-local ID of the notification | No |
##### Response
| Parameter (type) | Description | Optional |
| -------------------- | ------------------------------------------------------------ | -------- |
| `kind` (string) | One of `mention`, `reblog`, `favourite`, or `follow` | No |
| `date` (date) | The date the notification was created. | No |
| `accountID` (string) | The instance-local ID of the account that sent the notification | No |
| `statusID` (string) | The instance-local ID of the status associated with the notification. Not applicable for `kind=follow`. | Yes |
#### `getNotifications`
Retrieves the most recent notifications.
##### Request
| Parameter (type) | Description | Optional |
| ---------------- | ---------------------------------------------------- | -------- |
| `count` (int) | The number of notifications to retrieve. Default: 20 | Yes |
##### Response
| Parameter (type) | Description | Optional |
| ------------------------ | ---------------------------------------------------------- | -------- |
| `notifications` (string) | A comma-delimited array of instance-local notification IDs | No |
#### `dismissNotification`
Dismisses the given notification.
##### Request
| Parameter (type) | Description | Optional |
| ----------------------- | ----------------------------------------- | -------- |
| `notification` (string) | The instance-local ID of the notification | No |
##### Response
No response data if successful.
#### `dismissAllNotifications`
Dismisses all notifications.
##### Request
No parameters.
##### Response
No data if successful.
### Instances
#### `getCurrentInstance`
Retrieves the current instance details.
##### Request
No parameters.
##### Response
| Parameter (type) | Description | Optional |
| ------------------------- | ------------------------------------------------------- | -------- |
| `uri` (string) | The instance URI | No |
| `name` (string) | The instance name | No |
| `description` (string) | The instance description | No |
| `contactAccount` (string) | The instance-local ID of the instance's contact account | No |
### Misc
#### `search`
Performs a search in Tusker with the given query
##### Request
| Parameter (type) | Description | Optional |
| ---------------- | ------------------------ |--------- |
| `query` (string) | The search query to use. | No |
##### Response
No data if successful.

1
Embassy Submodule

@ -0,0 +1 @@
Subproject commit 189436100c00efbf5fb2653fe7972a9371db0a91

View File

@ -1,32 +0,0 @@
//
// GMAlbumsViewCell.h
// GMPhotoPicker
//
// Created by Guillermo Muntaner Perelló on 22/09/14.
// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved.
//
#include <UIKit/UIKit.h>
#include <Photos/Photos.h>
@interface GMAlbumsViewCell : UITableViewCell
@property (strong) PHFetchResult *assetsFetchResults;
@property (strong) PHAssetCollection *assetCollection;
//The labels
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UILabel *infoLabel;
//The imageView
@property (nonatomic, strong) UIImageView *imageView1;
@property (nonatomic, strong) UIImageView *imageView2;
@property (nonatomic, strong) UIImageView *imageView3;
//Video additional information
@property (nonatomic, strong) UIImageView *videoIcon;
@property (nonatomic, strong) UIImageView *slowMoIcon;
@property (nonatomic, strong) UIView *gradientView;
@property (nonatomic, strong) CAGradientLayer *gradient;
//Selection overlay
- (void)setVideoLayout:(BOOL)isVideo;
@end

View File

@ -1,131 +0,0 @@
//
// GMAlbumsViewCell.m
// GMPhotoPicker
//
// Created by Guillermo Muntaner Perelló on 22/09/14.
// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved.
//
#import "GMAlbumsViewCell.h"
#import "GMAlbumsViewController.h"
#import "GMImagePickerController.h"
#import <QuartzCore/QuartzCore.h>
@implementation GMAlbumsViewCell
- (void)awakeFromNib
{
[super awakeFromNib];
}
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier])
{
// self.isAccessibilityElement = YES;
self.contentView.backgroundColor = [UIColor clearColor];
self.backgroundColor = [UIColor clearColor];
self.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
// Border width of 1 pixel:
float borderWidth = 1.0/[UIScreen mainScreen].scale;
// ImageView
_imageView3 = [UIImageView new];
_imageView3.contentMode = UIViewContentModeScaleAspectFill;
_imageView3.frame = CGRectMake(kAlbumLeftToImageSpace+4, 8, kAlbumThumbnailSize3.width, kAlbumThumbnailSize3.height );
[_imageView3.layer setBorderColor: [[UIColor whiteColor] CGColor]];
[_imageView3.layer setBorderWidth: borderWidth];
_imageView3.clipsToBounds = YES;
_imageView3.translatesAutoresizingMaskIntoConstraints = YES;
_imageView3.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
[self.contentView addSubview:_imageView3];
// ImageView
_imageView2 = [UIImageView new];
_imageView2.contentMode = UIViewContentModeScaleAspectFill;
_imageView2.frame = CGRectMake(kAlbumLeftToImageSpace+2, 8+2, kAlbumThumbnailSize2.width, kAlbumThumbnailSize2.height );
[_imageView2.layer setBorderColor: [[UIColor whiteColor] CGColor]];
[_imageView2.layer setBorderWidth: borderWidth];
_imageView2.clipsToBounds = YES;
_imageView2.translatesAutoresizingMaskIntoConstraints = YES;
_imageView2.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
[self.contentView addSubview:_imageView2];
// ImageView
_imageView1 = [UIImageView new];
_imageView1.contentMode = UIViewContentModeScaleAspectFill;
_imageView1.frame = CGRectMake(kAlbumLeftToImageSpace, 8+4, kAlbumThumbnailSize1.width, kAlbumThumbnailSize1.height );
[_imageView1.layer setBorderColor: [[UIColor whiteColor] CGColor]];
[_imageView1.layer setBorderWidth: borderWidth];
_imageView1.clipsToBounds = YES;
_imageView1.translatesAutoresizingMaskIntoConstraints = YES;
_imageView1.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
[self.contentView addSubview:_imageView1];
// The video gradient, label & icon
UIColor *topGradient = [UIColor colorWithRed:0.00 green:0.00 blue:0.00 alpha:0.0];
UIColor *midGradient = [UIColor colorWithRed:0.00 green:0.00 blue:0.00 alpha:0.33];
UIColor *botGradient = [UIColor colorWithRed:0.00 green:0.00 blue:0.00 alpha:0.75];
_gradientView = [[UIView alloc] initWithFrame: CGRectMake(0.0f, kAlbumThumbnailSize1.height-kAlbumGradientHeight, kAlbumThumbnailSize1.width, kAlbumGradientHeight)];
_gradient = [CAGradientLayer layer];
_gradient.frame = _gradientView.bounds;
_gradient.colors = [NSArray arrayWithObjects:(id)[topGradient CGColor], (id)[midGradient CGColor], (id)[botGradient CGColor], nil];
_gradient.locations = @[ @0.0f, @0.5f, @1.0f ];
[_gradientView.layer insertSublayer:_gradient atIndex:0];
_gradientView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
_gradientView.translatesAutoresizingMaskIntoConstraints = YES;
[self.imageView1 addSubview:_gradientView];
_gradientView.hidden = YES;
// VideoIcon
_videoIcon = [UIImageView new];
_videoIcon.contentMode = UIViewContentModeScaleAspectFill;
_videoIcon.frame = CGRectMake(3,kAlbumThumbnailSize1.height - 4 - 8, 15, 8 );
_videoIcon.image = [UIImage imageNamed:@"GMVideoIcon" inBundle:[NSBundle bundleForClass:GMAlbumsViewCell.class] compatibleWithTraitCollection:nil];
_videoIcon.clipsToBounds = YES;
_videoIcon.translatesAutoresizingMaskIntoConstraints = YES;
_videoIcon.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
[self.imageView1 addSubview:_videoIcon];
_videoIcon.hidden = NO;
// TextLabel
self.textLabel.font = [UIFont fontWithName:@"HelveticaNeue" size:17.0];
self.textLabel.numberOfLines = 1;
self.detailTextLabel.font = [UIFont fontWithName:@"HelveticaNeue" size:14.0];
self.detailTextLabel.numberOfLines = 1;
}
return self;
}
-(void)layoutSubviews {
[super layoutSubviews];
self.textLabel.frame = CGRectMake(kAlbumLeftToImageSpace + kAlbumThumbnailSize1.width + kAlbumImageToTextSpace,self.textLabel.frame.origin.y,self.contentView.frame.size.width - kAlbumLeftToImageSpace - kAlbumThumbnailSize1.width - 8, self.textLabel.frame.size.height);
self.detailTextLabel.frame = CGRectMake(kAlbumLeftToImageSpace + kAlbumThumbnailSize1.width + kAlbumImageToTextSpace,self.detailTextLabel.frame.origin.y,self.contentView.frame.size.width - kAlbumLeftToImageSpace - kAlbumThumbnailSize1.width - 8 - kAlbumImageToTextSpace, self.detailTextLabel.frame.size.height);
}
- (void)setVideoLayout:(BOOL)isVideo
{
// TODO : Add additional icons for slowmo, burst, etc...
if (isVideo) {
_videoIcon.hidden = NO;
_gradientView.hidden = NO;
} else {
_videoIcon.hidden = YES;
_gradientView.hidden = YES;
}
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
@end

View File

@ -1,32 +0,0 @@
//
// GMAlbumsViewController.h
// GMPhotoPicker
//
// Created by Guillermo Muntaner Perelló on 19/09/14.
// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved.
//
#import <UIKit/UIKit.h>
// Measuring IOS8 Photos APP at @2x (iPhone5s):
// The rows are 180px/90pts
// Left image border is 21px/10.5pts
// Separation between image and text is 42px/21pts (double the previouse one)
// The bigger image measures 139px/69.5pts including 1px/0.5pts white border.
// The second image measures 131px/65.6pts including 1px/0.5pts white border. Only 3px/1.5pts visible
// The third image measures 123px/61.5pts including 1px/0.5pts white border. Only 3px/1.5pts visible
static int kAlbumRowHeight = 90;
static int kAlbumLeftToImageSpace = 10;
static int kAlbumImageToTextSpace = 21;
static float const kAlbumGradientHeight = 20.0f;
static CGSize const kAlbumThumbnailSize1 = {70.0f , 70.0f};
static CGSize const kAlbumThumbnailSize2 = {66.0f , 66.0f};
static CGSize const kAlbumThumbnailSize3 = {62.0f , 62.0f};
@interface GMAlbumsViewController : UITableViewController
- (void)selectAllAlbumsCell;
@end

View File

@ -1,416 +0,0 @@
//
// GMAlbumsViewController.m
// GMPhotoPicker
//
// Created by Guillermo Muntaner Perelló on 19/09/14.
// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved.
//
#import "GMImagePickerController.h"
#import "GMAlbumsViewController.h"
#import "GMGridViewCell.h"
#import "GMGridViewController.h"
#import "GMAlbumsViewCell.h"
#include <Photos/Photos.h>
@interface GMAlbumsViewController() <PHPhotoLibraryChangeObserver>
@property (strong,nonatomic) NSArray *collectionsFetchResults;
@property (strong,nonatomic) NSArray *collectionsLocalizedTitles;
@property (strong,nonatomic) NSArray *collectionsFetchResultsAssets;
@property (strong,nonatomic) NSArray *collectionsFetchResultsTitles;
@property (nonatomic, weak) GMImagePickerController *picker;
@property (strong,nonatomic) PHCachingImageManager *imageManager;
@end
@implementation GMAlbumsViewController
- (id)init
{
if (self = [super initWithStyle:UITableViewStylePlain]) {
self.preferredContentSize = kPopoverContentSize;
}
return self;
}
static NSString *const AllPhotosReuseIdentifier = @"AllPhotosCell";
static NSString *const CollectionCellReuseIdentifier = @"CollectionCell";
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = [self.picker pickerBackgroundColor];
// Navigation bar customization
if (self.picker.customNavigationBarPrompt) {
self.navigationItem.prompt = self.picker.customNavigationBarPrompt;
}
self.imageManager = [[PHCachingImageManager alloc] init];
// Table view aspect
self.tableView.rowHeight = kAlbumRowHeight;
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
// Buttons
NSDictionary *barButtonItemAttributes = @{NSFontAttributeName: [UIFont fontWithName:self.picker.pickerFontName size:self.picker.pickerFontHeaderSize]};
NSString *cancelTitle = self.picker.customCancelButtonTitle ? self.picker.customCancelButtonTitle : NSLocalizedStringFromTableInBundle(@"picker.navigation.cancel-button", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Cancel");
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:cancelTitle
style:UIBarButtonItemStylePlain
target:self.picker
action:@selector(dismiss:)];
if (self.picker.useCustomFontForNavigationBar) {
[self.navigationItem.leftBarButtonItem setTitleTextAttributes:barButtonItemAttributes forState:UIControlStateNormal];
[self.navigationItem.leftBarButtonItem setTitleTextAttributes:barButtonItemAttributes forState:UIControlStateSelected];
}
if (self.picker.allowsMultipleSelection) {
NSString *doneTitle = self.picker.customDoneButtonTitle ? self.picker.customDoneButtonTitle : NSLocalizedStringFromTableInBundle(@"picker.navigation.done-button", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Done");
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:doneTitle
style:UIBarButtonItemStyleDone
target:self.picker
action:@selector(finishPickingAssets:)];
if (self.picker.useCustomFontForNavigationBar) {
[self.navigationItem.rightBarButtonItem setTitleTextAttributes:barButtonItemAttributes forState:UIControlStateNormal];
[self.navigationItem.rightBarButtonItem setTitleTextAttributes:barButtonItemAttributes forState:UIControlStateSelected];
}
self.navigationItem.rightBarButtonItem.enabled = (self.picker.autoDisableDoneButton ? self.picker.selectedAssets.count > 0 : TRUE);
}
// Bottom toolbar
self.toolbarItems = self.picker.toolbarItems;
// Title
if (!self.picker.title) {
self.title = NSLocalizedStringFromTableInBundle(@"picker.navigation.title", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Navigation bar default title");
} else {
self.title = self.picker.title;
}
// Fetch PHAssetCollections:
PHFetchResult *topLevelUserCollections = [PHCollectionList fetchTopLevelUserCollectionsWithOptions:nil];
PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil];
self.collectionsFetchResults = @[topLevelUserCollections, smartAlbums];
self.collectionsLocalizedTitles = @[NSLocalizedStringFromTableInBundle(@"picker.table.smart-albums-header", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Smart Albums"),NSLocalizedStringFromTableInBundle(@"picker.table.user-albums-header", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Albums")];
[self updateFetchResults];
// Register for changes
[[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self];
if ([self respondsToSelector:@selector(setEdgesForExtendedLayout:)])
{
self.edgesForExtendedLayout = UIRectEdgeNone;
}
}
- (void)dealloc
{
[[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self];
}
- (UIStatusBarStyle)preferredStatusBarStyle {
return self.picker.pickerStatusBarStyle;
}
- (void)selectAllAlbumsCell {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[self tableView:self.tableView didSelectRowAtIndexPath:indexPath];
}
-(void)updateFetchResults
{
//What I do here is fetch both the albums list and the assets of each album.
//This way I have acces to the number of items in each album, I can load the 3
//thumbnails directly and I can pass the fetched result to the gridViewController.
self.collectionsFetchResultsAssets=nil;
self.collectionsFetchResultsTitles=nil;
//Fetch PHAssetCollections:
PHFetchResult *topLevelUserCollections = [self.collectionsFetchResults objectAtIndex:0];
PHFetchResult *smartAlbums = [self.collectionsFetchResults objectAtIndex:1];
//All album: Sorted by descending creation date.
NSMutableArray *allFetchResultArray = [[NSMutableArray alloc] init];
NSMutableArray *allFetchResultLabel = [[NSMutableArray alloc] init];
{
PHFetchOptions *options = [[PHFetchOptions alloc] init];
options.predicate = [NSPredicate predicateWithFormat:@"mediaType in %@", self.picker.mediaTypes];
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
PHFetchResult *assetsFetchResult = [PHAsset fetchAssetsWithOptions:options];
[allFetchResultArray addObject:assetsFetchResult];
[allFetchResultLabel addObject:NSLocalizedStringFromTableInBundle(@"picker.table.all-photos-label", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"All photos")];
}
//User albums:
NSMutableArray *userFetchResultArray = [[NSMutableArray alloc] init];
NSMutableArray *userFetchResultLabel = [[NSMutableArray alloc] init];
for(PHCollection *collection in topLevelUserCollections)
{
if ([collection isKindOfClass:[PHAssetCollection class]])
{
PHFetchOptions *options = [[PHFetchOptions alloc] init];
options.predicate = [NSPredicate predicateWithFormat:@"mediaType in %@", self.picker.mediaTypes];
PHAssetCollection *assetCollection = (PHAssetCollection *)collection;
//Albums collections are allways PHAssetCollectionType=1 & PHAssetCollectionSubtype=2
PHFetchResult *assetsFetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:options];
[userFetchResultArray addObject:assetsFetchResult];
[userFetchResultLabel addObject:collection.localizedTitle];
}
}
//Smart albums: Sorted by descending creation date.
NSMutableArray *smartFetchResultArray = [[NSMutableArray alloc] init];
NSMutableArray *smartFetchResultLabel = [[NSMutableArray alloc] init];
for(PHCollection *collection in smartAlbums)
{
if ([collection isKindOfClass:[PHAssetCollection class]])
{
PHAssetCollection *assetCollection = (PHAssetCollection *)collection;
//Smart collections are PHAssetCollectionType=2;
if(self.picker.customSmartCollections && [self.picker.customSmartCollections containsObject:@(assetCollection.assetCollectionSubtype)])
{
PHFetchOptions *options = [[PHFetchOptions alloc] init];
options.predicate = [NSPredicate predicateWithFormat:@"mediaType in %@", self.picker.mediaTypes];
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
PHFetchResult *assetsFetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:options];
if(assetsFetchResult.count>0)
{
[smartFetchResultArray addObject:assetsFetchResult];
[smartFetchResultLabel addObject:collection.localizedTitle];
}
}
}
}
self.collectionsFetchResultsAssets= @[allFetchResultArray,smartFetchResultArray,userFetchResultArray];
self.collectionsFetchResultsTitles= @[allFetchResultLabel,smartFetchResultLabel,userFetchResultLabel];
}
#pragma mark - Accessors
- (GMImagePickerController *)picker
{
return (GMImagePickerController *)self.navigationController.parentViewController;
}
#pragma mark - Rotation
- (BOOL)shouldAutorotate
{
return YES;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskAllButUpsideDown;
}
#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return (NSInteger)self.collectionsFetchResultsAssets.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
PHFetchResult *fetchResult = self.collectionsFetchResultsAssets[(NSUInteger)section];
return (NSInteger)fetchResult.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
GMAlbumsViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[GMAlbumsViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
// Increment the cell's tag
NSInteger currentTag = cell.tag + 1;
cell.tag = currentTag;
// Set the label
cell.textLabel.font = [UIFont fontWithName:self.picker.pickerFontName size:self.picker.pickerFontHeaderSize];
cell.textLabel.text = (self.collectionsFetchResultsTitles[(NSUInteger)indexPath.section])[(NSUInteger)indexPath.row];
cell.textLabel.textColor = self.picker.pickerTextColor;
// Retrieve the pre-fetched assets for this album:
PHFetchResult *assetsFetchResult = (self.collectionsFetchResultsAssets[(NSUInteger)indexPath.section])[(NSUInteger)indexPath.row];
// Display the number of assets
if (self.picker.displayAlbumsNumberOfAssets) {
cell.detailTextLabel.font = [UIFont fontWithName:self.picker.pickerFontName size:self.picker.pickerFontNormalSize];
cell.detailTextLabel.text = [self tableCellSubtitle:assetsFetchResult];
cell.detailTextLabel.textColor = self.picker.pickerTextColor;
}
// Set the 3 images (if exists):
if ([assetsFetchResult count] > 0) {
CGFloat scale = [UIScreen mainScreen].scale;
//Compute the thumbnail pixel size:
CGSize tableCellThumbnailSize1 = CGSizeMake(kAlbumThumbnailSize1.width*scale, kAlbumThumbnailSize1.height*scale);
PHAsset *asset = assetsFetchResult[0];
[cell setVideoLayout:(asset.mediaType==PHAssetMediaTypeVideo)];
[self.imageManager requestImageForAsset:asset
targetSize:tableCellThumbnailSize1
contentMode:PHImageContentModeAspectFill
options:nil
resultHandler:^(UIImage *result, NSDictionary *info) {
if (cell.tag == currentTag) {
cell.imageView1.image = result;
}
}];
// Second & third images:
// TODO: Only preload the 3pixels height visible frame!
if ([assetsFetchResult count] > 1) {
//Compute the thumbnail pixel size:
CGSize tableCellThumbnailSize2 = CGSizeMake(kAlbumThumbnailSize2.width*scale, kAlbumThumbnailSize2.height*scale);
PHAsset *asset = assetsFetchResult[1];
[self.imageManager requestImageForAsset:asset
targetSize:tableCellThumbnailSize2
contentMode:PHImageContentModeAspectFill
options:nil
resultHandler:^(UIImage *result, NSDictionary *info) {
if (cell.tag == currentTag) {
cell.imageView2.image = result;
}
}];
} else {
cell.imageView2.image = nil;
}
if ([assetsFetchResult count] > 2) {
CGSize tableCellThumbnailSize3 = CGSizeMake(kAlbumThumbnailSize3.width*scale, kAlbumThumbnailSize3.height*scale);
PHAsset *asset = assetsFetchResult[2];
[self.imageManager requestImageForAsset:asset
targetSize:tableCellThumbnailSize3
contentMode:PHImageContentModeAspectFill
options:nil
resultHandler:^(UIImage *result, NSDictionary *info) {
if (cell.tag == currentTag) {
cell.imageView3.image = result;
}
}];
} else {
cell.imageView3.image = nil;
}
} else {
[cell setVideoLayout:NO];
cell.imageView3.image = [UIImage imageNamed:@"GMEmptyFolder"];
cell.imageView2.image = [UIImage imageNamed:@"GMEmptyFolder"];
cell.imageView1.image = [UIImage imageNamed:@"GMEmptyFolder"];
}
return cell;
}
#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
// Init the GMGridViewController
GMGridViewController *gridViewController = [[GMGridViewController alloc] initWithPicker:[self picker]];
// Set the title
gridViewController.title = cell.textLabel.text;
// Use the prefetched assets!
gridViewController.assetsFetchResults = [[_collectionsFetchResultsAssets objectAtIndex:(NSUInteger)indexPath.section] objectAtIndex:(NSUInteger)indexPath.row];
// Remove selection so it looks better on slide in
[tableView deselectRowAtIndexPath:indexPath animated:true];
// Push GMGridViewController
[self.navigationController pushViewController:gridViewController animated:YES];
}
-(void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section
{
UITableViewHeaderFooterView *header = (UITableViewHeaderFooterView *)view;
// header.contentView.backgroundColor = [UIColor clearColor];
// header.backgroundView.backgroundColor = [UIColor clearColor];
// Default is a bold font, but keep this styled as a normal font
header.textLabel.font = [UIFont fontWithName:self.picker.pickerFontName size:self.picker.pickerFontNormalSize];
header.textLabel.textColor = self.picker.pickerTextColor;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
//Tip: Returning nil hides the section header!
NSString *title = nil;
if (section > 0) {
// Only show title for non-empty sections:
PHFetchResult *fetchResult = self.collectionsFetchResultsAssets[(NSUInteger)section];
if (fetchResult.count > 0) {
title = self.collectionsLocalizedTitles[(NSUInteger)(section - 1)];
}
}
return title;
}
#pragma mark - PHPhotoLibraryChangeObserver
- (void)photoLibraryDidChange:(PHChange *)changeInstance
{
// Call might come on any background queue. Re-dispatch to the main queue to handle it.
dispatch_async(dispatch_get_main_queue(), ^{
NSMutableArray *updatedCollectionsFetchResults = nil;
for (PHFetchResult *collectionsFetchResult in self.collectionsFetchResults) {
PHFetchResultChangeDetails *changeDetails = [changeInstance changeDetailsForFetchResult:collectionsFetchResult];
if (changeDetails) {
if (!updatedCollectionsFetchResults) {
updatedCollectionsFetchResults = [self.collectionsFetchResults mutableCopy];
}
[updatedCollectionsFetchResults replaceObjectAtIndex:[self.collectionsFetchResults indexOfObject:collectionsFetchResult] withObject:[changeDetails fetchResultAfterChanges]];
}
}
// This only affects to changes in albums level (add/remove/edit album)
if (updatedCollectionsFetchResults) {
self.collectionsFetchResults = updatedCollectionsFetchResults;
}
// However, we want to update if photos are added, so the counts of items & thumbnails are updated too.
// Maybe some checks could be done here , but for now is OKey.
[self updateFetchResults];
[self.tableView reloadData];
});
}
#pragma mark - Cell Subtitle
- (NSString *)tableCellSubtitle:(PHFetchResult*)assetsFetchResult
{
// Just return the number of assets. Album app does this:
return [NSString stringWithFormat:@"%ld", (long)[assetsFetchResult count]];
}
@end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,32 +0,0 @@
//
// GMGridViewCell.h
// GMPhotoPicker
//
// Created by Guillermo Muntaner Perelló on 19/09/14.
// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved.
//
#include <UIKit/UIKit.h>
#include <Photos/Photos.h>
@interface GMGridViewCell : UICollectionViewCell
@property (nonatomic, strong) PHAsset *asset;
//The imageView
@property (nonatomic, strong) UIImageView *imageView;
//Video additional information
@property (nonatomic, strong) UIImageView *videoIcon;
@property (nonatomic, strong) UILabel *videoDuration;
@property (nonatomic, strong) UIView *gradientView;
@property (nonatomic, strong) CAGradientLayer *gradient;
//Selection overlay
@property (nonatomic) BOOL shouldShowSelection;
@property (nonatomic, strong) UIView *coverView;
@property (nonatomic, strong) UIButton *selectedButton;
@property (nonatomic, assign, getter = isEnabled) BOOL enabled;
- (void)bind:(PHAsset *)asset;
@end

View File

@ -1,176 +0,0 @@
//
// GMGridViewCell.m
// GMPhotoPicker
//
// Created by Guillermo Muntaner Perelló on 19/09/14.
// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved.
//
#import "GMGridViewCell.h"
@interface GMGridViewCell ()
@end
@implementation GMGridViewCell
static UIFont *titleFont;
static CGFloat titleHeight;
static UIImage *videoIcon;
static UIColor *titleColor;
static UIImage *checkedIcon;
static UIColor *selectedColor;
static UIColor *disabledColor;
+ (void)initialize
{
titleFont = [UIFont systemFontOfSize:12];
titleHeight = 20.0f;
videoIcon = [UIImage imageNamed:@"GMImagePickerVideo"];
titleColor = [UIColor whiteColor];
checkedIcon = [UIImage imageNamed:@"CTAssetsPickerChecked"];
selectedColor = [UIColor colorWithWhite:1 alpha:0.3];
disabledColor = [UIColor colorWithWhite:1 alpha:0.9];
}
- (void)awakeFromNib
{
[super awakeFromNib];
self.contentView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
self.contentView.translatesAutoresizingMaskIntoConstraints = YES;
}
- (id)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
self.opaque = NO;
self.enabled = YES;
CGFloat cellSize = self.contentView.bounds.size.width;
// The image view
_imageView = [UIImageView new];
_imageView.frame = CGRectMake(0, 0, cellSize, cellSize);
_imageView.contentMode = UIViewContentModeScaleAspectFill;
/*if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
{
_imageView.contentMode = UIViewContentModeScaleAspectFit;
}
else
{
_imageView.contentMode = UIViewContentModeScaleAspectFill;
}*/
_imageView.clipsToBounds = YES;
_imageView.translatesAutoresizingMaskIntoConstraints = NO;
_imageView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
[self addSubview:_imageView];
// The video gradient, label & icon
float x_offset = 4.0f;
UIColor *topGradient = [UIColor colorWithRed:0.00 green:0.00 blue:0.00 alpha:0.0];
UIColor *botGradient = [UIColor colorWithRed:0.00 green:0.00 blue:0.00 alpha:0.8];
_gradientView = [[UIView alloc] initWithFrame: CGRectMake(0.0f, self.bounds.size.height-titleHeight, self.bounds.size.width, titleHeight)];
_gradient = [CAGradientLayer layer];
_gradient.frame = _gradientView.bounds;
_gradient.colors = [NSArray arrayWithObjects:(id)[topGradient CGColor], (id)[botGradient CGColor], nil];
[_gradientView.layer insertSublayer:_gradient atIndex:0];
_gradientView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
_gradientView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_gradientView];
_gradientView.hidden = YES;
_videoIcon = [UIImageView new];
_videoIcon.frame = CGRectMake(x_offset, self.bounds.size.height-titleHeight, self.bounds.size.width-2*x_offset, titleHeight);
_videoIcon.contentMode = UIViewContentModeLeft;
_videoIcon.image = [UIImage imageNamed:@"GMVideoIcon" inBundle:[NSBundle bundleForClass:GMGridViewCell.class] compatibleWithTraitCollection:nil];
_videoIcon.translatesAutoresizingMaskIntoConstraints = NO;
_videoIcon.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth;
[self addSubview:_videoIcon];
_videoIcon.hidden = YES;
_videoDuration = [UILabel new];
_videoDuration.font = titleFont;
_videoDuration.textColor = titleColor;
_videoDuration.textAlignment = NSTextAlignmentRight;
_videoDuration.frame = CGRectMake(x_offset, self.bounds.size.height-titleHeight, self.bounds.size.width-2*x_offset, titleHeight);
_videoDuration.contentMode = UIViewContentModeRight;
_videoDuration.translatesAutoresizingMaskIntoConstraints = NO;
_videoDuration.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth;
[self addSubview:_videoDuration];
_videoDuration.hidden = YES;
// Selection overlay & icon
_coverView = [[UIView alloc] initWithFrame:self.bounds];
_coverView.translatesAutoresizingMaskIntoConstraints = NO;
_coverView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
_coverView.backgroundColor = [UIColor colorWithRed:0.24 green:0.47 blue:0.85 alpha:0.6];
[self addSubview:_coverView];
_coverView.hidden = YES;
_selectedButton = [UIButton buttonWithType:UIButtonTypeCustom];
_selectedButton.frame = CGRectMake(2*self.bounds.size.width/3, 0*self.bounds.size.width/3, self.bounds.size.width/3, self.bounds.size.width/3);
_selectedButton.contentMode = UIViewContentModeTopRight;
_selectedButton.adjustsImageWhenHighlighted = NO;
[_selectedButton setImage:nil forState:UIControlStateNormal];
_selectedButton.translatesAutoresizingMaskIntoConstraints = NO;
_selectedButton.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
[_selectedButton setImage:[UIImage imageNamed:@"GMSelected" inBundle:[NSBundle bundleForClass:GMGridViewCell.class] compatibleWithTraitCollection:nil] forState:UIControlStateSelected];
_selectedButton.hidden = NO;
_selectedButton.userInteractionEnabled = NO;
[self addSubview:_selectedButton];
}
// Note: the views above are created in case this is toggled per cell, on the fly, etc.!
self.shouldShowSelection = YES;
return self;
}
// Required to resize the CAGradientLayer because it does not support auto resizing.
- (void)layoutSubviews {
[super layoutSubviews];
_gradient.frame = _gradientView.bounds;
}
- (void)bind:(PHAsset *)asset
{
self.asset = asset;
if (self.asset.mediaType == PHAssetMediaTypeVideo) {
_videoIcon.hidden = NO;
_videoDuration.hidden = NO;
_gradientView.hidden = NO;
_videoDuration.text = [self getDurationWithFormat:self.asset.duration];
} else {
_videoIcon.hidden = YES;
_videoDuration.hidden = YES;
_gradientView.hidden = YES;
}
}
// Override setSelected
- (void)setSelected:(BOOL)selected
{
[super setSelected:selected];
if (!self.shouldShowSelection) {
return;
}
_coverView.hidden = !selected;
_selectedButton.selected = selected;
}
-(NSString*)getDurationWithFormat:(NSTimeInterval)duration
{
NSInteger ti = (NSInteger)duration;
NSInteger seconds = ti % 60;
NSInteger minutes = (ti / 60) % 60;
//NSInteger hours = (ti / 3600);
return [NSString stringWithFormat:@"%02ld:%02ld", (long)minutes, (long)seconds];
}
@end

View File

@ -1,21 +0,0 @@
//
// GMGridViewController.h
// GMPhotoPicker
//
// Created by Guillermo Muntaner Perelló on 19/09/14.
// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved.
//
#import "GMImagePickerController.h"
#include <UIKit/UIKit.h>
#include <Photos/Photos.h>
@interface GMGridViewController : UICollectionViewController
@property (strong,nonatomic) PHFetchResult *assetsFetchResults;
-(id)initWithPicker:(GMImagePickerController *)picker;
@end

View File

@ -1,611 +0,0 @@
//
// GMGridViewController.m
// GMPhotoPicker
//
// Created by Guillermo Muntaner Perelló on 19/09/14.
// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved.
//
#import "GMGridViewController.h"
#import "GMImagePickerController.h"
#import "GMAlbumsViewController.h"
#import "GMGridViewCell.h"
#include <Photos/Photos.h>
//Helper methods
@implementation NSIndexSet (Convenience)
- (NSArray *)aapl_indexPathsFromIndexesWithSection:(NSUInteger)section {
NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:self.count];
[self enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
[indexPaths addObject:[NSIndexPath indexPathForItem:(NSInteger)idx inSection:(NSInteger)section]];
}];
return indexPaths;
}
@end
@implementation UICollectionView (Convenience)
- (NSArray *)aapl_indexPathsForElementsInRect:(CGRect)rect {
NSArray *allLayoutAttributes = [self.collectionViewLayout layoutAttributesForElementsInRect:rect];
if (allLayoutAttributes.count == 0) { return nil; }
NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:allLayoutAttributes.count];
for (UICollectionViewLayoutAttributes *layoutAttributes in allLayoutAttributes) {
NSIndexPath *indexPath = layoutAttributes.indexPath;
[indexPaths addObject:indexPath];
}
return indexPaths;
}
@end
@interface GMImagePickerController ()
- (void)finishPickingAssets:(id)sender;
- (void)dismiss:(id)sender;
- (NSString *)toolbarTitle;
- (UIView *)noAssetsView;
@end
@interface GMGridViewController () <PHPhotoLibraryChangeObserver>
@property (nonatomic, weak) GMImagePickerController *picker;
@property (strong,nonatomic) PHCachingImageManager *imageManager;
@property (assign, nonatomic) CGRect previousPreheatRect;
@end
static CGSize AssetGridThumbnailSize;
NSString * const GMGridViewCellIdentifier = @"GMGridViewCellIdentifier";
@implementation GMGridViewController
{
CGFloat screenWidth;
CGFloat screenHeight;
UICollectionViewFlowLayout *portraitLayout;
UICollectionViewFlowLayout *landscapeLayout;
}
-(id)initWithPicker:(GMImagePickerController *)picker
{
//Custom init. The picker contains custom information to create the FlowLayout
self.picker = picker;
//Ipad popover is not affected by rotation!
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
{
screenWidth = CGRectGetWidth(picker.view.bounds);
screenHeight = CGRectGetHeight(picker.view.bounds);
}
else
{
if(UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation))
{
screenHeight = CGRectGetWidth(picker.view.bounds);
screenWidth = CGRectGetHeight(picker.view.bounds);
}
else
{
screenWidth = CGRectGetWidth(picker.view.bounds);
screenHeight = CGRectGetHeight(picker.view.bounds);
}
}
UICollectionViewFlowLayout *layout = [self collectionViewFlowLayoutForOrientation:[UIApplication sharedApplication].statusBarOrientation];
if (self = [super initWithCollectionViewLayout:layout])
{
//Compute the thumbnail pixel size:
CGFloat scale = [UIScreen mainScreen].scale;
//NSLog(@"This is @%fx scale device", scale);
if(scale >= 3)
{
scale = 2;
}
AssetGridThumbnailSize = CGSizeMake(layout.itemSize.width * scale, layout.itemSize.height * scale);
self.collectionView.allowsMultipleSelection = picker.allowsMultipleSelection;
[self.collectionView registerClass:GMGridViewCell.class
forCellWithReuseIdentifier:GMGridViewCellIdentifier];
self.preferredContentSize = kPopoverContentSize;
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self setupViews];
// Navigation bar customization
if (self.picker.customNavigationBarPrompt) {
self.navigationItem.prompt = self.picker.customNavigationBarPrompt;
}
self.imageManager = [[PHCachingImageManager alloc] init];
[self resetCachedAssets];
[[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self];
if ([self respondsToSelector:@selector(setEdgesForExtendedLayout:)])
{
self.edgesForExtendedLayout = UIRectEdgeNone;
}
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self setupButtons];
[self setupToolbar];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self updateCachedAssets];
}
- (void)dealloc
{
[self resetCachedAssets];
[[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self];
}
- (UIStatusBarStyle)preferredStatusBarStyle {
return self.picker.pickerStatusBarStyle;
}
#pragma mark - Rotation
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
return;
}
UICollectionViewFlowLayout *layout = [self collectionViewFlowLayoutForOrientation:toInterfaceOrientation];
//Update the AssetGridThumbnailSize:
CGFloat scale = [UIScreen mainScreen].scale;
AssetGridThumbnailSize = CGSizeMake(layout.itemSize.width * scale, layout.itemSize.height * scale);
[self resetCachedAssets];
//This is optional. Reload visible thumbnails:
for (GMGridViewCell *cell in [self.collectionView visibleCells]) {
NSInteger currentTag = cell.tag;
[self.imageManager requestImageForAsset:cell.asset
targetSize:AssetGridThumbnailSize
contentMode:PHImageContentModeAspectFill
options:nil
resultHandler:^(UIImage *result, NSDictionary *info)
{
// Only update the thumbnail if the cell tag hasn't changed. Otherwise, the cell has been re-used.
if (cell.tag == currentTag) {
[cell.imageView setImage:result];
}
}];
}
[self.collectionView setCollectionViewLayout:layout animated:YES];
}
#pragma mark - Setup
- (void)setupViews
{
self.collectionView.backgroundColor = [UIColor clearColor];
self.view.backgroundColor = [self.picker pickerBackgroundColor];
}
- (void)setupButtons
{
if (self.picker.allowsMultipleSelection) {
NSString *doneTitle = self.picker.customDoneButtonTitle ? self.picker.customDoneButtonTitle : NSLocalizedStringFromTableInBundle(@"picker.navigation.done-button", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Done");
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:doneTitle
style:UIBarButtonItemStyleDone
target:self.picker
action:@selector(finishPickingAssets:)];
self.navigationItem.rightBarButtonItem.enabled = (self.picker.autoDisableDoneButton ? self.picker.selectedAssets.count > 0 : TRUE);
} else {
NSString *cancelTitle = self.picker.customCancelButtonTitle ? self.picker.customCancelButtonTitle : NSLocalizedStringFromTableInBundle(@"picker.navigation.cancel-button", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Cancel");
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:cancelTitle
style:UIBarButtonItemStyleDone
target:self.picker
action:@selector(dismiss:)];
}
if (self.picker.useCustomFontForNavigationBar) {
if (self.picker.useCustomFontForNavigationBar) {
NSDictionary* barButtonItemAttributes = @{NSFontAttributeName: [UIFont fontWithName:self.picker.pickerFontName size:self.picker.pickerFontHeaderSize]};
[self.navigationItem.rightBarButtonItem setTitleTextAttributes:barButtonItemAttributes forState:UIControlStateNormal];
[self.navigationItem.rightBarButtonItem setTitleTextAttributes:barButtonItemAttributes forState:UIControlStateSelected];
}
}
}
- (void)setupToolbar
{
self.toolbarItems = self.picker.toolbarItems;
}
#pragma mark - Collection View Layout
- (UICollectionViewFlowLayout *)collectionViewFlowLayoutForOrientation:(UIInterfaceOrientation)orientation
{
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
{
if(!portraitLayout)
{
portraitLayout = [[UICollectionViewFlowLayout alloc] init];
portraitLayout.minimumInteritemSpacing = self.picker.minimumInteritemSpacing;
int cellTotalUsableWidth = (int)(screenWidth - (self.picker.colsInPortrait-1)*self.picker.minimumInteritemSpacing);
portraitLayout.itemSize = CGSizeMake(cellTotalUsableWidth/self.picker.colsInPortrait, cellTotalUsableWidth/self.picker.colsInPortrait);
double cellTotalUsedWidth = (double)portraitLayout.itemSize.width*self.picker.colsInPortrait;
double spaceTotalWidth = (double)screenWidth-cellTotalUsedWidth;
double spaceWidth = spaceTotalWidth/(double)(self.picker.colsInPortrait-1);
portraitLayout.minimumLineSpacing = spaceWidth;
}
return portraitLayout;
}
else
{
if(UIInterfaceOrientationIsLandscape(orientation))
{
if(!landscapeLayout)
{
landscapeLayout = [[UICollectionViewFlowLayout alloc] init];
landscapeLayout.minimumInteritemSpacing = self.picker.minimumInteritemSpacing;
int cellTotalUsableWidth = (int)(screenHeight - (self.picker.colsInLandscape-1)*self.picker.minimumInteritemSpacing);
landscapeLayout.itemSize = CGSizeMake(cellTotalUsableWidth/self.picker.colsInLandscape, cellTotalUsableWidth/self.picker.colsInLandscape);
double cellTotalUsedWidth = (double)landscapeLayout.itemSize.width*self.picker.colsInLandscape;
double spaceTotalWidth = (double)screenHeight-cellTotalUsedWidth;
double spaceWidth = spaceTotalWidth/(double)(self.picker.colsInLandscape-1);
landscapeLayout.minimumLineSpacing = spaceWidth;
}
return landscapeLayout;
}
else
{
if(!portraitLayout)
{
portraitLayout = [[UICollectionViewFlowLayout alloc] init];
portraitLayout.minimumInteritemSpacing = self.picker.minimumInteritemSpacing;
int cellTotalUsableWidth = (int)(screenWidth - (self.picker.colsInPortrait-1) * self.picker.minimumInteritemSpacing);
portraitLayout.itemSize = CGSizeMake(cellTotalUsableWidth/self.picker.colsInPortrait, cellTotalUsableWidth/self.picker.colsInPortrait);
double cellTotalUsedWidth = (double)portraitLayout.itemSize.width*self.picker.colsInPortrait;
double spaceTotalWidth = (double)screenWidth-cellTotalUsedWidth;
double spaceWidth = spaceTotalWidth/(double)(self.picker.colsInPortrait-1);
portraitLayout.minimumLineSpacing = spaceWidth;
}
return portraitLayout;
}
}
}
#pragma mark - Collection View Data Source
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
GMGridViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:GMGridViewCellIdentifier
forIndexPath:indexPath];
// Increment the cell's tag
NSInteger currentTag = cell.tag + 1;
cell.tag = currentTag;
PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item];
/*if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
{
NSLog(@"Image manager: Requesting FIT image for iPad");
[self.imageManager requestImageForAsset:asset
targetSize:AssetGridThumbnailSize
contentMode:PHImageContentModeAspectFit
options:nil
resultHandler:^(UIImage *result, NSDictionary *info) {
// Only update the thumbnail if the cell tag hasn't changed. Otherwise, the cell has been re-used.
if (cell.tag == currentTag) {
[cell.imageView setImage:result];
}
}];
}
else*/
{
//NSLog(@"Image manager: Requesting FILL image for iPhone");
[self.imageManager requestImageForAsset:asset
targetSize:AssetGridThumbnailSize
contentMode:PHImageContentModeAspectFill
options:nil
resultHandler:^(UIImage *result, NSDictionary *info) {
// Only update the thumbnail if the cell tag hasn't changed. Otherwise, the cell has been re-used.
if (cell.tag == currentTag) {
[cell.imageView setImage:result];
}
}];
}
[cell bind:asset];
cell.shouldShowSelection = self.picker.allowsMultipleSelection;
// Optional protocol to determine if some kind of assets can't be selected (pej long videos, etc...)
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldEnableAsset:)]) {
cell.enabled = [self.picker.delegate assetsPickerController:self.picker shouldEnableAsset:asset];
} else {
cell.enabled = YES;
}
// Setting `selected` property blocks further deselection. Have to call selectItemAtIndexPath too. ( ref: http://stackoverflow.com/a/17812116/1648333 )
if ([self.picker.selectedAssets containsObject:asset]) {
cell.selected = YES;
[collectionView selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone];
} else {
cell.selected = NO;
}
return cell;
}
#pragma mark - Collection View Delegate
- (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item];
GMGridViewCell *cell = (GMGridViewCell *)[collectionView cellForItemAtIndexPath:indexPath];
if (!cell.isEnabled) {
return NO;
} else if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldSelectAsset:)]) {
return [self.picker.delegate assetsPickerController:self.picker shouldSelectAsset:asset];
} else {
return YES;
}
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item];
[self.picker selectAsset:asset];
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didSelectAsset:)]) {
[self.picker.delegate assetsPickerController:self.picker didSelectAsset:asset];
}
}
- (BOOL)collectionView:(UICollectionView *)collectionView shouldDeselectItemAtIndexPath:(NSIndexPath *)indexPath
{
PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item];
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldDeselectAsset:)]) {
return [self.picker.delegate assetsPickerController:self.picker shouldDeselectAsset:asset];
} else {
return YES;
}
}
- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath
{
PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item];
[self.picker deselectAsset:asset];
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didDeselectAsset:)]) {
[self.picker.delegate assetsPickerController:self.picker didDeselectAsset:asset];
}
}
- (BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath
{
PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item];
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldHighlightAsset:)]) {
return [self.picker.delegate assetsPickerController:self.picker shouldHighlightAsset:asset];
} else {
return YES;
}
}
- (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath
{
PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item];
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didHighlightAsset:)]) {
[self.picker.delegate assetsPickerController:self.picker didHighlightAsset:asset];
}
}
- (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath
{
PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item];
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didUnhighlightAsset:)]) {
[self.picker.delegate assetsPickerController:self.picker didUnhighlightAsset:asset];
}
}
#pragma mark - UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
NSInteger count = (NSInteger)self.assetsFetchResults.count;
return count;
}
#pragma mark - PHPhotoLibraryChangeObserver
- (void)photoLibraryDidChange:(PHChange *)changeInstance
{
// Call might come on any background queue. Re-dispatch to the main queue to handle it.
dispatch_async(dispatch_get_main_queue(), ^{
// check if there are changes to the assets (insertions, deletions, updates)
PHFetchResultChangeDetails *collectionChanges = [changeInstance changeDetailsForFetchResult:self.assetsFetchResults];
if (collectionChanges) {
// get the new fetch result
self.assetsFetchResults = [collectionChanges fetchResultAfterChanges];
UICollectionView *collectionView = self.collectionView;
if (![collectionChanges hasIncrementalChanges] || [collectionChanges hasMoves]) {
// we need to reload all if the incremental diffs are not available
[collectionView reloadData];
} else {
// if we have incremental diffs, tell the collection view to animate insertions and deletions
[collectionView performBatchUpdates:^{
NSIndexSet *removedIndexes = [collectionChanges removedIndexes];
if ([removedIndexes count]) {
[collectionView deleteItemsAtIndexPaths:[removedIndexes aapl_indexPathsFromIndexesWithSection:0]];
}
NSIndexSet *insertedIndexes = [collectionChanges insertedIndexes];
if ([insertedIndexes count]) {
[collectionView insertItemsAtIndexPaths:[insertedIndexes aapl_indexPathsFromIndexesWithSection:0]];
if (self.picker.showCameraButton && self.picker.autoSelectCameraImages) {
for (NSIndexPath *path in [insertedIndexes aapl_indexPathsFromIndexesWithSection:0]) {
[self collectionView:collectionView didSelectItemAtIndexPath:path];
}
}
}
NSIndexSet *changedIndexes = [collectionChanges changedIndexes];
if ([changedIndexes count]) {
[collectionView reloadItemsAtIndexPaths:[changedIndexes aapl_indexPathsFromIndexesWithSection:0]];
}
} completion:NULL];
}
[self resetCachedAssets];
}
});
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[self updateCachedAssets];
}
#pragma mark - Asset Caching
- (void)resetCachedAssets
{
[self.imageManager stopCachingImagesForAllAssets];
self.previousPreheatRect = CGRectZero;
}
- (void)updateCachedAssets
{
BOOL isViewVisible = [self isViewLoaded] && [[self view] window] != nil;
if (!isViewVisible) { return; }
// The preheat window is twice the height of the visible rect
CGRect preheatRect = self.collectionView.bounds;
preheatRect = CGRectInset(preheatRect, 0.0f, -0.5f * CGRectGetHeight(preheatRect));
// If scrolled by a "reasonable" amount...
CGFloat delta = ABS(CGRectGetMidY(preheatRect) - CGRectGetMidY(self.previousPreheatRect));
if (delta > CGRectGetHeight(self.collectionView.bounds) / 3.0f) {
// Compute the assets to start caching and to stop caching.
NSMutableArray *addedIndexPaths = [NSMutableArray array];
NSMutableArray *removedIndexPaths = [NSMutableArray array];
[self computeDifferenceBetweenRect:self.previousPreheatRect andRect:preheatRect removedHandler:^(CGRect removedRect) {
NSArray *indexPaths = [self.collectionView aapl_indexPathsForElementsInRect:removedRect];
[removedIndexPaths addObjectsFromArray:indexPaths];
} addedHandler:^(CGRect addedRect) {
NSArray *indexPaths = [self.collectionView aapl_indexPathsForElementsInRect:addedRect];
[addedIndexPaths addObjectsFromArray:indexPaths];
}];
NSArray *assetsToStartCaching = [self assetsAtIndexPaths:addedIndexPaths];
NSArray *assetsToStopCaching = [self assetsAtIndexPaths:removedIndexPaths];
[self.imageManager startCachingImagesForAssets:assetsToStartCaching
targetSize:AssetGridThumbnailSize
contentMode:PHImageContentModeAspectFill
options:nil];
[self.imageManager stopCachingImagesForAssets:assetsToStopCaching
targetSize:AssetGridThumbnailSize
contentMode:PHImageContentModeAspectFill
options:nil];
self.previousPreheatRect = preheatRect;
}
}
- (void)computeDifferenceBetweenRect:(CGRect)oldRect andRect:(CGRect)newRect removedHandler:(void (^)(CGRect removedRect))removedHandler addedHandler:(void (^)(CGRect addedRect))addedHandler
{
if (CGRectIntersectsRect(newRect, oldRect)) {
CGFloat oldMaxY = CGRectGetMaxY(oldRect);
CGFloat oldMinY = CGRectGetMinY(oldRect);
CGFloat newMaxY = CGRectGetMaxY(newRect);
CGFloat newMinY = CGRectGetMinY(newRect);
if (newMaxY > oldMaxY) {
CGRect rectToAdd = CGRectMake(newRect.origin.x, oldMaxY, newRect.size.width, (newMaxY - oldMaxY));
addedHandler(rectToAdd);
}
if (oldMinY > newMinY) {
CGRect rectToAdd = CGRectMake(newRect.origin.x, newMinY, newRect.size.width, (oldMinY - newMinY));
addedHandler(rectToAdd);
}
if (newMaxY < oldMaxY) {
CGRect rectToRemove = CGRectMake(newRect.origin.x, newMaxY, newRect.size.width, (oldMaxY - newMaxY));
removedHandler(rectToRemove);
}
if (oldMinY < newMinY) {
CGRect rectToRemove = CGRectMake(newRect.origin.x, oldMinY, newRect.size.width, (newMinY - oldMinY));
removedHandler(rectToRemove);
}
} else {
addedHandler(newRect);
removedHandler(oldRect);
}
}
- (NSArray *)assetsAtIndexPaths:(NSArray *)indexPaths
{
if (indexPaths.count == 0) { return nil; }
NSMutableArray *assets = [NSMutableArray arrayWithCapacity:indexPaths.count];
for (NSIndexPath *indexPath in indexPaths) {
PHAsset *asset = self.assetsFetchResults[(NSUInteger)indexPath.item];
[assets addObject:asset];
}
return assets;
}
@end

View File

@ -1,24 +0,0 @@
//
// GMImagePicker.h
// GMImagePicker
//
// Created by Shadowfacts on 1/14/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
#import <UIKit/UIKit.h>
//! Project version number for GMImagePicker.
FOUNDATION_EXPORT double GMImagePickerVersionNumber;
//! Project version string for GMImagePicker.
FOUNDATION_EXPORT const unsigned char GMImagePickerVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <GMImagePicker/PublicHeader.h>
#import <GMImagePicker/GMImagePickerController.h>
#import <GMImagePicker/GMAlbumsViewCell.h>
#import <GMImagePicker/GMAlbumsViewController.h>
#import <GMImagePicker/GMGridViewCell.h>
#import <GMImagePicker/GMGridViewController.h>

View File

@ -1,332 +0,0 @@
//
// GMImagePickerController.h
// GMPhotoPicker
//
// Created by Guillermo Muntaner Perelló on 19/09/14.
// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved.
//
#import <UIKit/UIKit.h>
#import <Photos/Photos.h>
//This is the default image picker size!
//static CGSize const kPopoverContentSize = {320, 480};
//However, the iPad is 1024x768 so it can allow popups up to 768!
static CGSize const kPopoverContentSize = {480, 720};
@protocol GMImagePickerControllerDelegate;
/**
* A controller that allows picking multiple photos and videos from user's photo library.
*/
@interface GMImagePickerController : UIViewController
/**
* The assets pickers delegate object.
*/
@property (nonatomic, weak) id <GMImagePickerControllerDelegate> delegate;
/**
* It contains the selected `PHAsset` objects. The order of the objects is the selection order.
*
* You can add assets before presenting the picker to show the user some preselected assets.
*/
@property (nonatomic, strong) NSMutableArray *selectedAssets;
/** UI Customizations **/
/**
* Determines which smart collections are displayed (int array of enum: PHAssetCollectionSubtypeSmartAlbum)
* The default smart collections are:
* - Favorites
* - RecentlyAdded
* - Videos
* - SlomoVideos
* - Timelapses
* - Bursts
* - Panoramas
*/
@property (nonatomic, strong) NSArray* customSmartCollections;
/**
* Determines which media types are allowed (int array of enum: PHAssetMediaType)
* This defaults to all media types (view, audio and images)
* This can override customSmartCollections behavior (ie, remove video-only smart collections)
*/
@property (nonatomic, strong) NSArray* mediaTypes;
/**
* If set, it displays a this string instead of the localised default of "Done" on the done button. Note also that this
* is not used when a single selection is active since the selection of the chosen photo closes the VC thus rendering
* the button pointless.
*/
@property (nonatomic) NSString* customDoneButtonTitle;
/**
* If set, it displays this string instead of the localised default of "Cancel" on the cancel button
*/
@property (nonatomic) NSString* customCancelButtonTitle;
/**
* If set, it displays a prompt in the navigation bar
*/
@property (nonatomic) NSString* customNavigationBarPrompt;
/**
* Determines whether or not a toolbar with info about user selection is shown.
* The InfoToolbar is visible by default.
*/
@property (nonatomic) BOOL displaySelectionInfoToolbar;
/**
* Determines whether or not the number of assets is shown in the Album list.
* The number of assets is visible by default.
*/
@property (nonatomic, assign) BOOL displayAlbumsNumberOfAssets;
/**
* Automatically disables the "Done" button if nothing is selected. Defaults to YES.
*/
@property (nonatomic, assign) BOOL autoDisableDoneButton;
/**
* Use the picker either for miltiple image selections, or just a single selection. In the case of a single selection
* the VC is closed on selection so the Done button is neither displayed or used. Default is YES.
*/
@property (nonatomic, assign) BOOL allowsMultipleSelection;
/**
* In the case where allowsMultipleSelection = NO, set this to YES to have the user confirm their selection. Default is NO.
*/
@property (nonatomic, assign) BOOL confirmSingleSelection;
/**
* If set, it displays this string (if confirmSingleSelection = YES) instead of the localised default.
*/
@property (nonatomic) NSString *confirmSingleSelectionPrompt;
/**
* True to always show the toolbar, with a camera button allowing new photos to be taken. False to auto show/hide the
* toolbar, and have no camera button. Default is false. If true, this renders displaySelectionInfoToolbar a no-op.
*/
@property (nonatomic, assign) BOOL showCameraButton;
/**
* True to auto select the image(s) taken with the camera if showCameraButton = YES. In the case of allowsMultipleSelection = YES,
* this will trigger the selection handler too.
*/
@property (nonatomic, assign) BOOL autoSelectCameraImages;
/**
* If set, the user is allowed to edit captured still images
*/
@property (nonatomic, assign) BOOL allowsEditingCameraImages;
/**
* Grid customizations:
*
* - colsInPortrait: Number of columns in portrait (3 by default)
* - colsInLandscape: Number of columns in landscape (5 by default)
* - minimumInteritemSpacing: Horizontal and vertical minimum space between grid cells (2.0 by default)
*/
@property (nonatomic) NSInteger colsInPortrait;
@property (nonatomic) NSInteger colsInLandscape;
@property (nonatomic) double minimumInteritemSpacing;
/**
* UI customizations:
*
* - pickerBackgroundColor: The colour for all backgrounds; behind the table and cells. Defaults to [UIColor whiteColor]
* - pickerTextColor: The color for text in the views. This needs to work with pickerBackgroundColor! Default of darkTextColor
* - toolbarBackgroundColor: The background color of the toolbar. Defaults to nil.
* - toolbarBarTintColor: The color for the background tint of the toolbar. Defaults to nil.
* - toolbarTextColor: The color of the text on the toolbar
* - toolbarTintColor: The tint colour used for any buttons on the toolbar
* - navigationBarBackgroundColor: The background of the navigation bar. Defaults to nil.
* - navigationBarBarTintColor: The color for the background tint of the navigation bar. Defaults to nil.
* - navigationBarTextColor: The color for the text in the navigation bar. Defaults to [UIColor darkTextColor]
* - navigationBarTintColor: The tint color used for any buttons on the navigation Bar
* - pickerFontName: The font to use everywhere. Defaults to HelveticaNeue. It is advised if you set this to check, and possibly set, appropriately the custom font sizes. For font information, check http://www.iosfonts.com/
* - pickerFontName: The font to use everywhere. Defaults to HelveticaNeue-Bold. It is advised if you set this to check, and possibly set, appropriately the custom font sizes.
* - pickerFontNormalSize: The size of the custom font used in most places. Defaults to 14.0f
* - pickerFontHeaderSize: The size of the custom font for album names. Defaults to 17.0f
* - pickerStatusBarsStyle: On iPhones this will matter if custom navigation bar colours are being used. Defaults to UIStatusBarStyleDefault
* - useCustomFontForNavigationBar: True to use the custom font (or it's default) in the navigation bar, false to leave to iOS Defaults.
* - arrangeSmartCollectionsFirst: True will put the users smart collections above their albums, false will set it opposite. Default is NO.
*/
@property (nonatomic, strong) UIColor *pickerBackgroundColor;
@property (nonatomic, strong) UIColor *pickerTextColor;
@property (nonatomic, strong) UIColor *toolbarBackgroundColor;
@property (nonatomic, strong) UIColor *toolbarBarTintColor;
@property (nonatomic, strong) UIColor *toolbarTextColor;
@property (nonatomic, strong) UIColor *toolbarTintColor;
@property (nonatomic, strong) UIColor *navigationBarBackgroundColor;
@property (nonatomic, strong) UIColor *navigationBarBarTintColor;
@property (nonatomic, strong) UIColor *navigationBarTextColor;
@property (nonatomic, strong) UIColor *navigationBarTintColor;
@property (nonatomic, strong) NSString *pickerFontName;
@property (nonatomic, strong) NSString *pickerBoldFontName;
@property (nonatomic) CGFloat pickerFontNormalSize;
@property (nonatomic) CGFloat pickerFontHeaderSize;
@property (nonatomic) UIStatusBarStyle pickerStatusBarStyle;
@property (nonatomic) BOOL useCustomFontForNavigationBar;
@property (nonatomic) BOOL arrangeSmartCollectionsFirst;
/**
* A reference to the navigation controller used to manage the whole picking process
*/
@property (nonatomic, strong) UINavigationController *navigationController;
/**
* Managing Asset Selection
*/
- (void)selectAsset:(PHAsset *)asset;
- (void)deselectAsset:(PHAsset *)asset;
/**
* User finish Actions
*/
- (void)dismiss:(id)sender;
- (void)finishPickingAssets:(id)sender;
@end
@protocol GMImagePickerControllerDelegate <NSObject>
/**
* @name Closing the Picker
*/
/**
* Tells the delegate that the user finish picking photos or videos.
* @param picker The controller object managing the assets picker interface.
* @param assets An array containing picked PHAssets objects.
*/
- (void)assetsPickerController:(GMImagePickerController *)picker didFinishPickingAssets:(NSArray *)assets;
@optional
/**
* Tells the delegate that the user cancelled the pick operation.
* @param picker The controller object managing the assets picker interface.
*/
- (void)assetsPickerControllerDidCancel:(GMImagePickerController *)picker;
/**
* @name Enabling Assets
*/
/**
* Ask the delegate if the specified asset should be shown.
*
* @param picker The controller object managing the assets picker interface.
* @param asset The asset to be shown.
*
* @return `YES` if the asset should be shown or `NO` if it should not.
*/
- (BOOL)assetsPickerController:(GMImagePickerController *)picker shouldShowAsset:(PHAsset *)asset;
/**
* Ask the delegate if the specified asset should be enabled for selection.
*
* @param picker The controller object managing the assets picker interface.
* @param asset The asset to be enabled.
*
* @return `YES` if the asset should be enabled or `NO` if it should not.
*/
- (BOOL)assetsPickerController:(GMImagePickerController *)picker shouldEnableAsset:(PHAsset *)asset;
/**
* @name Managing the Selected Assets
*/
/**
* Asks the delegate if the specified asset should be selected.
*
* @param picker The controller object managing the assets picker interface.
* @param asset The asset to be selected.
*
* @return `YES` if the asset should be selected or `NO` if it should not.
*
*/
- (BOOL)assetsPickerController:(GMImagePickerController *)picker shouldSelectAsset:(PHAsset *)asset;
/**
* Tells the delegate that the asset was selected.
*
* @param picker The controller object managing the assets picker interface.
* @param asset The asset that was selected.
*
*/
- (void)assetsPickerController:(GMImagePickerController *)picker didSelectAsset:(PHAsset *)asset;
/**
* Asks the delegate if the specified asset should be deselected.
*
* @param picker The controller object managing the assets picker interface.
* @param asset The asset to be deselected.
*
* @return `YES` if the asset should be deselected or `NO` if it should not.
*
*/
- (BOOL)assetsPickerController:(GMImagePickerController *)picker shouldDeselectAsset:(PHAsset *)asset;
/**
* Tells the delegate that the item at the specified path was deselected.
*
* @param picker The controller object managing the assets picker interface.
* @param asset The asset that was deselected.
*
*/
- (void)assetsPickerController:(GMImagePickerController *)picker didDeselectAsset:(PHAsset *)asset;
/**
* @name Managing Asset Highlighting
*/
/**
* Asks the delegate if the specified asset should be highlighted.
*
* @param picker The controller object managing the assets picker interface.
* @param asset The asset to be highlighted.
*
* @return `YES` if the asset should be highlighted or `NO` if it should not.
*/
- (BOOL)assetsPickerController:(GMImagePickerController *)picker shouldHighlightAsset:(PHAsset *)asset;
/**
* Tells the delegate that asset was highlighted.
*
* @param picker The controller object managing the assets picker interface.
* @param asset The asset that was highlighted.
*
*/
- (void)assetsPickerController:(GMImagePickerController *)picker didHighlightAsset:(PHAsset *)asset;
/**
* Tells the delegate that the highlight was removed from the asset.
*
* @param picker The controller object managing the assets picker interface.
* @param asset The asset that had its highlight removed.
*
*/
- (void)assetsPickerController:(GMImagePickerController *)picker didUnhighlightAsset:(PHAsset *)asset;
@end

View File

@ -1,388 +0,0 @@
//
// GMImagePickerController.m
// GMPhotoPicker
//
// Created by Guillermo Muntaner Perelló on 19/09/14.
// Copyright (c) 2014 Guillermo Muntaner Perelló. All rights reserved.
//
#import <MobileCoreServices/MobileCoreServices.h>
#import "GMImagePickerController.h"
#import "GMAlbumsViewController.h"
#import <Photos/Photos.h>
@interface GMImagePickerController () <UINavigationControllerDelegate, UIImagePickerControllerDelegate, UIAlertViewDelegate>
@end
@implementation GMImagePickerController
- (id)init
{
if (self = [super init]) {
_selectedAssets = [[NSMutableArray alloc] init];
// Default values:
_displaySelectionInfoToolbar = YES;
_displayAlbumsNumberOfAssets = YES;
_autoDisableDoneButton = YES;
_allowsMultipleSelection = YES;
_confirmSingleSelection = NO;
_showCameraButton = NO;
// Grid configuration:
_colsInPortrait = 3;
_colsInLandscape = 5;
_minimumInteritemSpacing = 2.0;
// Sample of how to select the collections you want to display:
_customSmartCollections = @[@(PHAssetCollectionSubtypeSmartAlbumFavorites),
@(PHAssetCollectionSubtypeSmartAlbumRecentlyAdded),
@(PHAssetCollectionSubtypeSmartAlbumVideos),
@(PHAssetCollectionSubtypeSmartAlbumSlomoVideos),
@(PHAssetCollectionSubtypeSmartAlbumTimelapses),
@(PHAssetCollectionSubtypeSmartAlbumBursts),
@(PHAssetCollectionSubtypeSmartAlbumPanoramas)];
// If you don't want to show smart collections, just put _customSmartCollections to nil;
//_customSmartCollections=nil;
// Which media types will display
_mediaTypes = @[@(PHAssetMediaTypeAudio),
@(PHAssetMediaTypeVideo),
@(PHAssetMediaTypeImage)];
self.preferredContentSize = kPopoverContentSize;
// UI Customisation
_pickerBackgroundColor = [UIColor whiteColor];
_pickerTextColor = [UIColor darkTextColor];
_pickerFontName = @"HelveticaNeue";
_pickerBoldFontName = @"HelveticaNeue-Bold";
_pickerFontNormalSize = 14.0f;
_pickerFontHeaderSize = 17.0f;
_navigationBarBackgroundColor = [UIColor whiteColor];
_navigationBarTextColor = [UIColor darkTextColor];
_navigationBarTintColor = [UIColor darkTextColor];
_toolbarBarTintColor = [UIColor whiteColor];
_toolbarTextColor = [UIColor darkTextColor];
_toolbarTintColor = [UIColor darkTextColor];
_pickerStatusBarStyle = UIStatusBarStyleDefault;
[self setupNavigationController];
}
return self;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// Ensure nav and toolbar customisations are set. Defaults are in place, but the user may have changed them
self.view.backgroundColor = _pickerBackgroundColor;
_navigationController.toolbar.translucent = YES;
_navigationController.toolbar.barTintColor = _toolbarBarTintColor;
_navigationController.toolbar.tintColor = _toolbarTintColor;
_navigationController.navigationBar.backgroundColor = _navigationBarBackgroundColor;
_navigationController.navigationBar.tintColor = _navigationBarTintColor;
NSDictionary *attributes;
if (_useCustomFontForNavigationBar) {
attributes = @{NSForegroundColorAttributeName : _navigationBarTextColor,
NSFontAttributeName : [UIFont fontWithName:_pickerBoldFontName size:_pickerFontHeaderSize]};
} else {
attributes = @{NSForegroundColorAttributeName : _navigationBarTextColor};
}
_navigationController.navigationBar.titleTextAttributes = attributes;
[self updateToolbar];
}
- (UIStatusBarStyle)preferredStatusBarStyle {
return _pickerStatusBarStyle;
}
#pragma mark - Setup Navigation Controller
- (void)setupNavigationController
{
GMAlbumsViewController *albumsViewController = [[GMAlbumsViewController alloc] init];
_navigationController = [[UINavigationController alloc] initWithRootViewController:albumsViewController];
_navigationController.delegate = self;
[_navigationController.navigationBar setTranslucent:NO];
[_navigationController willMoveToParentViewController:self];
[_navigationController.view setFrame:self.view.frame];
[self.view addSubview:_navigationController.view];
[self addConstraintsToChildViewControllersView:_navigationController.view];
[self addChildViewController:_navigationController];
[_navigationController didMoveToParentViewController:self];
}
- (void)addConstraintsToChildViewControllersView:(UIView *)view {
view.translatesAutoresizingMaskIntoConstraints = NO;
NSArray * hConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[view]-0-|" options:0 metrics:0 views:NSDictionaryOfVariableBindings(view)];
NSLayoutConstraint * topConstraint = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.topLayoutGuide attribute:NSLayoutAttributeBottom multiplier:1 constant:0];
NSLayoutConstraint * bottomConstraint = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:view.superview attribute:NSLayoutAttributeBottom multiplier:1 constant:0];
[view.superview addConstraints:@[topConstraint,bottomConstraint]];
[view.superview addConstraints:hConstraints];
}
#pragma mark - UIAlertViewDelegate
-(void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
{
if (buttonIndex == 1) {
// Only if OK was pressed do we want to completge the selection
[self finishPickingAssets:self];
}
}
#pragma mark - Select / Deselect Asset
- (void)selectAsset:(PHAsset *)asset
{
[self.selectedAssets insertObject:asset atIndex:self.selectedAssets.count];
[self updateDoneButton];
if (!self.allowsMultipleSelection) {
if (self.confirmSingleSelection) {
NSString *message = self.confirmSingleSelectionPrompt ? self.confirmSingleSelectionPrompt : [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"picker.confirm.message", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Do you want to select the image you tapped on?")];
[[[UIAlertView alloc] initWithTitle:[NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"picker.confirm.title", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Are You Sure?")]
message:message
delegate:self
cancelButtonTitle:[NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"picker.action.no", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"No")]
otherButtonTitles:[NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"picker.action.yes", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"Yes")], nil] show];
} else {
[self finishPickingAssets:self];
}
} else if (self.displaySelectionInfoToolbar || self.showCameraButton) {
[self updateToolbar];
}
}
- (void)deselectAsset:(PHAsset *)asset
{
[self.selectedAssets removeObjectAtIndex:[self.selectedAssets indexOfObject:asset]];
if (self.selectedAssets.count == 0) {
[self updateDoneButton];
}
if (self.displaySelectionInfoToolbar || self.showCameraButton) {
[self updateToolbar];
}
}
- (void)updateDoneButton
{
if (!self.allowsMultipleSelection) {
return;
}
UINavigationController *nav = (UINavigationController *)self.childViewControllers[0];
for (UIViewController *viewController in nav.viewControllers) {
viewController.navigationItem.rightBarButtonItem.enabled = (self.autoDisableDoneButton ? self.selectedAssets.count > 0 : TRUE);
}
}
- (void)updateToolbar
{
if (!self.allowsMultipleSelection && !self.showCameraButton) {
return;
}
UINavigationController *nav = (UINavigationController *)self.childViewControllers[0];
for (UIViewController *viewController in nav.viewControllers) {
NSUInteger index = 1;
if (_showCameraButton) {
index++;
}
[[viewController.toolbarItems objectAtIndex:index] setTitleTextAttributes:[self toolbarTitleTextAttributes] forState:UIControlStateNormal];
[[viewController.toolbarItems objectAtIndex:index] setTitleTextAttributes:[self toolbarTitleTextAttributes] forState:UIControlStateDisabled];
[[viewController.toolbarItems objectAtIndex:index] setTitle:[self toolbarTitle]];
[viewController.navigationController setToolbarHidden:(self.selectedAssets.count == 0 && !self.showCameraButton) animated:YES];
}
}
#pragma mark - User finish Actions
- (void)dismiss:(id)sender
{
if ([self.delegate respondsToSelector:@selector(assetsPickerControllerDidCancel:)]) {
[self.delegate assetsPickerControllerDidCancel:self];
}
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
- (void)finishPickingAssets:(id)sender
{
if ([self.delegate respondsToSelector:@selector(assetsPickerController:didFinishPickingAssets:)]) {
[self.delegate assetsPickerController:self didFinishPickingAssets:self.selectedAssets];
}
}
#pragma mark - Toolbar Title
- (NSPredicate *)predicateOfAssetType:(PHAssetMediaType)type
{
return [NSPredicate predicateWithBlock:^BOOL(PHAsset *asset, NSDictionary *bindings) {
return (asset.mediaType == type);
}];
}
- (NSString *)toolbarTitle
{
if (self.selectedAssets.count == 0) {
return nil;
}
NSPredicate *photoPredicate = [self predicateOfAssetType:PHAssetMediaTypeImage];
NSPredicate *videoPredicate = [self predicateOfAssetType:PHAssetMediaTypeVideo];
NSInteger nImages = [self.selectedAssets filteredArrayUsingPredicate:photoPredicate].count;
NSInteger nVideos = [self.selectedAssets filteredArrayUsingPredicate:videoPredicate].count;
if (nImages > 0 && nVideos > 0) {
return [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"picker.selection.multiple-items", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"%@ Items Selected" ), @(nImages + nVideos)];
} else if (nImages > 1) {
return [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"picker.selection.multiple-photos", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"%@ Photos Selected"), @(nImages)];
} else if (nImages == 1) {
return NSLocalizedStringFromTableInBundle(@"picker.selection.single-photo", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"1 Photo Selected" );
} else if (nVideos > 1) {
return [NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"picker.selection.multiple-videos", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"%@ Videos Selected"), @(nVideos)];
} else if (nVideos == 1) {
return NSLocalizedStringFromTableInBundle(@"picker.selection.single-video", @"GMImagePicker", [NSBundle bundleForClass:GMImagePickerController.class], @"1 Video Selected");
} else {
return nil;
}
}
#pragma mark - Toolbar Items
- (void)cameraButtonPressed:(UIBarButtonItem *)button
{
if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"No Camera!"
message:@"Sorry, this device does not have a camera."
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[alert show];
return;
}
// This allows the selection of the image taken to be better seen if the user is not already in that VC
if (self.autoSelectCameraImages && [self.navigationController.topViewController isKindOfClass:[GMAlbumsViewController class]]) {
[((GMAlbumsViewController *)self.navigationController.topViewController) selectAllAlbumsCell];
}
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
picker.sourceType = UIImagePickerControllerSourceTypeCamera;
picker.mediaTypes = @[(NSString *)kUTTypeImage];
picker.allowsEditing = self.allowsEditingCameraImages;
picker.delegate = self;
picker.modalPresentationStyle = UIModalPresentationPopover;
UIPopoverPresentationController *popPC = picker.popoverPresentationController;
popPC.permittedArrowDirections = UIPopoverArrowDirectionAny;
popPC.barButtonItem = button;
[self showViewController:picker sender:button];
}
- (NSDictionary *)toolbarTitleTextAttributes {
return @{NSForegroundColorAttributeName : _toolbarTextColor,
NSFontAttributeName : [UIFont fontWithName:_pickerFontName size:_pickerFontHeaderSize]};
}
- (UIBarButtonItem *)titleButtonItem
{
UIBarButtonItem *title = [[UIBarButtonItem alloc] initWithTitle:self.toolbarTitle
style:UIBarButtonItemStylePlain
target:nil
action:nil];
NSDictionary *attributes = [self toolbarTitleTextAttributes];
[title setTitleTextAttributes:attributes forState:UIControlStateNormal];
[title setTitleTextAttributes:attributes forState:UIControlStateDisabled];
[title setEnabled:NO];
return title;
}
- (UIBarButtonItem *)spaceButtonItem
{
return [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
}
- (UIBarButtonItem *)cameraButtonItem
{
return [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCamera target:self action:@selector(cameraButtonPressed:)];
}
- (NSArray *)toolbarItems
{
UIBarButtonItem *camera = [self cameraButtonItem];
UIBarButtonItem *title = [self titleButtonItem];
UIBarButtonItem *space = [self spaceButtonItem];
NSMutableArray *items = [[NSMutableArray alloc] init];
if (_showCameraButton) {
[items addObject:camera];
}
[items addObject:space];
[items addObject:title];
[items addObject:space];
return [NSArray arrayWithArray:items];
}
#pragma mark - Camera Delegate
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info
{
[picker.presentingViewController dismissViewControllerAnimated:YES completion:nil];
NSString *mediaType = info[UIImagePickerControllerMediaType];
if ([mediaType isEqualToString:(NSString *)kUTTypeImage]) {
UIImage *image = info[UIImagePickerControllerEditedImage] ? : info[UIImagePickerControllerOriginalImage];
UIImageWriteToSavedPhotosAlbum(image,
self,
@selector(image:finishedSavingWithError:contextInfo:),
nil);
}
}
-(void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
{
[picker.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
-(void)image:(UIImage *)image finishedSavingWithError:(NSError *)error contextInfo:(void *)contextInfo
{
if (error) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Image Not Saved"
message:@"Sorry, unable to save the new image!"
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[alert show];
}
// Note: The image view will auto refresh as the photo's are being observed in the other VCs
}
@end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 B

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
</plist>

1
Gifu

@ -1 +0,0 @@
Subproject commit ed572f53ce58b8e23499abeb3a926033cbe480f7

29
OpenInTusker/Action.js Normal file
View File

@ -0,0 +1,29 @@
//
// Action.js
// OpenInTusker
//
// Created by Shadowfacts on 5/22/21.
// Copyright © 2021 Shadowfacts. All rights reserved.
//
var Action = function() {};
Action.prototype = {
run: function(arguments) {
const results = {
url: window.location.href,
};
const el = document.querySelector('link[rel=alternate][type="application/activity+json"]');
if (el) {
results.activityPubURL = el.href;
}
arguments.completionFunction(results);
},
finalize: function(arguments) {
}
};
var ExtensionPreprocessingJS = new Action();

View File

@ -0,0 +1,100 @@
//
// ActionViewController.swift
// OpenInTusker
//
// Created by Shadowfacts on 5/23/21.
// Copyright © 2021 Shadowfacts. All rights reserved.
//
import UIKit
import MobileCoreServices
class ActionViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
findURLFromWebPage { (components) in
if let components = components {
self.searchForURLInApp(components)
} else {
self.findURLItem { (components) in
if let components = components {
self.searchForURLInApp(components)
}
}
}
}
}
private func findURLFromWebPage(completion: @escaping (URLComponents?) -> Void) {
for item in extensionContext!.inputItems as! [NSExtensionItem] {
for provider in item.attachments! {
guard provider.hasItemConformingToTypeIdentifier(kUTTypePropertyList as String) else {
continue
}
provider.loadItem(forTypeIdentifier: kUTTypePropertyList as String, options: nil) { (result, error) in
guard let result = result as? [String: Any],
let jsResult = result[NSExtensionJavaScriptPreprocessingResultsKey] as? [String: Any],
let urlString = jsResult["activityPubURL"] as? String ?? jsResult["url"] as? String,
let components = URLComponents(string: urlString) else {
completion(nil)
return
}
completion(components)
}
return
}
}
completion(nil)
}
private func findURLItem(completion: @escaping (URLComponents?) -> Void) {
for item in extensionContext!.inputItems as! [NSExtensionItem] {
for provider in item.attachments! {
guard provider.hasItemConformingToTypeIdentifier(kUTTypeURL as String) else {
continue
}
provider.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil) { (result, error) in
guard let result = result as? URL,
let components = URLComponents(url: result, resolvingAgainstBaseURL: false) else {
completion(nil)
return
}
completion(components)
}
return
}
}
completion(nil)
}
private func searchForURLInApp(_ components: URLComponents) {
var components = components
components.scheme = "tusker"
self.openURL(components.url!)
self.extensionContext!.completeRequest(returningItems: nil, completionHandler: nil)
}
@objc private func openURL(_ url: URL) {
var responder: UIResponder = self
while let parent = responder.next {
if let application = parent as? UIApplication {
application.perform(#selector(openURL(_:)), with: url)
break
} else {
responder = parent
}
}
}
@IBAction func done() {
extensionContext!.completeRequest(returningItems: nil, completionHandler: nil)
}
}

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="ObA-dk-sSI">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Image-->
<scene sceneID="7MM-of-jgj">
<objects>
<viewController title="Image" id="ObA-dk-sSI" customClass="ActionViewController" customModule="OpenInTusker" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="zMn-AG-sqS">
<rect key="frame" x="0.0" y="0.0" width="320" height="528"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<navigationBar contentMode="scaleToFill" horizontalCompressionResistancePriority="751" verticalCompressionResistancePriority="751" translatesAutoresizingMaskIntoConstraints="NO" id="NOA-Dm-cuz">
<rect key="frame" x="0.0" y="44" width="320" height="44"/>
<items>
<navigationItem id="3HJ-uW-3hn">
<barButtonItem key="leftBarButtonItem" title="Done" style="done" id="WYi-yp-eM6">
<connections>
<action selector="done" destination="ObA-dk-sSI" id="Qdu-qn-U6V"/>
</connections>
</barButtonItem>
</navigationItem>
</items>
</navigationBar>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Unable to find Mastodon link on this page." textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Yho-gp-VyR">
<rect key="frame" x="0.0" y="254" width="320" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<viewLayoutGuide key="safeArea" id="VVe-Uw-JpX"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="VVe-Uw-JpX" firstAttribute="trailing" secondItem="NOA-Dm-cuz" secondAttribute="trailing" id="A05-Pj-hrr"/>
<constraint firstItem="NOA-Dm-cuz" firstAttribute="leading" secondItem="VVe-Uw-JpX" secondAttribute="leading" id="HxO-8t-aoh"/>
<constraint firstItem="Yho-gp-VyR" firstAttribute="centerY" secondItem="zMn-AG-sqS" secondAttribute="centerY" id="R7q-OB-hhA"/>
<constraint firstItem="Yho-gp-VyR" firstAttribute="leading" secondItem="VVe-Uw-JpX" secondAttribute="leading" id="TEy-zi-dP7"/>
<constraint firstItem="Yho-gp-VyR" firstAttribute="trailing" secondItem="VVe-Uw-JpX" secondAttribute="trailing" id="Uvn-0x-Y6N"/>
<constraint firstItem="NOA-Dm-cuz" firstAttribute="top" secondItem="VVe-Uw-JpX" secondAttribute="top" id="we0-1t-bgp"/>
</constraints>
</view>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<size key="freeformSize" width="320" height="528"/>
<connections>
<outlet property="view" destination="zMn-AG-sqS" id="Qma-de-2ek"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="X47-rx-isc" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-61" y="-57"/>
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

55
OpenInTusker/Info.plist Normal file
View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Open in Tusker</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsAttachmentsWithMinCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsWebPageWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>
<key>NSExtensionJavaScriptPreprocessingFile</key>
<string>Action</string>
<key>NSExtensionServiceAllowsFinderPreviewItem</key>
<true/>
<key>NSExtensionServiceAllowsTouchBarItem</key>
<true/>
<key>NSExtensionServiceFinderPreviewIconName</key>
<string>NSActionTemplate</string>
<key>NSExtensionServiceTouchBarBezelColorName</key>
<string>TouchBarBezel</string>
<key>NSExtensionServiceTouchBarIconName</key>
<string>NSActionTemplate</string>
</dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.ui-services</string>
</dict>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,103 @@
{
"images" : [
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "60x60@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "60x60@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"filename" : "76x76@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "83.5x83.5@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "1024x1024@1x.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,14 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"colors" : [
{
"idiom" : "mac",
"color" : {
"reference" : "systemPurpleColor"
}
}
]
}

View File

@ -2,7 +2,9 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@ -1,318 +0,0 @@
//
// Client.swift
// Pachyderm
//
// Created by Shadowfacts on 9/8/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
/**
The base Mastodon API client.
*/
public class Client {
public typealias Callback<Result: Decodable> = (Response<Result>) -> Void
let baseURL: URL
let session: URLSession
public var accessToken: String?
public var appID: String?
public var clientID: String?
public var clientSecret: String?
public var timeoutInterval: TimeInterval = 60
lazy var decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return decoder
}()
public init(baseURL: URL, accessToken: String? = nil, session: URLSession = .shared) {
self.baseURL = baseURL
self.accessToken = accessToken
self.session = session
}
public func run<Result>(_ request: Request<Result>, completion: @escaping Callback<Result>) {
guard let request = createURLRequest(request: request) else {
completion(.failure(Error.invalidRequest))
return
}
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data,
let response = response as? HTTPURLResponse else {
completion(.failure(Error.invalidResponse))
return
}
guard response.statusCode == 200 else {
let mastodonError = try? self.decoder.decode(MastodonError.self, from: data)
let error = mastodonError.flatMap { Error.mastodonError($0.description) } ?? Error.unknownError
completion(.failure(error))
return
}
guard let result = try? self.decoder.decode(Result.self, from: data) else {
completion(.failure(Error.invalidModel))
return
}
if var result = result as? ClientModel {
result.client = self
} else if var result = result as? [ClientModel] {
result.client = self
}
let pagination = response.allHeaderFields["Link"].flatMap { $0 as? String }.flatMap(Pagination.init)
completion(.success(result, pagination))
}
task.resume()
}
func createURLRequest<Result>(request: Request<Result>) -> URLRequest? {
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) else { return nil }
components.path = request.path
components.queryItems = request.queryParameters.queryItems
guard let url = components.url else { return nil }
var urlRequest = URLRequest(url: url, timeoutInterval: timeoutInterval)
urlRequest.httpMethod = request.method.name
urlRequest.httpBody = request.body.data
urlRequest.setValue(request.body.mimeType, forHTTPHeaderField: "Content-Type")
if let accessToken = accessToken {
urlRequest.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
}
return urlRequest
}
// MARK: - Authorization
public func registerApp(name: String, redirectURI: String, scopes: [Scope], website: URL? = nil, completion: @escaping Callback<RegisteredApplication>) {
let request = Request<RegisteredApplication>(method: .post, path: "/api/v1/apps", body: .parameters([
"client_name" => name,
"redirect_uris" => redirectURI,
"scopes" => scopes.scopeString,
"website" => website?.absoluteString
]))
run(request) { result in
defer { completion(result) }
guard case let .success(application, _) = result else { return }
self.appID = application.id
self.clientID = application.clientID
self.clientSecret = application.clientSecret
}
}
public func getAccessToken(authorizationCode: String, redirectURI: String, completion: @escaping Callback<LoginSettings>) {
let request = Request<LoginSettings>(method: .post, path: "/oauth/token", body: .parameters([
"client_id" => clientID,
"client_secret" => clientSecret,
"grant_type" => "authorization_code",
"code" => authorizationCode,
"redirect_uri" => redirectURI
]))
run(request) { result in
defer { completion(result) }
guard case let .success(loginSettings, _) = result else { return }
self.accessToken = loginSettings.accessToken
}
}
// MARK: - Self
public func getSelfAccount() -> Request<Account> {
return Request<Account>(method: .get, path: "/api/v1/accounts/verify_credentials")
}
public func getFavourites() -> Request<[Status]> {
return Request<[Status]>(method: .get, path: "/api/v1/favourites")
}
public func getRelationships(accounts: [String]? = nil) -> Request<[Relationship]> {
return Request<[Relationship]>(method: .get, path: "/api/v1/accounts/relationships", queryParameters: "id" => accounts)
}
public func getInstance() -> Request<Instance> {
return Request<Instance>(method: .get, path: "/api/v1/instance")
}
public func getCustomEmoji() -> Request<[Emoji]> {
return Request<[Emoji]>(method: .get, path: "/api/v1/custom_emojis")
}
// MARK: - Accounts
public func getAccount(id: String) -> Request<Account> {
return Request<Account>(method: .get, path: "/api/v1/accounts/\(id)")
}
public func searchForAccount(query: String, limit: Int? = nil, following: Bool? = nil) -> Request<[Account]> {
return Request<[Account]>(method: .get, path: "/api/v1/accounts/search", queryParameters: [
"q" => query,
"limit" => limit,
"following" => following
])
}
// MARK: - Blocks
public func getBlocks() -> Request<[Account]> {
return Request<[Account]>(method: .get, path: "/api/v1/blocks")
}
public func getDomainBlocks() -> Request<[String]> {
return Request<[String]>(method: .get, path: "api/v1/domain_blocks")
}
public func block(domain: String) -> Request<Empty> {
return Request<Empty>(method: .post, path: "/api/v1/domain_blocks", body: .parameters([
"domain" => domain
]))
}
public func unblock(domain: String) -> Request<Empty> {
return Request<Empty>(method: .delete, path: "/api/v1/domain_blocks", body: .parameters([
"domain" => domain
]))
}
// MARK: - Filters
public func getFilters() -> Request<[Filter]> {
return Request<[Filter]>(method: .get, path: "/api/v1/filters")
}
public func createFilter(phrase: String, context: [Filter.Context], irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresAt: Date? = nil) -> Request<Filter> {
return Request<Filter>(method: .post, path: "/api/v1/filters", body: .parameters([
"phrase" => phrase,
"irreversible" => irreversible,
"whole_word" => wholeWord,
"expires_at" => expiresAt
] + "context" => context.contextStrings))
}
public func getFilter(id: String) -> Request<Filter> {
return Request<Filter>(method: .get, path: "/api/v1/filters/\(id)")
}
// MARK: - Follows
public func getFollowRequests(range: RequestRange = .default) -> Request<[Account]> {
var request = Request<[Account]>(method: .get, path: "/api/v1/follow_requests")
request.range = range
return request
}
public func getFollowSuggestions() -> Request<[Account]> {
return Request<[Account]>(method: .get, path: "/api/v1/suggestions")
}
public func followRemote(acct: String) -> Request<Account> {
return Request<Account>(method: .post, path: "/api/v1/follows", body: .parameters(["uri" => acct]))
}
// MARK: - Lists
public func getLists() -> Request<[List]> {
return Request<[List]>(method: .get, path: "/api/v1/lists")
}
public func getList(id: String) -> Request<List> {
return Request<List>(method: .get, path: "/api/v1/lists/\(id)")
}
public func createList(title: String) -> Request<List> {
return Request<List>(method: .post, path: "/api/v1/lists", body: .parameters(["title" => title]))
}
// MARK: - Media
public func upload(attachment: FormAttachment, description: String? = nil, focus: (Float, Float)? = nil) -> Request<Attachment> {
return Request<Attachment>(method: .post, path: "/api/v1/media", body: .formData([
"description" => description,
"focus" => focus
], attachment))
}
// MARK: - Mutes
public func getMutes(range: RequestRange) -> Request<[Account]> {
var request = Request<[Account]>(method: .get, path: "/api/v1/mutes")
request.range = range
return request
}
// MARK: - Notifications
public func getNotifications(excludeTypes: [Notification.Kind], range: RequestRange = .default) -> Request<[Notification]> {
var request = Request<[Notification]>(method: .get, path: "/api/v1/notifications", queryParameters:
"exclude_types" => excludeTypes.map { $0.rawValue }
)
request.range = range
return request
}
public func clearNotifications() -> Request<Empty> {
return Request<Empty>(method: .post, path: "/api/v1/notifications/clear")
}
// MARK: - Reports
public func getReports() -> Request<[Report]> {
return Request<[Report]>(method: .get, path: "/api/v1/reports")
}
public func report(account: Account, statuses: [Status], comment: String) -> Request<Report> {
return Request<Report>(method: .post, path: "/api/v1/reports", body: .parameters([
"account_id" => account.id,
"comment" => comment
] + "status_ids" => statuses.map { $0.id }))
}
// MARK: - Search
public func search(query: String, resolve: Bool? = nil, limit: Int? = nil) -> Request<SearchResults> {
return Request<SearchResults>(method: .get, path: "/api/v2/search", queryParameters: [
"q" => query,
"resolve" => resolve,
"limit" => limit
])
}
// MARK: - Statuses
public func getStatus(id: String) -> Request<Status> {
return Request<Status>(method: .get, path: "/api/v1/statuses/\(id)")
}
public func createStatus(text: String,
contentType: StatusContentType = .plain,
inReplyTo: String? = nil,
media: [Attachment]? = nil,
sensitive: Bool? = nil,
spoilerText: String? = nil,
visibility: Status.Visibility? = nil,
language: String? = nil) -> Request<Status> {
return Request<Status>(method: .post, path: "/api/v1/statuses", body: .parameters([
"status" => text,
"content_type" => contentType.mimeType,
"in_reply_to_id" => inReplyTo,
"sensitive" => sensitive,
"spoiler_text" => spoilerText,
"visibility" => visibility?.rawValue,
"language" => language
] + "media_ids" => media?.map { $0.id }))
}
// MARK: - Timelines
public func getStatuses(timeline: Timeline, range: RequestRange = .default) -> Request<[Status]> {
return timeline.request(range: range)
}
}
extension Client {
public enum Error: Swift.Error {
case unknownError
case invalidRequest
case invalidResponse
case invalidModel
case mastodonError(String)
}
}

View File

@ -1,39 +0,0 @@
//
// ClientModel.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
protocol ClientModel {
var client: Client! { get set }
}
extension Array where Element == ClientModel {
var client: Client! {
get {
return first?.client
}
set {
for var el in self {
el.client = newValue
}
}
}
}
extension Array where Element: ClientModel {
var client: Client! {
get {
return first?.client
}
set {
for var el in self {
el.client = newValue
}
}
}
}

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
</plist>

View File

@ -1,130 +0,0 @@
//
// Account.swift
// Pachyderm
//
// Created by Shadowfacts on 9/8/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Account: Decodable {
public let id: String
public let username: String
public let acct: String
public let displayName: String
public let locked: Bool
public let createdAt: Date
public let followersCount: Int
public let followingCount: Int
public let statusesCount: Int
public let note: String
public let url: URL
public let avatar: URL
public let avatarStatic: URL
public let header: URL
public let headerStatic: URL
public private(set) var emojis: [Emoji]
public let moved: Bool?
public let fields: [Field]?
public let bot: Bool?
public static func authorizeFollowRequest(_ account: Account) -> Request<Empty> {
return Request<Empty>(method: .post, path: "/api/v1/follow_requests/\(account.id)/authorize")
}
public static func rejectFollowRequest(_ account: Account) -> Request<Empty> {
return Request<Empty>(method: .post, path: "/api/v1/follow_requests/\(account.id)/reject")
}
public static func removeFromFollowRequests(_ account: Account) -> Request<Empty> {
return Request<Empty>(method: .delete, path: "/api/v1/suggestions/\(account.id)")
}
public static func getFollowers(_ account: Account, range: RequestRange = .default) -> Request<[Account]> {
var request = Request<[Account]>(method: .get, path: "/api/v1/accounts/\(account.id)/followers")
request.range = range
return request
}
public static func getFollowing(_ account: Account, range: RequestRange = .default) -> Request<[Account]> {
var request = Request<[Account]>(method: .get, path: "/api/v1/accounts/\(account.id)/following")
request.range = range
return request
}
public static func getStatuses(_ accountID: String, range: RequestRange = .default, onlyMedia: Bool? = nil, pinned: Bool? = nil, excludeReplies: Bool? = nil) -> Request<[Status]> {
var request = Request<[Status]>(method: .get, path: "/api/v1/accounts/\(accountID)/statuses", queryParameters: [
"only_media" => onlyMedia,
"pinned" => pinned,
"exclude_replies" => excludeReplies
])
request.range = range
return request
}
public static func follow(_ accountID: String) -> Request<Relationship> {
return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(accountID)/follow")
}
public static func unfollow(_ accountID: String) -> Request<Relationship> {
return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(accountID)/unfollow")
}
public static func block(_ account: Account) -> Request<Relationship> {
return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(account.id)/block")
}
public static func unblock(_ account: Account) -> Request<Relationship> {
return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(account.id)/unblock")
}
public static func mute(_ account: Account, notifications: Bool? = nil) -> Request<Relationship> {
return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(account.id)/mute", body: .parameters([
"notifications" => notifications
]))
}
public static func unmute(_ account: Account) -> Request<Relationship> {
return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(account.id)/unmute")
}
public static func getLists(_ account: Account) -> Request<[List]> {
return Request<[List]>(method: .get, path: "/api/v1/accounts/\(account.id)/lists")
}
private enum CodingKeys: String, CodingKey {
case id
case username
case acct
case displayName = "display_name"
case locked
case createdAt = "created_at"
case followersCount = "followers_count"
case followingCount = "following_count"
case statusesCount = "statuses_count"
case note
case url
case avatar
case avatarStatic = "avatar_static"
case header
case headerStatic = "header_static"
case emojis
case moved
case fields
case bot
}
}
extension Account: CustomDebugStringConvertible {
public var debugDescription: String {
return "Account(\(id), \(acct))"
}
}
extension Account {
public struct Field: Codable {
public let name: String
public let value: String
}
}

View File

@ -1,19 +0,0 @@
//
// Application.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Application: Decodable {
public let name: String
public let website: URL?
private enum CodingKeys: String, CodingKey {
case name
case website
}
}

View File

@ -1,48 +0,0 @@
//
// Card.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Card: Decodable {
public let url: URL
public let title: String
public let description: String
public let image: URL?
public let kind: Kind
public let authorName: String?
public let authorURL: URL?
public let providerName: String?
public let providerURL: URL?
public let html: String?
public let width: Int?
public let height: Int?
private enum CodingKeys: String, CodingKey {
case url
case title
case description
case image
case kind = "type"
case authorName = "author_name"
case authorURL = "author_url"
case providerName = "provider_name"
case providerURL = "provider_url"
case html
case width
case height
}
}
extension Card {
public enum Kind: String, Decodable {
case link
case photo
case video
case rich
}
}

View File

@ -1,29 +0,0 @@
//
// Emoji.swift
// Pachyderm
//
// Created by Shadowfacts on 9/8/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Emoji: Decodable {
public let shortcode: String
public let url: URL
public let staticURL: URL
public let visibleInPicker: Bool
private enum CodingKeys: String, CodingKey {
case shortcode
case url
case staticURL = "static_url"
case visibleInPicker = "visible_in_picker"
}
}
extension Emoji: CustomDebugStringConvertible {
public var debugDescription: String {
return ":\(shortcode):"
}
}

View File

@ -1,51 +0,0 @@
//
// Hashtag.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Hashtag: Decodable {
public let name: String
public let url: URL
public let history: [History]?
public init(name: String, url: URL) {
self.name = name
self.url = url
self.history = nil
}
private enum CodingKeys: String, CodingKey {
case name
case url
case history
}
}
extension Hashtag {
public class History: Decodable {
public let day: Date
public let uses: Int
public let accounts: Int
private enum CodingKeys: String, CodingKey {
case day
case uses
case accounts
}
}
}
extension Hashtag: Equatable, Hashable {
public static func ==(lhs: Hashtag, rhs: Hashtag) -> Bool {
return lhs.url == rhs.url
}
public func hash(into hasher: inout Hasher) {
hasher.combine(url)
}
}

View File

@ -1,87 +0,0 @@
//
// Instance.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Instance: Decodable {
public let uri: String
public let title: String
public let description: String
public let email: String?
public let version: String
public let urls: [String: URL]
public let thumbnail: URL?
public let languages: [String]?
public let stats: Stats?
// pleroma doesn't currently implement these
public let contactAccount: Account?
// MARK: Unofficial additions to the Mastodon API.
public let maxStatusCharacters: Int?
// we need a custom decoder, because all API-compatible implementations don't return some data in the same format
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.uri = try container.decode(String.self, forKey: .uri)
self.title = try container.decode(String.self, forKey: .title)
self.description = try container.decode(String.self, forKey: .description)
self.email = try container.decodeIfPresent(String.self, forKey: .email)
self.version = try container.decode(String.self, forKey: .version)
if let urls = try? container.decodeIfPresent([String: URL].self, forKey: .urls) {
self.urls = urls
} else {
self.urls = [:]
}
self.languages = try? container.decodeIfPresent([String].self, forKey: .languages)
self.contactAccount = try? container.decodeIfPresent(Account.self, forKey: .contactAccount)
self.stats = try? container.decodeIfPresent(Stats.self, forKey: .stats)
self.thumbnail = try? container.decodeIfPresent(URL.self, forKey: .thumbnail)
if let maxStatusCharacters = try? container.decodeIfPresent(Int.self, forKey: .maxStatusCharacters) {
self.maxStatusCharacters = maxStatusCharacters
} else if let str = try? container.decodeIfPresent(String.self, forKey: .maxStatusCharacters),
let maxStatusCharacters = Int(str, radix: 10) {
self.maxStatusCharacters = maxStatusCharacters
} else {
self.maxStatusCharacters = nil
}
}
private enum CodingKeys: String, CodingKey {
case uri
case title
case description
case email
case version
case urls
case thumbnail
case languages
case stats
case contactAccount = "contact_account"
case maxStatusCharacters = "max_toot_chars"
}
}
extension Instance {
public class Stats: Decodable {
public let domainCount: Int?
public let statusCount: Int?
public let userCount: Int?
private enum CodingKeys: String, CodingKey {
case domainCount = "domain_count"
case statusCount = "status_count"
case userCount = "user_count"
}
}
}

View File

@ -1,23 +0,0 @@
//
// Mention.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Mention: Decodable {
public let url: URL
public let username: String
public let acct: String
public let id: String
private enum CodingKeys: String, CodingKey {
case url
case username
case acct
case id
}
}

View File

@ -1,42 +0,0 @@
//
// Notification.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Notification: Decodable {
public let id: String
public let kind: Kind
public let createdAt: Date
public let account: Account
public let status: Status?
public static func dismiss(id notificationID: String) -> Request<Empty> {
return Request<Empty>(method: .post, path: "/api/v1/notifications/dismiss", body: .parameters([
"id" => notificationID
]))
}
private enum CodingKeys: String, CodingKey {
case id
case kind = "type"
case createdAt = "created_at"
case account
case status
}
}
extension Notification {
public enum Kind: String, Decodable, CaseIterable {
case mention
case reblog
case favourite
case follow
}
}
extension Notification: Identifiable {}

View File

@ -1,21 +0,0 @@
//
// RegisteredApplication.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class RegisteredApplication: Decodable {
public let id: String
public let clientID: String
public let clientSecret: String
private enum CodingKeys: String, CodingKey {
case id
case clientID = "client_id"
case clientSecret = "client_secret"
}
}

View File

@ -1,19 +0,0 @@
//
// Pachyderm.h
// Pachyderm
//
// Created by Shadowfacts on 9/8/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
#import <UIKit/UIKit.h>
//! Project version number for Pachyderm.
FOUNDATION_EXPORT double PachydermVersionNumber;
//! Project version string for Pachyderm.
FOUNDATION_EXPORT const unsigned char PachydermVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <Pachyderm/PublicHeader.h>

View File

@ -1,63 +0,0 @@
//
// Body.swift
// Pachyderm
//
// Created by Shadowfacts on 9/8/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
enum Body {
case parameters([Parameter]?)
case formData([Parameter]?, FormAttachment?)
case empty
}
extension Body {
private static let boundary: String = "PachydermBoundary"
var data: Data? {
switch self {
case let .parameters(parameters):
return parameters?.urlEncoded.data(using: .utf8)
case let .formData(parameters, attachment):
var data = Data()
parameters?.forEach { param in
guard let value = param.value else { return }
data.append("--\(Body.boundary)\r\n")
data.append("Content-Disposition: form-data; name=\"\(param.name)\"\r\n\r\n")
data.append("\(value)\r\n")
}
if let attachment = attachment {
data.append("--\(Body.boundary)\r\n")
data.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(attachment.fileName)\"\r\n")
data.append("Content-Type: \(attachment.mimeType)\r\n\r\n")
data.append(attachment.data)
data.append("\r\n")
}
data.append("--\(Body.boundary)--\r\n")
return data
case .empty:
return nil
}
}
var mimeType: String? {
switch self {
case let .parameters(parameters):
if parameters == nil {
return nil
}
return "application/x-www-form-urlencoded; charset=utf-8"
case let .formData(parameters, attachment):
if parameters == nil && attachment == nil {
return nil
}
return "multipart/form-data; boundary=\(Body.boundary)"
case .empty:
return nil
}
}
}

View File

@ -1,24 +0,0 @@
//
// InstanceType.swift
// Pachyderm
//
// Created by Shadowfacts on 9/11/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import Foundation
public enum InstanceType {
case mastodon, pleroma
}
public extension Instance {
var instanceType: InstanceType {
let lowercased = version.lowercased()
if lowercased.contains("pleroma") {
return .pleroma
} else {
return .mastodon
}
}
}

View File

@ -1,42 +0,0 @@
//
// NotificationGroup.swift
// Pachyderm
//
// Created by Shadowfacts on 9/5/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import Foundation
public class NotificationGroup {
public let notificationIDs: [String]
public let id: String
public let kind: Notification.Kind
init?(notifications: [Notification]) {
guard !notifications.isEmpty else { return nil }
self.notificationIDs = notifications.map { $0.id }
self.id = notifications.first!.id
self.kind = notifications.first!.kind
}
public static func createGroups(notifications: [Notification], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] {
return notifications.reduce(into: [[Notification]]()) { (groups, notification) in
if allowedTypes.contains(notification.kind),
let lastGroup = groups.last,
let firstStatus = lastGroup.first,
firstStatus.kind == notification.kind,
firstStatus.status?.id == notification.status?.id {
groups[groups.count - 1].append(notification)
} else {
groups.append([notification])
}
}.map {
NotificationGroup(notifications: $0)!
}
}
}
extension NotificationGroup: Identifiable {}

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

View File

@ -1,34 +0,0 @@
//
// PachydermTests.swift
// PachydermTests
//
// Created by Shadowfacts on 9/8/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import XCTest
@testable import Pachyderm
class PachydermTests: XCTestCase {
override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}

9
Packages/Duckable/.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

View File

@ -0,0 +1,31 @@
// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "Duckable",
platforms: [
.iOS(.v15),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "Duckable",
targets: ["Duckable"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "Duckable",
dependencies: []),
.testTarget(
name: "DuckableTests",
dependencies: ["Duckable"]),
]
)

View File

@ -0,0 +1,3 @@
# Duckable
A package that allows modally-presented view controllers to be 'ducked' to make the content behind them accessible (à la Mail.app).

View File

@ -0,0 +1,44 @@
//
// API.swift
// Duckable
//
// Created by Shadowfacts on 11/7/22.
//
import UIKit
public protocol DuckableViewController: UIViewController {
var duckableDelegate: DuckableViewControllerDelegate? { get set }
func duckableViewControllerMayAttemptToDuck()
func duckableViewControllerWillAnimateDuck(withDuration duration: CGFloat, afterDelay delay: CGFloat)
func duckableViewControllerDidFinishAnimatingDuck()
}
extension DuckableViewController {
public func duckableViewControllerMayAttemptToDuck() {}
public func duckableViewControllerWillAnimateDuck(withDuration duration: CGFloat, afterDelay delay: CGFloat) {}
public func duckableViewControllerDidFinishAnimatingDuck() {}
}
public protocol DuckableViewControllerDelegate: AnyObject {
func duckableViewControllerWillDismiss(animated: Bool)
}
extension UIViewController {
@available(iOS 16.0, *)
public func presentDuckable(_ viewController: DuckableViewController, animated: Bool, isDucked: Bool = false) -> Bool {
var cur: UIViewController? = self
while let vc = cur {
if let container = vc as? DuckableContainerViewController {
container.presentDuckable(viewController, animated: animated, isDucked: isDucked, completion: nil)
return true
} else {
cur = vc.parent
}
}
return false
}
}

View File

@ -0,0 +1,12 @@
//
// DetentIdentifier.swift
// Duckable
//
// Created by Shadowfacts on 11/7/22.
//
import UIKit
extension UISheetPresentationController.Detent.Identifier {
static let bottom = Self("\(Bundle.main.bundleIdentifier!).bottom")
}

View File

@ -0,0 +1,89 @@
//
// DuckAnimationController.swift
// Duckable
//
// Created by Shadowfacts on 11/7/22.
//
import UIKit
@available(iOS 16.0, *)
class DuckAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
let owner: DuckableContainerViewController
let needsShrinkAnimation: Bool
init(owner: DuckableContainerViewController, needsShrinkAnimation: Bool) {
self.owner = owner
self.needsShrinkAnimation = needsShrinkAnimation
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.2
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard case .ducked(let duckable, placeholder: let placeholder) = owner.state,
let presented = transitionContext.viewController(forKey: .from) else {
transitionContext.completeTransition(false)
return
}
guard transitionContext.isAnimated else {
transitionContext.completeTransition(true)
return
}
let container = transitionContext.containerView
if needsShrinkAnimation {
duckable.duckableViewControllerWillAnimateDuck(withDuration: 0.2, afterDelay: 0.2)
let presentedFrameInContainer = container.convert(presented.view.bounds, from: presented.view)
let heightToSlide = container.bounds.height - container.safeAreaInsets.bottom - detentHeight - presentedFrameInContainer.minY
let slideAnimator = UIViewPropertyAnimator(duration: 0.4, dampingRatio: 1)
slideAnimator.addAnimations {
container.transform = CGAffineTransform(translationX: 0, y: heightToSlide + 10)
}
slideAnimator.addCompletion { _ in
duckable.duckableViewControllerDidFinishAnimatingDuck()
transitionContext.completeTransition(true)
}
slideAnimator.startAnimation()
placeholder.view.transform = CGAffineTransform(translationX: 0, y: 10)
let bounceAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .linear) {
placeholder.view.transform = .identity
container.transform = CGAffineTransform(translationX: 0, y: heightToSlide)
}
bounceAnimator.startAnimation(afterDelay: 0.3)
let fadeAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .linear) {
presented.view.layer.opacity = 0
}
fadeAnimator.startAnimation(afterDelay: 0.3)
} else {
duckable.duckableViewControllerWillAnimateDuck(withDuration: 0.2, afterDelay: 0)
placeholder.view.transform = CGAffineTransform(translationX: 0, y: 10)
let bounceAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .linear) {
placeholder.view.transform = .identity
container.transform = CGAffineTransform(translationX: 0, y: -10)
}
bounceAnimator.startAnimation(afterDelay: 0.2)
let fadeAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .linear) {
presented.view.layer.opacity = 0
}
fadeAnimator.addCompletion { _ in
duckable.duckableViewControllerDidFinishAnimatingDuck()
transitionContext.completeTransition(true)
}
fadeAnimator.startAnimation(afterDelay: 0.2)
}
}
}

View File

@ -0,0 +1,239 @@
//
// DuckableContainerViewController.swift
// Duckable
//
// Created by Shadowfacts on 11/7/22.
//
import UIKit
let duckedCornerRadius: CGFloat = 10
let detentHeight: CGFloat = 44
@available(iOS 16.0, *)
public class DuckableContainerViewController: UIViewController, DuckableViewControllerDelegate {
public let child: UIViewController
private var bottomConstraint: NSLayoutConstraint!
private(set) var state = State.idle
public var duckedViewController: DuckableViewController? {
if case .ducked(let vc, placeholder: _) = state {
return vc
} else {
return nil
}
}
public init(child: UIViewController) {
self.child = child
super.init(nibName: nil, bundle: nil)
swizzleSheetController()
}
required init?(coder: NSCoder) {
fatalError()
}
public override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
child.beginAppearanceTransition(true, animated: false)
addChild(child)
child.didMove(toParent: self)
child.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(child.view)
child.endAppearanceTransition()
bottomConstraint = child.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
NSLayoutConstraint.activate([
child.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
child.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
child.view.topAnchor.constraint(equalTo: view.topAnchor),
bottomConstraint,
])
}
func presentDuckable(_ viewController: DuckableViewController, animated: Bool, isDucked: Bool, completion: (() -> Void)?) {
guard case .idle = state else {
if animated,
case .ducked(_, placeholder: let placeholder) = state {
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
let origConstant = placeholder.topConstraint.constant
UIView.animateKeyframes(withDuration: 0.4, delay: 0) {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {
placeholder.topConstraint.constant = origConstant - 20
self.view.layoutIfNeeded()
}
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) {
placeholder.topConstraint.constant = origConstant
self.view.layoutIfNeeded()
}
}
}
return
}
if isDucked {
state = .ducked(viewController, placeholder: createPlaceholderForDuckedViewController(viewController))
configureChildForDuckedPlaceholder()
} else {
state = .presentingDucked(viewController, isFirstPresentation: true)
doPresentDuckable(viewController, animated: animated, completion: completion)
}
}
private func doPresentDuckable(_ viewController: DuckableViewController, animated: Bool, completion: (() -> Void)?) {
viewController.duckableDelegate = self
let nav = UINavigationController(rootViewController: viewController)
nav.modalPresentationStyle = .custom
nav.transitioningDelegate = self
present(nav, animated: animated) {
self.configureChildForDuckedPlaceholder()
completion?()
}
}
public func duckableViewControllerWillDismiss(animated: Bool) {
state = .idle
bottomConstraint.isActive = false
bottomConstraint = child.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
bottomConstraint.isActive = true
child.view.layer.cornerRadius = 0
setOverrideTraitCollection(nil, forChild: child)
}
func createPlaceholderForDuckedViewController(_ viewController: DuckableViewController) -> DuckedPlaceholderViewController {
let placeholder = DuckedPlaceholderViewController(for: viewController, owner: self)
placeholder.view.translatesAutoresizingMaskIntoConstraints = false
placeholder.beginAppearanceTransition(true, animated: false)
self.addChild(placeholder)
placeholder.didMove(toParent: self)
self.view.addSubview(placeholder.view)
placeholder.endAppearanceTransition()
let placeholderTopConstraint = placeholder.view.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -detentHeight)
placeholder.topConstraint = placeholderTopConstraint
NSLayoutConstraint.activate([
placeholder.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
placeholder.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
placeholder.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
placeholderTopConstraint
])
// otherwise the layout changes get lumped in with the system animation
UIView.performWithoutAnimation {
self.view.layoutIfNeeded()
}
return placeholder
}
func duckViewController() {
guard case .presentingDucked(let viewController, isFirstPresentation: _) = state else {
return
}
let placeholder = createPlaceholderForDuckedViewController(viewController)
state = .ducked(viewController, placeholder: placeholder)
configureChildForDuckedPlaceholder()
dismiss(animated: true)
}
private func configureChildForDuckedPlaceholder() {
bottomConstraint.isActive = false
bottomConstraint = child.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -detentHeight - 4)
bottomConstraint.isActive = true
child.view.layer.cornerRadius = duckedCornerRadius
child.view.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
child.view.layer.masksToBounds = true
}
@objc func unduckViewController() {
guard case .ducked(let viewController, placeholder: let placeholder) = state else {
return
}
state = .presentingDucked(viewController, isFirstPresentation: false)
doPresentDuckable(viewController, animated: true) {
placeholder.view.removeFromSuperview()
placeholder.willMove(toParent: nil)
placeholder.removeFromParent()
}
}
func sheetOffsetDidChange() {
if case .presentingDucked(let duckable, isFirstPresentation: _) = state {
duckable.duckableViewControllerMayAttemptToDuck()
}
}
enum State {
case idle
case presentingDucked(DuckableViewController, isFirstPresentation: Bool)
case ducked(DuckableViewController, placeholder: DuckedPlaceholderViewController)
}
}
@available(iOS 16.0, *)
extension DuckableContainerViewController: UIViewControllerTransitioningDelegate {
public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
let controller = UISheetPresentationController(presentedViewController: presented, presenting: presenting)
controller.delegate = self
controller.prefersGrabberVisible = true
controller.selectedDetentIdentifier = .large
controller.largestUndimmedDetentIdentifier = .bottom
controller.detents = [
.custom(identifier: .bottom, resolver: { context in
return detentHeight
}),
.large(),
]
return controller
}
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if case .ducked(_, placeholder: _) = state {
return DuckAnimationController(
owner: self,
needsShrinkAnimation: isDetentChangingDueToGrabberAction
)
} else {
return nil
}
}
}
@available(iOS 16.0, *)
extension DuckableContainerViewController: UISheetPresentationControllerDelegate {
public func presentationController(_ presentationController: UIPresentationController, willPresentWithAdaptiveStyle style: UIModalPresentationStyle, transitionCoordinator: UIViewControllerTransitionCoordinator?) {
guard let snapshot = child.view.snapshotView(afterScreenUpdates: false) else {
setOverrideTraitCollection(UITraitCollection(userInterfaceLevel: .elevated), forChild: child)
return
}
snapshot.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(snapshot)
NSLayoutConstraint.activate([
snapshot.leadingAnchor.constraint(equalTo: child.view.leadingAnchor),
snapshot.trailingAnchor.constraint(equalTo: child.view.trailingAnchor),
snapshot.topAnchor.constraint(equalTo: child.view.topAnchor),
snapshot.bottomAnchor.constraint(equalTo: child.view.bottomAnchor),
])
setOverrideTraitCollection(UITraitCollection(userInterfaceLevel: .elevated), forChild: child)
transitionCoordinator!.animate { context in
snapshot.layer.opacity = 0
} completion: { _ in
snapshot.removeFromSuperview()
}
}
public func sheetPresentationControllerDidChangeSelectedDetentIdentifier(_ sheetPresentationController: UISheetPresentationController) {
if sheetPresentationController.selectedDetentIdentifier == .bottom {
duckViewController()
}
}
}

View File

@ -0,0 +1,71 @@
//
// DuckedPlaceholderView.swift
// Duckable
//
// Created by Shadowfacts on 11/7/22.
//
import UIKit
@available(iOS 16.0, *)
class DuckedPlaceholderViewController: UIViewController {
private unowned let owner: DuckableContainerViewController
private let navBar = UINavigationBar()
var topConstraint: NSLayoutConstraint!
init(for duckableViewController: DuckableViewController, owner: DuckableContainerViewController) {
self.owner = owner
super.init(nibName: nil, bundle: nil)
let item = UINavigationItem()
item.title = duckableViewController.navigationItem.title
item.titleView = duckableViewController.navigationItem.titleView
navBar.setItems([item], animated: false)
}
required init?(coder: NSCoder) {
fatalError()
}
override func viewDidLoad() {
super.viewDidLoad()
setBackgroundColor()
view.layer.cornerRadius = duckedCornerRadius
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOpacity = 0.05
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(placeholderTapped)))
let appearance = UINavigationBarAppearance()
appearance.configureWithTransparentBackground()
navBar.standardAppearance = appearance
navBar.isUserInteractionEnabled = false
navBar.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(navBar)
NSLayoutConstraint.activate([
navBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
navBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
navBar.topAnchor.constraint(equalTo: view.topAnchor),
])
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
setBackgroundColor()
}
private func setBackgroundColor() {
// when just using .systemBackground and setting the override trait collection for the placeholder VC,
// the color doesn't change until after the dismiss animation occurs (but only when tapping the grabber to duck, not when swiping)
view.backgroundColor = .systemBackground.resolvedColor(with: UITraitCollection(traitsFrom: [traitCollection, UITraitCollection(userInterfaceLevel: .elevated)]))
}
@objc private func placeholderTapped() {
owner.unduckViewController()
}
}

View File

@ -0,0 +1,33 @@
//
// Swizzler.swift
// Duckable
//
// Created by Shadowfacts on 11/7/22.
//
import UIKit
import os.log
private var hasInitialized = false
var isDetentChangingDueToGrabberAction = false
@available(iOS 16.0, *)
func swizzleSheetController() {
guard !hasInitialized else {
return
}
hasInitialized = true
var originalIMP: IMP?
let imp = imp_implementationWithBlock({ (self: UISheetPresentationController, param: AnyObject) in
let original = unsafeBitCast(originalIMP!, to: (@convention(c) (UISheetPresentationController, AnyObject) -> Void).self)
isDetentChangingDueToGrabberAction = true
original(self, param)
isDetentChangingDueToGrabberAction = false
} as @convention(block) (UISheetPresentationController, AnyObject) -> Void)
let sel = [":", "PrimaryAction", "GrabberDidTrigger", "dropShadowView", "_"].reversed().joined()
originalIMP = class_replaceMethod(UISheetPresentationController.self, Selector(sel), imp, "v@:@")
if originalIMP == nil {
os_log(.fault, log: .default, "Unable to initialize Duckable grabber tap hook")
}
}

View File

@ -0,0 +1,34 @@
// swift-tools-version: 5.6
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "Pachyderm",
platforms: [
.iOS(.v14),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "Pachyderm",
targets: ["Pachyderm"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(url: "https://github.com/karwa/swift-url.git", branch: "main"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "Pachyderm",
dependencies: [
.product(name: "WebURL", package: "swift-url"),
.product(name: "WebURLFoundationExtras", package: "swift-url"),
]),
.testTarget(
name: "PachydermTests",
dependencies: ["Pachyderm"]),
]
)

View File

@ -0,0 +1,3 @@
# Pachyderm
A description of this package.

View File

@ -0,0 +1,493 @@
//
// Client.swift
// Pachyderm
//
// Created by Shadowfacts on 9/8/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
/**
The base Mastodon API client.
*/
public class Client {
public typealias Callback<Result: Decodable> = (Response<Result>) -> Void
let baseURL: URL
let session: URLSession
public var accessToken: String?
public var appID: String?
public var clientID: String?
public var clientSecret: String?
public var timeoutInterval: TimeInterval = 60
static let decoder: JSONDecoder = {
let decoder = JSONDecoder()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ"
formatter.timeZone = TimeZone(abbreviation: "UTC")
formatter.locale = Locale(identifier: "en_US_POSIX")
let iso8601 = ISO8601DateFormatter()
decoder.dateDecodingStrategy = .custom({ (decoder) in
let container = try decoder.singleValueContainer()
let str = try container.decode(String.self)
// for the next time mastodon accidentally changes date formats >.>
if let date = formatter.date(from: str) {
return date
} else if let date = iso8601.date(from: str) {
return date
} else {
throw DecodingError.typeMismatch(Date.self, .init(codingPath: container.codingPath, debugDescription: "unexpected date format"))
}
})
return decoder
}()
static let encoder: JSONEncoder = {
let encoder = JSONEncoder()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ"
formatter.timeZone = TimeZone(abbreviation: "UTC")
formatter.locale = Locale(identifier: "en_US_POSIX")
encoder.dateEncodingStrategy = .formatted(formatter)
return encoder
}()
public init(baseURL: URL, accessToken: String? = nil, session: URLSession = .shared) {
self.baseURL = baseURL
self.accessToken = accessToken
self.session = session
}
@discardableResult
public func run<Result>(_ request: Request<Result>, completion: @escaping Callback<Result>) -> URLSessionTask? {
guard let urlRequest = createURLRequest(request: request) else {
completion(.failure(Error(request: request, type: .invalidRequest)))
return nil
}
let task = session.dataTask(with: urlRequest) { data, response, error in
if let error = error {
completion(.failure(Error(request: request, type: .networkError(error))))
return
}
guard let data = data,
let response = response as? HTTPURLResponse else {
completion(.failure(Error(request: request, type: .invalidResponse)))
return
}
guard response.statusCode == 200 else {
let mastodonError = try? Client.decoder.decode(MastodonError.self, from: data)
let type: ErrorType = mastodonError.flatMap { .mastodonError(response.statusCode, $0.description) } ?? .unexpectedStatus(response.statusCode)
completion(.failure(Error(request: request, type: type)))
return
}
let result: Result
do {
result = try Client.decoder.decode(Result.self, from: data)
} catch {
completion(.failure(Error(request: request, type: .invalidModel(error))))
return
}
let pagination = response.allHeaderFields["Link"].flatMap { $0 as? String }.flatMap(Pagination.init)
completion(.success(result, pagination))
}
task.resume()
return task
}
func createURLRequest<Result>(request: Request<Result>) -> URLRequest? {
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) else { return nil }
components.path = request.endpoint.path
components.queryItems = request.queryParameters.isEmpty ? nil : request.queryParameters.queryItems
guard let url = components.url else { return nil }
var urlRequest = URLRequest(url: url, timeoutInterval: timeoutInterval)
urlRequest.httpMethod = request.method.name
urlRequest.httpBody = request.body.data
if let mimeType = request.body.mimeType {
urlRequest.setValue(mimeType, forHTTPHeaderField: "Content-Type")
}
if let accessToken = accessToken {
urlRequest.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
}
return urlRequest
}
// MARK: - Authorization
public func registerApp(name: String, redirectURI: String, scopes: [Scope], website: URL? = nil, completion: @escaping Callback<RegisteredApplication>) {
let request = Request<RegisteredApplication>(method: .post, path: "/api/v1/apps", body: ParametersBody([
"client_name" => name,
"redirect_uris" => redirectURI,
"scopes" => scopes.scopeString,
"website" => website?.absoluteString
]))
run(request) { result in
defer { completion(result) }
guard case let .success(application, _) = result else { return }
self.appID = application.id
self.clientID = application.clientID
self.clientSecret = application.clientSecret
}
}
public func getAccessToken(authorizationCode: String, redirectURI: String, completion: @escaping Callback<LoginSettings>) {
let request = Request<LoginSettings>(method: .post, path: "/oauth/token", body: ParametersBody([
"client_id" => clientID,
"client_secret" => clientSecret,
"grant_type" => "authorization_code",
"code" => authorizationCode,
"redirect_uri" => redirectURI
]))
run(request) { result in
defer { completion(result) }
guard case let .success(loginSettings, _) = result else { return }
self.accessToken = loginSettings.accessToken
}
}
public func nodeInfo(completion: @escaping Callback<NodeInfo>) {
let wellKnown = Request<WellKnown>(method: .get, path: "/.well-known/nodeinfo")
run(wellKnown) { result in
switch result {
case let .failure(error):
completion(.failure(error))
case let .success(wellKnown, _):
if let url = wellKnown.links.first(where: { $0.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0" }),
let components = URLComponents(string: url.href),
components.host == self.baseURL.host {
let nodeInfo = Request<NodeInfo>(method: .get, path: Endpoint(stringLiteral: components.path))
self.run(nodeInfo, completion: completion)
}
}
}
}
// MARK: - Self
public static func getSelfAccount() -> Request<Account> {
return Request<Account>(method: .get, path: "/api/v1/accounts/verify_credentials")
}
public static func getFavourites() -> Request<[Status]> {
return Request<[Status]>(method: .get, path: "/api/v1/favourites")
}
public static func getRelationships(accounts: [String]? = nil) -> Request<[Relationship]> {
return Request<[Relationship]>(method: .get, path: "/api/v1/accounts/relationships", queryParameters: "id" => accounts)
}
public static func getInstance() -> Request<Instance> {
return Request<Instance>(method: .get, path: "/api/v1/instance")
}
public static func getCustomEmoji() -> Request<[Emoji]> {
return Request<[Emoji]>(method: .get, path: "/api/v1/custom_emojis")
}
// MARK: - Accounts
public static func getAccount(id: String) -> Request<Account> {
return Request<Account>(method: .get, path: "/api/v1/accounts/\(id)")
}
public static func searchForAccount(query: String, limit: Int? = nil, following: Bool? = nil) -> Request<[Account]> {
return Request<[Account]>(method: .get, path: "/api/v1/accounts/search", queryParameters: [
"q" => query,
"limit" => limit,
"following" => following
])
}
// MARK: - Blocks
public static func getBlocks() -> Request<[Account]> {
return Request<[Account]>(method: .get, path: "/api/v1/blocks")
}
public static func getDomainBlocks() -> Request<[String]> {
return Request<[String]>(method: .get, path: "api/v1/domain_blocks")
}
public static func block(domain: String) -> Request<Empty> {
return Request<Empty>(method: .post, path: "/api/v1/domain_blocks", body: ParametersBody([
"domain" => domain
]))
}
public static func unblock(domain: String) -> Request<Empty> {
return Request<Empty>(method: .delete, path: "/api/v1/domain_blocks", body: ParametersBody([
"domain" => domain
]))
}
// MARK: - Filters
public static func getFiltersV1() -> Request<[FilterV1]> {
return Request<[FilterV1]>(method: .get, path: "/api/v1/filters")
}
public static func createFilterV1(phrase: String, context: [FilterV1.Context], irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresIn: TimeInterval? = nil) -> Request<FilterV1> {
return Request<FilterV1>(method: .post, path: "/api/v1/filters", body: ParametersBody([
"phrase" => phrase,
"irreversible" => irreversible,
"whole_word" => wholeWord,
"expires_in" => expiresIn,
] + "context" => context.contextStrings))
}
public static func getFilterV1(id: String) -> Request<FilterV1> {
return Request<FilterV1>(method: .get, path: "/api/v1/filters/\(id)")
}
public static func getFiltersV2() -> Request<[FilterV2]> {
return Request(method: .get, path: "/api/v2/filters")
}
// MARK: - Follows
public static func getFollowRequests(range: RequestRange = .default) -> Request<[Account]> {
var request = Request<[Account]>(method: .get, path: "/api/v1/follow_requests")
request.range = range
return request
}
public static func getFollowSuggestions() -> Request<[Account]> {
return Request<[Account]>(method: .get, path: "/api/v1/suggestions")
}
public static func followRemote(acct: String) -> Request<Account> {
return Request<Account>(method: .post, path: "/api/v1/follows", body: ParametersBody(["uri" => acct]))
}
public static func getFollowedHashtags() -> Request<[Hashtag]> {
return Request(method: .get, path: "/api/v1/followed_tags")
}
// MARK: - Lists
public static func getLists() -> Request<[List]> {
return Request<[List]>(method: .get, path: "/api/v1/lists")
}
public static func getList(id: String) -> Request<List> {
return Request<List>(method: .get, path: "/api/v1/lists/\(id)")
}
public static func createList(title: String) -> Request<List> {
return Request<List>(method: .post, path: "/api/v1/lists", body: ParametersBody(["title" => title]))
}
// MARK: - Media
public static func upload(attachment: FormAttachment, description: String? = nil, focus: (Float, Float)? = nil) -> Request<Attachment> {
return Request<Attachment>(method: .post, path: "/api/v1/media", body: FormDataBody([
"description" => description,
"focus" => focus
], attachment))
}
// MARK: - Mutes
public static func getMutes(range: RequestRange) -> Request<[Account]> {
var request = Request<[Account]>(method: .get, path: "/api/v1/mutes")
request.range = range
return request
}
// MARK: - Notifications
public static func getNotifications(allowedTypes: [Notification.Kind], range: RequestRange = .default) -> Request<[Notification]> {
var request = Request<[Notification]>(method: .get, path: "/api/v1/notifications", queryParameters:
"types" => allowedTypes.map { $0.rawValue }
)
request.range = range
return request
}
public static func getNotifications(excludedTypes: [Notification.Kind], range: RequestRange = .default) -> Request<[Notification]> {
var request = Request<[Notification]>(method: .get, path: "/api/v1/notifications", queryParameters:
"exclude_types" => excludedTypes.map { $0.rawValue }
)
request.range = range
return request
}
public static func clearNotifications() -> Request<Empty> {
return Request<Empty>(method: .post, path: "/api/v1/notifications/clear")
}
// MARK: - Reports
public static func getReports() -> Request<[Report]> {
return Request<[Report]>(method: .get, path: "/api/v1/reports")
}
public static func report(
account: String,
statuses: [String],
comment: String,
forward: Bool,
category: String,
ruleIDs: [String]
) -> Request<Report> {
return Request<Report>(method: .post, path: "/api/v1/reports", body: ParametersBody([
"account_id" => account,
"comment" => comment,
"forward" => forward,
"category" => category,
] + "status_ids" => statuses + "rule_ids" => ruleIDs))
}
// MARK: - Search
public static func search(query: String, types: [SearchResultType]? = nil, resolve: Bool? = nil, limit: Int? = nil, following: Bool? = nil) -> Request<SearchResults> {
return Request<SearchResults>(method: .get, path: "/api/v2/search", queryParameters: [
"q" => query,
"resolve" => resolve,
"limit" => limit,
"following" => following,
] + "types" => types?.map { $0.rawValue })
}
// MARK: - Statuses
public static func getStatus(id: String) -> Request<Status> {
return Request<Status>(method: .get, path: "/api/v1/statuses/\(id)")
}
public static func createStatus(text: String,
contentType: StatusContentType = .plain,
inReplyTo: String? = nil,
media: [Attachment]? = nil,
sensitive: Bool? = nil,
spoilerText: String? = nil,
visibility: Status.Visibility? = nil,
language: String? = nil,
pollOptions: [String]? = nil,
pollExpiresIn: Int? = nil,
pollMultiple: Bool? = nil,
localOnly: Bool? = nil /* hometown only, not glitch */) -> Request<Status> {
return Request<Status>(method: .post, path: "/api/v1/statuses", body: ParametersBody([
"status" => text,
"content_type" => contentType.mimeType,
"in_reply_to_id" => inReplyTo,
"sensitive" => sensitive,
"spoiler_text" => spoilerText,
"visibility" => visibility?.rawValue,
"language" => language,
"poll[expires_in]" => pollExpiresIn,
"poll[multiple]" => pollMultiple,
"local_only" => localOnly,
] + "media_ids" => media?.map { $0.id } + "poll[options]" => pollOptions))
}
// MARK: - Timelines
public static func getStatuses(timeline: Timeline, range: RequestRange = .default) -> Request<[Status]> {
return timeline.request(range: range)
}
// MARK: - Bookmarks
public static func getBookmarks(range: RequestRange = .default) -> Request<[Status]> {
var request = Request<[Status]>(method: .get, path: "/api/v1/bookmarks")
request.range = range
return request
}
// MARK: - Instance
public static func getTrendingHashtags(limit: Int? = nil) -> Request<[Hashtag]> {
let parameters: [Parameter]
if let limit = limit {
parameters = ["limit" => limit]
} else {
parameters = []
}
return Request<[Hashtag]>(method: .get, path: "/api/v1/trends", queryParameters: parameters)
}
public static func getTrendingStatuses(limit: Int? = nil) -> Request<[Status]> {
let parameters: [Parameter]
if let limit = limit {
parameters = ["limit" => limit]
} else {
parameters = []
}
return Request(method: .get, path: "/api/v1/trends/statuses", queryParameters: parameters)
}
public static func getTrendingLinks(limit: Int? = nil) -> Request<[Card]> {
let parameters: [Parameter]
if let limit = limit {
parameters = ["limit" => limit]
} else {
parameters = []
}
return Request(method: .get, path: "/api/v1/trends/links", queryParameters: parameters)
}
public static func getFeaturedProfiles(local: Bool, order: DirectoryOrder, offset: Int? = nil, limit: Int? = nil) -> Request<[Account]> {
var parameters = [
"order" => order.rawValue,
"local" => local,
]
if let offset = offset {
parameters.append("offset" => offset)
}
if let limit = limit {
parameters.append("limit" => limit)
}
return Request<[Account]>(method: .get, path: "/api/v1/directory", queryParameters: parameters)
}
public static func getSuggestions(limit: Int?) -> Request<[Suggestion]> {
return Request(method: .get, path: "/api/v2/suggestions", queryParameters: [
"limit" => limit,
])
}
}
extension Client {
public struct Error: LocalizedError {
public let requestMethod: Method
public let requestEndpoint: Endpoint
public let type: ErrorType
#if DEBUG
public static let debug = Error(request: Client.getStatuses(timeline: .home), type: .invalidResponse)
#endif
init<ResultType: Decodable>(request: Request<ResultType>, type: ErrorType) {
self.requestMethod = request.method
self.requestEndpoint = request.endpoint
self.type = type
}
public var localizedDescription: String {
switch type {
case .networkError(let error):
return "Network Error: \(error.localizedDescription)"
// todo: support more status codes
case .unexpectedStatus(413):
return "HTTP 413: Payload Too Large"
case .unexpectedStatus(let code):
return "HTTP Code \(code)"
case .invalidRequest:
return "Invalid Request"
case .invalidResponse:
return "Invalid Response"
case .invalidModel(_):
return "Invalid Model"
case .mastodonError(let code, let error):
return "Server Error (\(code)): \(error)"
}
}
}
public enum ErrorType: LocalizedError {
case networkError(Swift.Error)
case unexpectedStatus(Int)
case invalidRequest
case invalidResponse
case invalidModel(Swift.Error)
case mastodonError(Int, String)
}
}

View File

@ -0,0 +1,179 @@
//
// Account.swift
// Pachyderm
//
// Created by Shadowfacts on 9/8/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public final class Account: AccountProtocol, Decodable {
public let id: String
public let username: String
public let acct: String
public let displayName: String
public let locked: Bool
public let createdAt: Date
public let followersCount: Int
public let followingCount: Int
public let statusesCount: Int
public let note: String
public let url: URL
// required on mastodon, but optional on gotosocial
public let avatar: URL?
public let avatarStatic: URL?
public let header: URL?
public let headerStatic: URL?
public private(set) var emojis: [Emoji]
public let moved: Bool?
public let movedTo: Account?
public let fields: [Field]
public let bot: Bool?
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(String.self, forKey: .id)
self.username = try container.decode(String.self, forKey: .username)
self.acct = try container.decode(String.self, forKey: .acct)
self.displayName = try container.decode(String.self, forKey: .displayName)
self.locked = try container.decode(Bool.self, forKey: .locked)
self.createdAt = try container.decode(Date.self, forKey: .createdAt)
self.followersCount = try container.decode(Int.self, forKey: .followersCount)
self.followingCount = try container.decode(Int.self, forKey: .followingCount)
self.statusesCount = try container.decode(Int.self, forKey: .statusesCount)
self.note = try container.decode(String.self, forKey: .note)
self.url = try container.decode(URL.self, forKey: .url)
self.avatar = try? container.decode(URL.self, forKey: .avatar)
self.avatarStatic = try? container.decode(URL.self, forKey: .avatarStatic)
self.header = try? container.decode(URL.self, forKey: .header)
self.headerStatic = try? container.decode(URL.self, forKey: .headerStatic)
// even up-to-date pixelfed instances sometimes lack this, for reasons unclear
if let emojis = try container.decodeIfPresent([Emoji].self, forKey: .emojis) {
self.emojis = emojis
} else {
self.emojis = []
}
self.fields = (try? container.decode([Field].self, forKey: .fields)) ?? []
self.bot = try? container.decode(Bool.self, forKey: .bot)
if let moved = try? container.decode(Bool.self, forKey: .moved) {
self.moved = moved
self.movedTo = nil
} else if let account = try? container.decode(Account.self, forKey: .moved) {
self.moved = true
self.movedTo = account
} else {
self.moved = false
self.movedTo = nil
}
}
public static func authorizeFollowRequest(_ account: Account) -> Request<Relationship> {
return Request<Relationship>(method: .post, path: "/api/v1/follow_requests/\(account.id)/authorize")
}
public static func rejectFollowRequest(_ account: Account) -> Request<Relationship> {
return Request<Relationship>(method: .post, path: "/api/v1/follow_requests/\(account.id)/reject")
}
public static func removeFromFollowRequests(_ account: Account) -> Request<Empty> {
return Request<Empty>(method: .delete, path: "/api/v1/suggestions/\(account.id)")
}
public static func getFollowers(_ accountID: String, range: RequestRange = .default) -> Request<[Account]> {
var request = Request<[Account]>(method: .get, path: "/api/v1/accounts/\(accountID)/followers")
request.range = range
return request
}
public static func getFollowing(_ accountID: String, range: RequestRange = .default) -> Request<[Account]> {
var request = Request<[Account]>(method: .get, path: "/api/v1/accounts/\(accountID)/following")
request.range = range
return request
}
public static func getStatuses(_ accountID: String, range: RequestRange = .default, onlyMedia: Bool? = nil, pinned: Bool? = nil, excludeReplies: Bool? = nil, excludeReblogs: Bool? = nil) -> Request<[Status]> {
var request = Request<[Status]>(method: .get, path: "/api/v1/accounts/\(accountID)/statuses", queryParameters: [
"only_media" => onlyMedia,
"pinned" => pinned,
"exclude_replies" => excludeReplies,
"exclude_reblogs" => excludeReblogs,
])
request.range = range
return request
}
public static func follow(_ accountID: String) -> Request<Relationship> {
return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(accountID)/follow")
}
public static func unfollow(_ accountID: String) -> Request<Relationship> {
return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(accountID)/unfollow")
}
public static func block(_ accountID: String) -> Request<Relationship> {
return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(accountID)/block")
}
public static func unblock(_ accountID: String) -> Request<Relationship> {
return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(accountID)/unblock")
}
public static func mute(_ accountID: String, notifications: Bool? = nil) -> Request<Relationship> {
return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(accountID)/mute", body: ParametersBody([
"notifications" => notifications
]))
}
public static func unmute(_ accountID: String) -> Request<Relationship> {
return Request<Relationship>(method: .post, path: "/api/v1/accounts/\(accountID)/unmute")
}
public static func getLists(_ account: Account) -> Request<[List]> {
return Request<[List]>(method: .get, path: "/api/v1/accounts/\(account.id)/lists")
}
private enum CodingKeys: String, CodingKey {
case id
case username
case acct
case displayName = "display_name"
case locked
case createdAt = "created_at"
case followersCount = "followers_count"
case followingCount = "following_count"
case statusesCount = "statuses_count"
case note
case url
case avatar
case avatarStatic = "avatar_static"
case header
case headerStatic = "header_static"
case emojis
case moved
case fields
case bot
}
}
extension Account: CustomDebugStringConvertible {
public var debugDescription: String {
return "Account(\(id), \(acct))"
}
}
extension Account {
public struct Field: Codable {
public let name: String
public let value: String
public let verifiedAt: Date?
enum CodingKeys: String, CodingKey {
case name
case value
case verifiedAt = "verified_at"
}
}
}

View File

@ -0,0 +1,32 @@
//
// Application.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Application: Decodable {
public let name: String
public let website: URL?
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
if let websiteStr = try container.decodeIfPresent(String.self, forKey: .website),
let url = URL(string: websiteStr) {
self.website = url
} else {
self.website = nil
}
}
private enum CodingKeys: String, CodingKey {
case name
case website
}
}

View File

@ -8,18 +8,18 @@
import Foundation
public class Attachment: Decodable {
public class Attachment: Codable {
public let id: String
public let kind: Kind
public let url: URL
public let remoteURL: URL?
public let previewURL: URL
public let textURL: URL?
public let previewURL: URL?
public let meta: Metadata?
public let description: String?
public let blurHash: String?
public static func update(_ attachment: Attachment, focus: (Float, Float)?, description: String?) -> Request<Attachment> {
return Request<Attachment>(method: .put, path: "/api/v1/media/\(attachment.id)", body: .formData([
return Request<Attachment>(method: .put, path: "/api/v1/media/\(attachment.id)", body: FormDataBody([
"description" => (description ?? attachment.description),
"focus" => focus
], nil))
@ -29,20 +29,12 @@ public class Attachment: Decodable {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(String.self, forKey: .id)
self.kind = try container.decode(Kind.self, forKey: .kind)
self.url = URL(lenient: try container.decode(String.self, forKey: .url))!
if let remote = try? container.decode(String.self, forKey: .remoteURL) {
self.remoteURL = URL(lenient: remote.replacingOccurrences(of: " ", with: "%20"))
} else {
self.remoteURL = nil
}
self.previewURL = URL(lenient: try container.decode(String.self, forKey: .previewURL).replacingOccurrences(of: " ", with: "%20"))!
if let text = try? container.decode(String.self, forKey: .textURL) {
self.textURL = URL(lenient: text.replacingOccurrences(of: " ", with: "%20"))
} else {
self.textURL = nil
}
self.meta = try? container.decode(Metadata.self, forKey: .meta)
self.description = try? container.decode(String.self, forKey: .description)
self.url = try container.decode(URL.self, forKey: .url)
self.previewURL = try? container.decode(URL?.self, forKey: .previewURL)
self.remoteURL = try? container.decode(URL?.self, forKey: .remoteURL)
self.meta = try? container.decode(Metadata?.self, forKey: .meta)
self.description = try? container.decode(String?.self, forKey: .description)
self.blurHash = try? container.decode(String?.self, forKey: .blurHash)
}
private enum CodingKeys: String, CodingKey {
@ -51,24 +43,41 @@ public class Attachment: Decodable {
case url
case remoteURL = "remote_url"
case previewURL = "preview_url"
case textURL = "text_url"
case meta
case description
case blurHash = "blurhash"
}
}
extension Attachment {
public enum Kind: String, Decodable {
public enum Kind: String, Codable {
case image
case video
case gifv
case audio
case unknown
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
switch try container.decode(String.self) {
// gotosocial uses "gif" for gif images
case "image", "gif":
self = .image
case "video":
self = .video
case "gifv":
self = .gifv
case "audio":
self = .audio
default:
self = .unknown
}
}
}
}
extension Attachment {
public class Metadata: Decodable {
public struct Metadata: Codable {
public let length: String?
public let duration: Float?
public let audioEncoding: String?
@ -99,7 +108,7 @@ extension Attachment {
}
}
public class ImageMetadata: Decodable {
public struct ImageMetadata: Codable {
public let width: Int?
public let height: Int?
public let size: String?
@ -113,14 +122,3 @@ extension Attachment {
}
}
}
fileprivate extension URL {
private static let allowedChars = CharacterSet.urlHostAllowed.union(.urlPathAllowed).union(.urlQueryAllowed)
init?(lenient string: String) {
guard let escaped = string.addingPercentEncoding(withAllowedCharacters: URL.allowedChars) else {
return nil
}
self.init(string: escaped)
}
}

View File

@ -0,0 +1,84 @@
//
// Card.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
import WebURL
public class Card: Codable {
public let url: WebURL
public let title: String
public let description: String
public let image: WebURL?
public let kind: Kind
public let authorName: String?
public let authorURL: WebURL?
public let providerName: String?
public let providerURL: WebURL?
public let html: String?
public let width: Int?
public let height: Int?
public let blurhash: String?
/// Only present when returned from the trending links endpoint
public let history: [History]?
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.url = try container.decode(WebURL.self, forKey: .url)
self.title = try container.decode(String.self, forKey: .title)
self.description = try container.decode(String.self, forKey: .description)
self.kind = try container.decode(Kind.self, forKey: .kind)
self.image = try? container.decodeIfPresent(WebURL.self, forKey: .image)
self.authorName = try? container.decodeIfPresent(String.self, forKey: .authorName)
self.authorURL = try? container.decodeIfPresent(WebURL.self, forKey: .authorURL)
self.providerName = try? container.decodeIfPresent(String.self, forKey: .providerName)
self.providerURL = try? container.decodeIfPresent(WebURL.self, forKey: .providerURL)
self.html = try? container.decodeIfPresent(String.self, forKey: .html)
self.width = try? container.decodeIfPresent(Int.self, forKey: .width)
self.height = try? container.decodeIfPresent(Int.self, forKey: .height)
self.blurhash = try? container.decodeIfPresent(String.self, forKey: .blurhash)
self.history = try? container.decodeIfPresent([History].self, forKey: .history)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(url, forKey: .url)
try container.encode(title, forKey: .title)
try container.encode(description, forKey: .description)
try container.encode(kind, forKey: .kind)
try container.encode(image, forKey: .image)
try container.encode(blurhash, forKey: .blurhash)
}
private enum CodingKeys: String, CodingKey {
case url
case title
case description
case image
case kind = "type"
case authorName = "author_name"
case authorURL = "author_url"
case providerName = "provider_name"
case providerURL = "provider_url"
case html
case width
case height
case blurhash
case history
}
}
extension Card {
public enum Kind: String, Codable {
case link
case photo
case video
case rich
}
}

View File

@ -0,0 +1,14 @@
//
// DirectoryOrder.swift
// Pachyderm
//
// Created by Shadowfacts on 2/6/21.
// Copyright © 2021 Shadowfacts. All rights reserved.
//
import Foundation
public enum DirectoryOrder: String, CaseIterable {
case active
case new
}

View File

@ -0,0 +1,50 @@
//
// Emoji.swift
// Pachyderm
//
// Created by Shadowfacts on 9/8/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
import WebURL
public class Emoji: Codable {
public let shortcode: String
// these shouldn't need to be WebURLs as they're not external resources,
// but some instances (pleroma?) has emoji urls that Foundation considers malformed so we use WebURL to be more lenient
public let url: WebURL
public let staticURL: WebURL
public let visibleInPicker: Bool
public let category: String?
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.shortcode = try container.decode(String.self, forKey: .shortcode)
self.url = try container.decode(WebURL.self, forKey: .url)
self.staticURL = try container.decode(WebURL.self, forKey: .staticURL)
self.visibleInPicker = try container.decode(Bool.self, forKey: .visibleInPicker)
self.category = try container.decodeIfPresent(String.self, forKey: .category)
}
private enum CodingKeys: String, CodingKey {
case shortcode
case url
case staticURL = "static_url"
case visibleInPicker = "visible_in_picker"
case category
}
}
extension Emoji: CustomDebugStringConvertible {
public var debugDescription: String {
return ":\(shortcode):"
}
}
extension Emoji: Equatable {
public static func ==(lhs: Emoji, rhs: Emoji) -> Bool {
return lhs.shortcode == rhs.shortcode && lhs.url == rhs.url
}
}

View File

@ -1,5 +1,5 @@
//
// Filter.swift
// FilterV1.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
@ -8,7 +8,7 @@
import Foundation
public class Filter: Decodable {
public struct FilterV1: Decodable {
public let id: String
public let phrase: String
private let context: [String]
@ -22,17 +22,16 @@ public class Filter: Decodable {
}
}
public static func update(_ filter: Filter, phrase: String? = nil, context: [Context]? = nil, irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresAt: Date? = nil) -> Request<Filter> {
return Request<Filter>(method: .put, path: "/api/v1/filters/\(filter.id)", body: .parameters([
"phrase" => (phrase ?? filter.phrase),
"irreversible" => (irreversible ?? filter.irreversible),
"whole_word" => (wholeWord ?? filter.wholeWord),
"expires_at" => (expiresAt ?? filter.expiresAt)
] + "context" => (context?.contextStrings ?? filter.context)))
public static func update(_ filterID: String, phrase: String, context: [Context], irreversible: Bool, wholeWord: Bool, expiresIn: TimeInterval?) -> Request<FilterV1> {
return Request<FilterV1>(method: .put, path: "/api/v1/filters/\(filterID)", body: ParametersBody([
"phrase" => phrase,
"whole_word" => wholeWord,
"expires_in" => expiresIn,
] + "context" => context.contextStrings))
}
public static func delete(_ filter: Filter) -> Request<Empty> {
return Request<Empty>(method: .delete, path: "/api/v1/filters/\(filter.id)")
public static func delete(_ filterID: String) -> Request<Empty> {
return Request<Empty>(method: .delete, path: "/api/v1/filters/\(filterID)")
}
private enum CodingKeys: String, CodingKey {
@ -45,16 +44,17 @@ public class Filter: Decodable {
}
}
extension Filter {
public enum Context: String, Decodable {
extension FilterV1 {
public enum Context: String, Decodable, CaseIterable {
case home
case notifications
case `public`
case thread
case account
}
}
extension Array where Element == Filter.Context {
extension Array where Element == FilterV1.Context {
var contextStrings: [String] {
return map { $0.rawValue }
}

View File

@ -0,0 +1,115 @@
//
// FilterV2.swift
// Pachyderm
//
// Created by Shadowfacts on 12/2/22.
//
import Foundation
public struct FilterV2: Decodable {
public let id: String
public let title: String
public let context: [FilterV1.Context]
public let expiresAt: Date?
public let action: Action
public let keywords: [Keyword]
public static func update(
_ filterID: String,
title: String,
context: [FilterV1.Context],
expiresIn: TimeInterval?,
action: Action,
keywords keywordUpdates: [KeywordUpdate]
) -> Request<FilterV2> {
var keywordsParams = [Parameter]()
for (index, update) in keywordUpdates.enumerated() {
switch update {
case .update(id: let id, keyword: let keyword, wholeWord: let wholeWord):
keywordsParams.append("keywords_attributes[\(index)][id]" => id)
keywordsParams.append("keywords_attributes[\(index)][keyword]" => keyword)
keywordsParams.append("keywords_attributes[\(index)][whole_word]" => wholeWord)
case .add(keyword: let keyword, wholeWord: let wholeWord):
keywordsParams.append("keywords_attributes[\(index)][keyword]" => keyword)
keywordsParams.append("keywords_attributes[\(index)][whole_word]" => wholeWord)
case .destroy(id: let id):
keywordsParams.append("keywords_attributes[\(index)][id]" => id)
keywordsParams.append("keywords_attributes[\(index)][_destroy]" => true)
}
}
return Request(method: .put, path: "/api/v2/filters/\(filterID)", body: ParametersBody([
"title" => title,
"expires_in" => expiresIn,
"filter_action" => action.rawValue,
] + "context" => context.contextStrings + keywordsParams))
}
public static func create(
title: String,
context: [FilterV1.Context],
expiresIn: TimeInterval?,
action: Action,
keywords keywordUpdates: [KeywordUpdate]
) -> Request<FilterV2> {
var keywordsParams = [Parameter]()
for (index, update) in keywordUpdates.enumerated() {
switch update {
case .add(keyword: let keyword, wholeWord: let wholeWord):
keywordsParams.append("keywords_attributes[\(index)][keyword]" => keyword)
keywordsParams.append("keywords_attributes[\(index)][whole_word]" => wholeWord)
default:
fatalError("can only add keywords when creating filter")
}
}
return Request(method: .post, path: "/api/v2/filters", body: ParametersBody([
"title" => title,
"expires_in" => expiresIn,
"filter_action" => action.rawValue,
] + "context" => context.contextStrings + keywordsParams))
}
private enum CodingKeys: String, CodingKey {
case id
case title
case context
case expiresAt = "expires_at"
case action = "filter_action"
case keywords
}
}
extension FilterV2 {
public enum Action: String, Decodable, Hashable, CaseIterable {
case warn
case hide
}
}
extension FilterV2 {
public struct Keyword: Decodable {
public let id: String
public let keyword: String
public let wholeWord: Bool
public init(id: String, keyword: String, wholeWord: Bool) {
self.id = id
self.keyword = keyword
self.wholeWord = wholeWord
}
private enum CodingKeys: String, CodingKey {
case id
case keyword
case wholeWord = "whole_word"
}
}
}
extension FilterV2 {
public enum KeywordUpdate {
case update(id: String, keyword: String, wholeWord: Bool)
case add(keyword: String, wholeWord: Bool)
case destroy(id: String)
}
}

View File

@ -0,0 +1,69 @@
//
// Hashtag.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
import WebURL
import WebURLFoundationExtras
public class Hashtag: Codable {
public let name: String
public let url: WebURL
/// Only present when returned from the trending hashtags endpoint
public let history: [History]?
/// Only present on Mastodon >= 4 and when logged in
public let following: Bool?
public init(name: String, url: URL) {
self.name = name
self.url = WebURL(url)!
self.history = nil
self.following = nil
}
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
// pixelfed (possibly others) don't fully escape special characters in the hashtag url
self.url = try container.decode(WebURL.self, forKey: .url)
self.history = try container.decodeIfPresent([History].self, forKey: .history)
self.following = try container.decodeIfPresent(Bool.self, forKey: .following)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(url, forKey: .url)
try container.encodeIfPresent(history, forKey: .history)
try container.encodeIfPresent(following, forKey: .following)
}
public static func follow(name: String) -> Request<Hashtag> {
return Request(method: .post, path: "/api/v1/tags/\(name)/follow")
}
public static func unfollow(name: String) -> Request<Hashtag> {
return Request(method: .post, path: "/api/v1/tags/\(name)/unfollow")
}
private enum CodingKeys: String, CodingKey {
case name
case url
case history
case following
}
}
extension Hashtag: Equatable, Hashable {
public static func ==(lhs: Hashtag, rhs: Hashtag) -> Bool {
return lhs.name == rhs.name
}
public func hash(into hasher: inout Hasher) {
hasher.combine(url)
}
}

View File

@ -0,0 +1,54 @@
//
// History.swift
// Pachyderm
//
// Created by Shadowfacts on 4/2/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import Foundation
public class History: Codable {
public let day: Date
public let uses: Int
public let accounts: Int
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let day = try? container.decode(Date.self, forKey: .day) {
self.day = day
} else if let unixTimestamp = try? container.decode(Double.self, forKey: .day) {
self.day = Date(timeIntervalSince1970: unixTimestamp)
} else if let str = try? container.decode(String.self, forKey: .day),
let unixTimestamp = Double(str) {
self.day = Date(timeIntervalSince1970: unixTimestamp)
} else {
throw DecodingError.dataCorruptedError(forKey: .day, in: container, debugDescription: "day must be either date or UNIX timestamp")
}
if let uses = try? container.decode(Int.self, forKey: .uses) {
self.uses = uses
} else if let str = try? container.decode(String.self, forKey: .uses),
let uses = Int(str) {
self.uses = uses
} else {
throw DecodingError.dataCorruptedError(forKey: .uses, in: container, debugDescription: "uses must either be int or string containing int")
}
if let accounts = try? container.decode(Int.self, forKey: .accounts) {
self.accounts = accounts
} else if let str = try? container.decode(String.self, forKey: .accounts),
let accounts = Int(str) {
self.accounts = accounts
} else {
throw DecodingError.dataCorruptedError(forKey: .accounts, in: container, debugDescription: "accounts must either be int or string containing int")
}
}
private enum CodingKeys: String, CodingKey {
case day
case uses
case accounts
}
}

View File

@ -0,0 +1,179 @@
//
// Instance.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Instance: Decodable {
public let uri: String
public let title: String
public let description: String
public let shortDescription: String?
public let email: String?
public let version: String
public let urls: [String: URL]
public let thumbnail: URL?
public let languages: [String]?
public let stats: Stats?
public let configuration: Configuration?
public let rules: [Rule]?
// pleroma doesn't currently implement these
public let contactAccount: Account?
// superseded by mastodon's configuration.statuses.max_characters, still used by older instances & pleroma
let maxTootCharacters: Int?
let pollLimits: PollsConfiguration?
public var maxStatusCharacters: Int? {
configuration?.statuses.maxCharacters ?? maxTootCharacters
}
public var pollsConfiguration: PollsConfiguration? {
configuration?.polls ?? pollLimits
}
// we need a custom decoder, because all API-compatible implementations don't return some data in the same format
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.uri = try container.decode(String.self, forKey: .uri)
self.title = try container.decode(String.self, forKey: .title)
self.description = try container.decode(String.self, forKey: .description)
self.shortDescription = try container.decodeIfPresent(String.self, forKey: .shortDescription)
self.email = try container.decodeIfPresent(String.self, forKey: .email)
self.version = try container.decode(String.self, forKey: .version)
if let urls = try? container.decodeIfPresent([String: URL].self, forKey: .urls) {
self.urls = urls
} else {
self.urls = [:]
}
self.languages = try? container.decodeIfPresent([String].self, forKey: .languages)
self.contactAccount = try? container.decodeIfPresent(Account.self, forKey: .contactAccount)
self.stats = try? container.decodeIfPresent(Stats.self, forKey: .stats)
self.thumbnail = try? container.decodeIfPresent(URL.self, forKey: .thumbnail)
self.configuration = try? container.decodeIfPresent(Configuration.self, forKey: .configuration)
self.rules = try? container.decodeIfPresent([Rule].self, forKey: .rules)
if let maxTootCharacters = try? container.decodeIfPresent(Int.self, forKey: .maxTootCharacters) {
self.maxTootCharacters = maxTootCharacters
} else if let str = try? container.decodeIfPresent(String.self, forKey: .maxTootCharacters),
let maxTootCharacters = Int(str, radix: 10) {
self.maxTootCharacters = maxTootCharacters
} else {
self.maxTootCharacters = nil
}
self.pollLimits = try? container.decodeIfPresent(PollsConfiguration.self, forKey: .pollLimits)
}
private enum CodingKeys: String, CodingKey {
case uri
case title
case description
case shortDescription = "short_description"
case email
case version
case urls
case thumbnail
case languages
case stats
case configuration
case contactAccount = "contact_account"
case rules
case maxTootCharacters = "max_toot_chars"
case pollLimits = "poll_limits"
}
}
extension Instance {
public struct Stats: Decodable {
public let domainCount: Int?
public let statusCount: Int?
public let userCount: Int?
private enum CodingKeys: String, CodingKey {
case domainCount = "domain_count"
case statusCount = "status_count"
case userCount = "user_count"
}
}
}
extension Instance {
public struct Configuration: Decodable {
public let statuses: StatusesConfiguration
public let mediaAttachments: MediaAttachmentsConfiguration
/// Use Instance.pollsConfiguration to support older instance that don't have this nested
let polls: PollsConfiguration
private enum CodingKeys: String, CodingKey {
case statuses
case mediaAttachments = "media_attachments"
case polls
}
}
}
extension Instance {
public struct StatusesConfiguration: Decodable {
public let maxCharacters: Int
public let maxMediaAttachments: Int
public let charactersReservedPerURL: Int
private enum CodingKeys: String, CodingKey {
case maxCharacters = "max_characters"
case maxMediaAttachments = "max_media_attachments"
case charactersReservedPerURL = "characters_reserved_per_url"
}
}
}
extension Instance {
public struct MediaAttachmentsConfiguration: Decodable {
public let supportedMIMETypes: [String]
public let imageSizeLimit: Int
public let imageMatrixLimit: Int
public let videoSizeLimit: Int
public let videoFrameRateLimit: Int
public let videoMatrixLimit: Int
private enum CodingKeys: String, CodingKey {
case supportedMIMETypes = "supported_mime_types"
case imageSizeLimit = "image_size_limit"
case imageMatrixLimit = "image_matrix_limit"
case videoSizeLimit = "video_size_limit"
case videoFrameRateLimit = "video_frame_rate_limit"
case videoMatrixLimit = "video_matrix_limit"
}
}
}
extension Instance {
public struct PollsConfiguration: Decodable {
public let maxOptions: Int
public let maxCharactersPerOption: Int
public let minExpiration: TimeInterval
public let maxExpiration: TimeInterval
private enum CodingKeys: String, CodingKey {
case maxOptions = "max_options"
case maxCharactersPerOption = "max_characters_per_option"
case minExpiration = "min_expiration"
case maxExpiration = "max_expiration"
}
}
}
extension Instance {
public struct Rule: Decodable, Identifiable {
public let id: String
public let text: String
}
}

View File

@ -8,10 +8,23 @@
import Foundation
public class List: Decodable {
public class List: Decodable, Equatable, Hashable {
public let id: String
public let title: String
public var timeline: Timeline {
return .list(id: id)
}
public static func ==(lhs: List, rhs: List) -> Bool {
return lhs.id == rhs.id && lhs.title == rhs.title
}
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(title)
}
public static func getAccounts(_ list: List, range: RequestRange = .default) -> Request<[Account]> {
var request = Request<[Account]>(method: .get, path: "/api/v1/lists/\(list.id)/accounts")
request.range = range
@ -19,22 +32,22 @@ public class List: Decodable {
}
public static func update(_ list: List, title: String) -> Request<List> {
return Request<List>(method: .put, path: "/api/v1/lists/\(list.id)", body: .parameters(["title" => title]))
return Request<List>(method: .put, path: "/api/v1/lists/\(list.id)", body: ParametersBody(["title" => title]))
}
public static func delete(_ list: List) -> Request<Empty> {
return Request<Empty>(method: .delete, path: "/api/v1/lists/\(list.id)")
}
public static func add(_ list: List, accounts: [Account]) -> Request<Empty> {
return Request<Empty>(method: .post, path: "/api/v1/lists/\(list.id)/accounts", body: .parameters(
"account_ids" => accounts.map { $0.id }
public static func add(_ list: List, accounts accountIDs: [String]) -> Request<Empty> {
return Request<Empty>(method: .post, path: "/api/v1/lists/\(list.id)/accounts", body: ParametersBody(
"account_ids" => accountIDs
))
}
public static func remove(_ list: List, accounts: [Account]) -> Request<Empty> {
return Request<Empty>(method: .delete, path: "/api/v1/lists/\(list.id)/accounts", body: .parameters(
"account_ids" => accounts.map { $0.id }
public static func remove(_ list: List, accounts accountIDs: [String]) -> Request<Empty> {
return Request<Empty>(method: .delete, path: "/api/v1/lists/\(list.id)/accounts", body: ParametersBody(
"account_ids" => accountIDs
))
}

View File

@ -10,9 +10,10 @@ import Foundation
public class LoginSettings: Decodable {
public let accessToken: String
private let scope: String
private let scope: String?
public var scopes: [Scope] {
guard let scope = scope else { return [] }
return scope.components(separatedBy: .whitespaces).compactMap(Scope.init)
}

Some files were not shown because too many files have changed in this diff Show More