Compare commits

...

1844 Commits

Author SHA1 Message Date
Shadowfacts ca764811ed Bump build number and update changelog 2024-04-23 13:19:52 -04:00
Shadowfacts a589bb2863 Support emoji reaction push notifications on pleroma/akkoma 2024-04-18 13:17:55 -04:00
Shadowfacts 6f35fd2676 Show pleroma/akkoma emoji notifications
Closes #159
2024-04-18 12:59:44 -04:00
Shadowfacts e83cef1c8c Fix overzealously attempting to migrate local data to cloud store
Fix error when actually migrating due to not opening the store with
NSPersistentHistoryTrackingKey set to true.
2024-04-18 11:45:32 -04:00
Shadowfacts b89df3f27b Add instance announcements
Closes #356
2024-04-18 00:00:00 -04:00
Shadowfacts 4ecc16a93b Move FuzzyMatcher to TuskerComponents 2024-04-17 22:34:31 -04:00
Shadowfacts 8960873ff3 Remove redundant toastableViewController property 2024-04-17 22:34:31 -04:00
Shadowfacts 043a708515 Add more logging to onboarding VC 2024-04-17 17:04:32 -04:00
Shadowfacts c6b230414e Fix error decoding InstanceV2 response on certain instances 2024-04-17 10:18:01 -04:00
Shadowfacts f5e9f66f76 Fix app background colors not updating when preference changed
This only fully fixes it on iOS 17, but it seems to be the best we can do
2024-04-16 12:03:52 -04:00
Shadowfacts ee5f9a62ff Fix push subscription settings GroupBox background in dark mode
Closes #470
2024-04-16 11:37:36 -04:00
Shadowfacts a92cf8c812 Fix potential crash when hit testing StatusCollapseButton 2024-04-15 22:50:31 -04:00
Shadowfacts 756874949a Actually add notification extension privacy manifest to target 2024-04-15 22:42:18 -04:00
Shadowfacts 798e0c0cf1 Bump build number and update changelog 2024-04-15 22:40:54 -04:00
Shadowfacts 3f370945e6 Fix linker errors when building in release mode 2024-04-15 22:30:20 -04:00
Shadowfacts a759731eba Fix push notifications not working when account ID contains slashes 2024-04-15 22:19:24 -04:00
Shadowfacts 405d5def7c Disable non-stack navigation on Max iPhones 2024-04-15 11:33:52 -04:00
Shadowfacts 1f9806d02f Fix preferences post preview background on macOS 2024-04-15 11:04:33 -04:00
Shadowfacts c43c951b92 Enable iPad multi-column navigation by default 2024-04-15 11:00:36 -04:00
Shadowfacts 00c44c612f Fix feature flag preference decoding with old flags 2024-04-15 10:55:43 -04:00
Shadowfacts e5c4fceacd Add CustomCodablePreferenceKey 2024-04-15 10:50:08 -04:00
Shadowfacts 70227a7fa1 Add MigratablePreferenceKey protocol 2024-04-15 10:37:02 -04:00
Shadowfacts cb5488dcaa Reorganize preference keys to match Preferences 2024-04-15 09:50:49 -04:00
Shadowfacts 910e18fb5e Fix compiling for visionOS 2024-04-15 09:49:42 -04:00
Shadowfacts 66af946766 Use uniform deployment targets from project settings 2024-04-15 09:41:53 -04:00
Shadowfacts 6784ed7fdf Remove in-app Safari settings on macOS
Closes #469
2024-04-15 09:34:44 -04:00
Shadowfacts 66f0ba6891 Add icons for Preferences sections 2024-04-15 00:13:04 -04:00
Shadowfacts ee7bf5138c Tweak iCloud status appearance in advanced prefs 2024-04-15 00:13:04 -04:00
Shadowfacts c32181818a Use image for code formatting option 2024-04-15 00:13:04 -04:00
Shadowfacts 4665df228d More preferences reorganizing 2024-04-15 00:13:04 -04:00
Shadowfacts c7a56a9f61 Reorganize appearance prefs, add mock status preview 2024-04-14 14:11:43 -04:00
Shadowfacts 39251b9aa2 Fix TuskerTests not compiling 2024-04-14 13:37:10 -04:00
Shadowfacts db534e5993 Fix About screen link labels not being aligned 2024-04-13 23:19:28 -04:00
Shadowfacts e94bee4fc8 Fix a handful of strict concurrency warnings 2024-04-13 23:06:30 -04:00
Shadowfacts 216e58e5ec Merge branch 'prefs-refactor' into develop 2024-04-13 22:39:49 -04:00
Shadowfacts a4d13ad03b Only migrate changed preferences 2024-04-13 22:36:42 -04:00
Shadowfacts 05cfecb797 Fix push notifications on Pleroma/Akkoma and older Mastodon versions 2024-04-13 18:59:42 -04:00
Shadowfacts 132fcfa099 Refactor preferences 2024-04-13 18:44:43 -04:00
Shadowfacts 475b9911b1 Add privacy manifest to notification extension 2024-04-13 11:11:26 -04:00
Shadowfacts 7825ccbb3d Bump version and update changelog 2024-04-13 11:09:26 -04:00
Shadowfacts f87da10a29 Deep link to iOS Settings from Notifications prefs 2024-04-12 22:54:17 -04:00
Shadowfacts 1eec70449d Show notification when push notification banner tapped 2024-04-12 22:47:11 -04:00
Shadowfacts 19ca930ee8 Remove the need to register with the push proxy 2024-04-12 16:15:52 -04:00
Shadowfacts 2e31d34e9d Maybe fix continuation being reused 2024-04-11 22:30:43 -04:00
Shadowfacts 8a339ec171 Reregister client when adding push scope 2024-04-11 22:19:29 -04:00
Shadowfacts c7d79422bd Fix clean build failures 2024-04-11 21:48:41 -04:00
Shadowfacts baf96a8b06 Support settings -> app notification preferences link 2024-04-11 18:26:58 -04:00
Shadowfacts bc516a6326 Remove push proxy scheme build setting 2024-04-11 13:00:39 -04:00
Shadowfacts 1cd6af1236 Remove existing push subscriptions when unregistering from proxy 2024-04-11 12:58:43 -04:00
Shadowfacts 9f6910ba73 Implement communication notifications 2024-04-11 12:44:41 -04:00
Shadowfacts 9cf4975bfd Remove transaction ID from push notifications registration 2024-04-11 11:55:56 -04:00
Shadowfacts ee992bc0bf Improve per-instance push settings 2024-04-10 19:13:47 -04:00
Shadowfacts ff8a83ca2d Decrypt push notifications 2024-04-09 22:39:58 -04:00
Shadowfacts 4c957b86ae Fix push subscription policy/alerts not persisting 2024-04-09 21:07:14 -04:00
Shadowfacts ff11835333 Update oauth scopes when enabling push notifications
Closes #467
2024-04-09 19:05:31 -04:00
Shadowfacts 9353bbb56c Merge branch 'develop' into push-notifications 2024-04-09 18:43:53 -04:00
Shadowfacts edc887dd4c Rename PushManager properties 2024-04-09 12:38:24 -04:00
Shadowfacts 68dad77f81 Update Mastodon push subscriptions when endpoint changes 2024-04-09 12:38:24 -04:00
Shadowfacts 840b83012a Don't use Sentry in PushNotifications package 2024-04-09 11:56:22 -04:00
Shadowfacts e150856e91 Improve AsyncToggle behavior on failure 2024-04-09 11:49:55 -04:00
Shadowfacts 42a3f6c880 Use the right public key representation for push subscriptions 2024-04-09 11:48:53 -04:00
Shadowfacts 7a47b09b39 Remove push subscription when logging out of account 2024-04-08 22:50:39 -04:00
Shadowfacts 241e6f7e3a Notification type toggles 2024-04-08 22:32:46 -04:00
Shadowfacts f02afaac26 Move AsyncToggle to TuskerComponents 2024-04-08 22:32:46 -04:00
Shadowfacts bdd4a4d755 Scaffolding for push subscription alert types 2024-04-08 18:44:56 -04:00
Shadowfacts 94c1eb2c81 Create/remove instance push subscriptions 2024-04-08 12:25:39 -04:00
Shadowfacts b03991ae1d Move push notifications stuff to separate package 2024-04-08 10:48:28 -04:00
Shadowfacts f98589b419 Start account-specific push subscriptions 2024-04-07 23:14:12 -04:00
Shadowfacts 9fad2a882a More reliable registering/unregistering 2024-04-07 22:47:58 -04:00
Shadowfacts ec76754270 Bump build number and update changelog 2024-04-07 22:36:56 -04:00
Shadowfacts d0bb197e8c Correct button titles 2024-04-07 22:29:48 -04:00
Shadowfacts efd90bca3e Add Account Settings button to preferences 2024-04-07 22:28:30 -04:00
Shadowfacts 3efa017942 Push proxy registration 2024-04-07 14:04:42 -04:00
Shadowfacts c5226f6374 Add push scope 2024-04-06 11:04:03 -04:00
Shadowfacts 281585cdf0 Update release changelog 2024-04-04 23:13:02 -04:00
Shadowfacts 6d4ab4d54b Bump build number and update changelog 2024-04-04 18:32:13 -04:00
Shadowfacts 9e429463b2 Make the audio session work better
Closes #353
Closes #443
2024-04-04 17:31:16 -04:00
Shadowfacts 51db0066ac Bump build number and update changelog 2024-04-02 22:13:28 -04:00
Shadowfacts 9763edef47 Add See Results button to polls
Closes #445
2024-04-02 22:04:16 -04:00
Shadowfacts 442f57bfc4 Enable gallery interactive dismissal for statuses with >4 attachments
Closes #466
2024-04-02 21:21:39 -04:00
Shadowfacts ae7101bb30 Fix race between loading/animation when presenting gallery from attachment more view 2024-04-02 21:21:19 -04:00
Shadowfacts 490d48c635 Fix loading indicator never disappearing when presenting gallery from status with >4 attachments 2024-04-02 21:16:09 -04:00
Shadowfacts 69ee3bb4f0 Fix gallery content being incorrectly positioned on macOS when reduce motion is on
See #446
2024-04-02 21:01:54 -04:00
Shadowfacts 46b455c3d1 Fix crash when there are multiple follow notifications for the same account
Only reproducible on Pixelfed
2024-04-01 21:52:47 -04:00
Shadowfacts e522e30ce5 Bump build number and update changelog 2024-04-01 21:39:43 -04:00
Shadowfacts c73784aa81 Mark notifications on Mastodon web frontend as read once displayed
Fixes #357
2024-04-01 19:51:57 -04:00
Shadowfacts 7affa09e5e Remove timeline marker Sentry reporting
I'm 99% sure these timeouts are all due to bad network conditions
2024-04-01 19:43:14 -04:00
Shadowfacts 7435d02f6e Fiddle with how the timeline markers API is organized 2024-04-01 19:22:55 -04:00
Shadowfacts 2467297f04 Add preference for inverted alt text badge
Closes #423
2024-04-01 18:47:19 -04:00
Shadowfacts cf317e15e9 Designed for iPad: Possible fix for gallery content being positioned/sized incorrectly
See #446
2024-04-01 12:40:30 -04:00
Shadowfacts bcae60316b Fix changing list reply policy not reloading list timeline 2024-04-01 11:04:40 -04:00
Shadowfacts 1a2fa10708 Improve edit list account removal animation 2024-04-01 11:02:33 -04:00
Shadowfacts f79c2feea6 Fix edit list screen not updating after adding account 2024-04-01 11:02:15 -04:00
Shadowfacts 7ec87d7853 Add no content message to list timelines
Closes #215

Also fix interactive dismissal of edit screen not reloading list
2024-04-01 10:58:42 -04:00
Shadowfacts f5704e561b Support tapping selected sidebar item to scroll to top 2024-04-01 10:35:54 -04:00
Shadowfacts d6faf3a37b Fix fast account switching view not respecting safe area 2024-04-01 10:28:40 -04:00
Shadowfacts b0a6952643 Fix trending hashtags screen not clearing selection 2024-04-01 09:47:05 -04:00
Shadowfacts 06b58cfb9c Fix notifications screen not responding to tab bar/status bar scroll to top 2024-04-01 09:45:22 -04:00
Shadowfacts afcec24f86 Fix reference cycles in gallery 2024-03-31 23:29:28 -04:00
Shadowfacts 3f90a0df04 Fix gifvs preventing sleep 2024-03-31 23:20:55 -04:00
Shadowfacts 395ce6523d Fix follows account list using wrong separator insets 2024-03-31 23:14:54 -04:00
Shadowfacts cced930549 Bump build number and update changelog 2024-03-31 21:00:52 -04:00
Shadowfacts 7b2bd1a7af Apply grayscale attachments preference to videos in gallery 2024-03-31 20:56:20 -04:00
Shadowfacts f447150bbc Maybe improve grayscale gifv playback performance 2024-03-31 20:51:51 -04:00
Shadowfacts 08bd78d51b Fix changing greyscale images preference breaking gifvs looping 2024-03-31 20:51:33 -04:00
Shadowfacts f0ec372f50 Fix attachment blur view corners not being curved 2024-03-31 20:44:13 -04:00
Shadowfacts d2c28ada7f Improve gallery video autoplay behavior 2024-03-31 20:41:57 -04:00
Shadowfacts 375ad25919 Tweak gallery animation timing 2024-03-31 16:07:09 -04:00
Shadowfacts abf0568398 Improve gallery presentation/dismissal animation layering 2024-03-31 15:56:51 -04:00
Shadowfacts 2386f545e2 Fix gallery button delay on Catalyst/Designed for iPad 2024-03-31 15:44:15 -04:00
Shadowfacts 908c4ee085 Catalyst: Fix gallery close/share buttons 2024-03-31 14:12:07 -04:00
Shadowfacts 23e5e87915 Enable trackpad scrolling to dismiss gallery 2024-03-31 14:08:18 -04:00
Shadowfacts b4693252be Fix how we're getting the Sentry installation ID 2024-03-31 12:52:56 -04:00
Shadowfacts f3cf2dd8ec Catalyst: Fix gallery presentation animation not working 2024-03-31 12:35:10 -04:00
Shadowfacts d96ec2a732 Make gallery control buttons square 2024-03-30 15:21:23 -04:00
Shadowfacts b8fe0454b5 Inset gallery controls on devices without safe area insets 2024-03-30 15:18:17 -04:00
Shadowfacts 1166c6e639 Fix gallery transition dimming view not being removed 2024-03-30 15:18:02 -04:00
Shadowfacts eda552c7c9 Add pointer interactions to gallery controls 2024-03-30 15:17:26 -04:00
Shadowfacts 841c08be2c Fix crash when sharing attachment from context menu on iPad 2024-03-30 14:43:03 -04:00
Shadowfacts eafb506d64 Fix blurhash image being used as gallery content 2024-03-29 22:18:24 -04:00
Shadowfacts fe00015248 Add background to gallery close/share buttons 2024-03-29 22:10:14 -04:00
Shadowfacts 509ed305cd Ignore safe area for gallery content 2024-03-29 22:06:28 -04:00
Shadowfacts c05107bccd Scale evenly in both dimensions in gallery animations 2024-03-29 18:59:27 -04:00
Shadowfacts 4fcc32ca4b Fix gallery controls popping in over content after presentation 2024-03-29 18:22:33 -04:00
Shadowfacts 6857529d06 Video gallery controls
See #450
2024-03-28 23:19:32 -04:00
Shadowfacts 42e29862ac Fix crash when compose screen dismissed while adding attachments 2024-03-25 10:06:38 -04:00
Shadowfacts 3ecee61013 Fix Save to Photos UIActivity icon being stretched 2024-03-20 12:29:10 -04:00
Shadowfacts f9aee46bbe Asynchronously share video instead of fetching it on the main thread 2024-03-20 12:23:18 -04:00
Shadowfacts 1cf3ce48ce Support sharing/saving videos and gifvs from gallery
See #450
2024-03-20 12:00:57 -04:00
Shadowfacts 072bb0daf0 Fix grayscale images preference not applying to gifvs 2024-03-20 11:54:47 -04:00
Shadowfacts d36e0ad27d Grayscale images in new gallery
See #450
2024-03-20 11:54:35 -04:00
Shadowfacts a80cbe79c2 Re-add image analysis interaction
See #450
2024-03-20 11:49:00 -04:00
Shadowfacts cf71fc3f98 Remove old gallery implementation
See #450
2024-03-19 15:20:18 -04:00
Shadowfacts be977dbea9 Gallery rewrite
See #450
2024-03-19 15:04:14 -04:00
Shadowfacts f327cfd197 Move SwiftPM packages into separate group 2024-03-17 18:57:51 -04:00
Shadowfacts 4bb01becd2 Update Sentry to fix required reason API issues 2024-03-17 13:34:34 -04:00
Shadowfacts 64fcc87516 Add privacy manifest to share extension 2024-03-17 13:34:15 -04:00
Shadowfacts 62e528fc22 Bump build number and update changelog 2024-03-17 13:20:44 -04:00
Shadowfacts 030fd4467d Add privacy manifest 2024-03-17 13:17:13 -04:00
Shadowfacts 489840019e Add Save to Photos action to attachment context menu
Closes #462
2024-03-17 12:38:50 -04:00
Shadowfacts 9af8c06b1c Use ellipsis after share action title 2024-03-17 12:22:27 -04:00
Shadowfacts 55e0573a5c Add share menu action to attachment context menu 2024-03-17 12:22:13 -04:00
Shadowfacts ac142ae11c Update HTMLStreamer 2024-03-17 12:09:50 -04:00
Shadowfacts 99a58e2c33 Extract TimelineLikeDataSource into separate protocol 2024-03-10 14:49:57 -04:00
Shadowfacts c740fb1c1f Change status/account cell separator insets 2024-03-09 18:27:44 -05:00
Shadowfacts 175001d561 Fix more strict concurrency warnings 2024-03-09 14:18:28 -05:00
Shadowfacts d481ef6c9f Fix crash when removing the same poll option multiple times
SwiftUI doesn't detect updates to CoreData objects when directly
mutating the NSMutableOrderedSet of a relationship

Closes #458
2024-03-09 14:15:14 -05:00
Shadowfacts 3caa419659 Make profile header follower/following counts separate buttons 2024-03-09 14:07:23 -05:00
Shadowfacts 074b028015 Show first verified link on account collection view cell 2024-03-09 13:54:56 -05:00
Shadowfacts bab0dd3294 Bump HTMLStreamer
Closes #454
2024-02-28 18:00:16 -05:00
Shadowfacts 8a3acc6889 Use UIControl.performPrimaryAction instead of SPI on iOS 17.4 2024-02-28 12:20:55 -05:00
Shadowfacts d37c5dde2f Bump build number and update changelog 2024-02-23 00:00:15 -05:00
Shadowfacts 53260555f6 Remove now-redundant whitespace removal 2024-02-22 23:53:27 -05:00
Shadowfacts 70524dd642 Bump HTMLStreamer 2024-02-22 23:42:42 -05:00
Shadowfacts b6232a9f1e Use tab bar on visionOS 2024-02-22 23:32:38 -05:00
Shadowfacts 41481f465a Fix potential bug with matched geometry VC transition 2024-02-21 10:31:40 -05:00
Shadowfacts 527e7129af Bump HTMLStreamer 2024-02-06 18:48:10 -05:00
Shadowfacts 229b51686c Update HTMLStreamer, fix certain links not appearing
Closes #456
2024-02-04 15:12:14 -05:00
Shadowfacts e156a97861 visionOS: Don't use gallery VC transition 2024-02-04 11:52:48 -05:00
Shadowfacts bdec14c463 Remove dead code 2024-02-04 11:49:41 -05:00
Shadowfacts ec0509c645 visionOS: Don't use deprecated UI for scene placement 2024-02-04 11:46:04 -05:00
Shadowfacts 4500e9be27 visionOS: Don't use certain nib-based cells 2024-02-03 12:41:03 -05:00
Shadowfacts a2cc3a0436 visionOS: Exclude unused code 2024-02-03 12:29:06 -05:00
Shadowfacts dc654812b1 visionOS: Don't use deprecated UITextViewDelegate method 2024-02-03 12:24:24 -05:00
Shadowfacts f122383d0b visionOS: Disable in-app Safari 2024-02-03 12:24:18 -05:00
Shadowfacts 0f6492a051 Disable strict concurrency checking 2024-01-28 14:59:40 -05:00
Shadowfacts b235f0e826 Another round of strict concurrency fixes 2024-01-28 14:59:03 -05:00
Shadowfacts 27d44340e8 Even more strict concurrency fixes 2024-01-27 15:48:58 -05:00
Shadowfacts fc26c9fb54 More strict concurrency fixes 2024-01-27 14:58:36 -05:00
Shadowfacts ba60f92223 Compiles with strict concurrency checking 2024-01-27 11:40:42 -05:00
Shadowfacts c489d018bd Merge branch 'develop' into strict-concurrency
# Conflicts:
#	Tusker/Caching/ImageCache.swift
#	Tusker/Extensions/PKDrawing+Render.swift
#	Tusker/MultiThreadDictionary.swift
#	Tusker/Views/BaseEmojiLabel.swift
2024-01-26 11:32:12 -05:00
Shadowfacts a9a518c6c1 Fix trailing whitespace not being stripped from compose reply content 2024-01-26 11:25:29 -05:00
Shadowfacts b4bdf8b0dc Fix building for visionOS 2024-01-26 11:15:21 -05:00
Shadowfacts 94f71541f8 Merge branch 'develop' into vision
# Conflicts:
#	Packages/ComposeUI/Sources/ComposeUI/Controllers/ToolbarController.swift
#	Tusker/Screens/Timeline/TimelineViewController.swift
#	Tusker/Views/Status/TimelineStatusCollectionViewCell.swift
2024-01-26 11:11:41 -05:00
Shadowfacts c2402303cc First pass at strict concurrency checking 2024-01-26 11:02:40 -05:00
Shadowfacts 5cef76e494 Fix crash when searching for "from:me" 2024-01-22 17:24:55 -05:00
Shadowfacts bf27b8fd47 Fix issues when changing scope after searching 2024-01-22 17:21:53 -05:00
Shadowfacts 32b8d27949 Don't report network errors when syncing timeline marker 2024-01-22 17:05:03 -05:00
Shadowfacts fb5581ae67 Bump build number and update changelog 2024-01-21 11:33:32 -05:00
Shadowfacts cd01d2f8c3 Pin HTMLStreamer version 2024-01-17 15:52:56 -05:00
Shadowfacts 65c3c8026d Fix whitespace in statuses not being trimmed 2024-01-17 15:51:54 -05:00
Shadowfacts 534f83e716 Fix links not being converted from HTML correctly 2024-01-16 19:17:44 -05:00
Shadowfacts 93c859a3c4 Fix TextConverter inserting newlines 2023-12-23 10:47:40 -05:00
Shadowfacts 4d183fe0b2 Merge branch '2024' into develop 2023-12-22 20:45:19 -05:00
Shadowfacts fd72390a22 Replace SwiftSoup with HTMLStreamer 2023-12-22 20:44:46 -05:00
Shadowfacts 5a4323067a Bump build number and update changelog 2023-12-22 11:02:29 -05:00
Shadowfacts 43d8434e17 Fix crash due to Explore data source being update off main thread when list deleted 2023-12-22 10:39:24 -05:00
Shadowfacts e8576277e0 Bump build number and update changelog 2023-12-17 18:16:47 -05:00
Shadowfacts 7f0a9d8d5a Fix status that is reblogged and contains a followed hashtag not showing reblogger label 2023-12-17 18:09:25 -05:00
Shadowfacts 51f4a780e2 Show loading indicator while translating status 2023-12-16 16:14:18 -05:00
Shadowfacts 180a8eb18d Fix Status reblogs inverse relationship being to-one instead of to-many 2023-12-14 21:57:44 -05:00
Shadowfacts eb61043867 Fix timeline state restoration not returning to correct scroll position in certain circumstances
All of the work done by the restoreInitial callback needs to be async,
so that when the TimelineLikeController signals that the loading
indicator should be removed, the collection view is in the right place.

Closes #439
2023-12-14 18:28:22 -05:00
Shadowfacts e09935125f Fix copying/pasting images from Safari on macOS not working
Closes #453
2023-12-14 18:01:34 -05:00
Shadowfacts e8ef9345e9 Fix visibility/local-only buttons not appearing on Catalyst
You need to pass the configuration to the initializer to get it to show up in the Mac idiom

Fixes #452
2023-12-05 22:00:48 -05:00
Shadowfacts 28c1a9092b Add server-provided translation
Closes #331
2023-12-04 19:31:51 -05:00
Shadowfacts 5e609aa40d V2 instance API, add translation to InstanceFeatures 2023-12-04 17:55:03 -05:00
Shadowfacts 158940f8e6 Refactor StatusContentContainer to use an array of subviews 2023-12-04 17:06:10 -05:00
Shadowfacts 141e8b96a5 Show label when attachments are hidden in timelines 2023-12-04 16:38:04 -05:00
Shadowfacts 108a02826f Remove incorrect workaround for crash when LazilyDecoding used on nil MO 2023-12-04 16:20:22 -05:00
Shadowfacts be1ca70ebf Add preference for showing attachments in timeline
Closes #330
2023-12-04 16:18:54 -05:00
Shadowfacts 34edd8a13f Fix reblogged statuses being pruned while still referenced, add workaround for crash 2023-12-03 15:08:38 -05:00
Shadowfacts 23f383a7f9 Get rid of network request during share extension launch
Closes #438
2023-12-02 15:33:15 -05:00
Shadowfacts 99caaa0f28 Bump version and update changelog 2023-11-29 18:05:58 -05:00
Shadowfacts 0f70c9059e Fix error decoding certain statuses on pixelfed 2023-11-19 22:52:58 -05:00
Shadowfacts 6d7074e71d Tweak profile header separator 2023-11-19 21:22:00 -05:00
Shadowfacts 13809b91d1 Fix crash if window removed while fast account switcher is hiding 2023-11-18 11:36:59 -05:00
Shadowfacts 16f6dc84c9 Update Sentry package 2023-11-18 11:15:47 -05:00
Shadowfacts cdfb06f4a7 Render IDN domains in for logged-in accounts 2023-11-18 11:08:35 -05:00
Shadowfacts 4e98e569eb Fix avatars in follow request notification not being rounded
Closes #448
2023-11-18 11:00:19 -05:00
Shadowfacts 6d3ffd7dd3 Style blockquote appropriately
Closes #22
2023-11-18 10:56:05 -05:00
Shadowfacts ca7fe74a90 Add accessibility description/action to status edit history entry 2023-11-10 14:48:48 -05:00
Shadowfacts 380f878d81 Use server language preference for default search token suggestion 2023-11-10 14:42:48 -05:00
Shadowfacts 1c36312850 Fix status deletions not being handled properly in logged-out views 2023-11-10 14:35:36 -05:00
Shadowfacts de946be008 Fix crash if ContentTextView asked for context menu config w/o mastodon controller 2023-11-10 14:20:33 -05:00
Shadowfacts b40d815274 Ensure LazilyDecoding runs on the managed object context's thread
Maybe fix the crash in KeyPath machinery?
2023-11-10 14:16:16 -05:00
Shadowfacts bc7500bde9 Fix crash when uploading attachment without known MIME type or extension 2023-11-10 14:08:11 -05:00
Shadowfacts 676e603ffc Fix crash when showing trending hashtag with less than two days of history 2023-11-10 14:04:11 -05:00
Shadowfacts 01bbfc31f2 visionOS: Improve suggested profile card appearance 2023-11-08 21:49:21 -05:00
Shadowfacts a846954dcd visionOS: Improve trending link cell appearance 2023-11-08 17:47:01 -05:00
Shadowfacts 53302e3b26 visionOS: Remove trends loading indicator highlight 2023-11-08 17:05:58 -05:00
Shadowfacts c0301ce7e7 visionOS: Further Compose screen tweaks 2023-11-08 17:02:32 -05:00
Shadowfacts 14f32f24fa visionOS: Use bordered prominent style for status actions 2023-11-08 16:37:12 -05:00
Shadowfacts 19db78e352 visionOS: Don't highlight non-selectable list rows 2023-11-07 22:52:13 -05:00
Shadowfacts 9d01bbabd7 visionOS: Use UIColor.link for text links 2023-11-07 22:42:32 -05:00
Shadowfacts a93a4fccc1 visionOS: Fix timeline jump button appearance 2023-11-07 22:31:57 -05:00
Shadowfacts 1da25300ca Merge branch 'develop' into vision 2023-11-07 22:26:22 -05:00
Shadowfacts cb47443649 Bump version and update changelog 2023-11-07 22:16:48 -05:00
Shadowfacts 86862825f6 Assert that the compose draft belongs to the view context 2023-11-05 18:32:05 -05:00
Shadowfacts e6f1968609 Fix TimelineLikeCollectionViewController.apply not actually applying snapshots on the main thread 2023-11-05 18:22:20 -05:00
Shadowfacts 4c5da1b5a9 Add URL handler for opening Compose window 2023-11-05 15:24:55 -05:00
Shadowfacts e57ef210fd Fix language picker button not having a pointer effect 2023-11-05 11:32:49 -05:00
Shadowfacts dcdfe853e1 Fix Cmd+W closing sometimes closing non-foreground window on macOS
Closes #444
2023-11-05 11:14:58 -05:00
Shadowfacts 34e57c297b Tweak HEIF/HEIC handling 2023-11-03 11:07:43 -04:00
Shadowfacts 6c2c2e6ae7 More logging to try and pin down LazilyDecoding EXC_BAD_ACCESS 2023-11-02 18:18:08 -04:00
Shadowfacts aae3bd0bba Remove dead code 2023-11-02 17:53:26 -04:00
Shadowfacts 2b5d4681e3 Prevent mul/und from being used as language
Closes #440
2023-11-02 10:44:52 -04:00
Shadowfacts e4eff2d362 Bump version and update changelog 2023-10-28 14:14:02 -05:00
Shadowfacts 37311e5f17 Fix potential crash due to race condition in timeline gap filling 2023-10-28 14:03:08 -05:00
Shadowfacts af5a0b7bbd Fix crash with large image dismiss gesture 2023-10-28 13:58:39 -05:00
Shadowfacts 3aa45cb365 Maybe fix crash due to reading ScaledMetric on background thread
ScaledMetric.wrappedValue calls into Font.scaleFactor(textStyle:in:)
which uses a dictionary setter
2023-10-28 13:56:25 -05:00
Shadowfacts a07b398cbe Maybe fix crash due to VC hierarchy consistency check failing on split collapse/expand 2023-10-28 13:52:54 -05:00
Shadowfacts 2ccec2f4df Fix crash if URLComponents.url is nil in instance selector 2023-10-28 13:47:44 -05:00
Shadowfacts 0de9a9fd37 Fix list timeline refresh failing if initial load returned no statuses 2023-10-28 13:36:11 -05:00
Shadowfacts bd21e88e8b Add UI for changing list reply policy and exclusivity
Closes #428
2023-10-28 12:16:14 -05:00
Shadowfacts 2464e2530f Remove dead code 2023-10-27 17:29:51 -05:00
Shadowfacts 44021d3ad2 Convert edit list screen to collection view 2023-10-27 17:29:51 -05:00
Shadowfacts a46eaafbcf Add reply policy and exclusive fields to lists 2023-10-27 17:00:53 -05:00
Shadowfacts eb496243c7 Use server preference for local-only on Hometown
Closes #281
2023-10-27 15:12:48 -05:00
Shadowfacts 6e5e0c3bb5 Use server preferences for default visibility and language
Closes #282
2023-10-27 14:59:21 -05:00
Shadowfacts dfc8234908 Attribute authenticated API requests to the user
Closes #134
2023-10-26 17:30:31 -05:00
Shadowfacts 157c8629a9 Add underline links preference
Closes #397
2023-10-24 16:02:03 -04:00
Shadowfacts bde21fbc6c Fix crash due to prematurely pruned statuses being fetched
If the app hasn't launched in long enough, we may be displaying old statuses as a result of state restoration. If the user leaves the app, those statuses can't get pruned, because the user may return. We need to make sure the lastFetchedAt date is current, since awakeFromFetch won't be called until the object is faulted in (which wasn't happening immediately during state restoration).
2023-10-24 15:50:58 -04:00
Shadowfacts 74820e8922 Underline links when button shapes accessibility setting is on 2023-10-24 15:50:58 -04:00
Shadowfacts f7a9075b77 Fix timeline jump button having background when button shapes accessibility setting is on 2023-10-24 15:50:58 -04:00
Shadowfacts 4af56e48bf Clean up TimelineLikeCollectionViewController.apply(_:animatingDifferences:) 2023-10-24 14:56:39 -04:00
Shadowfacts 978486bc15 visionOS: Improve button appearance in Compose attachment list 2023-10-20 11:27:24 -04:00
Shadowfacts 27dd8a1927 visionOS: Hide light/dark mode prefs 2023-10-20 11:27:24 -04:00
Shadowfacts 78196e14c3 visionOS: Improve Compose main text view appearance 2023-10-20 11:27:24 -04:00
Shadowfacts a0eb5dc596 visionOS: Move Compose toolbar controls to ornament 2023-10-20 11:27:24 -04:00
Shadowfacts e4c22a0205 Compile for visionOS 2023-10-20 11:27:24 -04:00
Shadowfacts c4bf5d406d Fix older notifications not loading when initially visible set fits on one screen
Closes #346
2023-10-19 21:21:50 -04:00
Shadowfacts 53d43b5707 Update changelog 2023-10-01 22:14:26 -04:00
Shadowfacts b1564d822e Bump version and move to xcconfig to fix warnings 2023-10-01 22:14:01 -04:00
Shadowfacts a8a2f0a26c Add search operators UI on Mastodon 4.2
Closes #433
2023-10-01 21:40:53 -04:00
Shadowfacts 46e1205327 Fix delay before My Profile sidebar item appears on launch 2023-10-01 10:20:45 -04:00
Shadowfacts 6a2de2be55 Make suggested profile cells uniform height on trends screen 2023-10-01 10:15:00 -04:00
Shadowfacts db6ba0c62c Remove navigation mode preference feature flag 2023-10-01 00:14:20 -04:00
Shadowfacts 16029dc161 Fix Appearance > Interface prefs using wrong row background color 2023-10-01 00:12:01 -04:00
Shadowfacts 31a0db014a Improve multi-column layout for suggested profiles 2023-10-01 00:08:00 -04:00
Shadowfacts 5be8005e24 Use two columns for trending links/accounts on wide screens 2023-09-29 17:33:18 -04:00
Shadowfacts ad4e112e96 Fix switching back to previous navigation mode 2023-09-29 17:18:29 -04:00
Shadowfacts 7a2dc7d3c4 Improve readable-width content inset behavior 2023-09-28 21:30:30 -04:00
Shadowfacts 0948371f83 Improve appearance of lists when converting from HTML
Closes #434
2023-09-27 17:35:36 -04:00
Shadowfacts 3ba1a00257 Reconfigure visible updates when refreshing
Closes #300
2023-09-26 09:42:39 -04:00
Shadowfacts 1b42cd7816 Fix cell reuse bug with follow/action notifications 2023-09-26 09:18:01 -04:00
Shadowfacts a2fe0dfb78 Avoid unnecessarily recreating avatar views in notifications cells 2023-09-25 21:44:43 -04:00
Shadowfacts bf1ed57180 Allow authoring local-only posts on Akkoma
Closes #332
2023-09-25 21:23:28 -04:00
Shadowfacts 6821f1b9a0 Don't show doubled "New Post" in window titlebar on macOS
Closes #429
2023-09-24 23:50:08 -04:00
Shadowfacts 7ae741cd83 Fix Live Text control reappearing when swiping between gallery pages with controls hidden
Closes #431
2023-09-24 23:44:40 -04:00
Shadowfacts fe9ad83ddc Fix replies with content warnings showing confirm dialog when unchanged
Closes #430
2023-09-24 23:28:36 -04:00
Shadowfacts 6b7c828cc9 Try to compress videos to fit within instance limits
Closes #425
2023-09-16 14:07:49 -04:00
Shadowfacts 2be1ee19de Improve error message when uploading attachment to Pixelfed fails
See #425
2023-09-16 13:56:46 -04:00
Shadowfacts 3f15a453bd Update to recommended Xcode settings 2023-09-16 13:50:39 -04:00
Shadowfacts 53611d80d6 Bump version and update changelog 2023-09-16 13:48:05 -04:00
Shadowfacts 4614b25f33 Remove test code 2023-09-09 12:42:34 -04:00
Shadowfacts 519446c5a8 Fix crash if autocomplete controller dealloc'd before search task starts 2023-09-09 12:28:53 -04:00
Shadowfacts 4b52cafb9a Bump version and build number 2023-09-09 12:28:21 -04:00
Shadowfacts 1ca84a3b95 Don't swizzle unnecessarily on iOS 17 2023-09-09 11:45:54 -04:00
Shadowfacts b28792eb29 Report string when mention url decoding fails 2023-09-09 11:41:54 -04:00
Shadowfacts 9c3be68e1c Don't report 422 or 500 errors 2023-09-09 11:40:18 -04:00
Shadowfacts df9ce81060 Fix crash when ComposeUIConfig.dismiss called after hosting controller dealloc'd
I'm not sure how this can happen (possibly if the user dismissed the
compose screen while the status was being posted? but I haven't been
able to reproduce that), but guard against it since it's causing crashes
2023-09-09 11:37:25 -04:00
Shadowfacts 173eda1757 Prevent dismissing compose screen while posting 2023-09-09 11:35:46 -04:00
Shadowfacts b2b15b8b6e Disallow posting direct messages on Pixelfed 2023-09-09 11:07:27 -04:00
Shadowfacts f448090c2a Gate navigation mode preference behind feature flag 2023-09-09 10:57:56 -04:00
Shadowfacts 232e3285ae Fix widescreen navigation mode preference not persisting 2023-09-09 10:56:21 -04:00
Shadowfacts ebc127c921 Add readable content inset to certain screens 2023-09-09 10:56:11 -04:00
Shadowfacts 41665b1060 Indicate that edit history may be incomplete for remote posts
Closes #385
2023-09-07 18:04:08 -04:00
Shadowfacts 3a3b7aaee4 Use custom UITraitDefinition on iOS 17 2023-09-06 13:51:27 -04:00
Shadowfacts f2485f0ba1 Add feature flag for browser-style navigation 2023-09-06 13:27:42 -04:00
Shadowfacts 75caf2c1eb Enable switching between navigation modes 2023-09-06 13:19:06 -04:00
Shadowfacts f1a6a405c2 Fix crash when split VC collapses with multi-column nav controller 2023-09-05 20:47:11 -04:00
Shadowfacts 88105f22a0 Add widescreen navigation mode preference 2023-09-05 19:21:50 -04:00
Shadowfacts 9c368f295e Initial multi-column navigation controller implementation 2023-09-05 19:21:37 -04:00
Shadowfacts 04deb08bcf Add feature flags to advanced preferences 2023-09-04 23:35:40 -04:00
Shadowfacts f704d15dd7 Make UserActivityType.handle MainActor-bound 2023-08-23 17:07:41 -07:00
Shadowfacts 297af7b905 Tweak instance type matching, add iceshrimp 2023-07-24 22:42:17 -07:00
Shadowfacts 6c0564e0ee Bump version and update changelog 2023-07-23 10:36:11 -07:00
Shadowfacts 3d232d81ba Fix firefish instances not being detected 2023-07-22 11:23:16 -07:00
Shadowfacts 3109aafd20 Workaround for status collapse button overlapping other views in the cell 2023-07-18 21:14:43 -07:00
Shadowfacts 105a01811a Actual fix for links appearing as the wrong color
Closes #402
2023-07-18 21:01:30 -07:00
Shadowfacts 33999fe895 Fix crash/hang when showing emoji autocomplete with very many emojis
Closes #424
2023-07-13 23:41:02 -07:00
Shadowfacts 7f12479514 Fix not being able to share images from Shortcuts actions that have public.image and public.file-url representations
Closes #420
2023-07-08 15:37:45 -07:00
Shadowfacts 0eb000224e Fix double posting in poor network conditions
Closes #421
2023-07-08 15:24:40 -07:00
Shadowfacts 3c9692d5b2 Remove ambiguating constraint priorities, avoid removing and recreating the same constraints
Closes #407
2023-07-05 20:30:55 -07:00
Shadowfacts 50bfaf7236 Clamp uncropped attachment aspect ratio
Closes #418
2023-07-04 11:11:20 -07:00
Shadowfacts 385f31728d Fix sharing screenshot from markup not working
Closes #419
2023-07-04 11:07:35 -07:00
Shadowfacts bcd487d311 Fix favorites count button changing with when (un)faving
Closes #406
2023-07-04 10:25:32 -07:00
Shadowfacts 8f8e2a2aea Add unfollow hashtag action to Explore screen
Closes #417
2023-07-04 09:56:35 -07:00
Shadowfacts 54034ff727 Ignore HTTP 503 errors 2023-07-02 11:53:49 -07:00
Shadowfacts ee5db96c9e Workaround for links using the wrong tint color
Closes #402
2023-07-02 09:46:17 -07:00
Shadowfacts f825760fe9 Fix profile header follow button icon spacing 2023-06-26 22:18:27 -07:00
Shadowfacts a339884d1f Fix ScrollingSegmentedControl being cut off at smaller the default dynamic type size
Closes #410
2023-06-26 21:52:51 -07:00
Shadowfacts 1de586f907 Fix reblog with visibility not working 2023-06-26 21:41:43 -07:00
Shadowfacts bd162afdcc Fix showing incorrect visibilities in reblog confirmation alert 2023-06-26 21:40:43 -07:00
Shadowfacts 956b817045 Correct log level 2023-06-26 21:39:09 -07:00
Shadowfacts 28ee0908d7 Blur link card images when status is sensitive
Closes #412
2023-06-26 21:35:15 -07:00
Shadowfacts c3cf38b0c9 Fix not being able to refresh Mentions tab on Pleroma
Closes #411
2023-06-26 21:17:21 -07:00
Shadowfacts 7929e7530f Fix incorrect context menu preview on filtered post
Closes #413
2023-06-26 21:12:20 -07:00
Shadowfacts a11e453112 Fix reblog confirmation alert not being centered in non-fullscreen window
Closes #415
2023-06-26 21:01:23 -07:00
Shadowfacts 2e7ad1626e Fix avatars being squished in certain places
Closes #414
2023-06-26 20:47:38 -07:00
Shadowfacts 4182c15500 Fix invalid status notifications not being removed
Closes #416
2023-06-26 20:38:10 -07:00
Shadowfacts 4b43726e1d Fix not being able to follow hashtags on akkoma
Closes #408
2023-06-03 18:07:44 -07:00
Shadowfacts a4e7082ab8 Fix race condition in Compose screen when loading account 2023-05-28 22:28:41 -07:00
Shadowfacts f0b8f92791 Use cached logged-in account for things
Fixes various race conditions with loading account

Closes #251
2023-05-28 22:26:46 -07:00
Shadowfacts da88303a22 Cache active account ID in CoreData
See #251
2023-05-28 22:23:04 -07:00
Shadowfacts cb5b70a23a Remove direct accesses of MastodonController.instance
Fixes potential race conditions
2023-05-28 22:10:51 -07:00
Shadowfacts 2b5b749dc8 Avoid setting duplicate breadcrumbs 2023-05-28 22:10:51 -07:00
Shadowfacts ef00c0e2df Cache own instance in CoreData
See #251
2023-05-28 22:10:10 -07:00
Shadowfacts 06f7e306e0 Provide UserAccountInfo to MastodonController at initialization 2023-05-28 21:28:20 -07:00
Shadowfacts 878744b636 Tweak how Sentry installation ID is read 2023-05-28 21:04:29 -07:00
Shadowfacts f84694b809 Fix compose toolbar background not extending to full width on landscape iPhones 2023-05-28 15:34:56 -07:00
Shadowfacts 473ef018c9 Fix DuckableContainerVC not resetting when dismissed programatically
Fixes #396
2023-05-28 15:06:59 -07:00
Shadowfacts 9a734565b0 Fix backgrounding app on iPad dismissing modally-presented VC
Closes #399
Closes #316
2023-05-28 14:37:41 -07:00
Shadowfacts 2eda9657ac Don't use deprecated interfaceOrientation for detecting portrait mode 2023-05-28 14:18:13 -07:00
Shadowfacts 203c1852d4 Reuse poll option views when updating status cell
Fixes flicker/animation due to new option views begin added in default
state and then changed back to the state of the existing view.

Fixes #403
2023-05-28 12:19:45 -07:00
Shadowfacts 708112c486 Don't reconfigure conversation main status unnecessarily 2023-05-28 12:16:48 -07:00
Shadowfacts 5b321fcc78 Remove deferred loading indicator, causes more trouble than it's worth
Closes #404
2023-05-28 11:17:16 -07:00
Shadowfacts 59231e513f Fix crash if status for leaked collection view cell updates 2023-05-27 15:38:13 -07:00
Shadowfacts bf6dfab121 Fix not checking if section exists before getting item identifiers
Closes #398
2023-05-27 15:33:33 -07:00
Shadowfacts f5f1be9f7d Fix crash due to force-unwrapping uninitialized search controller
Closes #395
2023-05-27 15:31:02 -07:00
Shadowfacts c0148bb770 Fix Delete attachment context menu action not working
Closes #394
2023-05-27 15:28:39 -07:00
Shadowfacts d938c555b7 Fix Recognize Text action accessing view context MO off of main thread
Closes #393
2023-05-27 15:26:13 -07:00
Shadowfacts 52efc8b752 Fix crash if contextMenuConfiguration called on status cell that doesn't have a delegate
Closes #392
2023-05-27 15:23:49 -07:00
Shadowfacts 822e3f91c4 Fix crash if language code is less than 3 chars
Closes #391
2023-05-27 15:23:11 -07:00
Shadowfacts d0a1aec1c0 Fix crash when action notification cell doesn't have any statuses
Closes #390
2023-05-27 15:21:34 -07:00
Shadowfacts e8305184af Fix tip jar button width changing while purchasing
Closes #389
2023-05-27 15:20:42 -07:00
Shadowfacts e9727ac2c5 Fix reblogs count button not being leading-aligned
Closes #388
2023-05-27 15:18:03 -07:00
Shadowfacts d9a6bb0fd2 Fix ambiguous constraints in poll view 2023-05-27 15:11:53 -07:00
Shadowfacts 13a807ba4f Fix poll options view blocking context menu gesture
Closes #387
2023-05-27 15:00:10 -07:00
Shadowfacts 32c5eee0b5 Fix conversation main status cell flashing wrong background color
Closes #386
2023-05-27 14:52:59 -07:00
Shadowfacts 06f761bf56 Bump build number and update changelog 2023-05-16 13:10:42 -04:00
Shadowfacts 4b16a69275 Fix expand focused attachment animation not working 2023-05-16 13:02:28 -04:00
Shadowfacts a309b041bf Update release changelog 2023-05-16 12:49:11 -04:00
Shadowfacts 8c40a5a9e8 Bump build number and update changelog 2023-05-16 11:57:46 -04:00
Shadowfacts 3b11dd216f Change conversation main status favorites/reblogs count order to match Mastodon 2023-05-16 11:43:53 -04:00
Shadowfacts 8db5649cd5 Show unknown attachments
Closes #47
2023-05-16 11:40:59 -04:00
Shadowfacts f2f6eb81f7 Change favorite/reblog action order to match Mastodon 2023-05-16 11:28:28 -04:00
Shadowfacts f6831ec02b Add QuickLook fallback for showing unknown attachments in the gallery
Closes #169
2023-05-16 11:25:28 -04:00
Shadowfacts 7f64654800 Fix crash when adding drawing attachment 2023-05-16 10:57:27 -04:00
Shadowfacts 8e570027a1 Bump build number and update changelog 2023-05-16 00:18:34 -04:00
Shadowfacts df9fb3c527 Fix Save Draft action not working 2023-05-16 00:06:58 -04:00
Shadowfacts 2080fdc955 Fix replied-to status not updating when selecting different draft 2023-05-16 00:04:30 -04:00
Shadowfacts 70f8748364 Fix crash if draft attachment lacks data 2023-05-16 00:03:54 -04:00
Shadowfacts 0343e2e310 Bump build number and update changelog 2023-05-16 00:01:28 -04:00
Shadowfacts 80645a089c Remove deleted statuses on notifications screen 2023-05-15 23:45:18 -04:00
Shadowfacts 37442bcb48 Fix crash if selected search scope somehow changes before the view is loaded 2023-05-15 23:45:18 -04:00
Shadowfacts a99072dd7c Fix crash if there are duplicate accounts in fav/reblog notification list 2023-05-15 23:45:18 -04:00
Shadowfacts 6b57ec8b97 Cleanup orphaned local attachments 2023-05-15 23:45:18 -04:00
Shadowfacts d84d402271 Fix various issues when dealing with multiple Compose/Drafts screens simultaneously 2023-05-15 22:57:07 -04:00
Shadowfacts f004c82302 Fix crash if TimelineGapCollectionViewCell is somehow accessibility-activated 2023-05-15 22:03:51 -04:00
Shadowfacts 126e8c8858 Resolve Mastodon remote status links
Closes #384
2023-05-15 22:01:44 -04:00
Shadowfacts dbc89509d7 Fix expand thread cell using wrong background color
Closes #383
2023-05-15 21:25:01 -04:00
Shadowfacts 0ba38e4a3a Fix handoff to iPad/Mac modally presenting new screen rather than pushing nav 2023-05-15 21:17:26 -04:00
Shadowfacts 361ce456cf Bump build number and update changelog 2023-05-14 22:31:10 -04:00
Shadowfacts c1cfde9d49 Don't show Markdown formatting warning on Calckey 2023-05-14 21:44:20 -04:00
Shadowfacts daa38772b4 Fix crash when editing hide-action filter 2023-05-14 21:32:22 -04:00
Shadowfacts dc83172aea Support filtering on Notifications screen 2023-05-14 19:15:18 -04:00
Shadowfacts b909a633a6 Fix monospace font not being set on profile statuses HTML converter 2023-05-14 19:09:06 -04:00
Shadowfacts 1f95a6cb8e Fix constraints breaking on expand thread cell 2023-05-14 19:08:52 -04:00
Shadowfacts 468af3f9a6 Move CollapseState out of NotificationGroup 2023-05-14 18:55:34 -04:00
Shadowfacts 038e4b2e4e Fix crash when action notification cell label leaks 2023-05-14 18:44:08 -04:00
Shadowfacts de53e0dcd6 Fix editing Markdown/HTML statuses 2023-05-14 17:46:10 -04:00
Shadowfacts 1cf7434918 Fix editing posts not working on Akkoma 2023-05-14 17:31:08 -04:00
Shadowfacts fc7e7f502b Bump build number and update changelog 2023-05-14 17:30:01 -04:00
Shadowfacts 38a2ebd32b Fix link card images not loading on Mastodon 2023-05-14 16:24:54 -04:00
Shadowfacts 3b965b92f2 Don't update constraints from StatusContentContainer.setCollapsed 2023-05-14 15:53:24 -04:00
Shadowfacts 421cb7ba03 Fix conversation main status flickering when context is loaded 2023-05-14 15:25:09 -04:00
Shadowfacts 8319935a3d BaseEmojiLabel improvements
Avoid rechecking disk/memory caches when fetching

Use UIImage thumbnail API, rather than UIGraphicsImageRenderer, and make
thumbnail off main thread when possible
2023-05-14 15:19:00 -04:00
Shadowfacts 91ef386a41 Fix reblogger label getting updated twice for every cell 2023-05-14 14:58:46 -04:00
Shadowfacts c8eec17180 Fix custom emoji in display name being replaced multiple times unnecessarily 2023-05-14 14:41:36 -04:00
Shadowfacts c94e60d49b Enable editing on Pleroma 2.5+ 2023-05-14 13:55:28 -04:00
Shadowfacts b00170c3f9 Move InstanceFeatures.Version to separate file 2023-05-14 13:51:41 -04:00
Shadowfacts b37e5fffbf Silence CloudKit debug logging 2023-05-13 15:03:48 -04:00
Shadowfacts 8c27a9368f Estimate height when resolving status collapse state 2023-05-13 15:00:03 -04:00
Shadowfacts 735659dee6 Don't leave space for checkbox when no checkboxes are shown 2023-05-13 14:14:38 -04:00
Shadowfacts bf02b185ed Fix StatusState copying removing cached state
Closes #380
2023-05-13 13:53:04 -04:00
Shadowfacts 4ccf5d21a4 Disable boost to original audience for the users own DMs
Closes #382
2023-05-13 13:50:07 -04:00
Shadowfacts 9ac1c43511 Update favorite/reblog button appearance immediately on tap
Fixes #381
2023-05-13 13:48:49 -04:00
Shadowfacts 76b9496fe6 Revert "Unseparate out updateStatusState method"
This reverts commit 2157126332.
2023-05-13 13:18:57 -04:00
Shadowfacts ae8191ca0e Don't use prepareThumbnail in Compose screen
Fixes crash when sharing certain images to share sheet extension
2023-05-13 12:38:51 -04:00
Shadowfacts a9a9bfebeb Fix share sheet extension not working with Apple News
Closes #375
2023-05-12 22:00:00 -04:00
Shadowfacts 2d8e2f0824 Fix hitches due to AttachmentView not using pre-prepared images 2023-05-12 21:40:17 -04:00
Shadowfacts 6f18d46037 Properly conform Client.Error to LocalizedError 2023-05-11 23:26:06 -04:00
Shadowfacts 6261318df1 Bump build number and update changelog 2023-05-11 23:15:52 -04:00
Shadowfacts bff7585fa9 Move remote change processing to separate context to avoid blocking background context 2023-05-11 23:03:41 -04:00
Shadowfacts 4dbc4ebeb2 Include fractional seconds in log timestamps 2023-05-11 20:46:25 -04:00
Shadowfacts fc391cc18c Bump build number and update changelog 2023-05-11 20:24:21 -04:00
Shadowfacts 35b390d3c1 Fix MultiSourceEmojiLabel 2023-05-11 18:38:49 -04:00
Shadowfacts b21703f6d9 Fix decoding polls on Calckey
See #362
2023-05-11 16:15:36 -04:00
Shadowfacts d003098146 Better TimelineLikeController logging 2023-05-11 15:11:43 -04:00
Shadowfacts db7c183d06 Add status edit history view 2023-05-11 14:57:47 -04:00
Shadowfacts 7d3c82f4b7 Fix collapsible state not changing when post edited 2023-05-11 14:46:45 -04:00
Shadowfacts 13ec3366d3 Fix content warning not being removed by edit 2023-05-11 14:39:49 -04:00
Shadowfacts f9a41fd4f3 Show edit timestamps on statuses 2023-05-11 13:10:45 -04:00
Shadowfacts 2157126332 Unseparate out updateStatusState method 2023-05-11 10:03:09 -04:00
Shadowfacts e87dcfe48e Add support for editing posts
Closes #321
2023-05-11 10:03:09 -04:00
Shadowfacts 566c3d474d Don't show Show Reblogs action for non-followed people 2023-05-10 22:22:37 -04:00
Shadowfacts ca03cf3b08 Shorten hashtag action titles 2023-05-10 11:55:23 -04:00
Shadowfacts f0e530722f FIx hashtag timelines opened in new window not having save/follow actions 2023-05-10 11:54:36 -04:00
Shadowfacts dcd1b4ad94 Fix being able to scroll to top while fast account switcher is active 2023-05-10 11:41:59 -04:00
Shadowfacts 3394c2126c Fix list timelines opened in new window not showing Edit button 2023-05-10 11:32:08 -04:00
Shadowfacts 85765928b4 Fix crash when trying to remove popped view controller that doesn't exist 2023-05-10 11:04:56 -04:00
Shadowfacts f13874ee01 Improve rate limit exceeded error message 2023-05-10 10:59:22 -04:00
Shadowfacts bac272a2db Detect gotosocial and calckey instances 2023-05-10 10:48:52 -04:00
Shadowfacts 48bd957276 Fix nodeinfo not being fetched on punycode domains 2023-05-10 10:40:27 -04:00
Shadowfacts d4d42e7856 Report instance type/version in Sentry events 2023-05-10 10:34:48 -04:00
Shadowfacts 671a8e0cb3 Fix error decoding statuses on Calckey lacking emojis 2023-05-10 10:13:34 -04:00
Shadowfacts 822c2e0fa2 Bump build number and update changelog 2023-05-10 10:13:00 -04:00
Shadowfacts ee651ae96a Fix assorted issues collapsing/expanding split VC 2023-05-09 16:42:16 -04:00
Shadowfacts 9fc4aa8a40 Make various corners continuously rounded 2023-05-09 14:56:48 -04:00
Shadowfacts 8f6a012538 Fix decoding statuses on GtS with empty strings for urls
Closes #373
See #129
2023-05-08 17:05:06 -04:00
Shadowfacts 91d6430815 Fix various tests 2023-05-08 16:58:50 -04:00
Shadowfacts eac5a4c9a6 Fix notifications scrolling to top when refreshing 2023-05-07 19:46:15 -04:00
Shadowfacts 7449688bfe Bump build number and update changelog 2023-05-07 19:44:04 -04:00
Shadowfacts 63612b2fb0 Make notification cells subclasses of UICollectionViewListCell 2023-05-07 16:35:01 -04:00
Shadowfacts 8e010c7fa5 Remove unused notifications and status table view code 2023-05-07 15:11:35 -04:00
Shadowfacts 3181c47fde Convert rest of notifications screen to collection view 2023-05-07 15:11:35 -04:00
Shadowfacts a133955489 Fix using removed dismiss notification API endpoint 2023-05-07 15:11:35 -04:00
Shadowfacts 7551c79715 Convert status updated notification to collection view cell 2023-05-07 15:11:35 -04:00
Shadowfacts 5a4e387026 Convert poll finished notification to collection view cell 2023-05-07 15:11:35 -04:00
Shadowfacts 00945a0028 Convert follow request notification to collection view cell 2023-05-07 13:44:55 -04:00
Shadowfacts 2b9d384f8f Convert follow notification to collection view cell 2023-05-07 11:02:37 -04:00
Shadowfacts 90efee3f20 Convert action group notification to collection view cell 2023-05-07 11:02:06 -04:00
Shadowfacts 574d1f9134 Initial notifications collection view implementatioan 2023-05-06 20:32:48 -04:00
Shadowfacts 25e82d828f Fix presented VC getting dismissed after closing expanded attachment view 2023-05-06 14:33:05 -04:00
Shadowfacts 2eb9e63724 Make language picker sheet half-height, fix appearance in non-pure-black dark mode 2023-05-06 14:28:12 -04:00
Shadowfacts d85f74f365 Fix crash due to layout loop when laying out fields on certain profiles
Closes #378

Also make field layout more consistent, and tweak appearance
2023-05-06 14:16:43 -04:00
Shadowfacts f775527d63 Bump build number and update changelog 2023-05-05 10:33:17 -04:00
Shadowfacts a6d64282c0 Add language picker to Compose screen
Closes #236
2023-05-05 10:13:20 -04:00
Shadowfacts 24fb0e0e7b Remove automatically save drafts preference
Closes #369
2023-05-04 21:40:59 -04:00
Shadowfacts b6a5a60066 Fix full size image not being loaded on first appearance of focused attachment view 2023-05-04 21:06:59 -04:00
Shadowfacts f68d1009e5 Fix focused attachment view being incorrect size on iPad 2023-05-04 21:03:11 -04:00
Shadowfacts 99b74559da Don't duck Compose screen when the draft is empty
See #369
2023-05-04 18:40:00 -04:00
Shadowfacts 346888db41 Fix deadlock when drafts persistent container is initialized simultaneously on background and main threads
Fixes #374
2023-05-04 18:33:06 -04:00
Shadowfacts 7b218bfd75 Fix spinner on Send Report button being misplaced
Closes #377
2023-05-04 10:16:15 -04:00
Shadowfacts 098c4254d4 Fix deleted attachments reappearing 2023-05-04 10:12:44 -04:00
Shadowfacts bbdb7fe41f Fix crash on deleting draft with attachments in share extension 2023-05-04 10:11:04 -04:00
Shadowfacts 3c13d2083b Fix double nav controller in share extension 2023-05-04 10:01:32 -04:00
Shadowfacts ad55851090 Play back videos in focused attachment view 2023-05-03 23:30:33 -04:00
Shadowfacts a37423a119 Fix gifs being converted to images on upload
Closes #376
2023-05-03 23:20:58 -04:00
Shadowfacts 02daf88db3 Support gifs in new thumbnail controller and focused attachment view 2023-05-03 23:20:58 -04:00
Shadowfacts ce3b8ba4b3 Add zooming to focused attachment view 2023-05-03 23:20:58 -04:00
Shadowfacts 891fd3826b Add expanded attachment description view to Compose screen
Closes #365
2023-05-03 23:20:58 -04:00
Shadowfacts e0eba95b48 Remove double navigation controllers from compose screen 2023-04-25 18:51:46 -04:00
Shadowfacts 2febb37a8e Let duckable VCs prevent ducking 2023-04-23 21:55:14 -04:00
Shadowfacts a20e8b2f48 Don't require DuckableContainer to manage navigation controller 2023-04-23 20:08:57 -04:00
Shadowfacts b3d5ed8505 Delete local files when DraftAttachment deleted 2023-04-23 14:44:11 -04:00
Shadowfacts 4401503b85 More detailed error when status URL decoding fails
See #373
2023-04-23 14:38:51 -04:00
Shadowfacts 6c5909c800 Fix error when reloading empty profile
Closes #366
2023-04-23 14:30:56 -04:00
Shadowfacts af5109f86c Fix restored, ducked Compose screen lacking title 2023-04-23 14:27:18 -04:00
Shadowfacts b782e66a45 Fix draft being deleted when Compose screen ducked 2023-04-23 14:27:00 -04:00
Shadowfacts a1ffb23f0d Align link verification checkmark to link rather than screen edge
Closes #368
2023-04-23 14:01:51 -04:00
Shadowfacts ea5afeeb88 Persist sidebar visibility across app launches
Closes #372
2023-04-23 13:57:24 -04:00
Shadowfacts 49334766ef Fix crash when inserting poll from draft created in share sheet 2023-04-23 10:22:53 -04:00
Shadowfacts 3bba4edb45 Fix sharing extension not being available on iOS 15 2023-04-23 10:19:20 -04:00
Shadowfacts bda8fdb1b9 Bump build number and update changelog 2023-04-22 23:31:27 -04:00
Shadowfacts f361517a92 Fix crash on first launch after updating from build 77 2023-04-22 23:22:38 -04:00
Shadowfacts a12afb8dc2 Fix sharing extension only using first attachment 2023-04-22 22:43:00 -04:00
Shadowfacts de1a97d357 Use actual activation rule for sharing extension 2023-04-22 22:34:47 -04:00
Shadowfacts c17cf460d7 Fix post error messages not being displayed correctly 2023-04-22 22:30:27 -04:00
Shadowfacts 8ff20bf7aa Disable unused test targets 2023-04-22 22:23:43 -04:00
Shadowfacts 205056f636 Fix draft being deleted too early causing empty UI during dismiss compose animation 2023-04-22 22:18:46 -04:00
Shadowfacts 40197e04cf Fix attachment description observation trying to access properties of deleted object 2023-04-22 22:18:21 -04:00
Shadowfacts 2249e5a315 Fix DraftAttachment being accessed off main thread 2023-04-22 22:03:52 -04:00
Shadowfacts bff1ea8b9d Merge branch 'share-sheet-extension' into develop 2023-04-22 21:59:14 -04:00
Shadowfacts b614226871 Fix avatars in share sheet being blurry 2023-04-22 21:48:12 -04:00
Shadowfacts f51f3c8a94 Use CoreData for drafts store 2023-04-22 21:40:29 -04:00
Shadowfacts 074a296a68 Fix Post button always being disabled when require attachment descriptions is enabled
Also fix post button state not updating when description edited

Closes #371
2023-04-21 18:02:30 -04:00
Shadowfacts 2874e4bfd3 Coordinate DraftsManager reading writing between processes 2023-04-21 17:24:40 -04:00
Shadowfacts 74a157d26c Fix drafts from share sheet not being saved 2023-04-19 22:27:25 -04:00
Shadowfacts 3d3fc3f515 Allow switching accounts from share sheet 2023-04-19 22:20:05 -04:00
Shadowfacts 6c371f868f Initial share extension implementation 2023-04-18 21:55:14 -04:00
Shadowfacts 06855420da Move preferences to shared package 2023-04-18 19:47:49 -04:00
Shadowfacts 0d7cc69947 Fix not being able to close draft when automatic save preference is off 2023-04-18 15:17:42 -04:00
Shadowfacts cfc69627e5 Fix crash when creating menu actions for status w/o URL 2023-04-18 10:19:53 -04:00
Shadowfacts 160f48679b Handle HTTP 206 responses from timelines endpoint 2023-04-18 10:16:38 -04:00
Shadowfacts 4931665b45 Log Sentry installation ID
So when the user taps Get Support and logs are sent we can cross-ref
with recent crashes
2023-04-18 10:10:15 -04:00
Shadowfacts 849882287f Fix crash when pasting screenshots, not being able to paste gifs 2023-04-17 20:14:59 -04:00
Shadowfacts 436159bd46 Show reblogger's avatar on reblogged posts 2023-04-17 11:19:37 -04:00
Shadowfacts 2224dbebb8 Remove old code 2023-04-17 10:08:18 -04:00
Shadowfacts 9882250a9b Bump build number and update changelog 2023-04-16 19:06:45 -04:00
Shadowfacts bb22a6bf9e Remove more old asset picker code 2023-04-16 18:47:03 -04:00
Shadowfacts 15c83f8332 Fix keyboard focus background on list cells not showing correctly 2023-04-16 18:46:47 -04:00
Shadowfacts 5ec35b6009 Fix reblogged statuses appearing Bookmarks
Closes #359
2023-04-16 18:20:16 -04:00
Shadowfacts 22fe1e8ab1 Don't redact api endpoints in debug 2023-04-16 15:11:59 -04:00
Shadowfacts 813d0433d6 Fix profile no content cell not using non-pure-black background color 2023-04-16 15:11:47 -04:00
Shadowfacts cd9d64410f Add hashtag pinned timeline search improvements
Closes #348
2023-04-16 14:50:54 -04:00
Shadowfacts 2b66f98832 Remove old asset picker 2023-04-16 14:28:09 -04:00
Shadowfacts 6ebcc162e6 Add icons to About screen links 2023-04-16 14:12:27 -04:00
Shadowfacts 8b7c78e3b1 Log errors that result in showing a toast to the user 2023-04-16 14:07:30 -04:00
Shadowfacts ab8ccbb408 Exclude notifications that are missing statuses
It's still unclear why this ever happens, but crashing is untenable

See #274
2023-04-16 13:59:38 -04:00
Shadowfacts f89d2c1cca Merge branch 'compose-package' into develop 2023-04-16 13:50:23 -04:00
Shadowfacts 30449a2875 Rename NewComposeHostingController to ComposeHostingController 2023-04-16 13:47:48 -04:00
Shadowfacts afed157f29 Remove old compose screen code 2023-04-16 13:47:06 -04:00
Shadowfacts 6b4223a9d6 Migrate drafts to new file 2023-04-16 13:31:10 -04:00
Shadowfacts 0746e12737 Extract compose UI into separate package 2023-04-16 13:23:13 -04:00
Shadowfacts 350e331eb2 Move GIFImageView to TuskerComponents 2023-04-16 13:17:39 -04:00
Shadowfacts bb3f353dbc Fix Compose window title not being set initially 2023-04-13 10:04:48 -04:00
Shadowfacts 6bd2eacb88 Fix replied-to account not being first mention 2023-04-13 10:02:05 -04:00
Shadowfacts 29b594207c Fix crash when comments present in html 2023-04-03 23:39:22 -04:00
Shadowfacts e5363b2e21 Fix sidebar key commands not working on macOS
Closes #253
2023-04-03 23:25:33 -04:00
Shadowfacts d04259b253 Fix scroll to top not working in presented VCs
Closes #363
2023-04-03 22:45:15 -04:00
Shadowfacts f50c219f95 Send scopes in /oauth/token request
Closes #360
2023-04-03 22:43:01 -04:00
Shadowfacts b2fe2fdf9a Move Visibility to top-level type and move extensions to Pachyderm 2023-03-07 10:14:35 -05:00
Shadowfacts 850a0e90ce Move MenuPicker to separate package 2023-03-07 10:07:45 -05:00
Shadowfacts 391ea1b46a Move InstanceFeatures to separate package 2023-03-05 14:52:19 -05:00
Shadowfacts 247bb31c56 Move local user accounts to separate package 2023-03-05 14:35:25 -05:00
Shadowfacts 5471d810c8 Fix reblog error toast title 2023-03-01 21:09:56 -05:00
Shadowfacts ad0a9ecafe Fix crash when setting SegmentedPageViewController pages to [] after failing to decode pinned timelines 2023-02-28 22:42:28 -05:00
Shadowfacts ee630cf9df Bump build number and update changelog 2023-02-28 20:27:06 -05:00
Shadowfacts c786c022b8 Fix crash when trying to apply invalid game model update
Not sure how this could happen but w/e
2023-02-28 14:40:48 -05:00
Shadowfacts 33649cc5c0 Bump build number and update changelog 2023-02-28 14:40:27 -05:00
Shadowfacts 71a10f8514 Don't report 502 errors 2023-02-27 10:34:37 -05:00
Shadowfacts a864f4e344 Tweak timeline marker error reporting 2023-02-27 10:34:37 -05:00
Shadowfacts 007d5d6791 Don't report 404 errors 2023-02-26 14:09:29 -05:00
Shadowfacts f176a6c8eb Bump build number and update changelog 2023-02-25 18:38:52 -05:00
Shadowfacts 104981f3d3 Fix iPad Explore screen not restoring search state 2023-02-25 18:30:05 -05:00
Shadowfacts 2ba6b64485 Tweak marker API preference description 2023-02-25 18:28:19 -05:00
Shadowfacts 81ac3708a3 Tweak compose placeholders 2023-02-25 18:22:41 -05:00
Shadowfacts 8e9e0fa346 Persist state when switching accounts 2023-02-25 18:00:17 -05:00
Shadowfacts b6f32ca6be Make timeline load more button more prominent 2023-02-25 16:59:48 -05:00
Shadowfacts e042754be1 Fix crash when restoring state for timeline VC 2023-02-25 16:44:36 -05:00
Shadowfacts 38ac5858a9 Don't check present when refreshing timeline 2023-02-25 16:39:00 -05:00
Shadowfacts 0c0180264e Fix no content message incorrectly appearing on profiles
Caused by a spurious appearance transition from embedChild
2023-02-25 15:30:30 -05:00
Shadowfacts 3d9477f0c9 Hide card description label when it doesn't fit
Closes #336
2023-02-25 15:23:13 -05:00
Shadowfacts 6f51f321f6 Fix VC restored to secondary split nav missing Close button 2023-02-25 15:15:00 -05:00
Shadowfacts ab17a688cf Fix TrendHistoryView trying to create shape layers with NaNs 2023-02-25 15:11:17 -05:00
Shadowfacts 18bc6ce61e Don't use readable content inset for search results 2023-02-25 15:10:21 -05:00
Shadowfacts 765b5e1a7c Don't use KVO for updating timeline gap cell 2023-02-25 15:02:55 -05:00
Shadowfacts a3e64703ab Transfer timeline position in handoff user activity
Closes #315
2023-02-25 15:01:19 -05:00
Shadowfacts d74be9d81d Add handoff to various user activities 2023-02-25 15:00:55 -05:00
Shadowfacts 6ca5bb0c74 Unify state restoration with user activity handling code 2023-02-25 14:08:54 -05:00
Shadowfacts 76550d8fb8 Fix crash when ReportView opened before instance loaded 2023-02-24 18:32:29 -05:00
Shadowfacts daf3741c9a Hide placeholder image from link card when none provided
Closes #358
2023-02-24 18:27:31 -05:00
Shadowfacts b2977540e0 Add profile moved banner
Closes #284
2023-02-24 18:27:31 -05:00
Shadowfacts bcc70e9f8c Fix crash when data nodes present in converted HTML 2023-02-23 10:05:33 -05:00
Shadowfacts 2252b6d09e Fix crash duplicate main status and crash when conversation context is preloaded
For expand thread cells, the main status needs to be the one above the selected cell
2023-02-23 10:02:05 -05:00
Shadowfacts 8deb502140 Show message on remote profiles with no statuses
Closes #279
2023-02-22 22:23:18 -05:00
Shadowfacts 2582907919 Only show fav/reblog inaccurate count warning for remote posts 2023-02-22 22:00:12 -05:00
Shadowfacts 266868376d Allow refreshing conversations
Closes #157
2023-02-22 21:52:45 -05:00
Shadowfacts 71fa3910a1 Simplify NSUserActivity construction code 2023-02-22 21:42:09 -05:00
Shadowfacts 75f290ae8f Tab state restoration
Closes #32
2023-02-22 21:38:12 -05:00
Shadowfacts 073a1afbde Show percentage of voters for multi-choice polls 2023-02-19 18:21:20 -05:00
Shadowfacts aaa031f212 First pass at strict sendability checking 2023-02-19 15:23:25 -05:00
Shadowfacts 762d298c06 Report caught NSExceptions to Sentry 2023-02-19 14:19:39 -05:00
Shadowfacts 2a892fa6ec Disable custom status link previews on iOS 16.4 2023-02-19 13:49:56 -05:00
Shadowfacts cb82826fcf Catch NSExceptions when doing objc runtime shenanigans 2023-02-15 19:34:23 -05:00
Shadowfacts 6e5498430f Fix poll option tracking unselecting options when location moves in between views 2023-02-15 18:57:05 -05:00
Shadowfacts 57fb921573 Fix non-pure-black dark mode not applying to ohter scenes 2023-02-14 22:52:27 -05:00
Shadowfacts d1b5126288 Fix status action account list not adjusting to non-pure-black dark mode 2023-02-14 22:47:56 -05:00
Shadowfacts 9d2324b587 Add preference to use timeline marker API
Closes #40
2023-02-14 21:56:15 -05:00
Shadowfacts 60921cb95f Fix tapping reblog count in conv main status showing favorites list 2023-02-14 21:37:54 -05:00
Shadowfacts 9e76879ce6 Add preference to hide attachment badges
Closes #354
2023-02-14 21:37:54 -05:00
Shadowfacts 1992a4c60b Make search results VC dismiss keyboard interactively 2023-02-13 20:29:15 -05:00
Shadowfacts f833bc3a6f Apply accessibility labels to MenuPicker actions 2023-02-13 20:27:05 -05:00
Shadowfacts 4731801893 Bump build number and update changelog 2023-02-12 10:22:33 -05:00
Shadowfacts 4293b51c31 Add extended suggested profiles screen
Closes #355
2023-02-11 19:05:12 -05:00
Shadowfacts ecadb83c6d Add infinite scrolling to trending statuses
See #355
2023-02-11 18:47:39 -05:00
Shadowfacts 205bdffebd Add loading indicator to Trends screen 2023-02-11 18:32:37 -05:00
Shadowfacts ae7ca9c91c Fix wrong cells on trending links screen being selectable 2023-02-11 18:29:33 -05:00
Shadowfacts 841119949b Add infinite scrolling to trending hashtags screen
See #355
2023-02-11 18:29:33 -05:00
Shadowfacts b63f663947 Handle errors when loading trending links 2023-02-11 18:13:37 -05:00
Shadowfacts 00a23b525f Add share to trending link actions 2023-02-11 10:21:09 -05:00
Shadowfacts ea85b11945 Use cards for trending links screen, and add pagination
See #355
2023-02-11 10:09:56 -05:00
Shadowfacts d8c7eb5cf5 Add buttons to Explore screen 2023-02-10 18:19:00 -05:00
Shadowfacts 8bc185ecf9 Add jump to present button to timelines 2023-02-07 23:52:23 -05:00
Shadowfacts 1832e64ad7 Remove now-unused hashtag table view cell 2023-02-06 21:47:47 -05:00
Shadowfacts 87bc1f5f75 Rewrite search results VC using UICollectionView 2023-02-06 21:47:47 -05:00
Shadowfacts 6e2f6bb8e9 Apply non-pure black dark mode to Drafts screen 2023-02-06 19:53:15 -05:00
Shadowfacts 74d8adfffe Fix Compose background color not going under nav bar 2023-02-06 19:51:01 -05:00
Shadowfacts 99127b617b Tweak non-pure-black dark mode colors 2023-02-06 18:47:50 -05:00
Shadowfacts 65ea72c07f Don't show pure-black dark mode preference on Mac 2023-02-06 18:45:34 -05:00
Shadowfacts 04ca932a01 Mode non-pure-black dark mode stuff to dedicated modifiers 2023-02-06 18:43:00 -05:00
Shadowfacts 4ea2dff8f1 Merge branch 'develop' into non-pure-black-mode 2023-02-06 18:15:23 -05:00
Shadowfacts 9f0176350c Cleanup TuskerNavigationDelegate 2023-02-06 18:10:38 -05:00
Shadowfacts dac1e1fe3f Fix icon in suggested profile reason popover not adjusting to dark mode 2023-02-05 19:56:37 -05:00
Shadowfacts afed69e43e Bump build number and update changelog 2023-02-05 19:50:21 -05:00
Shadowfacts b2096f22c3 Rename Hide Discover Section pref to Hide Trends 2023-02-05 14:43:04 -05:00
Shadowfacts 14c456df22 Tweak trends orthogonal scroll behavior 2023-02-05 14:41:10 -05:00
Shadowfacts 3f34357692 Fix discover section sometimes appearing on non-Mastodon instances 2023-02-05 14:36:09 -05:00
Shadowfacts 429dcefa88 Use consolidated trends screen on iPhone 2023-02-05 14:34:01 -05:00
Shadowfacts d1a35620c9 Remove profile directory
The code remains for now, in case it needs to return
2023-02-05 14:27:26 -05:00
Shadowfacts ce741d6e1f Extract trends to separate VC 2023-02-05 14:23:29 -05:00
Shadowfacts 5a82851fe9 Fix custom emoji picker buttons not having accessibility labels
Closes #286
2023-02-05 14:00:08 -05:00
Shadowfacts 92ff900bc0 Improve VoiceOver labels for notifications
Closes #350
2023-02-05 13:56:48 -05:00
Shadowfacts 2a1deb8d7d Fix follow request accept/reject buttons not matching accent color 2023-02-05 13:55:31 -05:00
Shadowfacts 38eea44a8b VoiceOver improvements on fast account switcher
Closes #310
2023-02-05 13:33:42 -05:00
Shadowfacts 2d45fbbd91 Apply Mastodon poll limits in Compose view 2023-02-05 12:43:51 -05:00
Shadowfacts 32382c4783 Fix crash when previewing status cell that doesn't have delegate
Not sure how this is possible, but w/e
2023-02-05 11:27:03 -05:00
Shadowfacts 521c46c0be Don't capture certain error types 2023-02-05 11:23:10 -05:00
Shadowfacts c114749519 Handle 401 errors on instance timelines 2023-02-05 11:18:23 -05:00
Shadowfacts 825424cfba Fix crash when tapping My Profile tab before view is loaded
Closes #352
2023-02-05 11:09:08 -05:00
Shadowfacts 985eb24e88 Remove workaround for compiler bug breaking constrained existential types on iOS 15 release builds
Closes #178
2023-02-05 11:04:11 -05:00
Shadowfacts 7cadcf1e86 Reuse conversation tree where possible when selecting a status in a conversation 2023-02-04 15:15:41 -05:00
Shadowfacts a314521b96 Extract out conversation tree-building code 2023-02-04 13:49:20 -05:00
Shadowfacts ab3bad0e16 Fix trending statuses not being deselected on navigation back 2023-02-04 13:48:24 -05:00
Shadowfacts ec75906bc1 Add favorites screen
Closes #327
2023-02-04 13:21:58 -05:00
Shadowfacts 137a537f68 Extract loading and local updating handling code from bookmarks VC into separate VC 2023-02-04 13:14:08 -05:00
Shadowfacts 91123fd24a Make username label on profile copyable 2023-02-04 11:10:01 -05:00
Shadowfacts 597dd56032 Fix crash on split VC collapse/expand while in explore tab
Also fix iPad explore VC resetting when leaving/reopening the app

Closes #351
2023-02-03 18:30:37 -05:00
Shadowfacts 37847a2f9f Fix accent color circles not showing on iOS 15 2023-02-02 23:36:47 -05:00
Shadowfacts 471d3459a6 Apply non-pure-black dark mode to preferences screen 2023-02-02 23:29:44 -05:00
Shadowfacts 512eec09a8 Merge branch 'develop' into non-pure-black-mode 2023-02-02 23:14:27 -05:00
Shadowfacts af8a9faaeb Cleanup PreferencesView 2023-02-02 23:14:19 -05:00
Shadowfacts 20c4c4bb2f Start adding non-pure-black dark mode 2023-02-02 23:02:11 -05:00
Shadowfacts 76268e7a14 Make attachment description selectable in gallery 2023-01-31 14:17:59 -05:00
Shadowfacts 29596180a1 Using async/await for ImageCache implementation 2023-01-31 09:56:13 -05:00
Shadowfacts ebfd8b3efd Fix bookmarks VC sometimes going haywire 2023-01-30 10:07:34 -05:00
Shadowfacts 509acbde19 Fix status action account list VC not resizing on rotation 2023-01-29 16:02:47 -05:00
Shadowfacts 474064669d Bump build number and update changelog 2023-01-29 10:26:20 -05:00
Shadowfacts 1940368c43 Load account lists in pages of 40 2023-01-28 23:07:38 -05:00
Shadowfacts 49c9c69b5a Fix flicker when opening status action account list in split nav
The container VC background needs to match the content VC
2023-01-28 22:59:30 -05:00
Shadowfacts ff29f2768b Tweak follow count button colors
Try to make it clearer that it's a button
2023-01-28 18:18:53 -05:00
Shadowfacts 942df433b3 Allow refreshing bookmarks list 2023-01-28 15:30:41 -05:00
Shadowfacts 5e2b551045 Update bookmarks VC on bookmarked state changes
Closes #318
2023-01-28 15:30:41 -05:00
Shadowfacts 2e64500c35 Rewrite bookmarks VC using UICollectionView 2023-01-28 15:30:41 -05:00
Shadowfacts 7b7c05ff68 Fix timeline position sync not working due to LazilyDecoding cache not being invalidated upon remote change 2023-01-28 13:41:22 -05:00
Shadowfacts aec5c0b787 Update Sentry SDK 2023-01-28 00:16:11 -05:00
Shadowfacts d8901b38f5 Load timeline posts in pages of 40 2023-01-28 00:16:11 -05:00
Shadowfacts 9d7c876e3c Remove old sleeps 2023-01-27 21:48:47 -05:00
Shadowfacts 455273f322 Show more posts in report status screen 2023-01-27 21:45:02 -05:00
Shadowfacts 16347b2ad0 Automatic retry during onboarding, better UI while waiting 2023-01-27 20:52:34 -05:00
Shadowfacts 0e1cbce10d Revoke token and destroy stores when logging out 2023-01-27 18:53:20 -05:00
Shadowfacts 8bd6f53f01 Allow pinning instance public timelines 2023-01-27 18:12:54 -05:00
Shadowfacts fe32356bce Bump build number and update changelog 2023-01-27 10:38:56 -05:00
Shadowfacts 1f337613be Add animation when compose toolbar buttons (dis)appear 2023-01-26 22:33:47 -05:00
Shadowfacts 3f4a62f5f9 Fix changes being published during SwiftUI view update 2023-01-26 22:18:03 -05:00
Shadowfacts b506704716 Move Drafts button to nav bar when current composed post doesn't have any content 2023-01-26 22:17:49 -05:00
Shadowfacts 6a3dcca9ee Workaround for local-only posts not being decodable on Akkoma
See #332
2023-01-26 22:10:20 -05:00
Shadowfacts edd1e55cbb Unify haptic feedback
Closes #154
2023-01-26 21:52:12 -05:00
Shadowfacts f1facea929 Fix status URLs with fragments not being resolved 2023-01-26 21:15:02 -05:00
Shadowfacts d638ea054b Add gif/alt badges to attachments
Closes #255, #338
2023-01-26 19:16:34 -05:00
Shadowfacts e11784904b Add menu action to hide/show reblogs
Closes #206
2023-01-26 18:50:05 -05:00
Shadowfacts 9f1d3804d9 Apply Mastodon's link truncation
Closes #344
2023-01-26 18:38:31 -05:00
Shadowfacts 333295367a Add preference to hide link preview cards
Closes #329
2023-01-26 17:18:27 -05:00
Shadowfacts e9d14c6cbf Tweak status card background color in dark mode 2023-01-26 15:17:17 -05:00
Shadowfacts 8fc915d6a0 Bump build number and update changelog 2023-01-26 00:23:10 -05:00
Fahim Farook 2b4898329f Duckable Issue
* IF you have the .testTarget enabled, on Xcode 14.2 you get an error about the test target source needing to be under the "Tests" folder or something similar
2023-01-26 05:01:04 +00:00
Shadowfacts 5a9513bb30 Add tip jar 2023-01-25 23:58:51 -05:00
Shadowfacts e45459e556 Add support link to about screen 2023-01-25 18:54:09 -05:00
Shadowfacts 8b546daeaa Workaround for issues signing in to m.s 2023-01-25 09:56:24 -05:00
Shadowfacts 125f91257a Fix status notifications not being shown 2023-01-25 09:56:24 -05:00
Fahim Farook 507d9c23e7 xcconfig Fixes
* Ensure that all bundle prefixes are replaced correctly for all targets and in entitlements files too
2023-01-25 09:42:36 +04:00
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
722 changed files with 67836 additions and 14639 deletions

2
.gitignore vendored
View File

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

9
.gitmodules vendored
View File

@ -1,12 +1,3 @@
[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

205
CHANGELOG-release.md Normal file
View File

@ -0,0 +1,205 @@
## 2024.1
This update includes a significant improvements for the attachment gallery and displaying rich text posts. See below for a full list of improvements and fixes.
Features/Improvements:
- Improve attachment gallery
- Improve animations
- Display video captions
- Support sharing/saving videos
- Resume music playback after playing videos
- Improve rich text display in posts
- Add See Results button to polls
- Add Share and Save to Photos menu items to post attachments
- Show verified links in account lists
- Display message on empty list timelines
- Add preference to indicate attachments lacking alt text
- Mark notifications as read on Mastodon web frontend once displayed
- iPadOS: Support tapping the selected sidebar item to scroll to top
Bugfixes:
- Fix issue changing scope after searching
- Fix crash when searching "from:me"
- Fix tapping Followers button on profile opening Following screen
- Fix crash when removing poll option on Compose screen
- Fix hang when sharing video/GIFV attachments
- Fix stretched Save to Photos icon when sharing attachments
- Fix GIFV playback preventing device sleep
- Fix Notifications tab not scrolling to top when tab bar item tapped
- Fix selection not clearing on Trending Hashtags
- Fix fast account switcher overlapping iPhone sensor housing in landscape
- Fix Edit List screen not updating when adding/removing accounts
- Fix changing list reply policy not refreshing timeline
- Pixelfed: Fix crash when there are multiple follow notifications from the same account
- macOS: Fix attachment gallery displaying improperly when Reduce Motion is on
## 2023.8
This update adds support for search operators and post translation, and improves support for displaying rich-text posts. See below for a full list of improvements and fixes.
Features/Improvements:
- Show search operators on Mastodon 4.2
- Use server-set preference for default post visibility, language, and (on Hometown) local-only
- Allow changing list reply policy and exclusivity options on Edit List screen
- Add Translate action to conversations (on supported Mastodon instances)
- Style block quotes correclty in rich-text posts
- Improve the appearance of lists in rich-text posts
- Add preference to underline links
- Compress uploaded video attachments to fit within instance limits
- Add preference to hide attachments in timelines
- Update visible timestamps after refresh notifications/timelines
- iPadOS: Allow switching between split screen and fullscreen navigation modes
- Pixelfed: Improve error message when uploading attachment fails
- Akkoma: Enable composing local-only posts
Bugfixes:
- Fix older notifications not loading if all initiially-loaded ones are grouped together
- Fix List timelines failing to refresh if they were initially empty
- Fix replies to posts with CWs always showing confirmation dialog when cancelling
- Fix Compose screen permitting setting the language to multiple/undefined
- Fix crash when uploading attachments without file extensions
- Fix Live Text button reappearing with swiping between attachment gallery pages
- Fix avatars on certain notifications flickering when refreshing
- Fix avatars on follow request notifications not being rounded
- Fix timeline jump button appearing incorrectly when Button Shapes acccessibility setting is on
- Fix public instance timeline screen not handling post deletion correctly
- Fix post that's reblogged and contains a followed hashtag not showing the reblogger
- Fix crash on launch when reblogged posts are visible
- Fix crash when showing display names with custom emoji in certain places
- Fix crash when showing trending hashtags without history data
- Fix potential crash on instance selector screen
- Fix potential crash if the app is dismissed while fast account switcher is animating
- Fix potential crash after deleting List on the Eplore screen
- Pixelfed: Fix error decoding certain posts
- VoiceOver: Fix history entries on Edit History screen not having descriptions
- iPadOS: Fix delay on app launch before "My Profile" sidebar item appears
- iPadOS: Fix language picker button not highlighting when hovered with the cursor
- macOS: Fix "New Post" window title appearing twice
- macOS: Fix Cmd+W sometimes closing non-foreground windows
- macOS: Fix visibility/local-only buttons not appearing in Compose toolbar
- macOS: Fix images copied from Safari not pasting on Compose screen
## 2023.7
This update adds support for iOS 17 and includes some minor changes.
Changes:
- Support iOS 17
- Indicate that edit history may be incomplete for remote posts
- Fix crash when collapsing to tab-bar mode in certain circumstances
- Fix potential crashes when using autocomplete on the Compose screen
- Fix Iceshrimp instances not being detected
## 2023.6
This update fixes a number of bugs and improves stability throughout the app. See below for a list of fixes.
Bugfixes:
- Fix issues displaying main post in the Conversation screen
- Fix crash when opening the Compose screen in certain locales
- Fix issues when collapsing from sidebar to tab bar mode
- Fix incorrect UI being displayed when accessing certain parts of the app immediately after launch
- Fix link card images not being blurred on posts marked sensitive
- Fix links appearing with incorrect accent color intermittently
- Fix being unable to remove followed hashtags from the Explore screen
- Akkoma: Fix not being able to follow hashtags
- Pleroma: Fix refreshing Mentions failing
- iPhone: Fix ducked Compose screen disappearing when rotating on large phones
## 2023.5
This update adds new several Compose-related features, including the ability to edit posts, a share sheet extension, and a post language picker. See below for the full list of improvements and bugfixes.
Features/Improvements:
- Edit posts
- Indicate edited posts in timestamp
- Show post edit history from Conversation screen
- Add Share Sheet extension
- Add expanded attachment view on Compose screen
- Add an attachment, select the description text field, then tap the expand button
- Expanded view allows you to see the attachment while writing the description
- Allows playing back videos while writing description
- iOS 16: Allows zooming in to the attachment
- Add language picker to the Compose screen
- Improve Compose screen ducking behavior
- Show reblogger's avatar on reblogged posts
- Use system photo picker instead of custom interface
- Improve hashtag search UI in Customize Timelines
- Improve status collapse/expand animation on Notifications screen
- Apply filters to Notifications screen
- Improve performance when scrolling through timeline
- Improve error messages when editing filters
- Change favorite/reblog button order to match Mastodon UI
- Gracefully handle unknown attachment types
- iPadOS: Persist sidebar visibility across
Bugfixes:
- Fix scroll-to-top not working in in-app Safari
- Fix inaccruate titles in certain error popups
- Fix error decoding post HTML
- Fix replied-to account not being the first @-mention
- Fix "No Content" message on profiles using wrong background color
- Fix reblogged posts appearing in Bookmarks
- Fix spurious errors when loading timeline
- Fix crash when displaying certain profiles
- Fix crash when the server returns invalid notifications
- Fix link previews not appearing in Notifications
- Fix Notifications screen taking a long time to load
- Fix deleted posts not being removed from Notifications screen
- Fix crashes when switching between sidebar/tab-bar modes
- Fix instance features not being detected on IDNA domains
- Fix list/hashtag timelines missing controls when opened in new window
- Fix reblog button being enabled on the user's own direct posts
- Fix main post in Conversation flickering
- Fix link card images not loading on Mastodon
- Fix crash when editing filter with the Hide action
- Fix certain remote status links not being resolved
- Fix Handoff to iPad/Mac presenting new screen modally
- GoToSocial: Fix decoding certain posts
- Calckey: Fix decoding certain posts
- iPadOS: Fix Compose window lacking a title
- iPadOS: Fix keyboard focus highlight not showing
- macOS: Fix sidebar keyboard shortcuts not working
## 2023.4
Features/Improvements:
- Add preference for non-pure-black dark mode
- Add Jump to Present button to timelines on the home tab
- Consolidate Trends into a single screen
- Allow pinning instance public timelines to the Home tab
- Add GIF/ALT badges to attachments (and preference to hide them)
- Add action to show hide/show reblogs from specific accounts
- Add preference to hide link preview cards
- Hide placeholder image in link preview card for previews without images
- Truncate links in posts
- Move Drafts button in Compose screen to nav bar to reduce accidental presses
- Load more posts/notifications on each page
- Update Bookmarks screen when posts are bookmarked/unbookmarked
- Add infinite scrolling to Bookmarks screen
- Add Favorites screen to the Explore tab
- Make attachment description text selectable in gallery
- Add long press to copy username on profile screens
- Optimize conversation loading
- Apply server-configured poll limits in Compose screen
- Add infinite scrolling to trending links/hashtags/posts
- Add state restoration for more screens
- Persist state when switching between accounts
- Add Handoff support for various screens
- Add preference to sync timeline position using Mastodon API, rather than iCloud
- Show percentage of voters for multi-choice polls, rather than percentage of votes
- Display message on remote profiles with no posts
- Indicate moved profiles
- Make Load More button on timelines more prominent
- VoiceOver: Make fast account switcher accessible
- VoiceOver: Improve labels for notifications
- VoiceOver: Fix custom emoji picker not having labels
Bugfixes:
- Workaround for not being able to sign in to certain instances
- Fix timeline position sync not working in certain circumstances
- Fix local-only posts not being decodable when logged in to Akkoma instances
- Fix Trends sometimes appearing in Explore/sidebar on non-Mastodon instances
- Fix favoriters/rebloggers list not resizing on screen rotation
- Fix crash when tapping My Profile tab immediately after app launch
- Handle authentication required errors on instance public timelines
- Fix follow request accept/reject buttons not matching accent color preference
- Fix tapping reblog count in conversation main status showing favorites list
- Fix crash when certain tags are present in post HTML
- Fix crash when opening Report screen in certain circumstances
- iPadOS: Fix crash when resizing window while on the Explore screen
- iOS 15: Fix accent colors not being displayed in Preferences

1321
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

1
Cache

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

View File

@ -0,0 +1,7 @@
# Haptic Feedback
## Selection changed
`UISelectionFeedbackGenerator`
## Actions
On success, `UIImpactFeedbackGenerator` with the `.light` style. On error, `UINotificationFeedbackGenerator` with the `.error` type.

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
Gifu

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

View File

@ -0,0 +1,22 @@
<?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>TuskerInfo</key>
<dict>
<key>PushProxyHost</key>
<string>$(TUSKER_PUSH_PROXY_HOST)</string>
<key>PushProxyScheme</key>
<string>$(TUSKER_PUSH_PROXY_SCHEME)</string>
<key>SentryDSN</key>
<string>$(SENTRY_DSN)</string>
</dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.usernotifications.service</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,14 @@
<?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>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.$(BUNDLE_ID_PREFIX).Tusker</string>
</array>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,361 @@
//
// NotificationService.swift
// NotificationExtension
//
// Created by Shadowfacts on 4/9/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import UserNotifications
import UserAccounts
import PushNotifications
import CryptoKit
import OSLog
import Pachyderm
import Intents
import HTMLStreamer
import WebURL
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "NotificationService")
class NotificationService: UNNotificationServiceExtension {
private static let textConverter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLCallbacks.self)
private var pendingRequest: (UNMutableNotificationContent, (UNNotificationContent) -> Void, Task<Void, Never>)?
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
guard let mutableContent = request.content.mutableCopy() as? UNMutableNotificationContent else {
logger.error("Couldn't get mutable content")
contentHandler(request.content)
return
}
guard request.content.userInfo["v"] as? Int == 1,
let accountID = (request.content.userInfo["ctx"] as? String).flatMap(\.removingPercentEncoding),
let account = UserAccountsManager.shared.getAccount(id: accountID),
let subscription = getSubscription(account: account),
let encryptedBody = (request.content.userInfo["data"] as? String).flatMap({ Data(base64Encoded: $0) }),
let salt = (request.content.userInfo["salt"] as? String).flatMap(decodeBase64URL(_:)),
let serverPublicKeyData = (request.content.userInfo["pk"] as? String).flatMap(decodeBase64URL(_:)) else {
logger.error("Missing info from push notification")
contentHandler(request.content)
return
}
guard let body = decryptNotification(subscription: subscription, serverPublicKeyData: serverPublicKeyData, salt: salt, encryptedBody: encryptedBody) else {
contentHandler(request.content)
return
}
let withoutPadding = body.dropFirst(2)
let notification: PushNotification
do {
notification = try JSONDecoder().decode(PushNotification.self, from: withoutPadding)
} catch {
logger.error("Unable to decode push payload: \(String(describing: error))")
contentHandler(request.content)
return
}
mutableContent.title = notification.title
mutableContent.body = notification.body
mutableContent.userInfo["notificationID"] = notification.notificationID
mutableContent.userInfo["accountID"] = accountID
let task = Task {
await updateNotificationContent(mutableContent, account: account, push: notification)
if !Task.isCancelled {
contentHandler(pendingRequest?.0 ?? mutableContent)
pendingRequest = nil
}
}
pendingRequest = (mutableContent, contentHandler, task)
}
override func serviceExtensionTimeWillExpire() {
if let pendingRequest {
logger.debug("Expiring with pending request")
pendingRequest.2.cancel()
pendingRequest.1(pendingRequest.0)
self.pendingRequest = nil
} else {
logger.debug("Expiring without pending request")
}
}
private func updateNotificationContent(_ content: UNMutableNotificationContent, account: UserAccountInfo, push: PushNotification) async {
let client = Client(baseURL: account.instanceURL, accessToken: account.accessToken)
let notification: Pachyderm.Notification
do {
notification = try await client.run(Client.getNotification(id: push.notificationID)).0
} catch {
logger.error("Error fetching notification: \(String(describing: error))")
return
}
let kindStr: String?
switch notification.kind {
case .reblog:
kindStr = "🔁 Reblogged"
case .favourite:
kindStr = "⭐️ Favorited"
case .follow:
kindStr = "👤 Followed by @\(notification.account.acct)"
case .followRequest:
kindStr = "👤 Asked to follow by @\(notification.account.acct)"
case .poll:
kindStr = "📊 Poll finished"
case .update:
kindStr = "✏️ Edited"
case .emojiReaction:
if let emoji = notification.emoji {
kindStr = "\(emoji) Reacted"
} else {
kindStr = nil
}
default:
kindStr = nil
}
let notificationContent: String?
if let status = notification.status {
notificationContent = NotificationService.textConverter.convert(html: status.content)
} else if notification.kind == .follow || notification.kind == .followRequest {
notificationContent = nil
} else {
notificationContent = push.body
}
content.body = [kindStr, notificationContent].compactMap { $0 }.joined(separator: "\n")
let attachmentDataTask: Task<URL?, Never>?
// We deliberately don't include attachments for other types of notifications that have statuses (favs, etc.)
// because we risk just fetching the same thing a bunch of times for many senders.
if notification.kind == .mention || notification.kind == .status || notification.kind == .update,
let attachment = notification.status?.attachments.first {
let url = attachment.previewURL ?? attachment.url
attachmentDataTask = Task {
do {
let data = try await URLSession.shared.data(from: url).0
let localAttachmentURL = FileManager.default.temporaryDirectory.appendingPathComponent("attachment_\(attachment.id)").appendingPathExtension(url.pathExtension)
try data.write(to: localAttachmentURL)
return localAttachmentURL
} catch {
logger.error("Error setting notification attachments: \(String(describing: error))")
return nil
}
}
} else {
attachmentDataTask = nil
}
let conversationIdentifier: String?
if let status = notification.status {
if let context = status.pleromaExtras?.context {
conversationIdentifier = "context:\(context)"
} else if [Notification.Kind.reblog, .favourite, .poll, .update].contains(notification.kind) {
conversationIdentifier = "status:\(status.id)"
} else {
conversationIdentifier = nil
}
} else {
conversationIdentifier = nil
}
let account: Account?
switch notification.kind {
case .mention, .status:
account = notification.status?.account
default:
account = notification.account
}
let sender: INPerson?
if let account {
let handle = INPersonHandle(value: "@\(account.acct)", type: .unknown)
let image: INImage?
if let avatar = account.avatar,
let (data, resp) = try? await URLSession.shared.data(from: avatar),
let code = (resp as? HTTPURLResponse)?.statusCode,
(200...299).contains(code) {
image = INImage(imageData: data)
} else {
image = nil
}
sender = INPerson(
personHandle: handle,
nameComponents: nil,
displayName: account.displayName,
image: image,
contactIdentifier: nil,
customIdentifier: account.id
)
} else {
sender = nil
}
let intent = INSendMessageIntent(
recipients: nil,
outgoingMessageType: .outgoingMessageText,
content: notificationContent,
speakableGroupName: nil,
conversationIdentifier: conversationIdentifier,
serviceName: nil,
sender: sender,
attachments: nil
)
let interaction = INInteraction(intent: intent, response: nil)
interaction.direction = .incoming
do {
try await interaction.donate()
} catch {
logger.error("Error donating interaction: \(String(describing: error))")
return
}
let updatedContent: UNMutableNotificationContent
do {
let newContent = try content.updating(from: intent)
if let newMutableContent = newContent.mutableCopy() as? UNMutableNotificationContent {
pendingRequest?.0 = newMutableContent
updatedContent = newMutableContent
} else {
updatedContent = content
}
} catch {
logger.error("Error updating notification from intent: \(String(describing: error))")
updatedContent = content
}
if let localAttachmentURL = await attachmentDataTask?.value,
let attachment = try? UNNotificationAttachment(identifier: localAttachmentURL.lastPathComponent, url: localAttachmentURL) {
updatedContent.attachments = [
attachment
]
}
}
private func getSubscription(account: UserAccountInfo) -> PushNotifications.PushSubscription? {
DispatchQueue.main.sync {
// this is necessary because of a swift bug: https://github.com/apple/swift/pull/72507
MainActor.runUnsafely {
PushManager.shared.pushSubscription(account: account)
}
}
}
private func decryptNotification(subscription: PushNotifications.PushSubscription, serverPublicKeyData: Data, salt: Data, encryptedBody: Data) -> Data? {
// See https://github.com/ClearlyClaire/webpush/blob/f14a4d52e201128b1b00245d11b6de80d6cfdcd9/lib/webpush/encryption.rb
var context = Data()
context.append(0)
let clientPublicKey = subscription.secretKey.publicKey.x963Representation
let clientPublicKeyLength = UInt16(clientPublicKey.count)
context.append(UInt8((clientPublicKeyLength >> 8) & 0xFF))
context.append(UInt8(clientPublicKeyLength & 0xFF))
context.append(clientPublicKey)
let serverPublicKeyLength = UInt16(serverPublicKeyData.count)
context.append(UInt8((serverPublicKeyLength >> 8) & 0xFF))
context.append(UInt8(serverPublicKeyLength & 0xFF))
context.append(serverPublicKeyData)
func info(encoding: String) -> Data {
var info = Data("Content-Encoding: \(encoding)\0P-256".utf8)
info.append(context)
return info
}
let sharedSecret: SharedSecret
do {
let serverPublicKey = try P256.KeyAgreement.PublicKey(x963Representation: serverPublicKeyData)
sharedSecret = try subscription.secretKey.sharedSecretFromKeyAgreement(with: serverPublicKey)
} catch {
logger.error("Error getting shared secret: \(String(describing: error))")
return nil
}
let sharedInfo = Data("Content-Encoding: auth\0".utf8)
let pseudoRandomKey = sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: subscription.authSecret, sharedInfo: sharedInfo, outputByteCount: 32)
let contentEncryptionKeyInfo = info(encoding: "aesgcm")
let contentEncryptionKey = HKDF<SHA256>.deriveKey(inputKeyMaterial: pseudoRandomKey, salt: salt, info: contentEncryptionKeyInfo, outputByteCount: 16)
let nonceInfo = info(encoding: "nonce")
let nonce = HKDF<SHA256>.deriveKey(inputKeyMaterial: pseudoRandomKey, salt: salt, info: nonceInfo, outputByteCount: 12)
let nonceAndEncryptedBody = nonce.withUnsafeBytes { noncePtr in
var data = Data(buffer: noncePtr.bindMemory(to: UInt8.self))
data.append(encryptedBody)
return data
}
do {
let sealedBox = try AES.GCM.SealedBox(combined: nonceAndEncryptedBody)
let decrypted = try AES.GCM.open(sealedBox, using: contentEncryptionKey)
return decrypted
} catch {
logger.error("Error decrypting push: \(String(describing: error))")
return nil
}
}
}
extension MainActor {
@_unavailableFromAsync
@available(macOS, obsoleted: 14.0)
@available(iOS, obsoleted: 17.0)
@available(watchOS, obsoleted: 10.0)
@available(tvOS, obsoleted: 17.0)
@available(visionOS 1.0, *)
static func runUnsafely<T>(_ body: @MainActor () throws -> T) rethrows -> T {
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
return try MainActor.assumeIsolated(body)
}
dispatchPrecondition(condition: .onQueue(.main))
return try withoutActuallyEscaping(body) { fn in
try unsafeBitCast(fn, to: (() throws -> T).self)()
}
}
}
private func decodeBase64URL(_ s: String) -> Data? {
var str = s.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
if str.count % 4 != 0 {
str.append(String(repeating: "=", count: 4 - str.count % 4))
}
return Data(base64Encoded: str)
}
// copied from HTMLConverter.Callbacks, blergh
private struct HTMLCallbacks: HTMLConversionCallbacks {
static func makeURL(string: String) -> URL? {
// Converting WebURL to URL is a small but non-trivial expense (since it works by
// serializing the WebURL as a string and then having Foundation parse it again),
// so, if available, use the system parser which doesn't require another round trip.
if #available(iOS 16.0, macOS 13.0, *),
let url = try? URL.ParseStrategy().parse(string) {
url
} else if let web = WebURL(string),
let url = URL(web) {
url
} else {
URL(string: string)
}
}
static func elementAction(name: String, attributes: [Attribute]) -> ElementAction {
guard name == "span" else {
return .default
}
let clazz = attributes.attributeValue(for: "class")
if clazz == "invisible" {
return .skip
} else if clazz == "ellipsis" {
return .append("")
} else {
return .default
}
}
}

View File

@ -0,0 +1,17 @@
<?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>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>1C8F.1</string>
</array>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
</dict>
</array>
</dict>
</plist>

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,105 @@
//
// ActionViewController.swift
// OpenInTusker
//
// Created by Shadowfacts on 5/23/21.
// Copyright © 2021 Shadowfacts. All rights reserved.
//
import UIKit
import MobileCoreServices
import UniformTypeIdentifiers
class ActionViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
findURLFromWebPage { (components) in
DispatchQueue.main.async {
if let components {
self.searchForURLInApp(components)
} else {
self.findURLItem { (components) in
if let components {
DispatchQueue.main.async {
self.searchForURLInApp(components)
}
}
}
}
}
}
}
private func findURLFromWebPage(completion: @escaping @Sendable (URLComponents?) -> Void) {
for item in extensionContext!.inputItems as! [NSExtensionItem] {
for provider in item.attachments! {
guard provider.hasItemConformingToTypeIdentifier(UTType.propertyList.identifier) else {
continue
}
provider.loadItem(forTypeIdentifier: UTType.propertyList.identifier, 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 @Sendable (URLComponents?) -> Void) {
for item in extensionContext!.inputItems as! [NSExtensionItem] {
for provider in item.attachments! {
guard provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) else {
continue
}
provider.loadItem(forTypeIdentifier: UTType.url.identifier, 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,330 +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()
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")
decoder.dateDecodingStrategy = .formatted(formatter)
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 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: .parameters([
"domain" => domain
]))
}
public static func unblock(domain: String) -> Request<Empty> {
return Request<Empty>(method: .delete, path: "/api/v1/domain_blocks", body: .parameters([
"domain" => domain
]))
}
// MARK: - Filters
public static func getFilters() -> Request<[Filter]> {
return Request<[Filter]>(method: .get, path: "/api/v1/filters")
}
public static 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 static func getFilter(id: String) -> Request<Filter> {
return Request<Filter>(method: .get, path: "/api/v1/filters/\(id)")
}
// 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: .parameters(["uri" => acct]))
}
// 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: .parameters(["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: .formData([
"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(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 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: 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 static 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 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) -> 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 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
}
}
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,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,84 +0,0 @@
//
// Hashtag.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Hashtag: Codable {
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: 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
}
}
}
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

@ -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,49 +0,0 @@
//
// List.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class List: Decodable {
public let id: String
public let title: String
public var timeline: Timeline {
return .list(id: id)
}
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
return request
}
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]))
}
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 accountIDs: [String]) -> Request<Empty> {
return Request<Empty>(method: .post, path: "/api/v1/lists/\(list.id)/accounts", body: .parameters(
"account_ids" => accountIDs
))
}
public static func remove(_ list: List, accounts accountIDs: [String]) -> Request<Empty> {
return Request<Empty>(method: .delete, path: "/api/v1/lists/\(list.id)/accounts", body: .parameters(
"account_ids" => accountIDs
))
}
private enum CodingKeys: String, CodingKey {
case id
case title
}
}

View File

@ -1,17 +0,0 @@
//
// MastodonError.swift
// Pachyderm
//
// Created by Shadowfacts on 9/8/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
struct MastodonError: Decodable, CustomStringConvertible {
var description: String
private enum CodingKeys: String, CodingKey {
case description = "error"
}
}

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,43 +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
case followRequest = "follow_request"
}
}
extension Notification: Identifiable {}

View File

@ -1,24 +0,0 @@
//
// PushSubscription.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class PushSubscription: Decodable {
public let id: String
public let endpoint: URL
public let serverKey: String
// TODO: WTF is this?
// public let alerts
private enum CodingKeys: String, CodingKey {
case id
case endpoint
case serverKey = "server_key"
// case alerts
}
}

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,33 +0,0 @@
//
// Relationship.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Relationship: Decodable {
public let id: String
public let following: Bool
public let followedBy: Bool
public let blocking: Bool
public let muting: Bool
public let mutingNotifications: Bool
public let followRequested: Bool
public let domainBlocking: Bool
public let showingReblogs: Bool
private enum CodingKeys: String, CodingKey {
case id
case following
case followedBy = "followed_by"
case blocking
case muting
case mutingNotifications = "muting_notifications"
case followRequested = "requested"
case domainBlocking = "domain_blocking"
case showingReblogs = "showing_reblogs"
}
}

View File

@ -1,143 +0,0 @@
//
// Status.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Status: Decodable {
public let id: String
public let uri: String
public let url: URL?
public let account: Account
public let inReplyToID: String?
public let inReplyToAccountID: String?
public let reblog: Status?
public let content: String
public let createdAt: Date
public let emojis: [Emoji]
// TODO: missing from pleroma
// public let repliesCount: Int
public let reblogsCount: Int
public let favouritesCount: Int
public let reblogged: Bool?
public let favourited: Bool?
public let muted: Bool?
public let sensitive: Bool
public let spoilerText: String
public let visibility: Visibility
public let attachments: [Attachment]
public let mentions: [Mention]
public let hashtags: [Hashtag]
public let application: Application?
public let language: String?
public let pinned: Bool?
public let bookmarked: Bool?
public static func getContext(_ status: Status) -> Request<ConversationContext> {
return Request<ConversationContext>(method: .get, path: "/api/v1/statuses/\(status.id)/context")
}
public static func getCard(_ status: Status) -> Request<Card> {
return Request<Card>(method: .get, path: "/api/v1/statuses/\(status.id)/card")
}
public static func getFavourites(_ status: Status, range: RequestRange = .default) -> Request<[Account]> {
var request = Request<[Account]>(method: .get, path: "/api/v1/statuses/\(status.id)/favourited_by")
request.range = range
return request
}
public static func getReblogs(_ status: Status, range: RequestRange = .default) -> Request<[Account]> {
var request = Request<[Account]>(method: .get, path: "/api/v1/statuses/\(status.id)/reblogged_by")
request.range = range
return request
}
public static func delete(_ status: Status) -> Request<Empty> {
return Request<Empty>(method: .delete, path: "/api/v1/statuses/\(status.id)")
}
public static func reblog(_ status: Status) -> Request<Status> {
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/reblog")
}
public static func unreblog(_ status: Status) -> Request<Status> {
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/unreblog")
}
public static func favourite(_ status: Status) -> Request<Status> {
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/favourite")
}
public static func unfavourite(_ status: Status) -> Request<Status> {
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/unfavourite")
}
public static func pin(_ status: Status) -> Request<Status> {
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/pin")
}
public static func unpin(_ status: Status) -> Request<Status> {
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/unpin")
}
public static func bookmark(_ status: Status) -> Request<Status> {
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/bookmark")
}
public static func unbookmark(_ status: Status) -> Request<Status> {
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/unbookmark")
}
public static func muteConversation(_ status: Status) -> Request<Status> {
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/mute")
}
public static func unmuteConversation(_ status: Status) -> Request<Status> {
return Request<Status>(method: .post, path: "/api/v1/statuses/\(status.id)/unmute")
}
private enum CodingKeys: String, CodingKey {
case id
case uri
case url
case account
case inReplyToID = "in_reply_to_id"
case inReplyToAccountID = "in_reply_to_account_id"
case reblog
case content
case createdAt = "created_at"
case emojis
// case repliesCount = "replies_count"
case reblogsCount = "reblogs_count"
case favouritesCount = "favourites_count"
case reblogged
case favourited
case muted
case sensitive
case spoilerText = "spoiler_text"
case visibility
case attachments = "media_attachments"
case mentions
case hashtags = "tags"
case application
case language
case pinned
case bookmarked
}
}
extension Status {
public enum Visibility: String, Codable, CaseIterable {
case `public`
case unlisted
case `private`
case direct
}
}
extension Status: Identifiable {}

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,48 +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
public let statusState: StatusState?
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
if kind == .mention {
self.statusState = .unknown
} else {
self.statusState = nil
}
}
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/ComposeUI/.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,23 @@
{
"pins" : [
{
"identity" : "swift-system",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-system.git",
"state" : {
"revision" : "025bcb1165deab2e20d4eaba79967ce73013f496",
"version" : "1.2.1"
}
},
{
"identity" : "swift-url",
"kind" : "remoteSourceControl",
"location" : "https://github.com/karwa/swift-url.git",
"state" : {
"branch" : "main",
"revision" : "6f45f3cd6606f39c3753b302fe30aea980067b30"
}
}
],
"version" : 2
}

View File

@ -0,0 +1,34 @@
// 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: "ComposeUI",
platforms: [
.iOS(.v15),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "ComposeUI",
targets: ["ComposeUI"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(path: "../Pachyderm"),
.package(path: "../InstanceFeatures"),
.package(path: "../TuskerComponents"),
.package(path: "../MatchedGeometryPresentation"),
],
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: "ComposeUI",
dependencies: ["Pachyderm", "InstanceFeatures", "TuskerComponents", "MatchedGeometryPresentation"]),
.testTarget(
name: "ComposeUITests",
dependencies: ["ComposeUI"]),
]
)

View File

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

View File

@ -0,0 +1,197 @@
//
// PostService.swift
// ComposeUI
//
// Created by Shadowfacts on 4/27/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import Foundation
import Pachyderm
import UniformTypeIdentifiers
@MainActor
class PostService: ObservableObject {
private let mastodonController: ComposeMastodonContext
private let config: ComposeUIConfig
private let draft: Draft
@Published var currentStep = 1
@Published private(set) var totalSteps = 2
init(mastodonController: ComposeMastodonContext, config: ComposeUIConfig, draft: Draft) {
self.mastodonController = mastodonController
self.config = config
self.draft = draft
}
func post() async throws {
guard draft.hasContent || draft.editedStatusID != nil else {
return
}
// save before posting, so if a crash occurs during network request, the status won't be lost
DraftsPersistentContainer.shared.save()
let uploadedAttachments = try await uploadAttachments()
let contentWarning = draft.contentWarningEnabled ? draft.contentWarning : ""
let sensitive = !contentWarning.isEmpty
let request: Request<Status>
if let editedStatusID = draft.editedStatusID {
if mastodonController.instanceFeatures.needsEditAttachmentsInSeparateRequest {
await updateEditedAttachments()
}
request = Client.editStatus(
id: editedStatusID,
text: textForPosting(),
contentType: config.contentType,
spoilerText: contentWarning,
sensitive: sensitive,
language: mastodonController.instanceFeatures.createStatusWithLanguage ? draft.language : nil,
mediaIDs: uploadedAttachments,
mediaAttributes: draft.draftAttachments.compactMap {
if let id = $0.editedAttachmentID {
return EditStatusMediaAttributes(id: id, description: $0.attachmentDescription, focus: nil)
} else {
return nil
}
},
poll: draft.poll.map {
EditPollParameters(options: $0.pollOptions.map(\.text), expiresIn: Int($0.duration), multiple: $0.multiple)
}
)
} else {
request = Client.createStatus(
text: textForPosting(),
contentType: config.contentType,
inReplyTo: draft.inReplyToID,
mediaIDs: uploadedAttachments,
sensitive: sensitive,
spoilerText: contentWarning,
visibility: draft.localOnly && mastodonController.instanceFeatures.localOnlyPostsVisibility ? Status.localPostVisibility : draft.visibility.rawValue,
language: mastodonController.instanceFeatures.createStatusWithLanguage ? draft.language : nil,
pollOptions: draft.poll?.pollOptions.map(\.text),
pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration),
pollMultiple: draft.poll?.multiple,
localOnly: mastodonController.instanceFeatures.localOnlyPosts && !mastodonController.instanceFeatures.localOnlyPostsVisibility ? draft.localOnly : nil,
idempotencyKey: draft.id.uuidString
)
}
do {
let (status, _) = try await mastodonController.run(request)
currentStep += 1
mastodonController.storeCreatedStatus(status)
} catch let error as Client.Error {
throw Error.posting(error)
}
}
private func uploadAttachments() async throws -> [String] {
// 2 steps (request data, then upload) for each attachment
self.totalSteps += 2 * draft.attachments.count
var attachments: [String] = []
attachments.reserveCapacity(draft.attachments.count)
for (index, attachment) in draft.draftAttachments.enumerated() {
// if this attachment already exists and is being edited, we don't do anything
// edits to the description are handled as part of the edit status request
if let editedAttachmentID = attachment.editedAttachmentID {
attachments.append(editedAttachmentID)
currentStep += 2
continue
}
let data: Data
let utType: UTType
do {
(data, utType) = try await getData(for: attachment)
currentStep += 1
} catch let error as DraftAttachment.ExportError {
throw Error.attachmentData(index: index, cause: error)
}
let uploaded = try await uploadAttachment(index: index, data: data, utType: utType, description: attachment.attachmentDescription)
attachments.append(uploaded.id)
currentStep += 1
}
return attachments
}
private func getData(for attachment: DraftAttachment) async throws -> (Data, UTType) {
return try await withCheckedThrowingContinuation { continuation in
attachment.getData(features: mastodonController.instanceFeatures) { result in
switch result {
case let .success(res):
continuation.resume(returning: res)
case let .failure(error):
continuation.resume(throwing: error)
}
}
}
}
private func uploadAttachment(index: Int, data: Data, utType: UTType, description: String?) async throws -> Attachment {
guard let mimeType = utType.preferredMIMEType else {
throw Error.attachmentMissingMimeType(index: index, type: utType)
}
var filename = "file"
if let ext = utType.preferredFilenameExtension {
filename.append(".\(ext)")
}
let formAttachment = FormAttachment(mimeType: mimeType, data: data, fileName: filename)
let req = Client.upload(attachment: formAttachment, description: description)
do {
return try await mastodonController.run(req).0
} catch let error as Client.Error {
throw Error.attachmentUpload(index: index, cause: error)
}
}
private func textForPosting() -> String {
var text = draft.text
// when using dictation, iOS sometimes leaves a U+FFFC OBJECT REPLACEMENT CHARACTER behind in the text,
// which we want to strip out before actually posting the status
text = text.replacingOccurrences(of: "\u{fffc}", with: "")
if draft.localOnly && mastodonController.instanceFeatures.needsLocalOnlyEmojiHack {
text += " 👁"
}
return text
}
// only needed for akkoma, not used on regular mastodon
private func updateEditedAttachments() async {
for attachment in draft.draftAttachments {
guard let id = attachment.editedAttachmentID else {
continue
}
let req = Client.updateAttachment(id: id, description: attachment.attachmentDescription, focus: nil)
_ = try? await mastodonController.run(req)
}
}
enum Error: Swift.Error, LocalizedError {
case attachmentData(index: Int, cause: DraftAttachment.ExportError)
case attachmentMissingMimeType(index: Int, type: UTType)
case attachmentUpload(index: Int, cause: Client.Error)
case posting(Client.Error)
var localizedDescription: String {
switch self {
case let .attachmentData(index: index, cause: cause):
return "Attachment \(index + 1): \(cause.localizedDescription)"
case let .attachmentMissingMimeType(index: index, type: type):
return "Attachment \(index + 1): unknown MIME type for \(type.identifier)"
case let .attachmentUpload(index: index, cause: cause):
return "Attachment \(index + 1): \(cause.localizedDescription)"
case let .posting(error):
return error.localizedDescription
}
}
}
}

View File

@ -1,24 +1,25 @@
//
// CharacterCounter.swift
// Pachyderm
// ComposeUI
//
// Created by Shadowfacts on 9/29/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
import InstanceFeatures
public struct CharacterCounter {
static let linkDetector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
static let mention = try! NSRegularExpression(pattern: "(@[a-z0-9_]+)(?:@[a-z0-9\\-\\.]+[a-z0-9]+)?", options: .caseInsensitive)
private static let linkDetector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
private static let mention = try! NSRegularExpression(pattern: "(@[a-z0-9_]+)(?:@[a-z0-9\\-\\.]+[a-z0-9]+)?", options: .caseInsensitive)
public static func count(text: String) -> Int {
public static func count(text: String, for instanceFeatures: InstanceFeatures) -> Int {
let mentionsRemoved = removeMentions(in: text)
var count = mentionsRemoved.count
for match in linkDetector.matches(in: mentionsRemoved, options: [], range: NSRange(location: 0, length: mentionsRemoved.utf16.count)) {
count -= match.range.length
count += 23 // Mastodon link length
count += instanceFeatures.charsReservedPerURL
}
return count
}

View File

@ -0,0 +1,29 @@
//
// ComposeInput.swift
// ComposeUI
//
// Created by Shadowfacts on 3/5/23.
//
import Foundation
import Combine
import UIKit
protocol ComposeInput: AnyObject, ObservableObject {
var toolbarElements: [ToolbarElement] { get }
var textInputMode: UITextInputMode? { get }
var autocompleteState: AutocompleteState? { get }
var autocompleteStatePublisher: Published<AutocompleteState?>.Publisher { get }
func autocomplete(with string: String)
func applyFormat(_ format: StatusFormat)
func beginAutocompletingEmoji()
}
enum ToolbarElement {
case emojiPicker
case formattingButtons
}

View File

@ -0,0 +1,29 @@
//
// ComposeMastodonContext.swift
// ComposeUI
//
// Created by Shadowfacts on 3/5/23.
//
import Foundation
import Pachyderm
import InstanceFeatures
import UserAccounts
public protocol ComposeMastodonContext {
var accountInfo: UserAccountInfo? { get }
var instanceFeatures: InstanceFeatures { get }
func run<Result: Sendable>(_ request: Request<Result>) async throws -> (Result, Pagination?)
func getCustomEmojis() async -> [Emoji]
@MainActor
func searchCachedAccounts(query: String) -> [AccountProtocol]
@MainActor
func cachedRelationship(for accountID: String) -> RelationshipProtocol?
@MainActor
func searchCachedHashtags(query: String) -> [Hashtag]
func storeCreatedStatus(_ status: Status)
}

View File

@ -0,0 +1,42 @@
//
// ComposeUIConfig.swift
// ComposeUI
//
// Created by Shadowfacts on 3/4/23.
//
import SwiftUI
import Pachyderm
import PhotosUI
import PencilKit
import TuskerComponents
public struct ComposeUIConfig {
// Config
public var allowSwitchingDrafts = true
public var textSelectionStartsAtBeginning = false
// Style
public var backgroundColor = Color(uiColor: .systemBackground)
public var groupedBackgroundColor = Color(uiColor: .systemGroupedBackground)
public var groupedCellBackgroundColor = Color(uiColor: .systemBackground)
public var fillColor = Color(uiColor: .systemFill)
public var avatarStyle = AvatarImageView.Style.roundRect
// Preferences
public var useTwitterKeyboard = false
public var contentType = StatusContentType.plain
public var requireAttachmentDescriptions = false
// Host callbacks
public var dismiss: @MainActor (DismissMode) -> Void = { _ in }
public var presentAssetPicker: ((@MainActor @escaping ([PHPickerResult]) -> Void) -> Void)?
public var presentDrawing: ((PKDrawing, @escaping (PKDrawing) -> Void) -> Void)?
public var userActivityForDraft: ((Draft) -> NSItemProvider?) = { _ in nil }
public init() {
}
}
extension ComposeUIConfig {
}

View File

@ -0,0 +1,236 @@
//
// AttachmentRowController.swift
// ComposeUI
//
// Created by Shadowfacts on 3/12/23.
//
import SwiftUI
import TuskerComponents
import Vision
import MatchedGeometryPresentation
class AttachmentRowController: ViewController {
let parent: ComposeController
let attachment: DraftAttachment
@Published var descriptionMode: DescriptionMode = .allowEntry
@Published var textRecognitionError: Error?
@Published var focusAttachmentOnTextEditorUnfocus = false
let thumbnailController: AttachmentThumbnailController
private var descriptionObservation: NSKeyValueObservation?
init(parent: ComposeController, attachment: DraftAttachment) {
self.parent = parent
self.attachment = attachment
self.thumbnailController = AttachmentThumbnailController(attachment: attachment, parent: parent)
descriptionObservation = attachment.observe(\.attachmentDescription, changeHandler: { [unowned self] _, _ in
// the faultingState is non-zero for objects that are being cascade deleted when the draft is deleted
if attachment.faultingState == 0 {
self.updateAttachmentDescriptionState()
}
})
}
private func updateAttachmentDescriptionState() {
if attachment.attachmentDescription.isEmpty {
parent.attachmentsMissingDescriptions.insert(attachment.id)
} else {
parent.attachmentsMissingDescriptions.remove(attachment.id)
}
}
var view: some View {
AttachmentView(attachment: attachment)
}
private func removeAttachment() {
withAnimation {
var newAttachments = parent.draft.draftAttachments
newAttachments.removeAll(where: { $0.id == attachment.id })
parent.draft.attachments = NSMutableOrderedSet(array: newAttachments)
}
}
private func editDrawing() {
guard case .drawing(let drawing) = attachment.data else {
return
}
parent.config.presentDrawing?(drawing) { newDrawing in
self.attachment.drawing = newDrawing
}
}
private func focusAttachment() {
focusAttachmentOnTextEditorUnfocus = false
parent.focusedAttachment = (attachment, thumbnailController)
}
private func recognizeText() {
descriptionMode = .recognizingText
self.attachment.getData(features: self.parent.mastodonController.instanceFeatures, skipAllConversion: true) { result in
DispatchQueue.main.async {
let data: Data
switch result {
case .success((let d, _)):
data = d
case .failure(let error):
self.descriptionMode = .allowEntry
self.textRecognitionError = error
return
}
let handler = VNImageRequestHandler(data: data)
let request = VNRecognizeTextRequest { request, error in
DispatchQueue.main.async {
if let results = request.results as? [VNRecognizedTextObservation] {
var text = ""
for observation in results {
let result = observation.topCandidates(1).first!
text.append(result.string)
text.append("\n")
}
self.attachment.attachmentDescription = text
}
self.descriptionMode = .allowEntry
}
}
request.recognitionLevel = .accurate
request.usesLanguageCorrection = true
DispatchQueue.global(qos: .userInitiated).async {
do {
try handler.perform([request])
} catch let error as NSError where error.code == 1 {
// The perform call throws an error with code 1 if the request is cancelled, which we don't want to show an alert for.
return
} catch {
DispatchQueue.main.async {
self.descriptionMode = .allowEntry
self.textRecognitionError = error
}
}
}
}
}
}
struct AttachmentView: View {
@ObservedObject private var attachment: DraftAttachment
@EnvironmentObject private var controller: AttachmentRowController
@FocusState private var textEditorFocused: Bool
init(attachment: DraftAttachment) {
self.attachment = attachment
}
var body: some View {
HStack(alignment: .center, spacing: 4) {
ControllerView(controller: { controller.thumbnailController })
.clipShape(RoundedRectangle(cornerRadius: 8))
.environment(\.attachmentThumbnailConfiguration, .init(contentMode: .fit, fullSize: false))
.matchedGeometrySource(id: attachment.id, presentationID: attachment.id)
.overlay {
thumbnailFocusedOverlay
}
.frame(width: thumbnailSize, height: thumbnailSize)
.onTapGesture {
textEditorFocused = false
// if we just focus the attachment immediately, the text editor doesn't actually unfocus
controller.focusAttachmentOnTextEditorUnfocus = true
}
.contextMenu {
if attachment.drawingData != nil {
Button(action: controller.editDrawing) {
Label("Edit Drawing", systemImage: "hand.draw")
}
} else if attachment.type == .image {
Button(action: controller.recognizeText) {
Label("Recognize Text", systemImage: "doc.text.viewfinder")
}
}
Button(role: .destructive, action: controller.removeAttachment) {
Label("Delete", systemImage: "trash")
}
} previewIfAvailable: {
ControllerView(controller: { controller.thumbnailController })
}
switch controller.descriptionMode {
case .allowEntry:
InlineAttachmentDescriptionView(attachment: attachment, minHeight: thumbnailSize)
.matchedGeometrySource(id: AttachmentDescriptionTextViewID(attachment), presentationID: attachment.id)
.focused($textEditorFocused)
case .recognizingText:
ProgressView()
.progressViewStyle(.circular)
}
}
.alertWithData("Text Recognition Failed", data: $controller.textRecognitionError) { _ in
Button("OK") {}
} message: { error in
Text(error.localizedDescription)
}
.onAppear(perform: controller.updateAttachmentDescriptionState)
#if os(visionOS)
.onChange(of: textEditorFocused) {
if !textEditorFocused && controller.focusAttachmentOnTextEditorUnfocus {
controller.focusAttachment()
}
}
#else
.onChange(of: textEditorFocused) { newValue in
if !newValue && controller.focusAttachmentOnTextEditorUnfocus {
controller.focusAttachment()
}
}
#endif
}
private var thumbnailSize: CGFloat {
#if os(visionOS)
120
#else
80
#endif
}
@ViewBuilder
private var thumbnailFocusedOverlay: some View {
Image(systemName: "arrow.up.backward.and.arrow.down.forward")
.foregroundColor(.white)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black.opacity(0.35))
.clipShape(RoundedRectangle(cornerRadius: 8))
// use .opacity and an animation, because .transition doesn't seem to play nice with @FocusState
.opacity(textEditorFocused ? 1 : 0)
.animation(.linear(duration: 0.1), value: textEditorFocused)
}
}
}
extension AttachmentRowController {
enum DescriptionMode {
case allowEntry, recognizingText
}
}
private extension View {
@available(iOS, obsoleted: 16.0)
@available(visionOS 1.0, *)
@ViewBuilder
func contextMenu<M: View, P: View>(@ViewBuilder menuItems: () -> M, @ViewBuilder previewIfAvailable preview: () -> P) -> some View {
if #available(iOS 16.0, *) {
self.contextMenu(menuItems: menuItems, preview: preview)
} else {
self.contextMenu(menuItems: menuItems)
}
}
}

View File

@ -0,0 +1,199 @@
//
// AttachmentThumbnailController.swift
// ComposeUI
//
// Created by Shadowfacts on 11/10/21.
// Copyright © 2021 Shadowfacts. All rights reserved.
//
import SwiftUI
import Photos
import TuskerComponents
class AttachmentThumbnailController: ViewController {
unowned let parent: ComposeController
let attachment: DraftAttachment
@Published private var image: UIImage?
@Published private var gifController: GIFController?
@Published private var fullSize: Bool = false
init(attachment: DraftAttachment, parent: ComposeController) {
self.attachment = attachment
self.parent = parent
}
func loadImageIfNecessary(fullSize: Bool) {
if (gifController != nil) || (image != nil && self.fullSize) {
return
}
self.fullSize = fullSize
switch attachment.data {
case .editing(_, let kind, let url):
switch kind {
case .image:
Task { @MainActor in
self.image = await parent.fetchAttachment(url)
}
case .video, .gifv:
let asset = AVURLAsset(url: url)
let imageGenerator = AVAssetImageGenerator(asset: asset)
#if os(visionOS)
#warning("Use async AVAssetImageGenerator.image(at:)")
#else
if let cgImage = try? imageGenerator.copyCGImage(at: .zero, actualTime: nil) {
self.image = UIImage(cgImage: cgImage)
}
#endif
case .audio, .unknown:
break
}
case .asset(let id):
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [id], options: nil).firstObject else {
return
}
let isGIF = PHAssetResource.assetResources(for: asset).contains(where: { $0.uniformTypeIdentifier == UTType.gif.identifier })
if isGIF {
PHImageManager.default().requestImageDataAndOrientation(for: asset, options: nil) { data, typeIdentifier, orientation, info in
guard let data else { return }
if typeIdentifier == UTType.gif.identifier {
self.gifController = GIFController(gifData: data)
} else {
let image = UIImage(data: data)
DispatchQueue.main.async {
self.image = image
}
}
}
} else {
let size: CGSize
if fullSize {
size = PHImageManagerMaximumSize
} else {
// currently only used as thumbnail in ComposeAttachmentRow
size = CGSize(width: 80, height: 80)
}
PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { (image, _) in
DispatchQueue.main.async {
self.image = image
}
}
}
case .drawing(let drawing):
image = drawing.imageInLightMode(from: drawing.bounds)
case .file(let url, let type):
if type.conforms(to: .movie) {
let asset = AVURLAsset(url: url)
let imageGenerator = AVAssetImageGenerator(asset: asset)
#if os(visionOS)
#warning("Use async AVAssetImageGenerator.image(at:)")
#else
if let cgImage = try? imageGenerator.copyCGImage(at: .zero, actualTime: nil) {
self.image = UIImage(cgImage: cgImage)
}
#endif
} else if let data = try? Data(contentsOf: url) {
if type == .gif {
self.gifController = GIFController(gifData: data)
} else if type.conforms(to: .image),
let image = UIImage(data: data) {
// using prepareThumbnail on images from PHPicker results in extremely high memory usage,
// crashing share extension. see FB12186346
// if fullSize {
image.prepareForDisplay { prepared in
DispatchQueue.main.async {
self.image = image
}
}
// } else {
// image.prepareThumbnail(of: CGSize(width: 80, height: 80)) { prepared in
// DispatchQueue.main.async {
// self.image = prepared
// }
// }
// }
}
}
case .none:
break
}
}
var view: some SwiftUI.View {
View()
}
struct View: SwiftUI.View {
@EnvironmentObject private var controller: AttachmentThumbnailController
@Environment(\.attachmentThumbnailConfiguration) private var config
var body: some SwiftUI.View {
content
.onAppear {
controller.loadImageIfNecessary(fullSize: config.fullSize)
}
}
@ViewBuilder
private var content: some SwiftUI.View {
if let gifController = controller.gifController {
GIFViewWrapper(controller: gifController)
} else if let image = controller.image {
Image(uiImage: image)
.resizable()
.aspectRatio(config.aspectRatio, contentMode: config.contentMode)
} else {
Image(systemName: "photo")
}
}
}
}
struct AttachmentThumbnailConfiguration {
let aspectRatio: CGFloat?
let contentMode: ContentMode
let fullSize: Bool
init(aspectRatio: CGFloat? = nil, contentMode: ContentMode = .fit, fullSize: Bool = false) {
self.aspectRatio = aspectRatio
self.contentMode = contentMode
self.fullSize = fullSize
}
}
private struct AttachmentThumbnailConfigurationEnvironmentKey: EnvironmentKey {
static let defaultValue = AttachmentThumbnailConfiguration()
}
extension EnvironmentValues {
var attachmentThumbnailConfiguration: AttachmentThumbnailConfiguration {
get { self[AttachmentThumbnailConfigurationEnvironmentKey.self] }
set { self[AttachmentThumbnailConfigurationEnvironmentKey.self] = newValue }
}
}
private struct GIFViewWrapper: UIViewRepresentable {
typealias UIViewType = GIFImageView
@State var controller: GIFController
func makeUIView(context: Context) -> GIFImageView {
let view = GIFImageView()
controller.attach(to: view)
controller.startAnimating()
view.contentMode = .scaleAspectFit
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
view.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
return view
}
func updateUIView(_ uiView: GIFImageView, context: Context) {
}
}

View File

@ -0,0 +1,263 @@
//
// AttachmentsListController.swift
// ComposeUI
//
// Created by Shadowfacts on 3/8/23.
//
import SwiftUI
import PhotosUI
import PencilKit
class AttachmentsListController: ViewController {
unowned let parent: ComposeController
var draft: Draft { parent.draft }
var isValid: Bool {
!requiresAttachmentDescriptions && validAttachmentCombination
}
private var requiresAttachmentDescriptions: Bool {
if parent.config.requireAttachmentDescriptions {
if draft.attachments.count == 0 {
return false
} else {
return !parent.attachmentsMissingDescriptions.isEmpty
}
} else {
return false
}
}
var validAttachmentCombination: Bool {
if !parent.mastodonController.instanceFeatures.mastodonAttachmentRestrictions {
return true
} else if draft.attachments.count > 1,
draft.draftAttachments.contains(where: { $0.type == .video }) {
return false
} else if draft.attachments.count > 4 {
return false
}
return true
}
init(parent: ComposeController) {
self.parent = parent
}
var canAddAttachment: Bool {
if parent.mastodonController.instanceFeatures.mastodonAttachmentRestrictions {
return draft.attachments.count < 4 && draft.draftAttachments.allSatisfy { $0.type == .image } && draft.poll == nil
} else {
return true
}
}
private var canAddPoll: Bool {
if parent.mastodonController.instanceFeatures.pollsAndAttachments {
return true
} else {
return draft.attachments.count == 0
}
}
var view: some View {
AttachmentsList()
}
private func moveAttachments(from source: IndexSet, to destination: Int) {
// just using moveObjects(at:to:) on the draft.attachments NSMutableOrderedSet
// results in the order switching back to the previous order and then to the correct one
// on the subsequent 2 view updates. creating a new set with the proper order doesn't have that problem
var array = draft.draftAttachments
array.move(fromOffsets: source, toOffset: destination)
draft.attachments = NSMutableOrderedSet(array: array)
}
private func deleteAttachments(at indices: IndexSet) {
var array = draft.draftAttachments
array.remove(atOffsets: indices)
draft.attachments = NSMutableOrderedSet(array: array)
}
private func insertAttachments(at offset: Int, itemProviders: [NSItemProvider]) {
for provider in itemProviders where provider.canLoadObject(ofClass: DraftAttachment.self) {
provider.loadObject(ofClass: DraftAttachment.self) { object, error in
guard let attachment = object as? DraftAttachment else { return }
DispatchQueue.main.async { [weak self] in
guard let self,
self.canAddAttachment else {
return
}
DraftsPersistentContainer.shared.viewContext.insert(attachment)
attachment.draft = self.draft
self.draft.attachments.add(attachment)
}
}
}
}
private func addImage() {
parent.deleteDraftOnDisappear = false
parent.config.presentAssetPicker?({ results in
self.insertAttachments(at: self.draft.attachments.count, itemProviders: results.map(\.itemProvider))
})
}
private func addDrawing() {
parent.deleteDraftOnDisappear = false
parent.config.presentDrawing?(PKDrawing()) { drawing in
let attachment = DraftAttachment(context: DraftsPersistentContainer.shared.viewContext)
attachment.id = UUID()
attachment.drawing = drawing
attachment.draft = self.draft
self.draft.attachments.add(attachment)
}
}
private func togglePoll() {
UIApplication.shared.sendAction(#selector(UIView.resignFirstResponder), to: nil, from: nil, for: nil)
withAnimation {
draft.poll = draft.poll == nil ? Poll(context: DraftsPersistentContainer.shared.viewContext) : nil
}
}
struct AttachmentsList: View {
private let cellHeight: CGFloat = 80
private let cellPadding: CGFloat = 12
@EnvironmentObject private var controller: AttachmentsListController
@EnvironmentObject private var draft: Draft
@Environment(\.colorScheme) private var colorScheme
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
var body: some View {
attachmentsList
Group {
if controller.parent.config.presentAssetPicker != nil {
addImageButton
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
}
if controller.parent.config.presentDrawing != nil {
addDrawingButton
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
}
togglePollButton
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
}
#if os(visionOS)
.buttonStyle(.bordered)
.labelStyle(AttachmentButtonLabelStyle())
#endif
}
private var attachmentsList: some View {
ForEach(draft.attachments.array as! [DraftAttachment]) { attachment in
ControllerView(controller: { AttachmentRowController(parent: controller.parent, attachment: attachment) })
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
.id(attachment.id)
}
.onMove(perform: controller.moveAttachments)
.onDelete(perform: controller.deleteAttachments)
.conditionally(controller.canAddAttachment) {
$0.onInsert(of: DraftAttachment.readableTypeIdentifiersForItemProvider, perform: { offset, providers in
controller.insertAttachments(at: offset, itemProviders: providers)
})
}
// only sort of works, see #240
.onDrop(of: DraftAttachment.readableTypeIdentifiersForItemProvider, isTargeted: nil) { providers in
controller.insertAttachments(at: 0, itemProviders: providers)
return true
}
}
private var addImageButton: some View {
Button(action: controller.addImage) {
Label("Add photo or video", systemImage: colorScheme == .dark ? "photo.fill" : "photo")
}
.disabled(!controller.canAddAttachment)
.foregroundColor(.accentColor)
.frame(height: cellHeight / 2)
}
private var addDrawingButton: some View {
Button(action: controller.addDrawing) {
Label("Draw something", systemImage: "hand.draw")
}
.disabled(!controller.canAddAttachment)
.foregroundColor(.accentColor)
.frame(height: cellHeight / 2)
}
private var togglePollButton: some View {
Button(action: controller.togglePoll) {
Label(draft.poll == nil ? "Add a poll" : "Remove poll", systemImage: "chart.bar.doc.horizontal")
}
.disabled(!controller.canAddPoll)
.foregroundColor(.accentColor)
.frame(height: cellHeight / 2)
}
}
}
fileprivate extension View {
@ViewBuilder
func conditionally(_ condition: Bool, body: (Self) -> some View) -> some View {
if condition {
body(self)
} else {
self
}
}
@available(iOS, obsoleted: 16.0)
@ViewBuilder
func sheetOrPopover(isPresented: Binding<Bool>, @ViewBuilder content: @escaping () -> some View) -> some View {
if #available(iOS 16.0, *) {
self.modifier(SheetOrPopover(isPresented: isPresented, view: content))
} else {
self.popover(isPresented: isPresented, content: content)
}
}
@available(iOS, obsoleted: 16.0)
@ViewBuilder
func withSheetDetentsIfAvailable() -> some View {
if #available(iOS 16.0, *) {
self
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
} else {
self
}
}
}
@available(iOS 16.0, *)
fileprivate struct SheetOrPopover<V: View>: ViewModifier {
@Binding var isPresented: Bool
@ViewBuilder let view: () -> V
@Environment(\.horizontalSizeClass) var sizeClass
func body(content: Content) -> some View {
if sizeClass == .compact {
content.sheet(isPresented: $isPresented, content: view)
} else {
content.popover(isPresented: $isPresented, content: view)
}
}
}
@available(visionOS 1.0, *)
fileprivate struct AttachmentButtonLabelStyle: LabelStyle {
func makeBody(configuration: Configuration) -> some View {
DefaultLabelStyle().makeBody(configuration: configuration)
.foregroundStyle(.white)
}
}

View File

@ -0,0 +1,83 @@
//
// AutocompleteController.swift
// ComposeUI
//
// Created by Shadowfacts on 3/25/23.
//
import SwiftUI
import Combine
class AutocompleteController: ViewController {
unowned let parent: ComposeController
@Published var mode: Mode?
init(parent: ComposeController) {
self.parent = parent
parent.$currentInput
.compactMap { $0 }
.flatMap { $0.autocompleteStatePublisher }
.map {
switch $0 {
case .mention(_):
return Mode.mention
case .emoji(_):
return Mode.emoji
case .hashtag(_):
return Mode.hashtag
case nil:
return nil
}
}
.assign(to: &$mode)
}
var view: some View {
AutocompleteView()
}
struct AutocompleteView: View {
@EnvironmentObject private var parent: ComposeController
@EnvironmentObject private var controller: AutocompleteController
@Environment(\.colorScheme) private var colorScheme: ColorScheme
var body: some View {
if let mode = controller.mode {
VStack(spacing: 0) {
Divider()
suggestionsView(mode: mode)
}
.background(backgroundColor)
}
}
@ViewBuilder
private func suggestionsView(mode: Mode) -> some View {
switch mode {
case .mention:
ControllerView(controller: { AutocompleteMentionsController(composeController: parent) })
case .emoji:
ControllerView(controller: { AutocompleteEmojisController(composeController: parent) })
case .hashtag:
ControllerView(controller: { AutocompleteHashtagsController(composeController: parent) })
}
}
private var backgroundColor: Color {
Color(white: colorScheme == .light ? 0.98 : 0.15)
}
private var borderColor: Color {
Color(white: colorScheme == .light ? 0.85 : 0.25)
}
}
enum Mode {
case mention
case emoji
case hashtag
}
}

View File

@ -0,0 +1,188 @@
//
// AutocompleteEmojisController.swift
// ComposeUI
//
// Created by Shadowfacts on 3/26/23.
//
import SwiftUI
import Pachyderm
import Combine
import TuskerComponents
class AutocompleteEmojisController: ViewController {
unowned let composeController: ComposeController
var mastodonController: ComposeMastodonContext { composeController.mastodonController }
private var stateCancellable: AnyCancellable?
private var searchTask: Task<Void, Never>?
@Published var expanded = false
@Published var emojis: [Emoji] = []
@Published var emojisBySection: [String: [Emoji]] = [:]
init(composeController: ComposeController) {
self.composeController = composeController
stateCancellable = composeController.$currentInput
.compactMap { $0 }
.flatMap { $0.autocompleteStatePublisher }
.compactMap {
if case .emoji(let s) = $0 {
return s
} else {
return nil
}
}
.removeDuplicates()
.sink { [unowned self] query in
self.searchTask?.cancel()
self.searchTask = Task { [weak self] in
await self?.queryChanged(query)
}
}
}
@MainActor
private func queryChanged(_ query: String) async {
var emojis = await composeController.mastodonController.getCustomEmojis()
guard !Task.isCancelled else {
return
}
if !query.isEmpty {
emojis =
emojis.map { emoji -> (Emoji, (matched: Bool, score: Int)) in
(emoji, FuzzyMatcher.match(pattern: query, str: emoji.shortcode))
}
.filter(\.1.matched)
.sorted { $0.1.score > $1.1.score }
.map(\.0)
}
var shortcodes = Set<String>()
var newEmojis = [Emoji]()
var newEmojisBySection = [String: [Emoji]]()
for emoji in emojis where !shortcodes.contains(emoji.shortcode) {
newEmojis.append(emoji)
shortcodes.insert(emoji.shortcode)
let category = emoji.category ?? ""
if newEmojisBySection.keys.contains(category) {
newEmojisBySection[category]!.append(emoji)
} else {
newEmojisBySection[category] = [emoji]
}
}
self.emojis = newEmojis
self.emojisBySection = newEmojisBySection
}
private func toggleExpanded() {
withAnimation {
expanded.toggle()
}
}
private func autocomplete(with emoji: Emoji) {
guard let input = composeController.currentInput else { return }
input.autocomplete(with: ":\(emoji.shortcode):")
}
var view: some View {
AutocompleteEmojisView()
}
struct AutocompleteEmojisView: View {
@EnvironmentObject private var composeController: ComposeController
@EnvironmentObject private var controller: AutocompleteEmojisController
@ScaledMetric private var emojiSize = 30
var body: some View {
// When exapnded, the toggle button should be at the top. When collapsed, it should be centered.
HStack(alignment: controller.expanded ? .top : .center, spacing: 0) {
emojiList
.transition(.move(edge: .bottom))
toggleExpandedButton
.padding(.trailing, 8)
.padding(.top, controller.expanded ? 8 : 0)
}
}
@ViewBuilder
private var emojiList: some View {
if controller.expanded {
verticalGrid
.frame(height: 150)
} else {
horizontalScrollView
}
}
private var verticalGrid: some View {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: emojiSize), spacing: 4)]) {
ForEach(controller.emojisBySection.keys.sorted(), id: \.self) { section in
Section {
ForEach(controller.emojisBySection[section]!, id: \.shortcode) { emoji in
Button(action: { controller.autocomplete(with: emoji) }) {
composeController.emojiImageView(emoji)
.frame(height: emojiSize)
}
.accessibilityLabel(emoji.shortcode)
}
} header: {
if !section.isEmpty {
VStack(alignment: .leading, spacing: 2) {
Text(section)
.font(.caption)
Divider()
}
.padding(.top, 4)
}
}
}
}
.padding(.all, 8)
// the spacing between the grid sections doesn't seem to be taken into account by the ScrollView?
.padding(.bottom, CGFloat(controller.emojisBySection.keys.count) * 4)
}
.frame(maxWidth: .infinity)
}
private var horizontalScrollView: some View {
ScrollView(.horizontal) {
LazyHStack(spacing: 8) {
ForEach(controller.emojis, id: \.shortcode) { emoji in
Button(action: { controller.autocomplete(with: emoji) }) {
HStack(spacing: 4) {
composeController.emojiImageView(emoji)
.frame(height: emojiSize)
Text(verbatim: ":\(emoji.shortcode):")
.foregroundColor(.primary)
}
}
.accessibilityLabel(emoji.shortcode)
.frame(height: emojiSize)
}
.animation(.linear(duration: 0.2), value: controller.emojis)
}
.padding(.horizontal, 8)
.frame(height: emojiSize + 16)
}
}
private var toggleExpandedButton: some View {
Button(action: controller.toggleExpanded) {
Image(systemName: "chevron.down")
.resizable()
.aspectRatio(contentMode: .fit)
.rotationEffect(controller.expanded ? .zero : .degrees(180))
}
.accessibilityLabel(controller.expanded ? "Collapse" : "Expand")
.frame(width: 20, height: 20)
}
}
}

View File

@ -0,0 +1,125 @@
//
// AutocompleteHashtagsController.swift
// ComposeUI
//
// Created by Shadowfacts on 4/1/23.
//
import SwiftUI
import Combine
import Pachyderm
import TuskerComponents
class AutocompleteHashtagsController: ViewController {
unowned let composeController: ComposeController
var mastodonController: ComposeMastodonContext { composeController.mastodonController }
private var stateCancellable: AnyCancellable?
private var searchTask: Task<Void, Never>?
@Published var hashtags: [Hashtag] = []
init(composeController: ComposeController) {
self.composeController = composeController
stateCancellable = composeController.$currentInput
.compactMap { $0 }
.flatMap { $0.autocompleteStatePublisher }
.compactMap {
if case .hashtag(let s) = $0 {
return s
} else {
return nil
}
}
.debounce(for: .milliseconds(250), scheduler: DispatchQueue.main)
.sink { [unowned self] query in
self.searchTask?.cancel()
self.searchTask = Task { [weak self] in
await self?.queryChanged(query)
}
}
}
@MainActor
private func queryChanged(_ query: String) async {
guard !query.isEmpty else {
hashtags = []
return
}
let localHashtags = mastodonController.searchCachedHashtags(query: query)
var onlyLocalTagsTask: Task<Void, any Error>?
if !localHashtags.isEmpty {
onlyLocalTagsTask = Task {
// we only want to do the local-only search if the trends API call takes more than .25sec or it fails
try await Task.sleep(nanoseconds: 250 * NSEC_PER_MSEC)
self.updateHashtags(searchResults: [], trendingTags: [], localHashtags: localHashtags, query: query)
}
}
async let trendingTags = try? mastodonController.run(Client.getTrendingHashtags()).0
async let searchResults = try? mastodonController.run(Client.search(query: "#\(query)", types: [.hashtags])).0.hashtags
let trends = await trendingTags ?? []
let search = await searchResults ?? []
onlyLocalTagsTask?.cancel()
guard !Task.isCancelled else { return }
updateHashtags(searchResults: search, trendingTags: trends, localHashtags: localHashtags, query: query)
}
@MainActor
private func updateHashtags(searchResults: [Hashtag], trendingTags: [Hashtag], localHashtags: [Hashtag], query: String) {
var addedHashtags = Set<String>()
var hashtags = [(Hashtag, Int)]()
for group in [searchResults, trendingTags, localHashtags] {
for tag in group where !addedHashtags.contains(tag.name) {
let (matched, score) = FuzzyMatcher.match(pattern: query, str: tag.name)
if matched {
hashtags.append((tag, score))
addedHashtags.insert(tag.name)
}
}
}
self.hashtags = hashtags
.sorted { $0.1 > $1.1 }
.map(\.0)
}
private func autocomplete(with hashtag: Hashtag) {
guard let currentInput = composeController.currentInput else { return }
currentInput.autocomplete(with: "#\(hashtag.name)")
}
var view: some View {
AutocompleteHashtagsView()
}
struct AutocompleteHashtagsView: View {
@EnvironmentObject private var controller: AutocompleteHashtagsController
var body: some View {
ScrollView(.horizontal) {
HStack(spacing: 8) {
ForEach(controller.hashtags, id: \.name) { hashtag in
Button(action: { controller.autocomplete(with: hashtag) }) {
Text(verbatim: "#\(hashtag.name)")
.foregroundColor(Color(uiColor: .label))
}
.frame(height: 30)
.padding(.vertical, 8)
}
Spacer()
}
.padding(.horizontal, 8)
.animation(.linear(duration: 0.2), value: controller.hashtags)
}
}
}
}

View File

@ -0,0 +1,179 @@
//
// AutocompleteMentionsController.swift
// ComposeUI
//
// Created by Shadowfacts on 3/25/23.
//
import SwiftUI
import Combine
import Pachyderm
import TuskerComponents
class AutocompleteMentionsController: ViewController {
unowned let composeController: ComposeController
var mastodonController: ComposeMastodonContext { composeController.mastodonController }
private var stateCancellable: AnyCancellable?
@Published private var accounts: [AnyAccount] = []
private var searchTask: Task<Void, Never>?
init(composeController: ComposeController) {
self.composeController = composeController
stateCancellable = composeController.$currentInput
.compactMap { $0 }
.flatMap { $0.autocompleteStatePublisher }
.compactMap {
if case .mention(let s) = $0 {
return s
} else {
return nil
}
}
.debounce(for: .milliseconds(250), scheduler: DispatchQueue.main)
.sink { [unowned self] query in
self.searchTask?.cancel()
// weak in case the autocomplete controller is dealloc'd racing with the task starting
self.searchTask = Task { [weak self] in
await self?.queryChanged(query)
}
}
}
@MainActor
private func queryChanged(_ query: String) async {
guard !query.isEmpty else {
accounts = []
return
}
let localSearchTask = Task {
// we only want to search locally if the search API call takes more than .25sec or it fails
try await Task.sleep(nanoseconds: 250 * NSEC_PER_MSEC)
let results = self.mastodonController.searchCachedAccounts(query: query)
try Task.checkCancellation()
if !results.isEmpty {
self.loadAccounts(results.map { .init(value: $0) }, query: query)
}
}
let accounts = try? await mastodonController.run(Client.searchForAccount(query: query)).0
guard let accounts,
!Task.isCancelled else {
return
}
localSearchTask.cancel()
loadAccounts(accounts.map { .init(value: $0) }, query: query)
}
@MainActor
private func loadAccounts(_ accounts: [AnyAccount], query: String) {
guard case .mention(query) = composeController.currentInput?.autocompleteState else {
return
}
// when sorting account suggestions, ignore the domain component of the acct unless the user is typing it themself
let ignoreDomain = !query.contains("@")
self.accounts =
accounts.map { (account) -> (AnyAccount, (matched: Bool, score: Int)) in
let fuzzyStr = ignoreDomain ? String(account.value.acct.split(separator: "@").first!) : account.value.acct
let res = (account, FuzzyMatcher.match(pattern: query, str: fuzzyStr))
return res
}
.filter(\.1.matched)
.map { (account, res) -> (AnyAccount, Int) in
// give higher weight to accounts that the user follows or is followed by
var score = res.score
if let relationship = mastodonController.cachedRelationship(for: account.value.id) {
if relationship.following {
score += 3
}
if relationship.followedBy {
score += 2
}
}
return (account, score)
}
.sorted { $0.1 > $1.1 }
.map(\.0)
}
private func autocomplete(with account: AnyAccount) {
guard let input = composeController.currentInput else {
return
}
input.autocomplete(with: "@\(account.value.acct)")
}
var view: some View {
AutocompleteMentionsView()
}
struct AutocompleteMentionsView: View {
@EnvironmentObject private var controller: AutocompleteMentionsController
var body: some View {
ScrollView(.horizontal) {
HStack(spacing: 8) {
ForEach(controller.accounts) { account in
AutocompleteMentionButton(account: account)
}
Spacer()
}
.padding(.horizontal, 8)
.animation(.linear(duration: 0.2), value: controller.accounts)
}
.onDisappear {
controller.searchTask?.cancel()
}
}
}
private struct AutocompleteMentionButton: View {
@EnvironmentObject private var composeController: ComposeController
@EnvironmentObject private var controller: AutocompleteMentionsController
let account: AnyAccount
var body: some View {
Button(action: { controller.autocomplete(with: account) }) {
HStack(spacing: 4) {
AvatarImageView(
url: account.value.avatar,
size: 30,
style: composeController.config.avatarStyle,
fetchAvatar: composeController.fetchAvatar
)
VStack(alignment: .leading) {
controller.composeController.displayNameLabel(account.value, .subheadline, 14)
.foregroundColor(.primary)
Text(verbatim: "@\(account.value.acct)")
.font(.caption)
.foregroundColor(.primary)
}
}
}
.frame(height: 30)
.padding(.vertical, 8)
}
}
}
fileprivate struct AnyAccount: Equatable, Identifiable {
let value: any AccountProtocol
var id: String { value.id }
static func ==(lhs: AnyAccount, rhs: AnyAccount) -> Bool {
return lhs.value.id == rhs.value.id
}
}

View File

@ -0,0 +1,520 @@
//
// ComposeController.swift
// ComposeUI
//
// Created by Shadowfacts on 3/4/23.
//
import SwiftUI
import Combine
import Pachyderm
import TuskerComponents
import MatchedGeometryPresentation
import CoreData
public final class ComposeController: ViewController {
public typealias FetchAttachment = (URL) async -> UIImage?
public typealias FetchStatus = (String) -> (any StatusProtocol)?
public typealias DisplayNameLabel = (any AccountProtocol, Font.TextStyle, CGFloat) -> AnyView
public typealias CurrentAccountContainerView = (AnyView) -> AnyView
public typealias ReplyContentView = (any StatusProtocol, @escaping (CGFloat) -> Void) -> AnyView
public typealias EmojiImageView = (Emoji) -> AnyView
@Published public private(set) var draft: Draft {
didSet {
assert(draft.managedObjectContext == DraftsPersistentContainer.shared.viewContext)
}
}
@Published public var config: ComposeUIConfig
@Published public var mastodonController: ComposeMastodonContext
let fetchAvatar: AvatarImageView.FetchAvatar
let fetchAttachment: FetchAttachment
let fetchStatus: FetchStatus
let displayNameLabel: DisplayNameLabel
let currentAccountContainerView: CurrentAccountContainerView
let replyContentView: ReplyContentView
let emojiImageView: EmojiImageView
@Published public var currentAccount: (any AccountProtocol)?
@Published public var showToolbar = true
@Published public var deleteDraftOnDisappear = true
@Published var autocompleteController: AutocompleteController!
@Published var toolbarController: ToolbarController!
@Published var attachmentsListController: AttachmentsListController!
// this property is here rather than on the AttachmentsListController so that the ComposeView
// updates when it changes, because changes to it may alter postButtonEnabled
@Published var attachmentsMissingDescriptions = Set<UUID>()
@Published var focusedAttachment: (DraftAttachment, AttachmentThumbnailController)?
let scrollToAttachment = PassthroughSubject<UUID, Never>()
@Published var contentWarningBecomeFirstResponder = false
@Published var mainComposeTextViewBecomeFirstResponder = false
@Published var currentInput: (any ComposeInput)? = nil
@Published var shouldEmojiAutocompletionBeginExpanded = false
@Published var isShowingSaveDraftSheet = false
@Published var isShowingDraftsList = false
@Published var poster: PostService?
@Published var postError: PostService.Error?
@Published public private(set) var didPostSuccessfully = false
@Published var hasChangedLanguageSelection = false
private var isDisappearing = false
private var userConfirmedDelete = false
public var isPosting: Bool {
poster != nil
}
var charactersRemaining: Int {
let instanceFeatures = mastodonController.instanceFeatures
let limit = instanceFeatures.maxStatusChars
let cwCount = draft.contentWarningEnabled ? draft.contentWarning.count : 0
return limit - (cwCount + CharacterCounter.count(text: draft.text, for: instanceFeatures))
}
var postButtonEnabled: Bool {
draft.editedStatusID != nil ||
(draft.hasContent
&& charactersRemaining >= 0
&& !isPosting
&& attachmentsListController.isValid
&& isPollValid)
}
private var isPollValid: Bool {
draft.poll == nil || draft.poll!.pollOptions.allSatisfy { !$0.text.isEmpty }
}
public var navigationTitle: String {
if let id = draft.inReplyToID,
let status = fetchStatus(id) {
return "Reply to @\(status.account.acct)"
} else if draft.editedStatusID != nil {
return "Edit Post"
} else {
return "New Post"
}
}
public init(
draft: Draft,
config: ComposeUIConfig,
mastodonController: ComposeMastodonContext,
fetchAvatar: @escaping AvatarImageView.FetchAvatar,
fetchAttachment: @escaping FetchAttachment,
fetchStatus: @escaping FetchStatus,
displayNameLabel: @escaping DisplayNameLabel,
currentAccountContainerView: @escaping CurrentAccountContainerView = { $0 },
replyContentView: @escaping ReplyContentView,
emojiImageView: @escaping EmojiImageView
) {
self.draft = draft
assert(draft.managedObjectContext == DraftsPersistentContainer.shared.viewContext)
self.config = config
self.mastodonController = mastodonController
self.fetchAvatar = fetchAvatar
self.fetchAttachment = fetchAttachment
self.fetchStatus = fetchStatus
self.displayNameLabel = displayNameLabel
self.currentAccountContainerView = currentAccountContainerView
self.replyContentView = replyContentView
self.emojiImageView = emojiImageView
self.autocompleteController = AutocompleteController(parent: self)
self.toolbarController = ToolbarController(parent: self)
self.attachmentsListController = AttachmentsListController(parent: self)
if #available(iOS 16.0, *) {
NotificationCenter.default.addObserver(self, selector: #selector(currentInputModeChanged), name: UITextInputMode.currentInputModeDidChangeNotification, object: nil)
}
NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: DraftsPersistentContainer.shared.viewContext)
}
public var view: some View {
ComposeView(poster: poster)
.environment(\.managedObjectContext, DraftsPersistentContainer.shared.viewContext)
.environmentObject(draft)
.environmentObject(mastodonController.instanceFeatures)
.environment(\.composeUIConfig, config)
}
@MainActor
@objc private func managedObjectsDidChange(_ notification: Foundation.Notification) {
if let deleted = notification.userInfo?[NSDeletedObjectsKey] as? Set<NSManagedObject>,
deleted.contains(where: { $0.objectID == self.draft.objectID }),
!isDisappearing {
self.config.dismiss(.cancel)
}
}
public func canPaste(itemProviders: [NSItemProvider]) -> Bool {
guard itemProviders.allSatisfy({ $0.canLoadObject(ofClass: DraftAttachment.self) }) else {
return false
}
if mastodonController.instanceFeatures.mastodonAttachmentRestrictions {
if draft.draftAttachments.allSatisfy({ $0.type == .image }) {
// if providers are videos, this technically allows invalid video/image combinations
return itemProviders.count + draft.attachments.count <= 4
} else {
return false
}
} else {
return true
}
}
public func paste(itemProviders: [NSItemProvider]) {
for provider in itemProviders where provider.canLoadObject(ofClass: DraftAttachment.self) {
provider.loadObject(ofClass: DraftAttachment.self) { object, error in
guard let attachment = object as? DraftAttachment else { return }
DispatchQueue.main.async {
guard self.attachmentsListController.canAddAttachment else { return }
DraftsPersistentContainer.shared.viewContext.insert(attachment)
attachment.draft = self.draft
self.draft.attachments.add(attachment)
}
}
}
}
@MainActor
func cancel() {
if draft.hasContent {
isShowingSaveDraftSheet = true
} else {
deleteDraftOnDisappear = true
config.dismiss(.cancel)
}
}
@MainActor
func cancel(deleteDraft: Bool) {
deleteDraftOnDisappear = true
userConfirmedDelete = deleteDraft
config.dismiss(.cancel)
}
func postStatus() {
guard !isPosting,
draft.editedStatusID != nil || draft.hasContent else {
return
}
Task { @MainActor in
let poster = PostService(mastodonController: mastodonController, config: config, draft: draft)
self.poster = poster
// try to resign the first responder, if there is one.
// otherwise, the existence of the poster changes the .disabled modifier which causes the keyboard to hide
// and the first responder to change during a view update, which in turn triggers a bunch of state changes
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
do {
try await poster.post()
deleteDraftOnDisappear = true
didPostSuccessfully = true
// wait .25 seconds so the user can see the progress bar has completed
try? await Task.sleep(nanoseconds: 250_000_000)
// don't unset the poster, so the ui remains disabled while dismissing
config.dismiss(.post)
} catch let error as PostService.Error {
self.postError = error
self.poster = nil
} catch {
fatalError("unreachable")
}
}
}
func showDrafts() {
isShowingDraftsList = true
}
func selectDraft(_ newDraft: Draft) {
let oldDraft = self.draft
self.draft = newDraft
if !oldDraft.hasContent {
DraftsPersistentContainer.shared.viewContext.delete(oldDraft)
}
DraftsPersistentContainer.shared.save()
}
func onDisappear() {
isDisappearing = true
if deleteDraftOnDisappear && (!draft.hasContent || didPostSuccessfully || userConfirmedDelete) {
DraftsPersistentContainer.shared.viewContext.delete(draft)
}
DraftsPersistentContainer.shared.save()
}
func toggleContentWarning() {
draft.contentWarningEnabled.toggle()
if draft.contentWarningEnabled {
contentWarningBecomeFirstResponder = true
}
}
@available(iOS 16.0, *)
@objc private func currentInputModeChanged() {
guard let mode = currentInput?.textInputMode,
let code = LanguagePicker.codeFromInputMode(mode),
!hasChangedLanguageSelection && !draft.hasContent else {
return
}
draft.language = code.identifier
}
struct ComposeView: View {
@OptionalObservedObject var poster: PostService?
@EnvironmentObject var controller: ComposeController
@EnvironmentObject var draft: Draft
#if !os(visionOS)
@StateObject private var keyboardReader = KeyboardReader()
#endif
@State private var globalFrameOutsideList = CGRect.zero
init(poster: PostService?) {
self.poster = poster
}
var config: ComposeUIConfig {
controller.config
}
var body: some View {
NavigationView {
navRoot
}
.navigationViewStyle(.stack)
}
private var navRoot: some View {
ZStack(alignment: .top) {
// just using .background doesn't work; for some reason it gets inset immediately after the software keyboard is dismissed
config.backgroundColor
.edgesIgnoringSafeArea(.all)
ScrollViewReader { proxy in
mainList
.onReceive(controller.scrollToAttachment) { id in
proxy.scrollTo(id, anchor: .center)
}
}
if let poster = poster {
// can't use SwiftUI.ProgressView because there's no UIProgressView.Style.bar equivalent, see FB8587149
WrappedProgressView(value: poster.currentStep, total: poster.totalSteps)
}
}
.safeAreaInset(edge: .bottom, spacing: 0) {
if controller.showToolbar {
VStack(spacing: 0) {
ControllerView(controller: { controller.autocompleteController })
.transition(.move(edge: .bottom))
.animation(.default, value: controller.currentInput?.autocompleteState)
#if !os(visionOS)
ControllerView(controller: { controller.toolbarController })
#endif
}
#if !os(visionOS)
// on iPadOS15, the toolbar ends up below the keyboard's toolbar without this
.padding(.bottom, keyboardInset)
#endif
.transition(.move(edge: .bottom))
}
}
.toolbar {
ToolbarItem(placement: .cancellationAction) { cancelButton }
ToolbarItem(placement: .confirmationAction) { postButton }
#if os(visionOS)
ToolbarItem(placement: .bottomOrnament) {
ControllerView(controller: { controller.toolbarController })
}
#endif
}
.background(GeometryReader { proxy in
Color.clear
.preference(key: GlobalFrameOutsideListPrefKey.self, value: proxy.frame(in: .global))
.onPreferenceChange(GlobalFrameOutsideListPrefKey.self) { newValue in
globalFrameOutsideList = newValue
}
})
.sheet(isPresented: $controller.isShowingDraftsList) {
ControllerView(controller: { DraftsController(parent: controller, isPresented: $controller.isShowingDraftsList) })
}
.alertWithData("Error Posting", data: $controller.postError, actions: { _ in
Button("OK") {}
}, message: { error in
Text(error.localizedDescription)
})
.matchedGeometryPresentation(id: Binding(get: { () -> UUID?? in
let id = controller.focusedAttachment?.0.id
// this needs to be a double optional, since the type used for for the presentationID in the geom source is a UUID?
return id.map { Optional.some($0) }
}, set: {
if $0 == nil {
controller.focusedAttachment = nil
} else {
fatalError()
}
}), backgroundColor: .black) {
ControllerView(controller: {
FocusedAttachmentController(
parent: controller,
attachment: controller.focusedAttachment!.0,
thumbnailController: controller.focusedAttachment!.1
)
})
}
.onDisappear(perform: controller.onDisappear)
.navigationTitle(controller.navigationTitle)
.navigationBarTitleDisplayMode(.inline)
}
private var mainList: some View {
List {
if let id = draft.inReplyToID,
let status = controller.fetchStatus(id) {
ReplyStatusView(
status: status,
rowTopInset: 8,
globalFrameOutsideList: globalFrameOutsideList
)
// i don't know why swiftui can't infer this from the status that's passed into the ReplyStatusView changing
.id(id)
.listRowInsets(EdgeInsets(top: 8, leading: 8, bottom: 4, trailing: 8))
.listRowSeparator(.hidden)
.listRowBackground(config.backgroundColor)
}
HeaderView(currentAccount: controller.currentAccount, charsRemaining: controller.charactersRemaining)
.listRowInsets(EdgeInsets(top: draft.inReplyToID == nil ? 8 : 4, leading: 8, bottom: 4, trailing: 8))
.listRowSeparator(.hidden)
.listRowBackground(config.backgroundColor)
if draft.contentWarningEnabled {
EmojiTextField(
text: $draft.contentWarning,
placeholder: "Write your warning here",
maxLength: nil,
becomeFirstResponder: $controller.contentWarningBecomeFirstResponder,
focusNextView: $controller.mainComposeTextViewBecomeFirstResponder
)
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
.listRowSeparator(.hidden)
.listRowBackground(config.backgroundColor)
}
MainTextView()
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
.listRowSeparator(.hidden)
.listRowBackground(config.backgroundColor)
if let poll = draft.poll {
ControllerView(controller: { PollController(parent: controller, poll: poll) })
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 8, trailing: 8))
.listRowSeparator(.hidden)
.listRowBackground(config.backgroundColor)
}
ControllerView(controller: { controller.attachmentsListController })
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 8, trailing: 8))
.listRowBackground(config.backgroundColor)
}
.listStyle(.plain)
#if !os(visionOS)
.scrollDismissesKeyboardInteractivelyIfAvailable()
#endif
.disabled(controller.isPosting)
}
private var cancelButton: some View {
Button(action: controller.cancel) {
Text("Cancel")
// otherwise all Buttons in the nav bar are made semibold
.font(.system(size: 17, weight: .regular))
}
.disabled(controller.isPosting)
.confirmationDialog("Are you sure?", isPresented: $controller.isShowingSaveDraftSheet) {
// edit drafts can't be saved
if draft.editedStatusID == nil {
Button(action: { controller.cancel(deleteDraft: false) }) {
Text("Save Draft")
}
Button(role: .destructive, action: { controller.cancel(deleteDraft: true) }) {
Text("Delete Draft")
}
} else {
Button(role: .destructive, action: { controller.cancel(deleteDraft: true) }) {
Text("Cancel Edit")
}
}
}
}
@ViewBuilder
private var postButton: some View {
if draft.hasContent || draft.editedStatusID != nil || !controller.config.allowSwitchingDrafts {
Button(action: controller.postStatus) {
Text(draft.editedStatusID == nil ? "Post" : "Edit")
}
.keyboardShortcut(.return, modifiers: .command)
.disabled(!controller.postButtonEnabled)
} else {
Button(action: controller.showDrafts) {
Text("Drafts")
}
}
}
#if !os(visionOS)
@available(iOS, obsoleted: 16.0)
private var keyboardInset: CGFloat {
if #unavailable(iOS 16.0),
UIDevice.current.userInterfaceIdiom == .pad,
keyboardReader.isVisible {
return ToolbarController.height
} else {
return 0
}
}
#endif
}
}
private extension View {
@available(iOS, obsoleted: 16.0)
@ViewBuilder
func scrollDismissesKeyboardInteractivelyIfAvailable() -> some View {
if #available(iOS 16.0, *) {
self.scrollDismissesKeyboard(.interactively)
} else {
self
}
}
}
private struct GlobalFrameOutsideListPrefKey: PreferenceKey {
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
value = nextValue()
}
}
private struct ComposeUIConfigEnvironmentKey: EnvironmentKey {
static let defaultValue = ComposeUIConfig()
}
extension EnvironmentValues {
var composeUIConfig: ComposeUIConfig {
get { self[ComposeUIConfigEnvironmentKey.self] }
set { self[ComposeUIConfigEnvironmentKey.self] = newValue }
}
}

View File

@ -0,0 +1,174 @@
//
// DraftsController.swift
// ComposeUI
//
// Created by Shadowfacts on 3/7/23.
//
import SwiftUI
import TuskerComponents
import CoreData
class DraftsController: ViewController {
unowned let parent: ComposeController
@Binding var isPresented: Bool
@Published var draftForDifferentReply: Draft?
init(parent: ComposeController, isPresented: Binding<Bool>) {
self.parent = parent
self._isPresented = isPresented
}
var view: some View {
DraftsRepresentable()
}
func maybeSelectDraft(_ draft: Draft) {
if draft.inReplyToID != parent.draft.inReplyToID,
parent.draft.hasContent {
draftForDifferentReply = draft
} else {
confirmSelectDraft(draft)
}
}
func cancelSelectingDraft() {
draftForDifferentReply = nil
}
func confirmSelectDraft(_ draft: Draft) {
parent.selectDraft(draft)
closeDrafts()
}
func deleteDraft(_ draft: Draft) {
DraftsPersistentContainer.shared.viewContext.delete(draft)
}
func closeDrafts() {
isPresented = false
DraftsPersistentContainer.shared.save()
}
struct DraftsRepresentable: UIViewControllerRepresentable {
typealias UIViewControllerType = UIHostingController<DraftsView>
func makeUIViewController(context: Context) -> UIHostingController<DraftsController.DraftsView> {
return UIHostingController(rootView: DraftsView())
}
func updateUIViewController(_ uiViewController: UIHostingController<DraftsController.DraftsView>, context: Context) {
}
}
struct DraftsView: View {
@EnvironmentObject private var controller: DraftsController
@EnvironmentObject private var currentDraft: Draft
@FetchRequest(sortDescriptors: [SortDescriptor(\Draft.lastModified, order: .reverse)]) private var drafts: FetchedResults<Draft>
var body: some View {
NavigationView {
List {
ForEach(drafts) { draft in
Button(action: { controller.maybeSelectDraft(draft) }) {
DraftRow(draft: draft)
}
.contextMenu {
Button(role: .destructive, action: { controller.deleteDraft(draft) }) {
Label("Delete Draft", systemImage: "trash")
}
}
.ifLet(controller.parent.config.userActivityForDraft(draft), modify: { view, activity in
view.onDrag { activity }
})
}
.onDelete { indices in
indices.map { drafts[$0] }.forEach(controller.deleteDraft)
}
}
.listStyle(.plain)
.navigationTitle("Drafts")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) { cancelButton }
}
}
.alertWithData("Different Reply", data: $controller.draftForDifferentReply) { draft in
Button(role: .cancel, action: controller.cancelSelectingDraft) {
Text("Cancel")
}
Button(action: { controller.confirmSelectDraft(draft) }) {
Text("Restore Draft")
}
} message: { _ in
Text("The selected draft is a reply to a different post, do you wish to use it?")
}
.onAppear {
drafts.nsPredicate = NSPredicate(format: "accountID == %@ AND id != %@ AND lastModified != nil", controller.parent.mastodonController.accountInfo!.id, currentDraft.id as NSUUID)
}
}
private var cancelButton: some View {
Button(action: controller.closeDrafts) {
Text("Cancel")
}
}
}
}
private struct DraftRow: View {
@ObservedObject var draft: Draft
@EnvironmentObject private var controller: DraftsController
var body: some View {
HStack {
VStack(alignment: .leading) {
if draft.editedStatusID != nil {
// shouldn't happen unless the app crashed/was killed during an edit
Text("Edit")
.font(.body.bold())
.foregroundColor(.orange)
}
if draft.contentWarningEnabled {
Text(draft.contentWarning)
.font(.body.bold())
.foregroundColor(.secondary)
}
Text(draft.text)
.font(.body)
HStack(spacing: 8) {
ForEach(draft.draftAttachments) { attachment in
ControllerView(controller: { AttachmentThumbnailController(attachment: attachment, parent: controller.parent) })
.aspectRatio(contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 5))
.frame(height: 50)
}
}
}
Spacer()
if let lastModified = draft.lastModified {
Text(lastModified.formatted(.abbreviatedTimeAgo))
.font(.body)
.foregroundColor(.secondary)
}
}
}
}
private extension View {
@ViewBuilder
func ifLet<T, V: View>(_ value: T?, modify: (Self, T) -> V) -> some View {
if let value {
modify(self, value)
} else {
self
}
}
}

View File

@ -0,0 +1,122 @@
//
// FocusedAttachmentController.swift
// ComposeUI
//
// Created by Shadowfacts on 4/29/23.
//
import SwiftUI
import MatchedGeometryPresentation
import AVKit
class FocusedAttachmentController: ViewController {
unowned let parent: ComposeController
let attachment: DraftAttachment
let thumbnailController: AttachmentThumbnailController
private let player: AVPlayer?
init(parent: ComposeController, attachment: DraftAttachment, thumbnailController: AttachmentThumbnailController) {
self.parent = parent
self.attachment = attachment
self.thumbnailController = thumbnailController
if case let .file(url, type) = attachment.data,
type.conforms(to: .movie) {
self.player = AVPlayer(url: url)
self.player!.isMuted = true
} else {
self.player = nil
}
}
var view: some View {
FocusedAttachmentView(attachment: attachment)
}
struct FocusedAttachmentView: View {
@ObservedObject var attachment: DraftAttachment
@EnvironmentObject private var controller: FocusedAttachmentController
@Environment(\.dismiss) private var dismiss
@FocusState private var textEditorFocused: Bool
@EnvironmentObject private var matchedGeomState: MatchedGeometryState
var body: some View {
VStack(spacing: 0) {
Spacer(minLength: 0)
if let player = controller.player {
VideoPlayer(player: player)
.matchedGeometryDestination(id: attachment.id)
.onAppear {
player.play()
}
} else if #available(iOS 16.0, *) {
ZoomableScrollView {
attachmentView
.matchedGeometryDestination(id: attachment.id)
}
} else {
attachmentView
.matchedGeometryDestination(id: attachment.id)
}
Spacer(minLength: 0)
FocusedAttachmentDescriptionView(attachment: attachment)
.environment(\.colorScheme, .dark)
.matchedGeometryDestination(id: AttachmentDescriptionTextViewID(attachment))
.frame(height: 150)
.focused($textEditorFocused)
}
.background(.black)
.overlay(alignment: .topLeading, content: {
Button {
// set the mode to dismissing immediately, so that layout changes due to the keyboard hiding
// (which happens before the dismiss animation controller starts running) don't alter the destination frames
if textEditorFocused {
matchedGeomState.mode = .dismissing
}
dismiss()
} label: {
Image(systemName: "arrow.down.forward.and.arrow.up.backward")
}
.buttonStyle(DismissFocusedAttachmentButtonStyle())
.padding([.top, .leading], 4)
})
}
private var attachmentView: some View {
ControllerView(controller: { controller.thumbnailController })
.environment(\.attachmentThumbnailConfiguration, .init(contentMode: .fit, fullSize: true))
}
}
}
private struct DismissFocusedAttachmentButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
ZStack {
RoundedRectangle(cornerRadius: 4)
.fill(.black.opacity(0.5))
configuration.label
.foregroundColor(.white)
.imageScale(.large)
}
.frame(width: 40, height: 40)
}
}
struct AttachmentDescriptionTextViewID: Hashable {
let attachmentID: UUID!
init(_ attachment: DraftAttachment) {
self.attachmentID = attachment.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(attachmentID)
hasher.combine("descriptionTextView")
}
}

View File

@ -0,0 +1,48 @@
//
// PlaceholderController.swift
// ComposeUI
//
// Created by Shadowfacts on 3/6/23.
//
import SwiftUI
final class PlaceholderController: ViewController, PlaceholderViewProvider {
private let placeholderView: PlaceholderView = PlaceholderController.makePlaceholderView()
static func makePlaceholderView() -> some View {
let components = Calendar.current.dateComponents([.month, .day], from: Date())
if components.month == 3 && components.day == 14,
Date().formatted(date: .numeric, time: .omitted).starts(with: "3") {
Text("Happy π day!")
} else if components.month == 4 && components.day == 1 {
Text("April Fool's!").rotationEffect(.radians(.pi), anchor: .center)
} else if components.month == 9 && components.day == 5 {
// https://weirder.earth/@noracodes/109276419847254552
// https://retrocomputing.stackexchange.com/questions/14763/what-warning-was-given-on-attempting-to-post-to-usenet-circa-1990
Text("This program posts news to thousands of machines throughout the entire populated world. Please be sure you know what you are doing.").italic()
} else if components.month == 9 && components.day == 21 {
Text("Do you remember?")
} else if components.month == 10 && components.day == 31 {
if .random() {
Text("Post something spooky!")
} else {
Text("Any questions?")
}
} else {
Text("What's on your mind?")
}
}
var view: some View {
placeholderView
}
}
// exists to provide access to the type alias since the @State property needs it to be explicit
private protocol PlaceholderViewProvider {
associatedtype PlaceholderView: View
@ViewBuilder
static func makePlaceholderView() -> PlaceholderView
}

View File

@ -0,0 +1,195 @@
//
// PollController.swift
// ComposeUI
//
// Created by Shadowfacts on 3/25/23.
//
import SwiftUI
import TuskerComponents
class PollController: ViewController {
unowned let parent: ComposeController
var draft: Draft { parent.draft }
let poll: Poll
@Published var duration: Duration
init(parent: ComposeController, poll: Poll) {
self.parent = parent
self.poll = poll
self.duration = .fromTimeInterval(poll.duration) ?? .oneDay
}
var view: some View {
PollView()
.environmentObject(poll)
}
private func removePoll() {
withAnimation {
draft.poll = nil
}
}
private func moveOptions(indices: IndexSet, newIndex: Int) {
// see AttachmentsListController.moveAttachments
var array = poll.pollOptions
array.move(fromOffsets: indices, toOffset: newIndex)
poll.options = NSMutableOrderedSet(array: array)
}
private func removeOption(_ option: PollOption) {
var array = poll.pollOptions
array.remove(at: poll.options.index(of: option))
poll.options = NSMutableOrderedSet(array: array)
}
private var canAddOption: Bool {
if let max = parent.mastodonController.instanceFeatures.maxPollOptionsCount {
return poll.options.count < max
} else {
return true
}
}
private func addOption() {
let option = PollOption(context: DraftsPersistentContainer.shared.viewContext)
option.poll = poll
poll.options.add(option)
}
struct PollView: View {
@EnvironmentObject private var controller: PollController
@EnvironmentObject private var poll: Poll
@Environment(\.colorScheme) private var colorScheme
var body: some View {
VStack {
HStack {
Text("Poll")
.font(.headline)
Spacer()
Button(action: controller.removePoll) {
Image(systemName: "xmark")
.imageScale(.small)
.padding(4)
}
.accessibilityLabel("Remove poll")
.buttonStyle(.plain)
.accentColor(buttonForegroundColor)
.background(Circle().foregroundColor(buttonBackgroundColor))
.hoverEffect()
}
List {
ForEach($poll.pollOptions) { $option in
PollOptionView(option: option, remove: { controller.removeOption(option) })
.frame(height: 36)
.listRowInsets(EdgeInsets(top: 4, leading: 0, bottom: 4, trailing: 0))
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
}
.onMove(perform: controller.moveOptions)
}
.listStyle(.plain)
.scrollDisabledIfAvailable(true)
.frame(height: 44 * CGFloat(poll.options.count))
Button(action: controller.addOption) {
Label {
Text("Add Option")
} icon: {
Image(systemName: "plus")
.foregroundColor(.accentColor)
}
}
.buttonStyle(.borderless)
.disabled(!controller.canAddOption)
HStack {
MenuPicker(selection: $poll.multiple, options: [
.init(value: true, title: "Allow multiple"),
.init(value: false, title: "Single choice"),
])
.frame(maxWidth: .infinity)
MenuPicker(selection: $controller.duration, options: Duration.allCases.map {
.init(value: $0, title: Duration.formatter.string(from: $0.timeInterval)!)
})
.frame(maxWidth: .infinity)
}
}
.padding(8)
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.foregroundColor(backgroundColor)
)
#if os(visionOS)
.onChange(of: controller.duration) {
poll.duration = controller.duration.timeInterval
}
#else
.onChange(of: controller.duration) { newValue in
poll.duration = newValue.timeInterval
}
#endif
}
private var backgroundColor: Color {
// in light mode, .secondarySystemBackground has a blue-ish hue, which we don't want
colorScheme == .dark ? controller.parent.config.fillColor : Color(white: 0.95)
}
private var buttonForegroundColor: Color {
Color(uiColor: .label)
}
private var buttonBackgroundColor: Color {
Color(white: colorScheme == .dark ? 0.1 : 0.8)
}
}
}
extension PollController {
enum Duration: Hashable, Equatable, CaseIterable {
case fiveMinutes, thirtyMinutes, oneHour, sixHours, oneDay, threeDays, sevenDays
static let formatter: DateComponentsFormatter = {
let f = DateComponentsFormatter()
f.maximumUnitCount = 1
f.unitsStyle = .full
f.allowedUnits = [.weekOfMonth, .day, .hour, .minute]
return f
}()
static func fromTimeInterval(_ ti: TimeInterval) -> Duration? {
for it in allCases where it.timeInterval == ti {
return it
}
return nil
}
var timeInterval: TimeInterval {
switch self {
case .fiveMinutes:
return 5 * 60
case .thirtyMinutes:
return 30 * 60
case .oneHour:
return 60 * 60
case .sixHours:
return 6 * 60 * 60
case .oneDay:
return 24 * 60 * 60
case .threeDays:
return 3 * 24 * 60 * 60
case .sevenDays:
return 7 * 24 * 60 * 60
}
}
}
}

View File

@ -0,0 +1,201 @@
//
// ToolbarController.swift
// ComposeUI
//
// Created by Shadowfacts on 3/7/23.
//
import SwiftUI
import Pachyderm
import TuskerComponents
class ToolbarController: ViewController {
static let height: CGFloat = 44
unowned let parent: ComposeController
@Published var minWidth: CGFloat?
@Published var realWidth: CGFloat?
init(parent: ComposeController) {
self.parent = parent
}
var view: some View {
ToolbarView()
}
func showEmojiPicker() {
guard parent.currentInput?.autocompleteState == nil else {
return
}
parent.shouldEmojiAutocompletionBeginExpanded = true
parent.currentInput?.beginAutocompletingEmoji()
}
func formatAction(_ format: StatusFormat) -> () -> Void {
{ [weak self] in
self?.parent.currentInput?.applyFormat(format)
}
}
struct ToolbarView: View {
@EnvironmentObject private var draft: Draft
@EnvironmentObject private var controller: ToolbarController
@EnvironmentObject private var composeController: ComposeController
@ScaledMetric(relativeTo: .body) private var imageSize: CGFloat = 22
#if !os(visionOS)
@State private var minWidth: CGFloat?
@State private var realWidth: CGFloat?
#endif
var body: some View {
#if os(visionOS)
buttons
#else
ScrollView(.horizontal, showsIndicators: false) {
buttons
.padding(.horizontal, 16)
.frame(minWidth: minWidth)
.background(GeometryReader { proxy in
Color.clear
.preference(key: ToolbarWidthPrefKey.self, value: proxy.size.width)
.onPreferenceChange(ToolbarWidthPrefKey.self) { width in
realWidth = width
}
})
}
.scrollDisabledIfAvailable(realWidth ?? 0 <= minWidth ?? 0)
.frame(height: ToolbarController.height)
.frame(maxWidth: .infinity)
.background(.regularMaterial, ignoresSafeAreaEdges: [.bottom, .leading, .trailing])
.overlay(alignment: .top) {
Divider()
.edgesIgnoringSafeArea([.leading, .trailing])
}
.background(GeometryReader { proxy in
Color.clear
.preference(key: ToolbarWidthPrefKey.self, value: proxy.size.width)
.onPreferenceChange(ToolbarWidthPrefKey.self) { width in
minWidth = width
}
})
#endif
}
@ViewBuilder
private var buttons: some View {
HStack(spacing: 0) {
cwButton
MenuPicker(selection: visibilityBinding, options: visibilityOptions, buttonStyle: .iconOnly)
#if !targetEnvironment(macCatalyst) && !os(visionOS)
// the button has a bunch of extra space by default, but combined with what we add it's too much
.padding(.horizontal, -8)
#endif
.disabled(draft.editedStatusID != nil)
.disabled(composeController.mastodonController.instanceFeatures.localOnlyPostsVisibility && draft.localOnly)
if composeController.mastodonController.instanceFeatures.localOnlyPosts {
localOnlyPicker
#if targetEnvironment(macCatalyst)
.padding(.leading, 4)
#elseif !os(visionOS)
.padding(.horizontal, -8)
#endif
.disabled(draft.editedStatusID != nil)
}
if let currentInput = composeController.currentInput,
currentInput.toolbarElements.contains(.emojiPicker) {
customEmojiButton
}
if let currentInput = composeController.currentInput,
currentInput.toolbarElements.contains(.formattingButtons),
composeController.config.contentType != .plain {
Spacer()
formatButtons
}
Spacer()
if #available(iOS 16.0, *),
composeController.mastodonController.instanceFeatures.createStatusWithLanguage {
LanguagePicker(draftLanguage: $draft.language, hasChangedSelection: $composeController.hasChangedLanguageSelection)
}
}
}
private var cwButton: some View {
Button("CW", action: controller.parent.toggleContentWarning)
.accessibilityLabel(draft.contentWarningEnabled ? "Remove content warning" : "Add content warning")
.padding(5)
.hoverEffect()
}
private var visibilityBinding: Binding<Pachyderm.Visibility> {
// On instances that conflate visibliity and local only, we still show two separate controls but don't allow
// changing the visibility when local-only.
if draft.localOnly,
composeController.mastodonController.instanceFeatures.localOnlyPostsVisibility {
return .constant(.public)
} else {
return $draft.visibility
}
}
private var visibilityOptions: [MenuPicker<Pachyderm.Visibility>.Option] {
let visibilities: [Pachyderm.Visibility]
if !composeController.mastodonController.instanceFeatures.composeDirectStatuses {
visibilities = [.public, .unlisted, .private]
} else {
visibilities = Pachyderm.Visibility.allCases
}
return visibilities.map { vis in
.init(value: vis, title: vis.displayName, subtitle: vis.subtitle, image: UIImage(systemName: vis.unfilledImageName), accessibilityLabel: "Visibility: \(vis.displayName)")
}
}
private var localOnlyPicker: some View {
let domain = composeController.mastodonController.accountInfo!.instanceURL.host!
return MenuPicker(selection: $draft.localOnly, options: [
.init(value: true, title: "Local-only", subtitle: "Only \(domain)", image: UIImage(named: "link.broken")),
.init(value: false, title: "Federated", image: UIImage(systemName: "link")),
], buttonStyle: .iconOnly)
}
private var customEmojiButton: some View {
Button(action: controller.showEmojiPicker) {
Label("Insert custom emoji", systemImage: "face.smiling")
}
.labelStyle(.iconOnly)
.font(.system(size: imageSize))
.padding(5)
.hoverEffect()
.transition(.opacity.animation(.linear(duration: 0.2)))
}
private var formatButtons: some View {
ForEach(StatusFormat.allCases, id: \.rawValue) { format in
Button(action: controller.formatAction(format)) {
Image(systemName: format.imageName)
.font(.system(size: imageSize))
}
.accessibilityLabel(format.accessibilityLabel)
.padding(5)
.hoverEffect()
.transition(.opacity.animation(.linear(duration: 0.2)))
}
}
}
}
private struct ToolbarWidthPrefKey: PreferenceKey {
static var defaultValue: CGFloat? = nil
static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
value = nextValue()
}
}

View File

@ -0,0 +1,73 @@
//
// Draft.swift
// ComposeUI
//
// Created by Shadowfacts on 4/22/23.
//
import CoreData
import Pachyderm
@objc
public class Draft: NSManagedObject, Identifiable {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Draft> {
return NSFetchRequest<Draft>(entityName: "Draft")
}
@nonobjc public class func fetchRequest(id: UUID) -> NSFetchRequest<Draft> {
let req = NSFetchRequest<Draft>(entityName: "Draft")
req.predicate = NSPredicate(format: "id = %@", id as NSUUID)
return req
}
@NSManaged public var accountID: String
@NSManaged public var contentWarning: String
@NSManaged public var contentWarningEnabled: Bool
@NSManaged public var editedStatusID: String?
@NSManaged public var id: UUID
@NSManaged public var initialContentWarning: String?
@NSManaged public var initialText: String
@NSManaged public var inReplyToID: String?
@NSManaged public var language: String? // ISO 639 language code
@NSManaged public var lastModified: Date!
@NSManaged public var localOnly: Bool
@NSManaged public var text: String
@NSManaged private var visibilityStr: String
@NSManaged internal var attachments: NSMutableOrderedSet
@NSManaged public var poll: Poll?
public var visibility: Visibility {
get {
Visibility(rawValue: visibilityStr) ?? .public
}
set {
visibilityStr = newValue.rawValue
}
}
public var draftAttachments: [DraftAttachment] {
get {
attachments.array as! [DraftAttachment]
}
set {
attachments = NSMutableOrderedSet(array: newValue)
}
}
public override func awakeFromInsert() {
super.awakeFromInsert()
id = UUID()
lastModified = Date()
}
}
extension Draft {
public var hasContent: Bool {
(!text.isEmpty && text != initialText) ||
(contentWarningEnabled && !contentWarning.isEmpty && contentWarning != initialContentWarning) ||
attachments.count > 0 ||
poll?.hasContent == true
}
}

View File

@ -0,0 +1,329 @@
//
// DraftAttachment.swift
// CoreData
//
// Created by Shadowfacts on 4/22/23.
//
import CoreData
import PencilKit
import UniformTypeIdentifiers
import Photos
import InstanceFeatures
import Pachyderm
private let decoder = PropertyListDecoder()
private let encoder = PropertyListEncoder()
@objc
public final class DraftAttachment: NSManagedObject, Identifiable {
@nonobjc public class func fetchRequest() -> NSFetchRequest<DraftAttachment> {
return NSFetchRequest<DraftAttachment>(entityName: "DraftAttachment")
}
@NSManaged internal var assetID: String?
@NSManaged public var attachmentDescription: String
@NSManaged internal private(set) var drawingData: Data?
@NSManaged public var editedAttachmentID: String?
@NSManaged private var editedAttachmentKindString: String?
@NSManaged public var editedAttachmentURL: URL?
@NSManaged public var fileURL: URL?
@NSManaged internal var fileType: String?
@NSManaged public var id: UUID!
@NSManaged internal var draft: Draft
public var drawing: PKDrawing? {
get {
if let drawingData,
let drawing = try? decoder.decode(PKDrawing.self, from: drawingData) {
return drawing
} else {
return nil
}
}
set {
drawingData = try! encoder.encode(newValue)
}
}
public var data: AttachmentData {
if let editedAttachmentID {
return .editing(editedAttachmentID, editedAttachmentKind!, editedAttachmentURL!)
} else if let assetID {
return .asset(assetID)
} else if let drawing {
return .drawing(drawing)
} else if let fileURL, let fileType {
return .file(fileURL, UTType(fileType)!)
} else {
return .none
}
}
public var editedAttachmentKind: Attachment.Kind? {
get {
editedAttachmentKindString.flatMap(Attachment.Kind.init(rawValue:))
}
set {
editedAttachmentKindString = newValue?.rawValue
}
}
public enum AttachmentData {
case asset(String)
case drawing(PKDrawing)
case file(URL, UTType)
case editing(String, Attachment.Kind, URL)
case none
}
public override func prepareForDeletion() {
super.prepareForDeletion()
if let fileURL {
try? FileManager.default.removeItem(at: fileURL)
}
}
}
extension DraftAttachment {
var type: AttachmentType {
if let editedAttachmentKind {
switch editedAttachmentKind {
case .image:
return .image
case .video:
return .video
case .gifv:
return .video
case .audio, .unknown:
return .unknown
}
} else if let assetID {
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetID], options: nil).firstObject else {
return .unknown
}
switch asset.mediaType {
case .image:
return .image
case .video:
return .video
default:
return .unknown
}
} else if drawingData != nil {
return .image
} else if let fileType,
let type = UTType(fileType) {
if type.conforms(to: .image) {
return .image
} else if type.conforms(to: .movie) {
return .video
} else {
return .unknown
}
} else {
return .unknown
}
}
enum AttachmentType {
case image, video, unknown
}
}
//private let attachmentTypeIdentifier = "space.vaccor.Tusker.composition-attachment"
private let imageType = UTType.image.identifier
private let heifType = UTType.heif.identifier
private let heicType = UTType.heic.identifier
private let jpegType = UTType.jpeg.identifier
private let pngType = UTType.png.identifier
private let mp4Type = UTType.mpeg4Movie.identifier
private let quickTimeType = UTType.quickTimeMovie.identifier
private let gifType = UTType.gif.identifier
extension DraftAttachment: NSItemProviderReading {
public static var readableTypeIdentifiersForItemProvider: [String] {
// todo: is there a better way of handling movies than manually adding all possible UTI types?
// just using kUTTypeMovie doesn't work, because we need the actually type in order to get the file extension
// without the file extension, getting the thumbnail and exporting the video for attachment upload fails
[/*typeIdentifier, */ gifType, heifType, heicType, jpegType, pngType, imageType, mp4Type, quickTimeType]
}
public static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> DraftAttachment {
var data = data
var type = UTType(typeIdentifier)!
// the type is .image in certain circumstances:
// - macOS: image copied from macOS Safari -> only UIImage(data: data) works
// - iOS: sharing screenshot from markup -> only NSKeyedUnarchiver works
if type == .image,
let image = UIImage(data: data) ?? (try? NSKeyedUnarchiver.unarchivedObject(ofClass: UIImage.self, from: data)),
let pngData = image.pngData() {
data = pngData
type = .png
}
let attachment = DraftAttachment(entity: DraftsPersistentContainer.shared.persistentStoreCoordinator.managedObjectModel.entitiesByName["DraftAttachment"]!, insertInto: nil)
attachment.id = UUID()
attachment.fileURL = try writeDataToFile(data, id: attachment.id, type: type)
attachment.fileType = type.identifier
attachment.attachmentDescription = ""
return attachment
}
static var attachmentsDirectory: URL {
let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.space.vaccor.Tusker")!
return containerURL.appendingPathComponent("Documents").appendingPathComponent("attachments")
}
static func writeDataToFile(_ data: Data, id: UUID, type: UTType) throws -> URL {
let directoryURL = attachmentsDirectory
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true)
let attachmentURL = directoryURL.appendingPathComponent(id.uuidString, conformingTo: type)
try data.write(to: attachmentURL)
return attachmentURL
}
}
// MARK: Exporting
extension DraftAttachment {
func getData(features: InstanceFeatures, skipAllConversion: Bool = false, completion: @escaping (Result<(Data, UTType), ExportError>) -> Void) {
if let assetID {
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetID], options: nil).firstObject else {
completion(.failure(.noAsset))
return
}
if asset.mediaType == .image {
let options = PHImageRequestOptions()
options.version = .current
options.deliveryMode = .highQualityFormat
options.resizeMode = .none
options.isNetworkAccessAllowed = true
PHImageManager.default().requestImageDataAndOrientation(for: asset, options: options) { data, dataUTI, orientation, info in
guard let data, let dataUTI else {
completion(.failure(.missingAssetData))
return
}
let processed = Self.processImageData(data, type: UTType(dataUTI)!, features: features, skipAllConversion: skipAllConversion)
completion(.success(processed))
}
} else if asset.mediaType == .video {
let options = PHVideoRequestOptions()
options.version = .current
options.deliveryMode = .automatic
options.isNetworkAccessAllowed = true
PHImageManager.default().requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetHighestQuality) { exportSession, info in
if let exportSession {
Self.exportVideoData(session: exportSession, features: features, completion: completion)
} else if let error = info?[PHImageErrorKey] as? Error {
completion(.failure(.videoExport(error)))
} else {
completion(.failure(.noVideoExportSession))
}
}
} else {
completion(.failure(.unknownAssetType))
}
} else if let drawingData {
guard let drawing = try? decoder.decode(PKDrawing.self, from: drawingData) else {
completion(.failure(.loadingDrawing))
return
}
let image = drawing.imageInLightMode(from: drawing.bounds, scale: 1)
completion(.success((image.pngData()!, .png)))
} else if let fileURL, let fileType {
let type = UTType(fileType)!
if type.conforms(to: .movie) {
let asset = AVURLAsset(url: fileURL)
guard let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {
completion(.failure(.noVideoExportSession))
return
}
Self.exportVideoData(session: session, features: features, completion: completion)
} else {
let fileData: Data
do {
fileData = try Data(contentsOf: fileURL)
} catch {
completion(.failure(.loadingData))
return
}
if type != .gif,
type.conforms(to: .image) {
let result = Self.processImageData(fileData, type: type, features: features, skipAllConversion: skipAllConversion)
completion(.success(result))
} else {
completion(.success((fileData, type)))
}
}
} else {
completion(.failure(.noData))
}
}
private static func processImageData(_ data: Data, type: UTType, features: InstanceFeatures, skipAllConversion: Bool) -> (Data, UTType) {
guard !skipAllConversion else {
return (data, type)
}
var data = data
var type = type
let image = CIImage(data: data)!
let needsColorSpaceConversion = features.needsWideColorGamutHack && image.colorSpace?.name != CGColorSpace.sRGB
// neither Mastodon nor Pleroma handles HEIC well, so convert to JPEG
// they also do a bad job converting wide color gamut images (they seem to just drop the profile, letting the wide-gamut values be reinterprete as sRGB)
// if that changes in the future, we'll need to pass the InstanceFeatures in here somehow and gate the conversion
if needsColorSpaceConversion || type == .heic || type == .heif {
let context = CIContext()
let colorSpace = needsColorSpaceConversion || image.colorSpace != nil ? CGColorSpace(name: CGColorSpace.sRGB)! : image.colorSpace!
if type == .png {
data = context.pngRepresentation(of: image, format: .ARGB8, colorSpace: colorSpace)!
} else {
data = context.jpegRepresentation(of: image, colorSpace: colorSpace)!
type = .jpeg
}
}
return (data, type)
}
private static func exportVideoData(session: AVAssetExportSession, features: InstanceFeatures, completion: @escaping (Result<(Data, UTType), ExportError>) -> Void) {
session.outputFileType = .mp4
session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4")
if let configuration = features.mediaAttachmentsConfiguration {
session.fileLengthLimit = Int64(configuration.videoSizeLimit)
}
session.exportAsynchronously {
guard session.status == .completed else {
completion(.failure(.videoExport(session.error!)))
return
}
do {
let data = try Data(contentsOf: session.outputURL!)
completion(.success((data, .mpeg4Movie)))
} catch {
completion(.failure(.videoExport(error)))
}
}
}
enum ExportError: Error {
case noAsset
case unknownAssetType
case missingAssetData
case videoExport(Error)
case noVideoExportSession
case loadingDrawing
case loadingData
case noData
}
}

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22222" systemVersion="22G91" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Draft" representedClassName="ComposeUI.Draft" syncable="YES">
<attribute name="accountID" attributeType="String"/>
<attribute name="contentWarning" attributeType="String" defaultValueString=""/>
<attribute name="contentWarningEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="editedStatusID" optional="YES" attributeType="String"/>
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="initialContentWarning" optional="YES" attributeType="String"/>
<attribute name="initialText" attributeType="String"/>
<attribute name="inReplyToID" optional="YES" attributeType="String"/>
<attribute name="language" optional="YES" attributeType="String"/>
<attribute name="lastModified" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="localOnly" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="text" attributeType="String" defaultValueString=""/>
<attribute name="visibilityStr" optional="YES" attributeType="String"/>
<relationship name="attachments" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="DraftAttachment" inverseName="draft" inverseEntity="DraftAttachment"/>
<relationship name="poll" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="Poll" inverseName="draft" inverseEntity="Poll"/>
</entity>
<entity name="DraftAttachment" representedClassName="ComposeUI.DraftAttachment" syncable="YES">
<attribute name="assetID" optional="YES" attributeType="String"/>
<attribute name="attachmentDescription" attributeType="String" defaultValueString=""/>
<attribute name="drawingData" optional="YES" attributeType="Binary"/>
<attribute name="editedAttachmentID" optional="YES" attributeType="String"/>
<attribute name="editedAttachmentKindString" optional="YES" attributeType="String"/>
<attribute name="editedAttachmentURL" optional="YES" attributeType="URI"/>
<attribute name="fileType" optional="YES" attributeType="String"/>
<attribute name="fileURL" optional="YES" attributeType="URI"/>
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
<relationship name="draft" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Draft" inverseName="attachments" inverseEntity="Draft"/>
</entity>
<entity name="Poll" representedClassName="ComposeUI.Poll" syncable="YES">
<attribute name="duration" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="multiple" attributeType="Boolean" usesScalarValueType="YES"/>
<relationship name="draft" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Draft" inverseName="poll" inverseEntity="Draft"/>
<relationship name="options" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="PollOption" inverseName="poll" inverseEntity="PollOption"/>
</entity>
<entity name="PollOption" representedClassName="ComposeUI.PollOption" syncable="YES">
<attribute name="text" attributeType="String" defaultValueString=""/>
<relationship name="poll" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Poll" inverseName="options" inverseEntity="Poll"/>
</entity>
</model>

View File

@ -0,0 +1,214 @@
//
// DraftsPersistentContainer.swift
// ComposeUI
//
// Created by Shadowfacts on 4/22/23.
//
import Foundation
import CoreData
import OSLog
import Pachyderm
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "DraftsPersistentContainer")
public class DraftsPersistentContainer: NSPersistentContainer {
public static let shared = DraftsPersistentContainer()
private static let managedObjectModel: NSManagedObjectModel = {
let url = Bundle.module.url(forResource: "Drafts", withExtension: "momd")!
return NSManagedObjectModel(contentsOf: url)!
}()
private var lastHistoryToken: NSPersistentHistoryToken!
init() {
super.init(name: "Drafts", managedObjectModel: DraftsPersistentContainer.managedObjectModel)
let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.space.vaccor.Tusker")!
let documentsURL = containerURL.appendingPathComponent("Documents")
let storeDesc = NSPersistentStoreDescription(url: documentsURL.appendingPathComponent("drafts").appendingPathExtension("sqlite"))
storeDesc.type = NSSQLiteStoreType
storeDesc.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
storeDesc.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
persistentStoreDescriptions = [
storeDesc
]
loadPersistentStores { _, error in
if let error {
fatalError("Loading persistent store: \(error)")
}
}
viewContext.automaticallyMergesChangesFromParent = true
viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
lastHistoryToken = persistentStoreCoordinator.currentPersistentHistoryToken(fromStores: nil)
NotificationCenter.default.addObserver(self, selector: #selector(remoteChanges(_:)), name: .NSPersistentStoreRemoteChange, object: persistentStoreCoordinator)
}
public func save() {
guard viewContext.hasChanges else {
return
}
do {
try viewContext.save()
} catch {
logger.error("Failed to save: \(String(describing: error))")
}
}
public func migrate(from url: URL, completion: @escaping (Result<(), any Error>) -> Void) {
performBackgroundTask { context in
let result = DraftsMigrator.migrate(from: url, to: context)
completion(result)
try! context.save()
}
}
public func getDraft(id: UUID) -> Draft? {
let req = Draft.fetchRequest(id: id)
return try? viewContext.fetch(req).first
}
public func createDraft(
accountID: String,
text: String,
contentWarning: String,
inReplyToID: String?,
visibility: Visibility,
language: String?,
localOnly: Bool
) -> Draft {
let draft = Draft(context: viewContext)
draft.accountID = accountID
draft.text = text
draft.initialText = text
draft.contentWarning = contentWarning
draft.initialContentWarning = contentWarning
draft.contentWarningEnabled = !contentWarning.isEmpty
draft.inReplyToID = inReplyToID
draft.visibility = visibility
draft.language = language
draft.localOnly = localOnly
save()
return draft
}
public func createEditDraft(
accountID: String,
source: StatusSource,
inReplyToID: String?,
visibility: Visibility,
localOnly: Bool,
attachments: [Attachment],
poll: Pachyderm.Poll?
) -> Draft {
let draft = Draft(context: viewContext)
draft.accountID = accountID
draft.editedStatusID = source.id
draft.text = source.text
draft.initialText = source.text
draft.contentWarning = source.spoilerText
draft.contentWarningEnabled = !source.spoilerText.isEmpty
draft.initialContentWarning = source.spoilerText
draft.inReplyToID = inReplyToID
draft.visibility = visibility
draft.localOnly = localOnly
for attachment in attachments {
createEditDraftAttachment(attachment, in: draft)
}
if let existingPoll = poll {
let poll = Poll(context: viewContext)
poll.draft = draft
draft.poll = poll
if let expiresAt = existingPoll.expiresAt,
!existingPoll.effectiveExpired {
poll.duration = PollController.Duration.allCases.max(by: {
(expiresAt.timeIntervalSinceNow - $0.timeInterval) < (expiresAt.timeIntervalSinceNow - $1.timeInterval)
})!.timeInterval
} else {
poll.duration = PollController.Duration.oneDay.timeInterval
}
poll.multiple = existingPoll.multiple
// rmeove default empty options
for opt in poll.pollOptions {
viewContext.delete(opt)
}
for existingOpt in existingPoll.options {
let opt = PollOption(context: viewContext)
opt.poll = poll
poll.options.add(opt)
opt.text = existingOpt.title
}
}
save()
return draft
}
private func createEditDraftAttachment(_ attachment: Attachment, in draft: Draft) {
let draftAttachment = DraftAttachment(context: viewContext)
draftAttachment.id = UUID()
draftAttachment.attachmentDescription = attachment.description ?? ""
draftAttachment.editedAttachmentID = attachment.id
draftAttachment.editedAttachmentKind = attachment.kind
draftAttachment.editedAttachmentURL = attachment.url
draftAttachment.draft = draft
draft.attachments.add(draftAttachment)
}
public func removeOrphanedAttachments(completion: @escaping () -> Void) {
guard let files = try? FileManager.default.contentsOfDirectory(at: DraftAttachment.attachmentsDirectory, includingPropertiesForKeys: nil),
!files.isEmpty else {
return
}
performBackgroundTask { context in
let allAttachmentsReq = DraftAttachment.fetchRequest()
allAttachmentsReq.predicate = NSPredicate(format: "fileURL != nil")
guard let allAttachments = try? context.fetch(allAttachmentsReq) else {
return
}
let orphaned = Set(files).subtracting(allAttachments.lazy.compactMap(\.fileURL))
for url in orphaned {
do {
try FileManager.default.removeItem(at: url)
} catch {
logger.error("Failed to remove orphaned attachment: \(String(describing: error), privacy: .public)")
}
}
completion()
}
}
@objc private func remoteChanges(_ notification: Foundation.Notification) {
guard let newHistoryToken = notification.userInfo?[NSPersistentHistoryTokenKey] as? NSPersistentHistoryToken else {
return
}
// todo: should this be on a background context?
let context = viewContext
context.perform {
let predicate = NSPredicate(format: "(%@ < token) AND (token <= %@)", self.lastHistoryToken, newHistoryToken)
let historyRequest = NSPersistentHistoryTransaction.fetchRequest!
historyRequest.predicate = predicate
let request = NSPersistentHistoryChangeRequest.fetchHistory(withFetch: historyRequest)
if let result = try? context.execute(request) as? NSPersistentHistoryResult,
let transactions = result.result as? [NSPersistentHistoryTransaction] {
for transaction in transactions {
guard let userInfo = transaction.objectIDNotification().userInfo else {
continue
}
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: userInfo, into: [context])
}
}
self.lastHistoryToken = newHistoryToken
}
}
}

View File

@ -0,0 +1,46 @@
//
// Poll.swift
// ComposeUI
//
// Created by Shadowfacts on 4/22/23.
//
import CoreData
@objc
public class Poll: NSManagedObject {
@NSManaged public var duration: TimeInterval
@NSManaged public var multiple: Bool
@NSManaged public var draft: Draft
@NSManaged public var options: NSMutableOrderedSet
public var pollOptions: [PollOption] {
get {
options.array as! [PollOption]
}
set {
options = NSMutableOrderedSet(array: newValue)
}
}
public override func awakeFromInsert() {
super.awakeFromInsert()
self.multiple = false
self.duration = 24 * 60 * 60 // 1 day
if let managedObjectContext {
self.options = [
PollOption(context: managedObjectContext),
PollOption(context: managedObjectContext),
]
}
}
}
extension Poll {
public var hasContent: Bool {
pollOptions.allSatisfy { !$0.text.isEmpty }
}
}

View File

@ -0,0 +1,21 @@
//
// PollOption.swift
// ComposeUI
//
// Created by Shadowfacts on 4/22/23.
//
import CoreData
@objc
public class PollOption: NSManagedObject, Identifiable {
public var id: NSManagedObjectID {
objectID
}
@NSManaged public var text: String
@NSManaged public var poll: Poll
}

View File

@ -0,0 +1,255 @@
//
// DraftsMigrator.swift
// ComposeUI
//
// Created by Shadowfacts on 4/22/23.
//
import Foundation
import OSLog
import UniformTypeIdentifiers
import Pachyderm
import PencilKit
import CoreData
struct DraftsMigrator {
private init() {}
private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "DraftsMigrator")
private static let decoder = PropertyListDecoder()
static func migrate(from url: URL, to context: NSManagedObjectContext) -> Result<(), any Error> {
do {
let data = try Data(contentsOf: url)
let container = try decoder.decode(DraftsContainer.self, from: data)
for old in container.drafts.values {
let new = Draft(context: context)
new.id = old.id
new.lastModified = old.lastModified
new.accountID = old.accountID
new.text = old.text
new.contentWarningEnabled = old.contentWarningEnabled
new.contentWarning = old.contentWarning
new.inReplyToID = old.inReplyToID
new.visibility = old.visibility
new.localOnly = old.localOnly
new.initialText = old.initialText
if let oldPoll = old.poll {
let newPoll = Poll(context: context)
newPoll.draft = new
new.poll = newPoll
newPoll.multiple = oldPoll.multiple
newPoll.duration = oldPoll.duration
for oldOption in oldPoll.options {
let newOption = PollOption(context: context)
newOption.text = oldOption.text
newOption.poll = newPoll
newPoll.options.add(newOption)
}
}
for oldAttachment in old.attachments {
let newAttachment = DraftAttachment(context: context)
newAttachment.draft = new
new.attachments.add(newAttachment)
newAttachment.id = oldAttachment.id
newAttachment.attachmentDescription = oldAttachment.attachmentDescription
switch oldAttachment.data {
case .asset(let assetID):
newAttachment.assetID = assetID
case .image(let data, originalType: let type):
newAttachment.fileURL = try? DraftAttachment.writeDataToFile(data, id: newAttachment.id, type: type)
newAttachment.fileType = type.identifier
case .video(_):
fatalError("unreachable, video attachments weren't encodable")
case .drawing(let drawing):
newAttachment.drawing = drawing
case .gif(let data):
newAttachment.fileURL = try? DraftAttachment.writeDataToFile(data, id: newAttachment.id, type: .gif)
newAttachment.fileType = UTType.gif.identifier
}
}
}
try FileManager.default.removeItem(at: url)
} catch {
logger.error("Error migrating: \(String(describing: error))")
return .failure(error)
}
return .success(())
}
// MARK: Supporting Types
struct DraftsContainer: Decodable {
let drafts: [UUID: OldDraft]
init(drafts: [UUID: OldDraft]) {
self.drafts = drafts
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.drafts = try container.decode([UUID: SafeDraft].self, forKey: .drafts).compactMapValues(\.draft)
}
enum CodingKeys: CodingKey {
case drafts
}
}
// a container that always succeeds at decoding
// so if a single draft can't be decoded, we don't lose all drafts
struct SafeDraft: Decodable {
let draft: OldDraft?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.draft = try? container.decode(OldDraft.self)
}
}
struct OldDraft: Decodable {
let id: UUID
let lastModified: Date
let accountID: String
let text: String
let contentWarningEnabled: Bool
let contentWarning: String
let attachments: [OldDraftAttachment]
let inReplyToID: String?
let visibility: Visibility
let poll: OldPoll?
let localOnly: Bool
let initialText: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(UUID.self, forKey: .id)
self.lastModified = try container.decode(Date.self, forKey: .lastModified)
self.accountID = try container.decode(String.self, forKey: .accountID)
self.text = try container.decode(String.self, forKey: .text)
self.contentWarningEnabled = try container.decode(Bool.self, forKey: .contentWarningEnabled)
self.contentWarning = try container.decode(String.self, forKey: .contentWarning)
self.attachments = try container.decode([OldDraftAttachment].self, forKey: .attachments)
self.inReplyToID = try container.decode(String?.self, forKey: .inReplyToID)
self.visibility = try container.decode(Visibility.self, forKey: .visibility)
self.poll = try container.decode(OldPoll?.self, forKey: .poll)
self.localOnly = try container.decodeIfPresent(Bool.self, forKey: .localOnly) ?? false
self.initialText = try container.decode(String.self, forKey: .initialText)
}
enum CodingKeys: String, CodingKey {
case id
case lastModified
case accountID
case text
case contentWarningEnabled
case contentWarning
case attachments
case inReplyToID
case visibility
case poll
case localOnly
case initialText
}
}
struct OldDraftAttachment: Decodable {
let id: UUID
let data: OldDraftAttachmentData
let attachmentDescription: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(UUID.self, forKey: .id)
self.data = try container.decode(OldDraftAttachmentData.self, forKey: .data)
self.attachmentDescription = try container.decode(String.self, forKey: .attachmentDescription)
}
enum CodingKeys: String, CodingKey {
case id
case data
case attachmentDescription
}
}
enum OldDraftAttachmentData: Decodable {
case asset(String)
case image(Data, originalType: UTType)
case video(URL)
case drawing(PKDrawing)
case gif(Data)
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
switch try container.decode(String.self, forKey: .type) {
case "asset":
let identifier = try container.decode(String.self, forKey: .assetIdentifier)
self = .asset(identifier)
case "image":
let data = try container.decode(Data.self, forKey: .imageData)
if let type = try container.decodeIfPresent(UTType.self, forKey: .imageType) {
self = .image(data, originalType: type)
} else {
guard let image = UIImage(data: data) else {
throw DecodingError.dataCorruptedError(forKey: .imageData, in: container, debugDescription: "CompositionAttachment data could not be decoded into UIImage")
}
let jpegData = image.jpegData(compressionQuality: 1)!
self = .image(jpegData, originalType: .jpeg)
}
case "drawing":
let drawingData = try container.decode(Data.self, forKey: .drawing)
let drawing = try PKDrawing(data: drawingData)
self = .drawing(drawing)
default:
throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "CompositionAttachment type must be one of image, asset, or drawing")
}
}
enum CodingKeys: CodingKey {
case type
case imageData
case imageType
/// The local identifier of the PHAsset for this attachment
case assetIdentifier
/// The PKDrawing object for this attachment.
case drawing
}
}
struct OldPoll: Decodable {
let options: [OldPollOption]
let multiple: Bool
let duration: TimeInterval
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.options = try container.decode([OldPollOption].self, forKey: .options)
self.multiple = try container.decode(Bool.self, forKey: .multiple)
self.duration = try container.decode(TimeInterval.self, forKey: .duration)
}
enum CodingKeys: String, CodingKey {
case options
case multiple
case duration
}
}
struct OldPollOption: Decodable {
let text: String
init(from decoder: Decoder) throws {
self.text = try decoder.singleValueContainer().decode(String.self)
}
}
}

View File

@ -0,0 +1,43 @@
//
// KeyboardReader.swift
// ComposeUI
//
// Created by Shadowfacts on 3/7/23.
//
#if !os(visionOS)
import UIKit
import Combine
@available(iOS, obsoleted: 16.0)
class KeyboardReader: ObservableObject {
// @Published var isVisible = false
@Published var keyboardHeight: CGFloat = 0
var isVisible: Bool {
// when a hardware keyboard is connected, the height is very short, so we don't consider that being "visible"
keyboardHeight > 72
}
init() {
NotificationCenter.default.addObserver(self, selector: #selector(willShow), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willHide), name: UIResponder.keyboardWillHideNotification, object: nil)
}
@objc func willShow(_ notification: Foundation.Notification) {
let endFrame = notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect
// isVisible = endFrame.height > 72
keyboardHeight = endFrame.height
}
@objc func willHide() {
// sometimes willHide is called during a SwiftUI view update
DispatchQueue.main.async {
// self.isVisible = false
self.keyboardHeight = 0
}
}
}
#endif

View File

@ -0,0 +1,12 @@
//
// DismissMode.swift
// ComposeUI
//
// Created by Shadowfacts on 3/7/23.
//
import Foundation
public enum DismissMode {
case cancel, post
}

View File

@ -1,19 +1,19 @@
//
// StatusFormat.swift
// Tusker
// ComposeUI
//
// Created by Shadowfacts on 1/12/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import Foundation
import UIKit
import Pachyderm
enum StatusFormat: CaseIterable {
case italics, bold, strikethrough, code
enum StatusFormat: Int, CaseIterable {
case bold, italics, strikethrough, code
var insertionResult: FormatInsertionResult? {
switch Preferences.shared.statusContentType {
func insertionResult(for contentType: StatusContentType) -> FormatInsertionResult? {
switch contentType {
case .plain:
return nil
case .markdown:
@ -23,26 +23,16 @@ enum StatusFormat: CaseIterable {
}
}
var image: UIImage? {
let name: String
var imageName: String {
switch self {
case .italics:
name = "italic"
return "italic"
case .bold:
name = "bold"
return "bold"
case .strikethrough:
name = "strikethrough"
default:
return nil
}
return UIImage(systemName: name)
}
var title: (String, [NSAttributedString.Key: Any])? {
if self == .code {
return ("</>", [.font: UIFont(name: "Menlo", size: 17)!])
} else {
return nil
return "strikethrough"
case .code:
return "chevron.left.forwardslash.chevron.right"
}
}
@ -62,7 +52,7 @@ enum StatusFormat: CaseIterable {
typealias FormatInsertionResult = (prefix: String, suffix: String, insertionPoint: Int)
protocol FormatType {
fileprivate protocol FormatType {
static func format(_ format: StatusFormat) -> FormatInsertionResult
}

View File

@ -0,0 +1,33 @@
//
// OptionalObservedObject.swift
// ComposeUI
//
// Created by Shadowfacts on 4/15/23.
//
import SwiftUI
import Combine
@propertyWrapper
struct OptionalObservedObject<T: ObservableObject>: DynamicProperty {
private class Republisher: ObservableObject {
var cancellable: AnyCancellable?
var wrapped: T? {
didSet {
cancellable?.cancel()
cancellable = wrapped?.objectWillChange
.receive(on: RunLoop.main)
.sink { [unowned self] _ in
self.objectWillChange.send()
}
}
}
}
@StateObject private var republisher = Republisher()
var wrappedValue: T?
func update() {
republisher.wrapped = wrappedValue
}
}

View File

@ -0,0 +1,33 @@
//
// PKDrawing+Render.swift
// ComposeUI
//
// Created by Shadowfacts on 5/9/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
import PencilKit
extension PKDrawing {
func imageInLightMode(from rect: CGRect, scale: CGFloat = 1) -> UIImage {
let lightTraitCollection = UITraitCollection(userInterfaceStyle: .light)
var drawingImage: UIImage!
lightTraitCollection.performAsCurrent {
drawingImage = self.image(from: rect, scale: scale)
}
let imageRect = CGRect(origin: .zero, size: rect.size)
let format = UIGraphicsImageRendererFormat()
format.opaque = false
format.scale = scale
let renderer = UIGraphicsImageRenderer(size: rect.size, format: format)
return renderer.image { (context) in
UIColor.white.setFill()
context.fill(imageRect)
drawingImage.draw(in: imageRect)
}
}
}

View File

@ -0,0 +1,60 @@
//
// TextViewCaretScrolling.swift
// Tusker
//
// Created by Shadowfacts on 11/11/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
protocol TextViewCaretScrolling: AnyObject {
var caretScrollPositionAnimator: UIViewPropertyAnimator? { get set }
}
extension TextViewCaretScrolling {
func ensureCursorVisible(textView: UITextView) {
guard textView.isFirstResponder,
let range = textView.selectedTextRange,
let scrollView = findParentScrollView(of: textView) else {
return
}
// We use a UIViewProperty animator to change the scroll view position so that we can store the currently
// running one on the Coordinator. This allows us to cancel the running one, preventing multiple animations
// from attempting to change the scroll view offset simultaneously, causing it to jitter around. This can
// happen if the user is pressing return and quickly creating many new lines.
if let existing = caretScrollPositionAnimator {
existing.stopAnimation(true)
}
let cursorRect = textView.caretRect(for: range.start)
var rectToMakeVisible = textView.convert(cursorRect, to: scrollView)
// expand the rect to be three times the cursor height centered on the cursor so that there's
// some space between the bottom of the line of text being edited and the top of the keyboard
rectToMakeVisible.origin.y -= cursorRect.height
rectToMakeVisible.size.height *= 3
let animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeInOut) {
scrollView.scrollRectToVisible(rectToMakeVisible, animated: false)
}
self.caretScrollPositionAnimator = animator
animator.startAnimation()
}
private func findParentScrollView(of view: UIView) -> UIScrollView? {
var current: UIView = view
while let superview = current.superview {
if let scrollView = superview as? UIScrollView,
scrollView.isScrollEnabled {
return scrollView
} else {
current = superview
}
}
return nil
}
}

View File

@ -0,0 +1,183 @@
//
// UITextInput+Autocomplete.swift
// ComposeUI
//
// Created by Shadowfacts on 3/5/23.
//
import UIKit
import SwiftUI
extension UITextInput {
func autocomplete(with string: String, permittedModes: AutocompleteModes, autocompleteState: inout AutocompleteState?) {
guard let selectedTextRange,
let wholeDocumentRange = self.textRange(from: self.beginningOfDocument, to: self.endOfDocument),
let text = self.text(in: wholeDocumentRange),
let (lastWordStartIndex, _) = findAutocompleteLastWord() else {
return
}
let distanceToEnd = self.offset(from: selectedTextRange.start, to: self.endOfDocument)
let selectedRangeStartUTF16 = self.offset(from: self.beginningOfDocument, to: selectedTextRange.start)
let characterBeforeCursorIndex = text.utf16.index(text.startIndex, offsetBy: selectedRangeStartUTF16)
let insertSpace: Bool
if distanceToEnd > 0 {
let charAfterCursor = text[characterBeforeCursorIndex]
insertSpace = charAfterCursor != " " && charAfterCursor != "\n"
} else {
insertSpace = true
}
let string = insertSpace ? string + " " : string
let startPosition = self.position(from: self.beginningOfDocument, offset: text.utf16.distance(from: text.startIndex, to: lastWordStartIndex))!
let lastWordRange = self.textRange(from: startPosition, to: selectedTextRange.start)!
replace(lastWordRange, withText: string)
autocompleteState = updateAutocompleteState(permittedModes: permittedModes)
// keep the cursor at the same position in the text, immediately after what was inserted
// if we inserted a space, move the cursor 1 farther so it's immediately after the pre-existing space
let insertSpaceOffset = insertSpace ? 0 : 1
let newCursorPosition = self.position(from: self.endOfDocument, offset: -distanceToEnd + insertSpaceOffset)!
self.selectedTextRange = self.textRange(from: newCursorPosition, to: newCursorPosition)
}
func updateAutocompleteState(permittedModes: AutocompleteModes) -> AutocompleteState? {
guard let selectedTextRange,
let wholeDocumentRange = self.textRange(from: self.beginningOfDocument, to: self.endOfDocument),
let text = self.text(in: wholeDocumentRange),
!text.isEmpty,
let (lastWordStartIndex, foundFirstAtSign) = findAutocompleteLastWord() else {
return nil
}
let triggerChars = permittedModes.triggerChars
if lastWordStartIndex > text.startIndex {
// if the character before the "word" beginning is a valid part of a "word",
// we aren't able to autocomplete
let c = text[text.index(before: lastWordStartIndex)]
if isPermittedForAutocomplete(c) || triggerChars.contains(c) {
return nil
}
}
let characterBeforeCursorIndex = text.utf16.index(text.startIndex, offsetBy: self.offset(from: self.beginningOfDocument, to: selectedTextRange.start))
if lastWordStartIndex >= text.startIndex {
let lastWord = text[lastWordStartIndex..<characterBeforeCursorIndex]
let exceptFirst = lastWord[lastWord.index(after: lastWord.startIndex)...]
// periods are only allowed in mentions in the domain part
if lastWord.contains(".") {
if lastWord.first == "@" && foundFirstAtSign && permittedModes.contains(.mentions) {
return .mention(String(exceptFirst))
} else {
return nil
}
}
switch lastWord.first {
case "@" where permittedModes.contains(.mentions):
return .mention(String(exceptFirst))
case ":" where permittedModes.contains(.emojis):
return .emoji(String(exceptFirst))
case "#" where permittedModes.contains(.hashtags):
return .hashtag(String(exceptFirst))
default:
return nil
}
} else {
return nil
}
}
private func findAutocompleteLastWord() -> (index: String.Index, foundFirstAtSign: Bool)? {
guard (self as? UIView)?.isFirstResponder == true,
let selectedTextRange,
selectedTextRange.isEmpty,
let wholeDocumentRange = self.textRange(from: self.beginningOfDocument, to: self.endOfDocument),
let text = self.text(in: wholeDocumentRange),
!text.isEmpty else {
return nil
}
let selectedRangeStartUTF16 = self.offset(from: self.beginningOfDocument, to: selectedTextRange.start)
let cursorIndex = text.utf16.index(text.startIndex, offsetBy: selectedRangeStartUTF16)
guard cursorIndex != text.startIndex else {
return nil
}
var lastWordStartIndex = text.index(before: cursorIndex)
var foundFirstAtSign = false
while true {
let c = text[lastWordStartIndex]
if !isPermittedForAutocomplete(c) {
if foundFirstAtSign {
if c != "@" {
// move the index forward by 1, so that the first char of the substring is the 1st @ instead of whatever comes before it
lastWordStartIndex = text.index(after: lastWordStartIndex)
}
break
} else {
if c == "@" {
foundFirstAtSign = true
} else if c != "." {
// periods are allowed for domain names in mentions
break
}
}
}
guard lastWordStartIndex > text.startIndex else {
break
}
lastWordStartIndex = text.index(before: lastWordStartIndex)
}
return (lastWordStartIndex, foundFirstAtSign)
}
}
enum AutocompleteState: Equatable {
case mention(String)
case emoji(String)
case hashtag(String)
}
struct AutocompleteModes: OptionSet {
static let mentions = AutocompleteModes(rawValue: 1 << 0)
static let hashtags = AutocompleteModes(rawValue: 1 << 2)
static let emojis = AutocompleteModes(rawValue: 1 << 3)
static let all: AutocompleteModes = [
.mentions,
.hashtags,
.emojis,
]
let rawValue: Int
var triggerChars: [Character] {
var chars: [Character] = []
if contains(.mentions) {
chars.append("@")
}
if contains(.hashtags) {
chars.append("#")
}
if contains(.emojis) {
chars.append(":")
}
return chars
}
}
private func isPermittedForAutocomplete(_ c: Character) -> Bool {
return (c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || (c >= "0" && c <= "9") || c == "_"
}

View File

@ -0,0 +1,26 @@
//
// View+ForwardsCompat.swift
// ComposeUI
//
// Created by Shadowfacts on 3/25/23.
//
import SwiftUI
extension View {
#if os(visionOS)
func scrollDisabledIfAvailable(_ disabled: Bool) -> some View {
self.scrollDisabled(disabled)
}
#else
@available(iOS, obsoleted: 16.0)
@ViewBuilder
func scrollDisabledIfAvailable(_ disabled: Bool) -> some View {
if #available(iOS 16.0, *) {
self.scrollDisabled(disabled)
} else {
self
}
}
#endif
}

View File

@ -0,0 +1,29 @@
//
// ViewController.swift
// ComposeUI
//
// Created by Shadowfacts on 3/4/23.
//
import SwiftUI
import Combine
public protocol ViewController: ObservableObject {
associatedtype ContentView: View
@ViewBuilder
var view: ContentView { get }
}
public struct ControllerView<Controller: ViewController>: View {
@StateObject private var controller: Controller
public init(controller: @escaping () -> Controller) {
self._controller = StateObject(wrappedValue: controller())
}
public var body: some View {
controller.view
.environmentObject(controller)
}
}

View File

@ -0,0 +1,151 @@
//
// AttachmentDescriptionTextView.swift
// ComposeUI
//
// Created by Shadowfacts on 3/12/23.
//
import SwiftUI
private var placeholder: some View {
Text("Describe for the visually impaired…")
}
struct InlineAttachmentDescriptionView: View {
@ObservedObject private var attachment: DraftAttachment
private let minHeight: CGFloat
@State private var height: CGFloat?
init(attachment: DraftAttachment, minHeight: CGFloat) {
self.attachment = attachment
self.minHeight = minHeight
}
private var placeholderOffset: CGSize {
#if os(visionOS)
CGSize(width: 8, height: 8)
#else
CGSize(width: 4, height: 8)
#endif
}
var body: some View {
ZStack(alignment: .topLeading) {
if attachment.attachmentDescription.isEmpty {
placeholder
.font(.body)
.foregroundColor(.secondary)
.offset(placeholderOffset)
}
WrappedTextView(
text: $attachment.attachmentDescription,
backgroundColor: .clear,
textDidChange: self.textDidChange
)
.frame(height: height ?? minHeight)
}
}
private func textDidChange(_ textView: UITextView) {
height = max(minHeight, textView.contentSize.height)
}
}
struct FocusedAttachmentDescriptionView: View {
@ObservedObject var attachment: DraftAttachment
var body: some View {
ZStack(alignment: .topLeading) {
WrappedTextView(
text: $attachment.attachmentDescription,
backgroundColor: .secondarySystemBackground,
textDidChange: nil
)
.edgesIgnoringSafeArea([.bottom, .leading, .trailing])
if attachment.attachmentDescription.isEmpty {
placeholder
.font(.body)
.foregroundColor(.secondary)
.offset(x: 4, y: 8)
.allowsHitTesting(false)
}
}
}
}
private struct WrappedTextView: UIViewRepresentable {
typealias UIViewType = UITextView
@Binding var text: String
let backgroundColor: UIColor
let textDidChange: (((UITextView) -> Void))?
@Environment(\.isEnabled) private var isEnabled
func makeUIView(context: Context) -> UITextView {
let view = UITextView()
view.delegate = context.coordinator
view.backgroundColor = backgroundColor
view.font = .preferredFont(forTextStyle: .body)
view.adjustsFontForContentSizeCategory = true
view.textContainer.lineBreakMode = .byWordWrapping
#if os(visionOS)
view.borderStyle = .roundedRect
view.textContainerInset = UIEdgeInsets(top: 8, left: 4, bottom: 8, right: 4)
#endif
return view
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
uiView.isEditable = isEnabled
context.coordinator.textView = uiView
context.coordinator.text = $text
context.coordinator.didChange = textDidChange
if let textDidChange {
// wait until the next runloop iteration so that SwiftUI view updates have finished and
// the text view knows its new content size
DispatchQueue.main.async {
textDidChange(uiView)
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(text: $text, didChange: textDidChange)
}
class Coordinator: NSObject, UITextViewDelegate, TextViewCaretScrolling {
weak var textView: UITextView?
var text: Binding<String>
var didChange: ((UITextView) -> Void)?
var caretScrollPositionAnimator: UIViewPropertyAnimator?
init(text: Binding<String>, didChange: ((UITextView) -> Void)?) {
self.text = text
self.didChange = didChange
super.init()
NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidShow), name: UIResponder.keyboardDidShowNotification, object: nil)
}
@objc private func keyboardDidShow() {
guard let textView,
textView.isFirstResponder else {
return
}
ensureCursorVisible(textView: textView)
}
func textViewDidChange(_ textView: UITextView) {
text.wrappedValue = textView.text
didChange?(textView)
ensureCursorVisible(textView: textView)
}
}
}

View File

@ -0,0 +1,45 @@
//
// CurrentAccountView.swift
// ComposeUI
//
// Created by Shadowfacts on 3/4/23.
//
import SwiftUI
import Pachyderm
import TuskerComponents
struct CurrentAccountView: View {
let account: (any AccountProtocol)?
@EnvironmentObject private var controller: ComposeController
var body: some View {
controller.currentAccountContainerView(AnyView(currentAccount))
}
private var currentAccount: some View {
HStack(alignment: .top) {
AvatarImageView(
url: account?.avatar,
size: 50,
style: controller.config.avatarStyle,
fetchAvatar: controller.fetchAvatar
)
.accessibilityHidden(true)
if let account {
VStack(alignment: .leading) {
controller.displayNameLabel(account, .title2, 24)
.lineLimit(1)
Text(verbatim: "@\(account.acct)")
.font(.body.weight(.light))
.foregroundColor(.secondary)
.lineLimit(1)
}
}
Spacer()
}
}
}

View File

@ -0,0 +1,148 @@
//
// EmojiTextField.swift
// ComposeUI
//
// Created by Shadowfacts on 3/5/23.
//
import SwiftUI
struct EmojiTextField: UIViewRepresentable {
typealias UIViewType = UITextField
@EnvironmentObject private var controller: ComposeController
@Environment(\.colorScheme) private var colorScheme
@Binding var text: String
let placeholder: String
let maxLength: Int?
let becomeFirstResponder: Binding<Bool>?
let focusNextView: Binding<Bool>?
init(text: Binding<String>, placeholder: String, maxLength: Int?, becomeFirstResponder: Binding<Bool>? = nil, focusNextView: Binding<Bool>? = nil) {
self._text = text
self.placeholder = placeholder
self.maxLength = maxLength
self.becomeFirstResponder = becomeFirstResponder
self.focusNextView = focusNextView
}
func makeUIView(context: Context) -> UITextField {
let view = UITextField()
view.borderStyle = .roundedRect
view.font = .preferredFont(forTextStyle: .body)
view.adjustsFontForContentSizeCategory = true
view.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: [
.foregroundColor: UIColor.secondaryLabel,
])
context.coordinator.textField = view
view.delegate = context.coordinator
view.addTarget(context.coordinator, action: #selector(Coordinator.didChange(_:)), for: .editingChanged)
view.addTarget(context.coordinator, action: #selector(Coordinator.returnKeyPressed), for: .primaryActionTriggered)
// otherwise when the text gets too wide it starts expanding the ComposeView
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return view
}
func updateUIView(_ uiView: UITextField, context: Context) {
if text != uiView.text {
uiView.text = text
}
if placeholder != uiView.attributedPlaceholder?.string {
uiView.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: [
.foregroundColor: UIColor.secondaryLabel,
])
}
context.coordinator.text = $text
context.coordinator.maxLength = maxLength
context.coordinator.focusNextView = focusNextView
#if !os(visionOS)
uiView.backgroundColor = colorScheme == .dark ? UIColor(controller.config.fillColor) : .secondarySystemBackground
#endif
if becomeFirstResponder?.wrappedValue == true {
DispatchQueue.main.async {
uiView.becomeFirstResponder()
becomeFirstResponder!.wrappedValue = false
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(controller: controller, text: $text, focusNextView: focusNextView)
}
class Coordinator: NSObject, UITextFieldDelegate, ComposeInput {
let controller: ComposeController
var text: Binding<String>
var focusNextView: Binding<Bool>?
var maxLength: Int?
@Published var autocompleteState: AutocompleteState?
var autocompleteStatePublisher: Published<AutocompleteState?>.Publisher { $autocompleteState }
weak var textField: UITextField?
init(controller: ComposeController, text: Binding<String>, focusNextView: Binding<Bool>?, maxLength: Int? = nil) {
self.controller = controller
self.text = text
self.focusNextView = focusNextView
self.maxLength = maxLength
}
@objc func didChange(_ textField: UITextField) {
text.wrappedValue = textField.text ?? ""
}
@objc func returnKeyPressed() {
focusNextView?.wrappedValue = true
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let maxLength {
return ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string).count <= maxLength
} else {
return true
}
}
func textFieldDidBeginEditing(_ textField: UITextField) {
controller.currentInput = self
autocompleteState = textField.updateAutocompleteState(permittedModes: .emojis)
}
func textFieldDidEndEditing(_ textField: UITextField) {
controller.currentInput = nil
autocompleteState = textField.updateAutocompleteState(permittedModes: .emojis)
}
func textFieldDidChangeSelection(_ textField: UITextField) {
autocompleteState = textField.updateAutocompleteState(permittedModes: .emojis)
}
// MARK: ComposeInput
var toolbarElements: [ToolbarElement] { [.emojiPicker] }
var textInputMode: UITextInputMode? {
textField?.textInputMode
}
func applyFormat(_ format: StatusFormat) {
}
func beginAutocompletingEmoji() {
textField?.insertText(":")
}
func autocomplete(with string: String) {
textField?.autocomplete(with: string, permittedModes: .emojis, autocompleteState: &autocompleteState)
}
}
}

View File

@ -0,0 +1,31 @@
//
// HeaderView.swift
// ComposeUI
//
// Created by Shadowfacts on 3/4/23.
//
import SwiftUI
import Pachyderm
import InstanceFeatures
struct HeaderView: View {
let currentAccount: (any AccountProtocol)?
let charsRemaining: Int
var body: some View {
HStack(alignment: .top) {
CurrentAccountView(account: currentAccount)
.accessibilitySortPriority(1)
Spacer()
Text(verbatim: charsRemaining.description)
.foregroundColor(charsRemaining < 0 ? .red : .secondary)
.font(Font.body.monospacedDigit())
.accessibility(label: Text(charsRemaining < 0 ? "\(-charsRemaining) characters too many" : "\(charsRemaining) characters remaining"))
// this should come first, so VO users can back to it from the main compose text view
.accessibilitySortPriority(0)
}.frame(height: 50)
}
}

View File

@ -0,0 +1,221 @@
//
// LanguagePicker.swift
// ComposeUI
//
// Created by Shadowfacts on 5/4/23.
//
import SwiftUI
@available(iOS 16.0, *)
struct LanguagePicker: View {
@Binding var draftLanguage: String?
@Binding var hasChangedSelection: Bool
@State private var isShowingSheet = false
private var codeFromDraft: Locale.LanguageCode? {
draftLanguage.map(Locale.LanguageCode.init(_:))
}
private var codeFromActiveInputMode: Locale.LanguageCode? {
UITextInputMode.activeInputModes.first.flatMap(Self.codeFromInputMode(_:))
}
static func codeFromInputMode(_ mode: UITextInputMode) -> Locale.LanguageCode? {
guard let bcp47Lang = mode.primaryLanguage,
!bcp47Lang.isEmpty else {
return nil
}
var maybeIso639Code = bcp47Lang[..<bcp47Lang.index(bcp47Lang.startIndex, offsetBy: min(3, bcp47Lang.count))]
if maybeIso639Code.last == "-" {
maybeIso639Code = maybeIso639Code[..<maybeIso639Code.index(before: maybeIso639Code.endIndex)]
}
let identifier = String(maybeIso639Code)
// mul (for multiple languages) and unk (unknown) are ISO codes, but not ones that akkoma permits, so we ignore them on all platforms
guard identifier != "mul",
identifier != "und" else {
return nil
}
let code = Locale.LanguageCode(identifier)
if code.isISOLanguage {
return code
} else {
return nil
}
}
private var codeFromPreferredLanguages: Locale.LanguageCode? {
if let identifier = Locale.preferredLanguages.first,
case let code = Locale.LanguageCode(identifier),
code.isISOLanguage {
return code
} else {
return nil
}
}
private var languageCode: Binding<Locale.LanguageCode> {
Binding {
return codeFromDraft ?? codeFromActiveInputMode ?? codeFromPreferredLanguages ?? .english
} set: { newValue in
draftLanguage = newValue.identifier
}
}
var body: some View {
Button {
isShowingSheet = true
} label: {
Text((languageCode.wrappedValue.identifier(.alpha2) ?? languageCode.wrappedValue.identifier).uppercased())
}
.accessibilityLabel("Post Language")
.padding(5)
.hoverEffect()
.sheet(isPresented: $isShowingSheet) {
NavigationStack {
LanguagePickerList(languageCode: languageCode, hasChangedSelection: $hasChangedSelection, isPresented: $isShowingSheet)
}
.presentationDetents([.large, .medium])
}
}
}
@available(iOS 16.0, *)
private struct LanguagePickerList: View {
@Binding var languageCode: Locale.LanguageCode
@Binding var hasChangedSelection: Bool
@Binding var isPresented: Bool
@Environment(\.composeUIConfig.groupedBackgroundColor) private var groupedBackgroundColor
@Environment(\.composeUIConfig.groupedCellBackgroundColor) private var groupedCellBackgroundColor
@State private var recentLangs: [Lang] = []
@State private var langs: [Lang] = []
@State private var filteredLangs: [Lang]?
@State private var query = ""
private var defaults: UserDefaults {
UserDefaults(suiteName: "group.space.vaccor.Tusker") ?? .standard
}
private var recentIdentifiers: [String] {
get {
defaults.object(forKey: "LanguagePickerRecents") as? [String] ?? []
}
nonmutating set {
defaults.set(newValue, forKey: "LanguagePickerRecents")
}
}
var body: some View {
List {
Section {
ForEach(recentLangs) { lang in
button(for: lang)
}
.listRowBackground(groupedCellBackgroundColor)
} header: {
Text("Recently Used")
}
Section {
ForEach(filteredLangs ?? langs) { lang in
button(for: lang)
}
.listRowBackground(groupedCellBackgroundColor)
} header: {
Text("All Languages")
}
}
.listStyle(.insetGrouped)
.scrollContentBackground(.hidden)
.background(groupedBackgroundColor.edgesIgnoringSafeArea(.all))
.searchable(text: $query)
#if !os(visionOS)
.scrollDismissesKeyboard(.interactively)
#endif
.navigationTitle("Post Language")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
isPresented = false
}
}
}
.onAppear {
// make sure recents always contains the currently selected lang
let recents = addRecentLang(languageCode)
recentLangs = recents
.filter { $0 != "mul" && $0 != "und" }
.map { Lang(code: .init($0)) }
.sorted { $0.name < $1.name }
langs = Locale.LanguageCode.isoLanguageCodes
.filter { $0.identifier != "mul" && $0.identifier != "und" }
.map { Lang(code: $0) }
.sorted { $0.name < $1.name }
}
#if os(visionOS)
.onChange(of: query, initial: true) {
filteredLangsChanged(query: query)
}
#else
.onChange(of: query) { newValue in
filteredLangsChanged(query: newValue)
}
#endif
}
private func filteredLangsChanged(query: String) {
if query.isEmpty {
filteredLangs = nil
} else {
filteredLangs = langs.filter {
$0.name.localizedCaseInsensitiveContains(query) || $0.code.identifier.localizedCaseInsensitiveContains(query)
}
}
}
@discardableResult
private func addRecentLang(_ code: Locale.LanguageCode) -> [String] {
var recents = recentIdentifiers
if !recents.contains(languageCode.identifier) {
recents.insert(languageCode.identifier, at: 0)
if recents.count > 5 {
recents = Array(recents[..<5])
}
recentIdentifiers = recents
}
return recents
}
private func button(for lang: Lang) -> some View {
Button {
languageCode = lang.code
hasChangedSelection = true
isPresented = false
addRecentLang(lang.code)
} label: {
HStack {
Text(lang.name)
Spacer()
if lang.code == languageCode {
Image(systemName: "checkmark")
}
}
}
}
struct Lang: Identifiable {
let code: Locale.LanguageCode
let name: String
var id: String {
code.identifier
}
init(code: Locale.LanguageCode) {
self.code = code
self.name = Locale.current.localizedString(forLanguageCode: code.identifier) ?? code.identifier
}
}
}

View File

@ -0,0 +1,336 @@
//
// MainTextView.swift
// ComposeUI
//
// Created by Shadowfacts on 3/6/23.
//
import SwiftUI
struct MainTextView: View {
@EnvironmentObject private var controller: ComposeController
@EnvironmentObject private var draft: Draft
@Environment(\.colorScheme) private var colorScheme
@ScaledMetric private var fontSize = 20
@State private var hasFirstAppeared = false
@State private var height: CGFloat?
@State private var updateSelection: ((UITextView) -> Void)?
private let minHeight: CGFloat = 150
private var effectiveHeight: CGFloat { height ?? minHeight }
var config: ComposeUIConfig {
controller.config
}
private var placeholderOffset: CGSize {
#if os(visionOS)
CGSize(width: 8, height: 8)
#else
CGSize(width: 4, height: 8)
#endif
}
private var textViewBackgroundColor: UIColor? {
#if os(visionOS)
nil
#else
colorScheme == .dark ? UIColor(config.fillColor) : .secondarySystemBackground
#endif
}
var body: some View {
ZStack(alignment: .topLeading) {
MainWrappedTextViewRepresentable(
text: $draft.text,
backgroundColor: textViewBackgroundColor,
becomeFirstResponder: $controller.mainComposeTextViewBecomeFirstResponder,
updateSelection: $updateSelection,
textDidChange: textDidChange
)
if draft.text.isEmpty {
ControllerView(controller: { PlaceholderController() })
.font(.system(size: fontSize))
.foregroundColor(.secondary)
.offset(placeholderOffset)
.accessibilityHidden(true)
.allowsHitTesting(false)
}
}
.frame(height: effectiveHeight)
.onAppear(perform: becomeFirstResponderOnFirstAppearance)
}
private func becomeFirstResponderOnFirstAppearance() {
if !hasFirstAppeared {
hasFirstAppeared = true
controller.mainComposeTextViewBecomeFirstResponder = true
if config.textSelectionStartsAtBeginning {
updateSelection = { textView in
textView.selectedTextRange = textView.textRange(from: textView.beginningOfDocument, to: textView.beginningOfDocument)
}
}
}
}
private func textDidChange(textView: UITextView) {
height = max(textView.contentSize.height, minHeight)
}
}
fileprivate struct MainWrappedTextViewRepresentable: UIViewRepresentable {
typealias UIViewType = UITextView
@Binding var text: String
let backgroundColor: UIColor?
@Binding var becomeFirstResponder: Bool
@Binding var updateSelection: ((UITextView) -> Void)?
let textDidChange: (UITextView) -> Void
@EnvironmentObject private var controller: ComposeController
@Environment(\.isEnabled) private var isEnabled: Bool
func makeUIView(context: Context) -> UITextView {
let textView = WrappedTextView(composeController: controller)
context.coordinator.textView = textView
textView.delegate = context.coordinator
textView.isEditable = true
textView.font = UIFontMetrics.default.scaledFont(for: .systemFont(ofSize: 20))
textView.adjustsFontForContentSizeCategory = true
textView.textContainer.lineBreakMode = .byWordWrapping
#if os(visionOS)
textView.borderStyle = .roundedRect
// yes, the X inset is 4 less than the placeholder offset
textView.textContainerInset = UIEdgeInsets(top: 8, left: 4, bottom: 8, right: 4)
#endif
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
if text != uiView.text {
context.coordinator.skipNextSelectionChangedAutocompleteUpdate = true
uiView.text = text
}
uiView.isEditable = isEnabled
uiView.keyboardType = controller.config.useTwitterKeyboard ? .twitter : .default
uiView.backgroundColor = backgroundColor
context.coordinator.text = $text
if let updateSelection {
updateSelection(uiView)
self.updateSelection = nil
}
// wait until the next runloop iteration so that SwiftUI view updates have finished and
// the text view knows its new content size
DispatchQueue.main.async {
textDidChange(uiView)
if becomeFirstResponder {
// calling becomeFirstResponder during the SwiftUI update causes a crash on iOS 13
uiView.becomeFirstResponder()
// can't update @State vars during the SwiftUI update
becomeFirstResponder = false
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(controller: controller, text: $text, textDidChange: textDidChange)
}
class WrappedTextView: UITextView {
private let formattingActions = [#selector(toggleBoldface(_:)), #selector(toggleItalics(_:))]
private let composeController: ComposeController
init(composeController: ComposeController) {
self.composeController = composeController
super.init(frame: .zero, textContainer: nil)
}
required init?(coder: NSCoder) {
fatalError()
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if formattingActions.contains(action) {
return composeController.config.contentType != .plain
}
return super.canPerformAction(action, withSender: sender)
}
override func toggleBoldface(_ sender: Any?) {
(delegate as! Coordinator).applyFormat(.bold)
}
override func toggleItalics(_ sender: Any?) {
(delegate as! Coordinator).applyFormat(.italics)
}
override func validate(_ command: UICommand) {
super.validate(command)
if formattingActions.contains(command.action),
composeController.config.contentType != .plain {
command.attributes.remove(.disabled)
}
}
override func paste(_ sender: Any?) {
// we deliberately exclude the other CompositionAttachment readable type identifiers, because that's too overzealous with the conversion
// and things like URLs end up pasting as attachments
if UIPasteboard.general.contains(pasteboardTypes: UIImage.readableTypeIdentifiersForItemProvider) {
composeController.paste(itemProviders: UIPasteboard.general.itemProviders)
} else {
super.paste(sender)
}
}
}
class Coordinator: NSObject, UITextViewDelegate, ComposeInput, TextViewCaretScrolling {
weak var textView: UITextView?
let controller: ComposeController
var text: Binding<String>
let textDidChange: (UITextView) -> Void
var caretScrollPositionAnimator: UIViewPropertyAnimator?
@Published var autocompleteState: AutocompleteState?
var autocompleteStatePublisher: Published<AutocompleteState?>.Publisher { $autocompleteState }
var skipNextSelectionChangedAutocompleteUpdate = false
init(controller: ComposeController, text: Binding<String>, textDidChange: @escaping (UITextView) -> Void) {
self.controller = controller
self.text = text
self.textDidChange = textDidChange
super.init()
NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidShow), name: UIResponder.keyboardDidShowNotification, object: nil)
}
@objc private func keyboardDidShow() {
guard let textView,
textView.isFirstResponder else {
return
}
ensureCursorVisible(textView: textView)
}
// MARK: UITextViewDelegate
func textViewDidChange(_ textView: UITextView) {
text.wrappedValue = textView.text
textDidChange(textView)
ensureCursorVisible(textView: textView)
}
func textViewDidBeginEditing(_ textView: UITextView) {
controller.currentInput = self
updateAutocompleteState()
}
func textViewDidEndEditing(_ textView: UITextView) {
controller.currentInput = nil
updateAutocompleteState()
}
func textViewDidChangeSelection(_ textView: UITextView) {
if skipNextSelectionChangedAutocompleteUpdate {
skipNextSelectionChangedAutocompleteUpdate = false
} else {
updateAutocompleteState()
}
}
func textView(_ textView: UITextView, editMenuForTextIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? {
var actions = suggestedActions
if controller.config.contentType != .plain,
let index = suggestedActions.firstIndex(where: { ($0 as? UIMenu)?.identifier.rawValue == "com.apple.menu.format" }) {
if range.length > 0 {
let formatMenu = suggestedActions[index] as! UIMenu
let newFormatMenu = formatMenu.replacingChildren(StatusFormat.allCases.map { fmt in
return UIAction(title: fmt.accessibilityLabel, image: UIImage(systemName: fmt.imageName)) { [weak self] _ in
self?.applyFormat(fmt)
}
})
actions[index] = newFormatMenu
} else {
actions.remove(at: index)
}
}
if range.length == 0 {
actions.append(UIAction(title: "Insert Emoji", image: UIImage(systemName: "face.smiling"), handler: { [weak self] _ in
self?.controller.shouldEmojiAutocompletionBeginExpanded = true
self?.beginAutocompletingEmoji()
}))
}
return UIMenu(children: actions)
}
// MARK: ComposeInput
var toolbarElements: [ToolbarElement] {
[.emojiPicker, .formattingButtons]
}
var textInputMode: UITextInputMode? {
textView?.textInputMode
}
func autocomplete(with string: String) {
textView?.autocomplete(with: string, permittedModes: .all, autocompleteState: &autocompleteState)
}
func applyFormat(_ format: StatusFormat) {
guard let textView,
textView.isFirstResponder,
let insertionResult = format.insertionResult(for: controller.config.contentType) else {
return
}
let currentSelectedRange = textView.selectedRange
if currentSelectedRange.length == 0 {
textView.insertText(insertionResult.prefix + insertionResult.suffix)
textView.selectedRange = NSRange(location: currentSelectedRange.location + insertionResult.insertionPoint, length: 0)
} else {
let start = textView.text.utf16.index(textView.text.utf16.startIndex, offsetBy: currentSelectedRange.lowerBound)
let end = textView.text.utf16.index(textView.text.utf16.startIndex, offsetBy: currentSelectedRange.upperBound)
let selectedText = textView.text.utf16[start..<end]
textView.insertText(insertionResult.prefix + String(Substring(selectedText)) + insertionResult.suffix)
textView.selectedRange = NSRange(location: currentSelectedRange.location + insertionResult.insertionPoint, length: currentSelectedRange.length)
}
}
func beginAutocompletingEmoji() {
guard let textView else {
return
}
var insertSpace = false
if let text = textView.text,
textView.selectedRange.upperBound > 0 {
let characterBeforeCursorIndex = text.utf16.index(before: text.utf16.index(text.startIndex, offsetBy: textView.selectedRange.upperBound))
insertSpace = !text[characterBeforeCursorIndex].isWhitespace
}
textView.insertText((insertSpace ? " " : "") + ":")
}
private func updateAutocompleteState() {
guard let textView else {
autocompleteState = nil
return
}
autocompleteState = textView.updateAutocompleteState(permittedModes: .all)
}
}
}

View File

@ -0,0 +1,72 @@
//
// PollOptionView.swift
// ComposeUI
//
// Created by Shadowfacts on 3/25/23.
//
import SwiftUI
struct PollOptionView: View {
@EnvironmentObject private var controller: PollController
@EnvironmentObject private var poll: Poll
@ObservedObject private var option: PollOption
let remove: () -> Void
init(option: PollOption, remove: @escaping () -> Void) {
self.option = option
self.remove = remove
}
var body: some View {
HStack(spacing: 4) {
Checkbox(radiusFraction: poll.multiple ? 0.1 : 0.5, background: controller.parent.config.backgroundColor)
.animation(.default, value: poll.multiple)
textField
Button(action: remove) {
Image(systemName: "minus.circle.fill")
}
.accessibilityLabel("Remove option")
.buttonStyle(.plain)
.foregroundColor(poll.options.count == 1 ? .gray : .red)
.disabled(poll.options.count == 1)
.hoverEffect()
}
}
private var textField: some View {
let index = poll.options.index(of: option)
let placeholder = index != NSNotFound ? "Option \(index + 1)" : ""
let maxLength = controller.parent.mastodonController.instanceFeatures.maxPollOptionChars
return EmojiTextField(text: $option.text, placeholder: placeholder, maxLength: maxLength)
}
struct Checkbox: View {
private let radiusFraction: CGFloat
private let size: CGFloat = 20
private let innerSize: CGFloat
private let background: Color
init(radiusFraction: CGFloat, background: Color) {
self.radiusFraction = radiusFraction
self.innerSize = self.size - 4
self.background = background
}
var body: some View {
ZStack {
Rectangle()
.foregroundColor(.gray)
.frame(width: size, height: size)
.cornerRadius(radiusFraction * size)
Rectangle()
.foregroundColor(background)
.frame(width: innerSize, height: innerSize)
.cornerRadius(radiusFraction * innerSize)
}
}
}
}

View File

@ -0,0 +1,96 @@
//
// ReplyStatusView.swift
// ComposeUI
//
// Created by Shadowfacts on 3/25/23.
//
import SwiftUI
import Pachyderm
import TuskerComponents
struct ReplyStatusView: View {
let status: any StatusProtocol
let rowTopInset: CGFloat
let globalFrameOutsideList: CGRect
@EnvironmentObject private var controller: ComposeController
@State private var displayNameHeight: CGFloat?
@State private var contentHeight: CGFloat?
private let horizSpacing: CGFloat = 8
var body: some View {
HStack(alignment: .top, spacing: horizSpacing) {
GeometryReader(content: self.replyAvatarImage)
.frame(width: 50)
VStack(alignment: .leading, spacing: 0) {
HStack {
controller.displayNameLabel(status.account, .body, 17)
.lineLimit(1)
.layoutPriority(1)
Text(verbatim: "@\(status.account.acct)")
.font(.body.weight(.light))
.foregroundColor(.secondary)
.lineLimit(1)
Spacer()
}
.background(GeometryReader { proxy in
Color.clear
.preference(key: DisplayNameHeightPrefKey.self, value: proxy.size.height)
.onPreferenceChange(DisplayNameHeightPrefKey.self) { newValue in
displayNameHeight = newValue
}
})
controller.replyContentView(status) { newHeight in
// otherwise, with long in-reply-to statuses, the main content text view position seems not to update
// and it ends up partially behind the header
DispatchQueue.main.async {
contentHeight = newHeight
}
}
.frame(height: contentHeight ?? 0)
}
}
.frame(minHeight: 50, alignment: .top)
}
private func replyAvatarImage(geometry: GeometryProxy) -> some View {
// using a coordinate space declared outside of the List doesn't work, so we do the math ourselves
let globalFrame = geometry.frame(in: .global)
let scrollOffset = -(globalFrame.minY - globalFrameOutsideList.minY)
// add rowTopInset so that the image is always at least rowTopInset away from the top
var offset = scrollOffset + rowTopInset
// offset can never be less than 0 (i.e., above the top of the in-reply-to content)
offset = max(offset, 0)
// subtract 50, because we care about where the bottom of the view is but the offset is relative to the top of the view
let maxOffset = max((contentHeight ?? 0) + (displayNameHeight ?? 0) - 50, 0)
// once you scroll past the in-reply-to-content, the bottom of the avatar should be pinned to the bottom of the content
offset = min(offset, maxOffset)
return AvatarImageView(
url: status.account.avatar,
size: 50,
style: controller.config.avatarStyle,
fetchAvatar: controller.fetchAvatar
)
.offset(x: 0, y: offset)
.accessibilityHidden(true)
}
}
private struct DisplayNameHeightPrefKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}

View File

@ -0,0 +1,29 @@
//
// WrappedProgressView.swift
// Tusker
//
// Created by Shadowfacts on 8/30/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import SwiftUI
struct WrappedProgressView: UIViewRepresentable {
typealias UIViewType = UIProgressView
let value: Int
let total: Int
func makeUIView(context: Context) -> UIProgressView {
return UIProgressView(progressViewStyle: .bar)
}
func updateUIView(_ uiView: UIProgressView, context: Context) {
if total > 0 {
let progress = Float(value) / Float(total)
uiView.setProgress(progress, animated: true)
} else {
uiView.setProgress(0, animated: true)
}
}
}

View File

@ -0,0 +1,111 @@
//
// ZoomableScrollView.swift
// ComposeUI
//
// Created by Shadowfacts on 4/29/23.
//
import SwiftUI
@available(iOS 16.0, *)
struct ZoomableScrollView<Content: View>: UIViewControllerRepresentable {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
func makeUIViewController(context: Context) -> Controller {
return Controller(content: content)
}
func updateUIViewController(_ uiViewController: Controller, context: Context) {
uiViewController.host.rootView = content
}
class Controller: UIViewController, UIScrollViewDelegate {
let scrollView = UIScrollView()
let host: UIHostingController<Content>
private var lastIntrinsicSize: CGSize?
private var contentViewTopConstraint: NSLayoutConstraint!
private var contentViewLeadingConstraint: NSLayoutConstraint!
private var hostBoundsObservation: NSKeyValueObservation?
init(content: Content) {
self.host = UIHostingController(rootView: content)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self
scrollView.bouncesZoom = true
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
host.sizingOptions = .intrinsicContentSize
host.view.backgroundColor = .clear
host.view.translatesAutoresizingMaskIntoConstraints = false
addChild(host)
scrollView.addSubview(host.view)
host.didMove(toParent: self)
contentViewLeadingConstraint = host.view.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor)
contentViewTopConstraint = host.view.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor)
NSLayoutConstraint.activate([
scrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.frameLayoutGuide.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.bottomAnchor),
contentViewLeadingConstraint,
contentViewTopConstraint,
])
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if !host.view.intrinsicContentSize.equalTo(.zero),
host.view.intrinsicContentSize != lastIntrinsicSize {
self.lastIntrinsicSize = host.view.intrinsicContentSize
let maxHeight = view.bounds.height - view.safeAreaInsets.top - view.safeAreaInsets.bottom
let maxWidth = view.bounds.width - view.safeAreaInsets.left - view.safeAreaInsets.right
let heightScale = maxHeight / host.view.intrinsicContentSize.height
let widthScale = maxWidth / host.view.intrinsicContentSize.width
let minScale = min(widthScale, heightScale)
let maxScale = minScale >= 1 ? minScale + 2 : 2
scrollView.minimumZoomScale = minScale
scrollView.maximumZoomScale = maxScale
scrollView.zoomScale = minScale
}
centerImage()
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return host.view
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
centerImage()
}
func centerImage() {
let yOffset = max(0, (view.bounds.size.height - host.view.bounds.height * scrollView.zoomScale) / 2)
contentViewTopConstraint.constant = yOffset
let xOffset = max(0, (view.bounds.size.width - host.view.bounds.width * scrollView.zoomScale) / 2)
contentViewLeadingConstraint.constant = xOffset
}
}
}

View File

@ -1,13 +1,14 @@
//
// CharacterCounterTests.swift
// PachydermTests
// ComposeUITests
//
// Created by Shadowfacts on 9/29/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import XCTest
@testable import Pachyderm
@testable import ComposeUI
import InstanceFeatures
class CharacterCounterTests: XCTestCase {
@ -16,32 +17,34 @@ class CharacterCounterTests: XCTestCase {
override func tearDown() {
}
let features = InstanceFeatures()
func testCountEmpty() {
XCTAssertEqual(CharacterCounter.count(text: ""), 0)
XCTAssertEqual(CharacterCounter.count(text: "", for: features), 0)
}
func testCountPlainText() {
XCTAssertEqual(CharacterCounter.count(text: "This is an example message"), 26)
XCTAssertEqual(CharacterCounter.count(text: "This is an example message with an Emoji: 😄"), 43)
XCTAssertEqual(CharacterCounter.count(text: "😄😄😄😄😄😄😄"), 7)
XCTAssertEqual(CharacterCounter.count(text: "This is an example message", for: features), 26)
XCTAssertEqual(CharacterCounter.count(text: "This is an example message with an Emoji: 😄", for: features), 43)
XCTAssertEqual(CharacterCounter.count(text: "😄😄😄😄😄😄😄", for: features), 7)
}
func testCountLinks() {
XCTAssertEqual(CharacterCounter.count(text: "This is an example with a link: https://example.com"), 55)
XCTAssertEqual(CharacterCounter.count(text: "This is an example with a link 😄: https://example.com"), 57)
XCTAssertEqual(CharacterCounter.count(text: "😄😄😄😄😄😄😄: https://example.com"), 32)
XCTAssertEqual(CharacterCounter.count(text: "This is an example with a link: https://a.much.longer.example.com/link?foo=bar#baz"), 55)
XCTAssertEqual(CharacterCounter.count(text: "This is an example with a link: https://example.com", for: features), 55)
XCTAssertEqual(CharacterCounter.count(text: "This is an example with a link 😄: https://example.com", for: features), 57)
XCTAssertEqual(CharacterCounter.count(text: "😄😄😄😄😄😄😄: https://example.com", for: features), 32)
XCTAssertEqual(CharacterCounter.count(text: "This is an example with a link: https://a.much.longer.example.com/link?foo=bar#baz", for: features), 55)
}
func testCountLocalMentions() {
XCTAssertEqual(CharacterCounter.count(text: "hello @example"), 14)
XCTAssertEqual(CharacterCounter.count(text: "@some_really_long_name"), 22)
XCTAssertEqual(CharacterCounter.count(text: "hello @example", for: features), 14)
XCTAssertEqual(CharacterCounter.count(text: "@some_really_long_name", for: features), 22)
}
func testCountRemoteMentions() {
XCTAssertEqual(CharacterCounter.count(text: "hello @example@some.remote.social"), 14)
XCTAssertEqual(CharacterCounter.count(text: "hello @some_really_long_name@some-long.remote-instance.social"), 28)
XCTAssertEqual(CharacterCounter.count(text: "hello @example@some.remote.social", for: features), 14)
XCTAssertEqual(CharacterCounter.count(text: "hello @some_really_long_name@some-long.remote-instance.social", for: features), 28)
}
}

View File

@ -0,0 +1,25 @@
//
// FuzzyMatcherTests.swift
// ComposeUITests
//
// Created by Shadowfacts on 10/11/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import XCTest
@testable import ComposeUI
class FuzzyMatcherTests: XCTestCase {
func testExample() throws {
XCTAssertEqual(FuzzyMatcher.match(pattern: "foo", str: "foo").score, 6)
XCTAssertEqual(FuzzyMatcher.match(pattern: "foo", str: "faoao").score, 4)
XCTAssertEqual(FuzzyMatcher.match(pattern: "foo", str: "aaa").score, -6)
XCTAssertEqual(FuzzyMatcher.match(pattern: "bar", str: "baz").score, 2)
XCTAssertEqual(FuzzyMatcher.match(pattern: "bar", str: "bur").score, 1)
XCTAssertGreaterThan(FuzzyMatcher.match(pattern: "sir", str: "sir").score, FuzzyMatcher.match(pattern: "sir", str: "georgespolitzer").score)
}
}

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,48 @@
//
// API.swift
// Duckable
//
// Created by Shadowfacts on 11/7/22.
//
import UIKit
@MainActor
public protocol DuckableViewController: UIViewController {
func duckableViewControllerShouldDuck() -> DuckAttemptAction
func duckableViewControllerMayAttemptToDuck()
func duckableViewControllerWillAnimateDuck(withDuration duration: CGFloat, afterDelay delay: CGFloat)
func duckableViewControllerDidFinishAnimatingDuck()
}
extension DuckableViewController {
public func duckableViewControllerShouldDuck() -> DuckAttemptAction { .duck }
public func duckableViewControllerMayAttemptToDuck() {}
public func duckableViewControllerWillAnimateDuck(withDuration duration: CGFloat, afterDelay delay: CGFloat) {}
public func duckableViewControllerDidFinishAnimatingDuck() {}
}
public enum DuckAttemptAction {
case duck
case dismiss
case block
}
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
}
}

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