├── .github ├── CODEOWNERS ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── danger.yml │ └── jira-sync.yml ├── .gitignore ├── .gitlab-ci.yml ├── .gitmodules ├── .readme └── fedigardens.svg ├── .swiftformat ├── .swiftlint.yml ├── CHANGELOG.md ├── Dangerfile ├── Dangerfile.swift ├── Fedigardens Test Plan.xctestplan ├── Fedigardens.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ ├── WorkspaceSettings.xcsettings │ └── swiftpm │ └── Package.resolved ├── Fedigardens ├── Acknowledgements.plist ├── Capstone--iOS--Info.plist ├── Fedigardens.entitlements ├── Fedigardens.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcshareddata │ │ └── xcschemes │ │ └── Gardens.xcscheme ├── Modules │ ├── Bootstrapping │ │ ├── ContentView.swift │ │ ├── Emojis.swift │ │ ├── Gardens.swift │ │ ├── GardensViewModel.swift │ │ └── UserProfileEnvironmentKey.swift │ ├── Components │ │ ├── BetaBadge.swift │ │ ├── Pages │ │ │ ├── GardensAppPage.swift │ │ │ └── GardensPageLink.swift │ │ ├── Profiles │ │ │ ├── Components │ │ │ │ ├── ProfileSheetFields.swift │ │ │ │ ├── ProfileSheetHeaderView.swift │ │ │ │ ├── ProfileSheetLabel.swift │ │ │ │ ├── ProfileSheetRecentActivityView.swift │ │ │ │ ├── ProfileSheetStatisticCellView.swift │ │ │ │ ├── ProfileSheetStatistics.swift │ │ │ │ └── ProfileSheetToolbar.swift │ │ │ ├── ProfileSheetView.swift │ │ │ └── ProfileSheetViewModel.swift │ │ └── Statuses │ │ │ ├── Status Components │ │ │ ├── StatusAttachmentAlternative.swift │ │ │ ├── StatusAuthorExtendedLabel.swift │ │ │ ├── StatusDisclosedContent.swift │ │ │ └── StatusVerifiedButton.swift │ │ │ ├── StatusQuickGlanceView.swift │ │ │ └── StatusView.swift │ ├── Features │ │ ├── Composer │ │ │ ├── AuthorReplySegment.swift │ │ │ ├── AuthorView.swift │ │ │ ├── AuthorViewContext.swift │ │ │ ├── AuthorViewModel.swift │ │ │ ├── AuthorViewParticipantsField.swift │ │ │ ├── AuthorViewToolbar.swift │ │ │ ├── AuthoringScene.swift │ │ │ └── GardensComposeButton.swift │ │ ├── Intervention │ │ │ └── InterventionAllowedMechanisms+UserDefaults.swift │ │ ├── Media Viewer │ │ │ ├── AttachmentMedia.swift │ │ │ ├── AttachmentMediaGroup.swift │ │ │ ├── AttachmentVideoPlayer.swift │ │ │ ├── AttachmentViewer.swift │ │ │ ├── AttachmentViewerToolbar.swift │ │ │ └── AttachmentViewerViewModel.swift │ │ ├── Messaging │ │ │ ├── MessagingAuthorView.swift │ │ │ ├── MessagingDetailView.swift │ │ │ ├── MessagingDetailViewModel.swift │ │ │ ├── MessagingList.swift │ │ │ ├── MessagingListCellView.swift │ │ │ └── MessagingPresentationView.swift │ │ ├── Polling │ │ │ ├── PollCallToActionView.swift │ │ │ ├── PollVotingView.swift │ │ │ ├── PollVotingViewModel.swift │ │ │ └── StatusPollView.swift │ │ └── Search │ │ │ ├── Pages │ │ │ ├── SearchDirectoryView.swift │ │ │ ├── SearchResultsView.swift │ │ │ └── SearchViewExploreList.swift │ │ │ ├── SearchAccountView.swift │ │ │ ├── SearchTagView.swift │ │ │ ├── SearchView.swift │ │ │ ├── SearchViewModel.swift │ │ │ └── Tagging │ │ │ ├── SearchTagResultPage.swift │ │ │ ├── SearchTagViewModel.swift │ │ │ └── TagHistoryChart.swift │ ├── Layout │ │ ├── Compact Layout │ │ │ ├── GardensAppCompactLayout.swift │ │ │ ├── GardensAppCompactMorePage.swift │ │ │ └── GardensAppCompactSidebarContent.swift │ │ ├── GardensAppLayoutViewModel.swift │ │ ├── Interactions │ │ │ ├── InteractionsListView.swift │ │ │ ├── InteractionsListViewModel.swift │ │ │ └── NotificationListCellView.swift │ │ ├── LayoutStateRepresentable.swift │ │ ├── Sidebar Sections │ │ │ ├── GardensAppSubscribedTagsDestination.swift │ │ │ └── GardensAppTrendingTagsDestination.swift │ │ ├── Status Detail View │ │ │ ├── Components │ │ │ │ ├── StatusDetailList.swift │ │ │ │ ├── StatusDetailProfileMenu.swift │ │ │ │ ├── StatusDetailQuote.swift │ │ │ │ └── StatusDetailToolbar.swift │ │ │ ├── StatusContextProvider.swift │ │ │ ├── StatusDetailView.swift │ │ │ └── StatusDetailViewModel.swift │ │ ├── Status Navigation List │ │ │ ├── StatusNavigationList.swift │ │ │ └── StatusNavigationListViewModel.swift │ │ ├── Timeline Split View │ │ │ ├── TimelineSplitView.swift │ │ │ └── TimelineSplitViewModel.swift │ │ └── Wide Layout │ │ │ ├── GardensAppSidebarContent.swift │ │ │ ├── GardensAppWideCommonDestinationsGroup.swift │ │ │ └── GardensAppWideLayout.swift │ ├── Settings │ │ ├── Label+SettingsStyle.swift │ │ ├── Pages │ │ │ ├── SettingsAboutPage.swift │ │ │ ├── SettingsAcknowledgementList.swift │ │ │ ├── SettingsAuthorPage.swift │ │ │ ├── SettingsBlocklistPage.swift │ │ │ ├── SettingsEnergyPage.swift │ │ │ ├── SettingsInstanceBlocklistPage.swift │ │ │ ├── SettingsInterventionPage.swift │ │ │ ├── SettingsLicensePage.swift │ │ │ └── SettingsReadingPage.swift │ │ ├── SettingsBlocklistViewModel.swift │ │ ├── SettingsFeedbackSection.swift │ │ ├── SettingsVersionBlock.swift │ │ └── SettingsView.swift │ ├── Source │ │ └── RunestoneViewer.swift │ └── Utilities │ │ ├── Acknowledgement+License.swift │ │ ├── Extensions │ │ ├── Alice │ │ │ ├── Account+AccountName.swift │ │ │ ├── Conversation+Authors.swift │ │ │ ├── Status+URIToUrl.swift │ │ │ ├── Tag+Chartable.swift │ │ │ └── Visibility+Localization.swift │ │ ├── Bundle+AppVersion.swift │ │ ├── DateFormatter+Mastodon.swift │ │ ├── NSTextCheckingResult+Substring.swift │ │ ├── String+AttributedContent.swift │ │ ├── UIDevice+Model.swift │ │ ├── URL+CustomInits.swift │ │ └── View+ExtraModifiers.swift │ │ ├── Mock │ │ ├── JSONDecoder+Resources.swift │ │ ├── Mock Data │ │ │ ├── Context.json │ │ │ ├── Conversation.json │ │ │ ├── Poll.json │ │ │ ├── Profile.json │ │ │ ├── README.md │ │ │ ├── SearchResult.json │ │ │ ├── Status.json │ │ │ └── Timeline.json │ │ └── MockData.swift │ │ └── Pip.swift ├── PrivacyInfo.xcprivacy ├── Resources │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-1024.png │ │ │ ├── Icon-120 1.png │ │ │ ├── Icon-120.png │ │ │ ├── Icon-152.png │ │ │ ├── Icon-167.png │ │ │ ├── Icon-180.png │ │ │ ├── Icon-20.png │ │ │ ├── Icon-29.png │ │ │ ├── Icon-40 1.png │ │ │ ├── Icon-40 2.png │ │ │ ├── Icon-40.png │ │ │ ├── Icon-58 1.png │ │ │ ├── Icon-58.png │ │ │ ├── Icon-60.png │ │ │ ├── Icon-76.png │ │ │ ├── Icon-80 1.png │ │ │ ├── Icon-80.png │ │ │ └── Icon-87.png │ │ ├── Contents.json │ │ ├── GardensIcon.imageset │ │ │ ├── Contents.json │ │ │ └── Fedigardens2-icon.png │ │ ├── LaunchScreen.colorset │ │ │ └── Contents.json │ │ ├── OneSec.imageset │ │ │ ├── Contents.json │ │ │ └── one sec app icon.png │ │ ├── Splash.imageset │ │ │ ├── Contents.json │ │ │ └── LaunchScreen.svg │ │ └── matrix.symbolset │ │ │ ├── Contents.json │ │ │ └── matrix.svg │ ├── Disallow.plist │ └── Localizations │ │ ├── Localizable.stringsdict │ │ └── en.lproj │ │ └── Localizable.strings ├── Supporting Files │ ├── Acknowledgements.plist │ ├── CNPL.md │ ├── MIT-HTML2Markdown.md │ ├── MPLv2.txt │ └── NPL.txt └── Tests │ ├── Shared.swift │ └── Statuses.swift ├── Gemfile ├── Gemfile.lock ├── LICENSE.md ├── Packages ├── Alice │ ├── .swiftlint.alice.yml │ ├── .swiftpm │ │ └── xcode │ │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Alice.xcscheme │ ├── LICENSE.txt │ ├── Package.resolved │ ├── Package.swift │ ├── Sources │ │ ├── Alice │ │ │ ├── Extensions │ │ │ │ ├── CustomStringConvertible.swift │ │ │ │ └── URL.swift │ │ │ ├── Helpers │ │ │ │ ├── Box.swift │ │ │ │ └── isOnMainThread.swift │ │ │ ├── Models │ │ │ │ ├── Accounts │ │ │ │ │ ├── Account.swift │ │ │ │ │ ├── FamiliarFollowers.swift │ │ │ │ │ ├── Field.swift │ │ │ │ │ ├── Filters │ │ │ │ │ │ ├── UserFilter.swift │ │ │ │ │ │ ├── UserFilterKeyword.swift │ │ │ │ │ │ ├── UserFilterResult.swift │ │ │ │ │ │ └── UserFilterStatus.swift │ │ │ │ │ ├── Notification.swift │ │ │ │ │ ├── Relationship.swift │ │ │ │ │ └── Source.swift │ │ │ │ ├── Authentication │ │ │ │ │ ├── Application.swift │ │ │ │ │ └── Token.swift │ │ │ │ ├── General │ │ │ │ │ ├── EmptyNode.swift │ │ │ │ │ ├── MastodonError.swift │ │ │ │ │ └── SearchResult.swift │ │ │ │ ├── Instances │ │ │ │ │ ├── Announcements │ │ │ │ │ │ ├── Announcement.swift │ │ │ │ │ │ └── AnnouncementReaction.swift │ │ │ │ │ ├── CustomEmoji.swift │ │ │ │ │ ├── DomainBlock.swift │ │ │ │ │ ├── ExtendedDescription.swift │ │ │ │ │ ├── FeaturedTag.swift │ │ │ │ │ └── Instance.swift │ │ │ │ ├── Statuses │ │ │ │ │ ├── Attachments │ │ │ │ │ │ ├── AttachmentType.swift │ │ │ │ │ │ └── MediaAttachment.swift │ │ │ │ │ ├── Cards │ │ │ │ │ │ ├── PreviewCard.swift │ │ │ │ │ │ └── PreviewCardType.swift │ │ │ │ │ ├── Context.swift │ │ │ │ │ ├── Conversation.swift │ │ │ │ │ ├── Mention.swift │ │ │ │ │ ├── Polls │ │ │ │ │ │ ├── Poll.swift │ │ │ │ │ │ └── PollOption.swift │ │ │ │ │ ├── PostVisibility.swift │ │ │ │ │ ├── Status.swift │ │ │ │ │ └── Tags │ │ │ │ │ │ ├── History.swift │ │ │ │ │ │ └── Tag.swift │ │ │ │ └── Timelines │ │ │ │ │ ├── MastodonList.swift │ │ │ │ │ └── TimelineScope.swift │ │ │ ├── Networking │ │ │ │ ├── AliceSecurityModule.swift │ │ │ │ ├── AliceSession.swift │ │ │ │ ├── AuthenticationModule.swift │ │ │ │ ├── Endpoint+Path.swift │ │ │ │ ├── Endpoint.swift │ │ │ │ └── FetchError.swift │ │ │ └── alice.swift │ │ └── AliceMockingbird │ │ │ ├── Mock │ │ │ ├── AliceMockSession.swift │ │ │ ├── MockConstants.swift │ │ │ └── MockKeychain.swift │ │ │ └── Resources │ │ │ ├── Context.json │ │ │ ├── Conversation.json │ │ │ ├── Empty.json │ │ │ ├── Poll.json │ │ │ ├── Profile.json │ │ │ ├── Registration.json │ │ │ ├── SearchResult.json │ │ │ ├── Status.json │ │ │ ├── Timeline.json │ │ │ ├── Token.json │ │ │ ├── UnauthorizedError.json │ │ │ └── UnprocessableEntity.json │ └── Tests │ │ └── AliceTests │ │ ├── AliceAuthenticationTests.swift │ │ └── AliceNetworkTests.swift ├── FrugalMode │ ├── .gitignore │ ├── Package.swift │ ├── Sources │ │ └── FrugalMode │ │ │ ├── FrugalModeEnvironmentKey.swift │ │ │ ├── FrugalModeFlow.swift │ │ │ ├── FrugalModeLabel.swift │ │ │ └── Resources │ │ │ └── Localizable.xcstrings │ └── Tests │ │ └── FrugalModeTests │ │ └── FrugalModeTests.swift ├── GardenComposer │ ├── .gitignore │ ├── Package.swift │ ├── Sources │ │ └── GardenComposer │ │ │ ├── AuthoringContext.swift │ │ │ ├── ComposerDraft.swift │ │ │ ├── ComposerFlow.swift │ │ │ ├── ComposerFlowError.swift │ │ │ ├── NSTextCheckingResult+Substring.swift │ │ │ ├── Resources │ │ │ └── Localizable.xcstrings │ │ │ └── Status+ReplyText.swift │ └── Tests │ │ └── GardenComposerTests │ │ ├── ComposerDraftTests.swift │ │ ├── ComposerFlowTests.swift │ │ └── ReplyMentionsTests.swift ├── GardenDiscussions │ ├── .gitignore │ ├── Package.swift │ ├── Sources │ │ └── GardenDiscussions │ │ │ ├── DiscussionVerifiedBadge.swift │ │ │ └── Status+Authorship.swift │ └── Tests │ │ └── GardenDiscussionsTests │ │ └── GardenDiscussionsTests.swift ├── GardenGate │ ├── .gitignore │ ├── Package.swift │ ├── Sources │ │ └── GardenGate │ │ │ ├── AuthenticationGate.swift │ │ │ ├── AuthenticationGateBrowser.swift │ │ │ ├── AuthenticationGateHeaderView.swift │ │ │ ├── AuthenticationGateTextField.swift │ │ │ ├── AuthenticationGateView.swift │ │ │ ├── DomainValidationError.swift │ │ │ └── Resources │ │ │ ├── Disallow.plist │ │ │ └── Localizable.xcstrings │ └── Tests │ │ └── GardenGateTests │ │ └── AuthenticationGateTests.swift ├── GardenProfiles │ ├── .gitignore │ ├── Package.swift │ ├── Sources │ │ └── GardenProfiles │ │ │ ├── Account+Verification.swift │ │ │ └── ProfileImage.swift │ └── Tests │ │ └── GardenProfilesTests │ │ └── GardenProfilesTests.swift ├── GardenSettings │ ├── .gitignore │ ├── .swiftpm │ │ └── xcode │ │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── GardenSettings.xcscheme │ ├── Package.swift │ ├── Sources │ │ └── GardenSettings │ │ │ ├── AppStorage+GardenSettingsKey.swift │ │ │ ├── GardenSetting.swift │ │ │ ├── GardenSettingsKey.swift │ │ │ └── UserDefaults+GardenSettingsKey.swift │ └── Tests │ │ └── GardenSettingsTests │ │ └── GardenSettingPropertyWrapperTests.swift ├── Interventions │ ├── .gitignore │ ├── Package.swift │ ├── Sources │ │ └── Interventions │ │ │ ├── InterventionAllowedMechanisms.swift │ │ │ ├── InterventionContext.swift │ │ │ ├── InterventionFlow.swift │ │ │ ├── InterventionLinkOpener.swift │ │ │ ├── InterventionMissingAlert.swift │ │ │ ├── InterventionRequestError.swift │ │ │ └── Resources │ │ │ └── Localizable.xcstrings │ └── Tests │ │ └── InterventionsTests │ │ └── InterventionsTests.swift ├── SeedUI │ ├── .gitignore │ ├── Package.swift │ └── Sources │ │ └── SeedUI │ │ ├── BadgedText.swift │ │ ├── GiantAlert.swift │ │ ├── HashableType.swift │ │ ├── InformationCard.swift │ │ ├── RecursiveNavigationStack.swift │ │ └── SeedUI.docc │ │ ├── RecursiveNavigationStack.md │ │ └── SeedUI.md └── WebString │ ├── .gitignore │ ├── Package.swift │ ├── Sources │ └── WebString │ │ └── WebString.swift │ └── Tests │ └── WebStringTests │ └── WebStringTests.swift └── README.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @alicerunsonfedora 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: Bug 6 | assignees: alicerunsonfedora 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Smartphone (please complete the following information):** 27 | - **App Version**: [e.g. 22] 28 | - **Hardware**: [e.g. iPhone6] 29 | - **Operating System**: [e.g. iOS8.1] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/workflows/danger.yml: -------------------------------------------------------------------------------- 1 | name: Danger 2 | 3 | on: 4 | pull_request: 5 | branches: [ root, devel ] 6 | 7 | jobs: 8 | danger: 9 | name: Danger 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | - name: Danger 16 | uses: docker://ghcr.io/danger/danger-swift-with-swiftlint:3.15.0 17 | with: 18 | args: --failOnErrors --no-publish-check 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | -------------------------------------------------------------------------------- /.github/workflows/jira-sync.yml: -------------------------------------------------------------------------------- 1 | name: Jira Sync 2 | on: 3 | issues: 4 | types: [opened, labeled, unlabeled] 5 | issue_comment: 6 | types: [created] 7 | jobs: 8 | sync: 9 | name: Sync Items 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Sync Github Issues to Jira 13 | uses: jordansilva/github-action-issue-to-jira@v1.1.2 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | with: 17 | jiraHost: ${{ secrets.JIRA_HOST }} 18 | jiraUsername: ${{ secrets.JIRA_USERNAME }} 19 | jiraPassword: ${{ secrets.JIRA_PASSWORD }} # See https://id.atlassian.com/manage/api-tokens 20 | project: FGD 21 | assignee: default_assignee 22 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - preflight 3 | - build 4 | - test 5 | 6 | danger: 7 | stage: preflight 8 | before_script: 9 | - bundle install 10 | script: 11 | - bundle exec danger 12 | only: 13 | - merge_requests -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Packages/FlowKit"] 2 | path = Packages/FlowKit 3 | url = https://github.com/alicerunsonfedora/FlowKit 4 | branch = v0.1.0 -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --binarygrouping none 2 | --commas inline 3 | --decimalgrouping none 4 | --hexgrouping none 5 | --ifdef outdent 6 | --octalgrouping none 7 | --patternlet inline 8 | --semicolons never 9 | --wrapcollections before-first 10 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | child_config: Packages/Alice/.swiftlint.alice.yml 2 | 3 | disabled_rules: 4 | - force_try 5 | - duplicate_imports 6 | - function_body_length 7 | 8 | opt_in_rules: 9 | - file_header 10 | - empty_count 11 | - inclusive_language 12 | - for_where 13 | 14 | included: 15 | - Fedigardens 16 | - Packages 17 | 18 | excluded: 19 | - Packages/FlowKit 20 | 21 | type_name: 22 | allowed_symbols: ["_"] 23 | 24 | type_body_length: 25 | warning: 250 26 | error: 400 27 | 28 | file_length: 29 | warning: 500 30 | error: 1000 31 | 32 | file_header: 33 | required_pattern: | 34 | \/\/ 35 | \/\/ .*?\.swift 36 | \/\/ Fedigardens 37 | \/\/ 38 | \/\/ Created by .*? on \d{1,2}\/\d{1,2}\/\d{2}\. 39 | \/\/ 40 | \/\/ This file is part of Fedigardens\. 41 | \/\/ 42 | \/\/ Fedigardens is non-violent software: you can use, redistribute, and\/or modify it under the terms of the CNPLv7\+ 43 | \/\/ as found in the LICENSE file in the source code root directory or at \\. 44 | \/\/ 45 | \/\/ Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law\. See the CNPL for 46 | \/\/ details\. 47 | -------------------------------------------------------------------------------- /Dangerfile: -------------------------------------------------------------------------------- 1 | pr_file_threshold = 20 2 | suggest_changes = false 3 | 4 | edited_files = (git.modified_files + git.added_files) 5 | 6 | if edited_files.count > pr_file_threshold 7 | warn "This appears to be a big merge request." \ 8 | "If this PR contains multiple features or tickets, please consider splitting them up for easier review." 9 | suggest_changes = true 10 | end 11 | 12 | deleted_files = git.deleted_files.count 13 | if deleted_files > pr_file_threshold 14 | warn "This merge request contains #{deleted_files} files that were deleted." 15 | suggest_changes = true 16 | end 17 | 18 | if edited_files.count > 3 && !edited_files.include?("CHANGELOG.md") 19 | warn "This merge request does not have an update in the CHANGELOG." 20 | markdown "If the changelog doesn't have a section for the latest unreleased version, you may create one." 21 | end 22 | 23 | warn "Unit tests have been changed." if !edited_files.grep(/Tests/).empty? 24 | 25 | if gitlab.mr_body.length < 1 26 | fail "Please provide a description of what this PR changes." 27 | markdown "Per the contribution guidelines:" \ 28 | "> - Pull requests should have an adequate description of the changes being made, and" \ 29 | "> any feedback reports it addresses." \ 30 | "> - Pull requests should be properly tagged with the modules it affects, such as" \ 31 | "> Authentication and Frugal Mode." \ 32 | "> - With some execeptions, pull requests should _always_ pass Danger checks and any" \ 33 | "> unit tests attached." 34 | suggest_changes = true 35 | end 36 | 37 | random_messages = ["Good on ya!", "Great work!", "Excellent work!", "Good job!", ";^)", "Perfection.", "Congrats!"] 38 | if !suggest_changes 39 | markdown "Code looks great. " + random_messages.sample 40 | markdown "Note that a successful Danger check does *not* mean an automatic merge request approval." 41 | end -------------------------------------------------------------------------------- /Fedigardens.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 18 | 19 | 22 | 25 | 27 | 28 | 30 | 31 | 33 | 34 | 36 | 37 | 39 | 40 | 41 | 44 | 46 | 47 | 49 | 50 | 52 | 53 | 54 | 57 | 59 | 60 | 62 | 63 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /Fedigardens.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Fedigardens.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Fedigardens/Capstone--iOS--Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleURLTypes 6 | 7 | 8 | CFBundleTypeRole 9 | Viewer 10 | CFBundleURLName 11 | net.marquiskurt.gardens 12 | CFBundleURLSchemes 13 | 14 | gardens 15 | 16 | 17 | 18 | ITSAppUsesNonExemptEncryption 19 | 20 | LSApplicationQueriesSchemes 21 | 22 | onesec 23 | 24 | UILaunchScreen 25 | 26 | UIColorName 27 | LaunchScreen 28 | UIImageName 29 | Splash 30 | UIImageRespectsSafeAreaInsets 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Fedigardens/Fedigardens.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | keychain-access-groups 10 | 11 | $(AppIdentifierPrefix)app.fedigardens.mail 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Fedigardens/Fedigardens.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Fedigardens/Fedigardens.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Fedigardens/Fedigardens.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "chica", 5 | "kind" : "remoteSourceControl", 6 | "location" : "http://github.com/alicerunsonfedora/chica", 7 | "state" : { 8 | "branch" : "main", 9 | "revision" : "28b9f6c5302915d78c26f664bab7a25d054caf84" 10 | } 11 | }, 12 | { 13 | "identity" : "html2markdown", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://gitlab.com/mflint/HTML2Markdown/", 16 | "state" : { 17 | "revision" : "00d7a9744bbd1e7762c587bbd248775e16345a65", 18 | "version" : "1.0.0" 19 | } 20 | }, 21 | { 22 | "identity" : "keychainaccess", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/kishikawakatsumi/KeychainAccess", 25 | "state" : { 26 | "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", 27 | "version" : "4.2.2" 28 | } 29 | }, 30 | { 31 | "identity" : "safariview", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/alicerunsonfedora/SafariView", 34 | "state" : { 35 | "branch" : "root", 36 | "revision" : "3f58d7915d97a0e15bc53618e54cbfbbba1f1b8f" 37 | } 38 | } 39 | ], 40 | "version" : 2 41 | } 42 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Bootstrapping/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 25/1/22. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import GardenGate 17 | import SwiftUI 18 | 19 | // MARK: - Content View 20 | 21 | /// The primary content view of the app. 22 | struct ContentView: View { 23 | @Environment(\.deviceModel) private var deviceModel 24 | 25 | /// The shared Chica authentication object. 26 | /// 27 | /// This is used to handle authentication to the Gopherdon server and watch for state changes. 28 | @ObservedObject private var chicaAuth: Alice.OAuth = .shared 29 | 30 | @State private var showAuthSheet: Bool = false 31 | 32 | var body: some View { 33 | VStack { 34 | Group { 35 | switch chicaAuth.authState { 36 | case .authenthicated: 37 | if deviceModel.starts(with: "iPad") { 38 | GardensAppWideLayout() 39 | } else { 40 | GardensAppCompactLayout() 41 | } 42 | default: 43 | authDialog 44 | } 45 | } 46 | } 47 | .animation(.spring(), value: chicaAuth.authState) 48 | .onAppear { print(deviceModel) } 49 | } 50 | 51 | var authDialog: AuthenticationGateView { 52 | AuthenticationGateView() 53 | } 54 | } 55 | 56 | // MARK: - Previews 57 | 58 | struct ContentView_Previews: PreviewProvider { 59 | static var previews: some View { 60 | ContentView() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Bootstrapping/Emojis.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Emojis.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/14/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import struct Alice.CustomEmoji 17 | import EmojiText 18 | import SwiftUI 19 | 20 | extension CustomEmoji { 21 | func remote() -> RemoteEmoji { 22 | return RemoteEmoji(shortcode: self.shortcode, url: self.url) 23 | } 24 | 25 | var remoteEmoji: RemoteEmoji { 26 | remote() 27 | } 28 | } 29 | 30 | private struct CustomEmojisEnvironmentKey: EnvironmentKey { 31 | static let defaultValue: [RemoteEmoji] = [] 32 | } 33 | 34 | extension EnvironmentValues { 35 | var customEmojis: [RemoteEmoji] { 36 | get { self[CustomEmojisEnvironmentKey.self] } 37 | set { self[CustomEmojisEnvironmentKey.self] = newValue } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Bootstrapping/UserProfileEnvironmentKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserProfileEnvironmentKey.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 12/25/22. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import Foundation 17 | import SwiftUI 18 | 19 | private struct UserProfileEnvironmentKey: EnvironmentKey { 20 | static let defaultValue: Account = MockData.profile! 21 | } 22 | 23 | extension EnvironmentValues { 24 | var userProfile: Account { 25 | get { self[UserProfileEnvironmentKey.self] } 26 | set { self[UserProfileEnvironmentKey.self] = newValue } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Components/BetaBadge.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BetaBadge.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/13/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import SwiftUI 16 | 17 | @available(*, deprecated, message: "Use SeedUI BadgedText instead.") 18 | struct BetaBadge: View { 19 | var body: some View { 20 | Text("BETA") 21 | .font(.system(.footnote, design: .rounded)) 22 | .bold() 23 | .padding(.horizontal, 6) 24 | .foregroundColor(.accentColor) 25 | .overlay { 26 | RoundedRectangle(cornerRadius: 24) 27 | .strokeBorder() 28 | .foregroundColor(.accentColor) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Components/Pages/GardensPageLink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GardensPageLink.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/7/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import SwiftUI 16 | 17 | struct GardensPageLink: View { 18 | var page: GardensAppPage 19 | 20 | var body: some View { 21 | NavigationLink(value: page) { 22 | Label(page.localizedTitle, systemImage: page.symbol) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Components/Profiles/Components/ProfileSheetFields.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileSheetFields.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/28/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import EmojiText 17 | import SwiftUI 18 | import WebString 19 | 20 | struct ProfileSheetFields: View { 21 | @Environment(\.enforcedFrugalMode) var enforcedFrugalMode 22 | @AppStorage(.frugalMode) var frugalMode: Bool = false 23 | var profile: Account 24 | 25 | private var emojis: [RemoteEmoji] { 26 | return (enforcedFrugalMode || frugalMode) ? [] : profile.emojis.map(\.remoteEmoji) 27 | } 28 | 29 | var body: some View { 30 | Group { 31 | ForEach(profile.fields) { (field: Field) in 32 | LabeledContent(field.name) { 33 | EmojiText(markdown: field.value.markdown, emojis: emojis) 34 | .multilineTextAlignment(.trailing) 35 | } 36 | .listRowBackground( 37 | field.value == profile.verifiedDomain() 38 | ? Color.green.opacity(0.2) 39 | : Color(uiColor: .secondarySystemGroupedBackground) 40 | ) 41 | .tint( 42 | field.value == profile.verifiedDomain() 43 | ? Color.green 44 | : Color.accentColor 45 | ) 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Components/Profiles/Components/ProfileSheetLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileSheetLabel.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/28/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import EmojiText 17 | import GardenProfiles 18 | import SwiftUI 19 | 20 | struct ProfileSheetLabel: View { 21 | @Environment(\.enforcedFrugalMode) var enforcedFrugalMode 22 | @AppStorage(.frugalMode) var frugalMode: Bool = false 23 | var profile: Account 24 | 25 | private var emojis: [RemoteEmoji] { 26 | return (enforcedFrugalMode || frugalMode) ? [] : profile.emojis.map(\.remoteEmoji) 27 | } 28 | 29 | var body: some View { 30 | VStack(spacing: 4) { 31 | ProfileImage(author: profile) 32 | .profileSize(.xxlarge) 33 | EmojiText(markdown: profile.getAccountName(), emojis: emojis) 34 | .font(.system(.largeTitle, design: .rounded)) 35 | .multilineTextAlignment(.center) 36 | .bold() 37 | Text("@\(profile.acct)") 38 | .font(.headline) 39 | .foregroundColor(.secondary) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Components/Profiles/Components/ProfileSheetStatisticCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileSheetStatisticCellView.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/15/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import SwiftUI 16 | 17 | struct ProfileSheetStatisticCellView: View { 18 | var key: LocalizedStringKey 19 | var value: LocalizedStringKey 20 | var systemName: String = "questionmark.square.dashed" 21 | 22 | var body: some View { 23 | VStack(spacing: 4) { 24 | Image(systemName: systemName) 25 | .foregroundColor(.accentColor) 26 | VStack { 27 | Text(value) 28 | .font(.headline) 29 | Text(key) 30 | .font(.caption) 31 | .foregroundColor(.secondary) 32 | } 33 | } 34 | .padding() 35 | .frame(maxWidth: .infinity) 36 | .background( 37 | Color(uiColor: .secondarySystemGroupedBackground) 38 | .cornerRadius(10) 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Components/Profiles/Components/ProfileSheetStatistics.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileSheetStatistics.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/28/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import SwiftUI 17 | 18 | struct ProfileSheetStatistics: View { 19 | var profile: Account 20 | var body: some View { 21 | HStack { 22 | ProfileSheetStatisticCellView( 23 | key: "profile.followers", 24 | value: "\(profile.followersCount)", 25 | systemName: "person.2.fill" 26 | ) 27 | ProfileSheetStatisticCellView( 28 | key: "profile.following", 29 | value: "\(profile.followingCount)", 30 | systemName: "person.3.fill" 31 | ) 32 | ProfileSheetStatisticCellView( 33 | key: "profile.statusescount", 34 | value: "\(profile.statusesCount)", 35 | systemName: "square.and.pencil" 36 | ) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Components/Statuses/Status Components/StatusAttachmentAlternative.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusAttachmentAlternative.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 5/4/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import SwiftUI 17 | 18 | struct StatusAttachmentAlernative: View { 19 | var attachment: MediaAttachment 20 | var body: some View { 21 | Label(attachment.description ?? "attachments.description.missing".localized(), systemImage: symbol) 22 | } 23 | 24 | var symbol: String { 25 | switch attachment.type { 26 | case .audio: 27 | return "speaker.wave.2.fill" 28 | case .gifv, .video: 29 | return "film" 30 | case .image: 31 | return "photo" 32 | case .unknown: 33 | return "questionmark.app.dashed" 34 | } 35 | } 36 | } 37 | 38 | struct StatusAttachmentAlernative_Previews: PreviewProvider { 39 | static let attachments: [MediaAttachment] = MockData.status!.mediaAttachments 40 | static var previews: some View { 41 | ForEach(attachments) { attachment in 42 | StatusAttachmentAlernative(attachment: attachment) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Components/Statuses/Status Components/StatusVerifiedButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusVerifiedButton.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 12/26/22. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import SwiftUI 17 | 18 | @available(*, deprecated, renamed: "DiscussionVerifiedBadge") 19 | struct StatusVerifiedButton: View { 20 | @State private var showVerifiedInformation = false 21 | 22 | var status: Status 23 | 24 | private var verifiedAccountDomain: String? { 25 | if let reblog = status.reblog { 26 | return reblog.account.verifiedDomain() 27 | } 28 | return status.account.verifiedDomain() 29 | } 30 | 31 | var body: some View { 32 | Button { 33 | showVerifiedInformation.toggle() 34 | } label: { 35 | Image(systemName: "checkmark.seal") 36 | .foregroundColor(.green) 37 | .imageScale(.small) 38 | }.popover(isPresented: $showVerifiedInformation) { 39 | VStack(alignment: .leading) { 40 | Label("status.verified.title", systemImage: "checkmark.seal") 41 | .font(.headline) 42 | Text( 43 | String( 44 | format: NSLocalizedString("status.verified.detail", comment: "Verified domain"), 45 | status.originalAuthor().getAccountName(), 46 | verifiedAccountDomain ?? "" 47 | ).attributedHTML() 48 | ) 49 | .font(.subheadline) 50 | } 51 | .tint(.green) 52 | .padding() 53 | .frame(minWidth: 200, idealWidth: 350, maxWidth: 400) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Components/Statuses/StatusQuickGlanceView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusQuickGlanceView.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/7/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import SwiftUI 17 | 18 | struct StatusQuickGlanceView: View { 19 | var status: Status 20 | 21 | var body: some View { 22 | HStack { 23 | if status.bookmarked == true || status.reblog?.bookmarked == true { 24 | Image(systemName: "bookmark.fill") 25 | .foregroundColor(.indigo) 26 | } 27 | if status.favourited == true || status.reblog?.favourited == true { 28 | Image(systemName: "star.fill") 29 | .foregroundColor(.yellow) 30 | } 31 | if status.reblogged == true || status.reblog?.reblogged == true { 32 | Image(systemName: "arrow.triangle.2.circlepath.circle.fill") 33 | .foregroundColor(.blue) 34 | } 35 | if status.favourited != true, status.bookmarked != true, status.reblogged != true { 36 | Image(systemName: "circle") 37 | .foregroundColor(Color(uiColor: .systemBackground)) 38 | .opacity(0) 39 | } 40 | } 41 | .font(.caption) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Features/Composer/AuthorReplySegment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthorReplySegment.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 17/2/22. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import Foundation 17 | import SwiftUI 18 | 19 | /// A view that displays a status that will be replied to. 20 | struct AuthorReplySegment: View { 21 | /// The status that the user will reply to. 22 | @State var reply: Status 23 | 24 | var body: some View { 25 | VStack(alignment: .leading, spacing: 8) { 26 | Label(String( 27 | format: "status.replytext".localized(comment: "Reply"), 28 | reply.originalAuthor().getAccountName() 29 | ), systemImage: "arrowshape.turn.up.left.fill") 30 | .font(.system(.callout, design: .rounded)) 31 | .bold() 32 | 33 | StatusView(status: reply) 34 | .datePlacement(.underContent) 35 | .showsDisclosedContent(.constant(true)) 36 | .profilePlacement(.hidden) 37 | .lineLimit(3) 38 | .foregroundColor(.secondary) 39 | .allowsHitTesting(false) 40 | } 41 | .padding(.leading, 10) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Features/Composer/AuthorViewContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthorViewContext.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 12/25/22. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Foundation 16 | 17 | struct AuthoringContext: Codable, Hashable { 18 | var replyingToID: String = "" 19 | var forwardingURI: String = "" 20 | } 21 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Features/Composer/AuthorViewParticipantsField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthorViewParticipantsField.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 2/4/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import Bunker 17 | import SwiftUI 18 | 19 | struct AuthorViewParticipantsField: View { 20 | @StateObject var viewModel: AuthorViewModel 21 | 22 | var body: some View { 23 | HStack { 24 | Text(viewModel.visibility == .direct ? "To: " : "Cc: ") 25 | TextField("status.participants.empty", text: $viewModel.mentionString, axis: .vertical) 26 | .lineLimit(1 ... 3) 27 | .textInputAutocapitalization(.none) 28 | .multilineTextAlignment(.trailing) 29 | .autocorrectionDisabled() 30 | .foregroundColor(.accentColor) 31 | .keyboardType(.emailAddress) 32 | if viewModel.mentionString.isNotEmpty { 33 | Button { 34 | viewModel.mentionString = "" 35 | } label: { 36 | Label("Clear", systemImage: "xmark.circle.fill") 37 | .labelStyle(.iconOnly) 38 | .foregroundColor(.secondary) 39 | } 40 | .buttonStyle(.borderless) 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Features/Composer/AuthoringScene.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthoringScene.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 12/25/22. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import enum Alice.Visibility 17 | import GardenComposer 18 | import SwiftUI 19 | 20 | struct AuthoringScene: Scene { 21 | @State var deeplinkedContext: AuthoringContext? 22 | 23 | var body: some Scene { 24 | WindowGroup("status.create", for: AuthoringContext.self) { authorContext in 25 | Group { 26 | NavigationStack { 27 | AuthorView(authoringContext: authorContext.wrappedValue ?? AuthoringContext()) 28 | } 29 | } 30 | } 31 | .commands { TextEditingCommands() } 32 | 33 | WindowGroup("status.create") { 34 | Group { 35 | NavigationStack { 36 | AuthorView(authoringContext: deeplinkedContext ?? AuthoringContext()) 37 | } 38 | } 39 | .handlesExternalEvents( 40 | preferring: ["create", "app.fedigardens.mail.authorscene"], 41 | allowing: ["create", "app.fedigardens.mail.authorscene"] 42 | ) 43 | .onOpenURL { url in 44 | getContextFromDeeplink(of: url) 45 | } 46 | } 47 | .handlesExternalEvents( 48 | matching: ["create", "app.fedigardens.mail.authorscene"] 49 | ) 50 | .commands { TextEditingCommands() } 51 | } 52 | 53 | private func getContextFromDeeplink(of url: URL) { 54 | deeplinkedContext = AuthoringContext(from: url) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Features/Intervention/InterventionAllowedMechanisms+UserDefaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InterventionAllowedMechanism+UserDefaults.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 26/6/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Foundation 16 | import GardenSettings 17 | import Interventions 18 | 19 | extension InterventionAllowedMechanisms { 20 | private struct InterventionConstants { 21 | @GardenSetting(key: .allowsInterventions) static var allowsInterventions = true 22 | @GardenSetting(key: .intervenesOnRefresh) static var intervenesOnRefresh = true 23 | @GardenSetting(key: .intervenesOnFetch) static var intervenesOnFetch = true 24 | } 25 | 26 | @available(*, deprecated, message: "Use InterventionAllowedMechanisms.fromDefaults without any arguments.") 27 | static func fromDefaults(_ store: UserDefaults = .standard) -> InterventionAllowedMechanisms { 28 | guard InterventionConstants.allowsInterventions else { return .none } 29 | var options: InterventionAllowedMechanisms = [] 30 | if InterventionConstants.intervenesOnRefresh { 31 | options.insert(.refresh) 32 | } 33 | 34 | if InterventionConstants.intervenesOnFetch { 35 | options.insert(.fetchMore) 36 | } 37 | 38 | return options 39 | } 40 | 41 | static func fromDefaults() -> InterventionAllowedMechanisms { 42 | guard InterventionConstants.allowsInterventions else { return .none } 43 | var options: InterventionAllowedMechanisms = [] 44 | if InterventionConstants.intervenesOnRefresh { 45 | options.insert(.refresh) 46 | } 47 | 48 | if InterventionConstants.intervenesOnFetch { 49 | options.insert(.fetchMore) 50 | } 51 | 52 | return options 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Features/Media Viewer/AttachmentMediaGroup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusMediaDrawer.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 12/24/22. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import AVKit 17 | import SwiftUI 18 | 19 | struct AttachmentMediaGroup: View { 20 | var status: Status 21 | 22 | private var mediaAttachments: [MediaAttachment] { 23 | status.reblog?.mediaAttachments ?? status.mediaAttachments 24 | } 25 | 26 | private var columns: [GridItem] { 27 | if mediaAttachments.count > 1 { return [GridItem(.flexible()), GridItem(.flexible())] } 28 | return [GridItem(.flexible())] 29 | } 30 | 31 | var body: some View { 32 | LazyVGrid(columns: columns, spacing: 10) { 33 | ForEach(mediaAttachments) { (attachment: MediaAttachment) in 34 | AttachmentMedia(attachment: attachment) 35 | } 36 | } 37 | .padding(.vertical) 38 | } 39 | } 40 | 41 | struct AttachmentMediaGroup_Previews: PreviewProvider { 42 | static var previews: some View { 43 | VStack { 44 | StatusView(status: MockData.status!) 45 | .lineLimit(6) 46 | .redacted(reason: .privacy) 47 | AttachmentMediaGroup(status: MockData.status!) 48 | Spacer() 49 | } 50 | .padding() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Features/Media Viewer/AttachmentVideoPlayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttachmentVideoPlayer.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 3/11/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import AVKit 16 | import SwiftUI 17 | import UIKit 18 | 19 | struct AttachmentVideoPlayer: UIViewControllerRepresentable { 20 | typealias UIViewControllerType = AVPlayerViewController 21 | var player: AVPlayer? 22 | 23 | func makeUIViewController(context _: Context) -> AVPlayerViewController { 24 | let controller = AVPlayerViewController() 25 | controller.player = player 26 | controller.showsPlaybackControls = true 27 | controller.entersFullScreenWhenPlaybackBegins = true 28 | controller.videoGravity = .resizeAspect 29 | controller.showsTimecodes = true 30 | return controller 31 | } 32 | 33 | func updateUIViewController(_: AVPlayerViewController, context _: Context) {} 34 | } 35 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Features/Media Viewer/AttachmentViewerViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttachmentViewerViewModel.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 2/5/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import Combine 17 | import SwiftUI 18 | 19 | class AttachmentViewerViewModel: ObservableObject { 20 | @Published var attachments = [MediaAttachment]() 21 | @Published var contentMode = ContentMode.fit 22 | @Published var currentAttachment: MediaAttachment? 23 | @Published var didAcknowledgeConsent = false 24 | @Published var magnification = 1.0 25 | @Published var shouldDisplayConsentAcknowledgementAlert = false 26 | 27 | func toggleContentMode() { 28 | contentMode = (contentMode == .fit) ? .fill : .fit 29 | } 30 | 31 | func systemImageForContentMode() -> String { 32 | switch contentMode { 33 | case .fill: 34 | return "arrow.down.right.and.arrow.up.left" 35 | case .fit: 36 | return "arrow.up.left.and.arrow.down.right" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Features/Messaging/MessagingDetailViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessagingDetailViewModel.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 12/27/22. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import Combine 17 | import Foundation 18 | 19 | class MessagingDetailViewModel: ObservableObject { 20 | @Published var conversation: Conversation? 21 | @Published var context: Context? 22 | 23 | init() { 24 | conversation = nil 25 | context = nil 26 | } 27 | 28 | init(conversation: Conversation) { 29 | self.conversation = conversation 30 | } 31 | 32 | func replaceConversation(with newConversation: Conversation) async -> LayoutState { 33 | DispatchQueue.main.async { 34 | self.context = nil 35 | self.conversation = newConversation 36 | } 37 | return await fetchContext(of: newConversation) 38 | } 39 | 40 | func fetchContext(of newConversation: Conversation?) async -> LayoutState { 41 | let fetchContextRequest = newConversation ?? conversation 42 | guard let request = fetchContextRequest?.lastStatus?.id else { return .loaded } 43 | let response: Alice.Response = await Alice.shared.get(.context(id: request)) 44 | switch response { 45 | case .success(let context): 46 | DispatchQueue.main.async { 47 | self.context = context 48 | } 49 | return .loaded 50 | case .failure(let error): 51 | print("Couldn't fetch context: \(error)") 52 | return .errored(message: error.localizedDescription) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Features/Polling/PollVotingViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PollVotingViewModel.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 2/11/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import Combine 17 | import Foundation 18 | 19 | class PollVotingViewModel: ObservableObject { 20 | @Published var poll: Poll? 21 | @Published var currentVote: Int = -1 22 | 23 | func expirationIsNear(difference: TimeInterval) -> Bool { 24 | guard let expiry = poll?.expiresAt, let date = DateFormatter.mastodon.date(from: expiry) else { return false } 25 | return abs(date.distance(to: .now)) <= difference 26 | } 27 | 28 | func pollExpirationDate() -> String? { 29 | guard let expiration = poll?.expiresAt, let date = DateFormatter.mastodon.date(from: expiration) else { 30 | return nil 31 | } 32 | 33 | let relativeFormatter = RelativeDateTimeFormatter() 34 | return relativeFormatter.localizedString(for: date, relativeTo: .now) 35 | } 36 | 37 | func submit(completion: @escaping (Poll) -> Void) async { 38 | guard let poll, (0 ..< poll.options.count).contains(currentVote) else { return } 39 | let response: Alice.Response = await Alice.shared.post( 40 | .votePoll(id: poll.id), 41 | params: ["choices[]": String(currentVote)] 42 | ) 43 | switch response { 44 | case .success(let result): 45 | DispatchQueue.main.async { 46 | self.poll = result 47 | completion(result) 48 | } 49 | case .failure(let error): 50 | print("Vote error: \(error.localizedDescription)") 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Features/Search/SearchTagView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchTagView.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/28/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import SwiftUI 17 | 18 | struct SearchTagView: View { 19 | var tag: Tag 20 | 21 | var accountsUsingTag: Int { 22 | guard let accounts = tag.history?.map(\.accounts) else { return 0 } 23 | return accounts.map { Int($0) ?? 0 }.reduce(0, +) / accounts.count 24 | } 25 | 26 | var body: some View { 27 | Label { 28 | VStack(alignment: .leading) { 29 | Text(tag.name) 30 | .font(.headline) 31 | Text( 32 | String(format: "search.tagspeople".localized(), accountsUsingTag) 33 | ) 34 | .font(.subheadline) 35 | } 36 | } icon: { 37 | Image(systemName: "tag") 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Features/Search/Tagging/SearchTagViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchTagViewModel.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/28/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import Combine 17 | import Foundation 18 | 19 | class SearchTagViewModel: ObservableObject { 20 | @Published var tag: Tag? 21 | @Published var timeline = [Status]() 22 | 23 | func fetchTimeline() async { 24 | guard let tag else { return } 25 | let scope: TimelineScope = .tag(tag: tag.name) 26 | let response: Alice.Response<[Status]> = await Alice.shared.get(.timeline(scope: scope)) 27 | switch response { 28 | case .success(let statuses): 29 | DispatchQueue.main.async { 30 | self.timeline = statuses 31 | } 32 | case .failure(let error): 33 | print("Failed to get timeline: \(error.localizedDescription)") 34 | } 35 | } 36 | 37 | func subscribeToTag() async { 38 | guard let tag else { return } 39 | let response: Alice.Response = await Alice.shared.post(.followTag(id: tag.name)) 40 | switch response { 41 | case .success(let newTag): 42 | DispatchQueue.main.async { 43 | self.tag = newTag 44 | } 45 | case .failure(let error): 46 | print("Follow tag error: \(error.localizedDescription)") 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Features/Search/Tagging/TagHistoryChart.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TagHistoryChart.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/28/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import Charts 17 | import SwiftUI 18 | 19 | struct TagHistorySeries: Identifiable { 20 | let stat: String 21 | let dataPoint: [TagHistoryDataPoint] 22 | 23 | var id: String { stat } 24 | } 25 | 26 | struct TagHistoryDataPoint: Identifiable { 27 | let date: Date 28 | let dataPoint: Int 29 | 30 | var id: Double { date.timeIntervalSince1970 } 31 | } 32 | 33 | struct TagHistoryChart: View { 34 | var history: [History] 35 | 36 | private var series: [TagHistorySeries] { 37 | [ 38 | .init( 39 | stat: "search.tag.chartuse".localized(), 40 | dataPoint: history.map { TagHistoryDataPoint(date: $0.date(), dataPoint: $0.numberOfUses()) } 41 | ), 42 | .init( 43 | stat: "search.tag.chartacct".localized(), 44 | dataPoint: history.map { TagHistoryDataPoint(date: $0.date(), dataPoint: $0.numberOfAccounts()) } 45 | ) 46 | ] 47 | } 48 | 49 | var body: some View { 50 | Chart(series) { seriesPoint in 51 | ForEach(seriesPoint.dataPoint) { element in 52 | LineMark( 53 | x: .value("Day", element.date, unit: .day), 54 | y: .value("Data Point", element.dataPoint) 55 | ) 56 | .foregroundStyle(by: .value("Data Point", seriesPoint.stat)) 57 | .symbol(by: .value("Data Point", seriesPoint.stat)) 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Layout/Interactions/InteractionsListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationsListView.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/7/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import SwiftUI 17 | 18 | struct InteractionsListView: View { 19 | @Binding var selectedStatus: Status? 20 | @StateObject private var viewModel = InteractionsListViewModel() 21 | 22 | var body: some View { 23 | Group { 24 | switch viewModel.state { 25 | case .initial: 26 | Image(systemName: "bell") 27 | .foregroundColor(.secondary) 28 | .font(.largeTitle) 29 | case .loading: 30 | ProgressView() 31 | .font(.largeTitle) 32 | case .loaded: 33 | List(selection: $selectedStatus) { 34 | ForEach(viewModel.notifications, id: \.uuid) { mention in 35 | if let response = mention.status { 36 | NavigationLink(value: response) { 37 | NotificationListCellView(notification: mention) 38 | } 39 | } else { 40 | Text("WTF?!: \(mention.type.rawValue)") 41 | } 42 | } 43 | } 44 | case .errored(let message): 45 | Text(message) 46 | } 47 | } 48 | .onAppear { 49 | Task { 50 | await viewModel.fetchNotifications() 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Layout/Interactions/InteractionsListViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationsListViewModel.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/7/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import struct Alice.Notification 17 | import Combine 18 | import Foundation 19 | import GardenSettings 20 | 21 | class InteractionsListViewModel: ObservableObject { 22 | @Published var notifications = [Notification]() 23 | @Published var state = LayoutState.initial 24 | @GardenSetting(key: .loadLimit) private var loadLimit = 10 25 | 26 | init() {} 27 | 28 | func fetchNotifications() async { 29 | DispatchQueue.main.async { self.state = .loading } 30 | let response: Alice.Response<[Notification]> = await Alice.shared.get( 31 | .notifications, 32 | params: [ 33 | "types[]": Notification.NotificationType.mention.rawValue, 34 | "limit": String(loadLimit) 35 | ] 36 | ) 37 | switch response { 38 | case .success(let notificationList): 39 | DispatchQueue.main.async { 40 | self.notifications.append(contentsOf: notificationList) 41 | self.state = .loaded 42 | } 43 | case .failure(let error): 44 | print("Notification fetch failure: \(error.localizedDescription)") 45 | DispatchQueue.main.async { self.state = .errored(message: error.localizedDescription) } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Layout/Interactions/NotificationListCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationListCellView.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/7/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import struct Alice.Notification 17 | import SwiftUI 18 | 19 | struct NotificationListCellView: View { 20 | var notification: Notification 21 | 22 | var body: some View { 23 | VStack(alignment: .trailing) { 24 | if let reply = notification.status { 25 | Group { 26 | StatusView(status: reply) 27 | .lineLimit(2) 28 | .profilePlacement(.hidden) 29 | .datePlacement(.automatic) 30 | .profileImageSize(44) 31 | .verifiedNoticePlacement(.byAuthorName) 32 | .tint(.secondary) 33 | StatusQuickGlanceView(status: reply) 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Layout/LayoutStateRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutStateRepresentable.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 12/2/22. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Foundation 16 | 17 | /// A protocol used on views to determine that their state can change. 18 | protocol LayoutStateRepresentable { 19 | /// The current state of the view. 20 | var state: LayoutState { get set } 21 | } 22 | 23 | /// An enumeration representing the various states for a view to undergo. 24 | enum LayoutState: Hashable { 25 | /// The view has been initialized, but no data has been loaded yet. 26 | case initial 27 | 28 | /// The view is currently fetching data. 29 | case loading 30 | 31 | /// The view has loaded data and is ready to render it. 32 | case loaded 33 | 34 | /// The view encountered an error when trying to load the data. 35 | case errored(message: String) 36 | } 37 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Layout/Sidebar Sections/GardensAppSubscribedTagsDestination.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GardensAppSubscribedTagsDestination.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/21/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import SwiftUI 17 | 18 | struct GardensAppSubscribedTagsDestination: View { 19 | @StateObject var viewModel: GardensAppLayoutViewModel 20 | 21 | var body: some View { 22 | Section { 23 | ForEach(viewModel.subscribedTags) { tag in 24 | NavigationLink(value: GardensAppPage.trending(id: tag.name)) { 25 | Label(tag.name, systemImage: "dot.radiowaves.up.forward") 26 | } 27 | } 28 | .onDelete { indexSet in 29 | withAnimation { 30 | viewModel.deleteSubscribedTags(at: indexSet) 31 | } 32 | } 33 | } header: { 34 | Text("endpoint.followedtags") 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Layout/Sidebar Sections/GardensAppTrendingTagsDestination.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GardensAppTrendingTagsDestination.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/21/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import SwiftUI 17 | 18 | struct GardensAppTrendingTagsDestination: View { 19 | @StateObject var viewModel: GardensAppLayoutViewModel 20 | 21 | var body: some View { 22 | Section { 23 | ForEach(viewModel.tags) { tag in 24 | NavigationLink(value: GardensAppPage.trending(id: tag.name)) { 25 | Label(tag.name, systemImage: "tag") 26 | } 27 | } 28 | } header: { 29 | Text(GardensAppPage.trending(id: "0").localizedTitle) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Layout/Status Detail View/StatusContextProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusContextProvider.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 12/26/22. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import SwiftUI 17 | 18 | struct StatusContextProvider: View { 19 | enum ContextDisplayMode { 20 | case ancestors 21 | case descendants 22 | } 23 | 24 | @ObservedObject var viewModel: StatusDetailViewModel 25 | var context: Context 26 | var displayMode: ContextDisplayMode = .descendants 27 | 28 | var statuses: [Status]? { 29 | switch displayMode { 30 | case .descendants: 31 | return context.descendants 32 | case .ancestors: 33 | return context.ancestors 34 | } 35 | } 36 | 37 | var body: some View { 38 | Section { 39 | if let replies = statuses, !replies.isEmpty { 40 | ForEach(replies, id: \.uuid) { reply in 41 | contextLink(for: reply) 42 | } 43 | } 44 | } 45 | .listRowSeparator(.hidden) 46 | } 47 | 48 | private func contextLink(for reply: Status) -> some View { 49 | NavigationLink(value: viewModel.contextCaller(for: reply)) { 50 | HStack(alignment: .top, spacing: 16) { 51 | Image(systemName: "text.bubble") 52 | .imageScale(.large) 53 | StatusView(status: reply) 54 | .profilePlacement(.byAuthorName) 55 | .profileImageSize(32) 56 | .datePlacement(.underContent) 57 | } 58 | .listRowInsets(.init(top: 8, leading: 32, bottom: 8, trailing: 16)) 59 | .listRowSeparator(.hidden, edges: .all) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Layout/Wide Layout/GardensAppWideCommonDestinationsGroup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GardensAppWideCommonDestinationsGroup.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/21/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import SwiftUI 16 | 17 | struct GardensAppWideCommonDestinationsGroup: View { 18 | var body: some View { 19 | Group { 20 | GardensPageLink(page: .forYou) 21 | .keyboardShortcut("h", modifiers: [.command, .shift]) 22 | GardensPageLink(page: .local) 23 | .keyboardShortcut("l", modifiers: [.command, .shift]) 24 | GardensPageLink(page: .mentions) 25 | .keyboardShortcut("i", modifiers: [.command, .shift]) 26 | GardensPageLink(page: .search) 27 | .keyboardShortcut(.space, modifiers: [.command, .shift]) 28 | GardensPageLink(page: .settings) 29 | .keyboardShortcut(",", modifiers: .command) 30 | 31 | Section { 32 | GardensPageLink(page: .public) 33 | GardensPageLink(page: .messages) 34 | GardensPageLink(page: .selfPosts) 35 | GardensPageLink(page: .saved) 36 | } header: { 37 | Text("general.more") 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Settings/Pages/SettingsEnergyPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsEnergyPage.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 4/30/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import GardenSettings 16 | import SwiftUI 17 | 18 | struct SettingsEnergyPage: View { 19 | @Environment(\.enforcedFrugalMode) var enforcedFrugalMode 20 | @AppStorage(.loadLimit) var loadLimit: Int = 10 21 | @AppStorage(.frugalMode) private var frugalMode: Bool = false 22 | 23 | var body: some View { 24 | Form { 25 | Section { 26 | Stepper(value: $loadLimit, step: 5) { 27 | Text( 28 | String( 29 | format: 30 | NSLocalizedString("settings.loadlimit.text", comment: "load limit"), 31 | String(loadLimit) 32 | ) 33 | ) 34 | } 35 | } footer: { 36 | Text("settings.loadlimit.detail") 37 | } 38 | .disabled(frugalMode) 39 | 40 | Section { 41 | Toggle(isOn: $frugalMode) { 42 | Text("settings.frugalmode.title") 43 | } 44 | .disabled(enforcedFrugalMode) 45 | } footer: { 46 | Text("settings.frugalmode.detail") 47 | } 48 | } 49 | .navigationTitle("settings.section.energy") 50 | } 51 | } 52 | 53 | struct SettingsEnergyPage_Previews: PreviewProvider { 54 | static var previews: some View { 55 | NavigationStack { 56 | SettingsEnergyPage() 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Settings/Pages/SettingsInstanceBlocklistPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsInstanceBlocklistPage.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 2/12/23. 6 | // 7 | 8 | import Alice 9 | import Bunker 10 | import SwiftUI 11 | 12 | struct SettingsInstanceBlocklistPage: View { 13 | var domainBlocks: [DomainBlock] 14 | @ScaledMetric private var size = 1.0 15 | 16 | private var silenced: [DomainBlock] { 17 | domainBlocks.filter { $0.severity == .silence } 18 | } 19 | 20 | private var suspended: [DomainBlock] { 21 | domainBlocks.filter { $0.severity == .suspend } 22 | } 23 | 24 | var body: some View { 25 | List { 26 | if silenced.isNotEmpty { 27 | Section { 28 | ForEach(silenced) { blockedServer in 29 | cell(for: blockedServer) 30 | } 31 | } header: { 32 | Text("settings.blocklist.silenced") 33 | } 34 | } 35 | 36 | if suspended.isNotEmpty { 37 | Section { 38 | ForEach(suspended) { blockedServer in 39 | cell(for: blockedServer) 40 | } 41 | } header: { 42 | Text("settings.blocklist.suspended") 43 | } 44 | } 45 | } 46 | .tint(.red) 47 | .navigationTitle("settings.blocklist.instancewide") 48 | } 49 | 50 | func cell(for domainBlock: DomainBlock) -> some View { 51 | Label { 52 | VStack(alignment: .leading) { 53 | Text(domainBlock.domain) 54 | .font(.headline) 55 | .bold() 56 | Text(domainBlock.comment ?? "settings.blocklist.nocomment".localized()) 57 | .font(.subheadline) 58 | .foregroundColor(.secondary) 59 | } 60 | } icon: { 61 | Image(systemName: domainBlock.severity == .suspend ? "hand.raised.square" : "speaker.slash.fill") 62 | .font(.headline) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Settings/Pages/SettingsLicensePage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsLicensePage.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/29/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import SwiftUI 16 | 17 | struct SettingsLicensePage: View { 18 | @State private var licenseText: String = "" 19 | 20 | var body: some View { 21 | Group { 22 | if licenseText.isEmpty { 23 | VStack { 24 | ProgressView() 25 | } 26 | } else { 27 | RunestoneViewer(text: licenseText) 28 | } 29 | } 30 | .navigationTitle("settings.about.license") 31 | .navigationBarTitleDisplayMode(.inline) 32 | .animation(.spring(), value: licenseText) 33 | .onAppear { 34 | Task { 35 | await fetchLicense() 36 | } 37 | } 38 | } 39 | 40 | private func fetchLicense() async { 41 | guard let resource = Bundle.main.path(forResource: "CNPL", ofType: "md") else { return } 42 | guard let string = try? String(contentsOfFile: resource) else { return } 43 | licenseText = string 44 | // let attributedString = try? AttributedString( 45 | // markdown: string, 46 | // options: .init( 47 | // allowsExtendedAttributes: true, 48 | // interpretedSyntax: .inlineOnlyPreservingWhitespace 49 | // ) 50 | // ) 51 | // DispatchQueue.main.async { 52 | // if let text = attributedString { 53 | // self.licenseText = text 54 | // } 55 | // } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Settings/SettingsVersionBlock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsVersionBlock.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/29/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import SwiftUI 16 | 17 | struct SettingsVersionBlock: View { 18 | var body: some View { 19 | HStack { 20 | Spacer() 21 | VStack { 22 | Image("GardensIcon") 23 | .resizable() 24 | .scaledToFit() 25 | .frame(minWidth: 64, idealWidth: 84, maxWidth: 96) 26 | Text("general.appname") 27 | .font(.title2) 28 | .bold() 29 | Text("v" + Bundle.main.getAppVersion()) 30 | .foregroundColor(.secondary) 31 | .monospacedDigit() 32 | } 33 | Spacer() 34 | } 35 | .padding(.vertical) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Source/RunestoneViewer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunestoneViewer.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 5/5/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Runestone 16 | import SwiftUI 17 | 18 | struct RunestoneViewer: UIViewRepresentable { 19 | typealias UIViewType = Runestone.TextView 20 | 21 | var text: String 22 | 23 | func makeUIView(context _: Context) -> Runestone.TextView { 24 | let viewer = Runestone.TextView() 25 | viewer.backgroundColor = .systemBackground 26 | viewer.setLanguageMode(PlainTextLanguageMode()) 27 | viewer.text = text 28 | viewer.isFindInteractionEnabled = true 29 | viewer.isEditable = false 30 | viewer.showLineNumbers = false 31 | viewer.lineHeightMultiplier = 1.25 32 | viewer.theme = DefaultTheme() 33 | viewer.isLineWrappingEnabled = true 34 | viewer.textContainerInset = .init(top: 2, left: 8, bottom: 2, right: 8) 35 | return viewer 36 | } 37 | 38 | func updateUIView(_: Runestone.TextView, context _: Context) {} 39 | } 40 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Utilities/Acknowledgement+License.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Acknowledgement+License.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 12/31/22. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import AckGen 16 | import Foundation 17 | 18 | extension Acknowledgement { 19 | static func license(named name: String, ofType type: String = "txt") -> String { 20 | guard let path = Bundle.main.path(forResource: name, ofType: type) else { return "" } 21 | do { 22 | return try String(contentsOfFile: path) 23 | } catch { 24 | return "" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Utilities/Extensions/Alice/Account+AccountName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Account+AccountName.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 17/2/22. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import Foundation 17 | import WebString 18 | 19 | extension Account { 20 | /// Returns the account name for this account. 21 | /// 22 | /// This will attempt to return the display name, username, or account name (in the order specified). 23 | func getAccountName() -> String { 24 | if !displayName.isEmpty { return displayName } 25 | if !username.isEmpty { return "@\(username)" } 26 | return "@\(acct)" 27 | } 28 | 29 | /// Returns whether the account is verified. 30 | func verified() -> Bool { 31 | return fields.contains { field in field.verifiedAt != nil } 32 | } 33 | 34 | /// Returns the domain from which that account was verified. 35 | func verifiedDomain() -> String? { 36 | return fields.first { field in field.verifiedAt != nil }?.value 37 | } 38 | 39 | /// Returns the Matrix ID of a user if they have left information about it in their bio. 40 | func matrixID() -> String? { 41 | guard let matrixValue = fields.first(where: { $0.name == "Matrix" })? 42 | .value.plainTextContent else { return nil } 43 | let matrixRegex = /[\@\+\!\#\$][a-z0-9\.\_\=\-\/]+:(.*)/ 44 | return try? matrixRegex.wholeMatch(in: matrixValue) == nil ? nil : matrixValue 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Utilities/Extensions/Alice/Conversation+Authors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Conversation+Authors.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 25/2/22. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import Foundation 17 | 18 | extension Conversation { 19 | /// Returns a localized string containing the names of the participants, excluding the specified member ID. 20 | /// - Parameter currentUserID: The ID of the account to exclude in the localized string generation. 21 | func getAuthors(excluding currentUserID: String) -> String { 22 | if accounts.count <= 2 { 23 | return accounts.last { account in account.id != currentUserID }?.getAccountName() ?? "Person" 24 | } 25 | 26 | let firstTwoNames = Array(accounts.filter { account in account.id != currentUserID }[0 ..< 2]) 27 | let firstAuthors = firstTwoNames.reduce("") { text, account in 28 | text + "\(account.getAccountName()), " 29 | } 30 | 31 | let remainingText = String( 32 | format: NSLocalizedString("direct.grouptitle", comment: "remains"), 33 | accounts.count - 2 34 | ) 35 | 36 | return firstAuthors + remainingText 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Utilities/Extensions/Alice/Tag+Chartable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tag+Chartable.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/28/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import UIKit 17 | 18 | extension History { 19 | func date() -> Date { 20 | let timeInterval = TimeInterval(day) ?? 0 21 | return Date(timeIntervalSince1970: timeInterval) 22 | } 23 | 24 | func numberOfUses() -> Int { 25 | return Int(uses) ?? 0 26 | } 27 | 28 | func numberOfAccounts() -> Int { 29 | return Int(accounts) ?? 0 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Utilities/Extensions/Alice/Visibility+Localization.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Visibility+Localization.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/21/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import SwiftUI 17 | 18 | extension PostVisibility { 19 | var localizedDescription: LocalizedStringKey { 20 | switch self { 21 | case .public: 22 | return "status.visibility.public" 23 | case .unlisted: 24 | return "status.visibility.unlisted" 25 | case .private: 26 | return "status.visibility.private" 27 | case .direct: 28 | return "status.visibility.direct" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Utilities/Extensions/Bundle+AppVersion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle+AppVersion.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 7/3/22. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Foundation 16 | 17 | extension Bundle { 18 | /// Returns the version and build number of the app in the format "Version (Build)". 19 | /// - Note: If `CFBundleShortVersionString` and/or `CFBundloeVersion` are not found, their values will be replaced 20 | /// with `0`. 21 | func getAppVersion() -> String { 22 | guard let info = infoDictionary else { 23 | return "0 (0.0)" 24 | } 25 | let appVersion = info["CFBundleShortVersionString"] as? String ?? "0.0" 26 | let appBuild = info["CFBundleVersion"] as? String ?? "0" 27 | return "\(appVersion) (\(appBuild))" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Utilities/Extensions/DateFormatter+Mastodon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateFormatter+Mastodon.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 11/2/22. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Foundation 16 | 17 | extension DateFormatter { 18 | /// A date formatter that can parse dates written for Mastodon posts. 19 | /// 20 | /// Due to the unusual nature of Mastodon's date format (even though it follows ISO-8601), a new formatter must 21 | /// be used to parse these dates correctly. 22 | /// - Format: `yyyy-MM-dd'T'HH:mm:ss.SZ` 23 | static var mastodon: DateFormatter { 24 | let format = DateFormatter() 25 | format.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ" 26 | format.timeZone = .init(abbreviation: "UTC") 27 | format.locale = .init(identifier: "en_US_POSIX") 28 | return format 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Utilities/Extensions/NSTextCheckingResult+Substring.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSTextCheckingResult+Substring.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/21/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Foundation 16 | 17 | extension NSTextCheckingResult { 18 | func text(in sourceText: String) -> Substring? { 19 | guard let range = Range(range, in: sourceText) else { return nil } 20 | return sourceText[range] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Utilities/Extensions/UIDevice+Model.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIDevice+Model.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/6/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import SwiftUI 16 | import UIKit 17 | 18 | extension UIDevice { 19 | typealias ModelName = String 20 | /// Returns the device model identifier. 21 | /// Pulled from https://stackoverflow.com/questions/26028918/how-to-determine-the-current-iphone-device-model. 22 | static let model: ModelName = { 23 | #if targetEnvironment(simulator) 24 | return ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "arm64" 25 | #else 26 | var systemInfo = utsname() 27 | uname(&systemInfo) 28 | let machineMirror = Mirror(reflecting: systemInfo.machine) 29 | let identifier = machineMirror.children.reduce("") { identifier, element in 30 | guard let value = element.value as? Int8, value != 0 else { return identifier } 31 | return identifier + String(UnicodeScalar(UInt8(value))) 32 | } 33 | return identifier 34 | #endif 35 | }() 36 | } 37 | 38 | private struct DeviceModelEnvironmentKey: EnvironmentKey { 39 | static let defaultValue: UIDevice.ModelName = UIDevice.model 40 | } 41 | 42 | extension EnvironmentValues { 43 | var deviceModel: UIDevice.ModelName { 44 | get { self[DeviceModelEnvironmentKey.self] } 45 | set { self[DeviceModelEnvironmentKey.self] = newValue } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Utilities/Extensions/View+ExtraModifiers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+ExtraModifiers.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 5/4/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import SwiftUI 16 | 17 | extension View { 18 | func onReceive(of name: Notification.Name, 19 | from center: NotificationCenter = .default, 20 | in object: AnyObject? = nil, 21 | perform action: @escaping (Notification) -> Void) -> some View { 22 | onReceive(center.publisher(for: name, object: object), perform: action) 23 | } 24 | 25 | func onOpenURL(perform task: @escaping (URL) async -> Void) -> some View { 26 | self.onOpenURL(perform: { url in 27 | Task { 28 | await task(url) 29 | } 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Utilities/Mock/JSONDecoder+Resources.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONDecoder+Resources.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 11/2/22. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Foundation 16 | 17 | extension JSONDecoder { 18 | /// Decode a JSON file located in a bundle's resources to a specified `Decodable` struct. 19 | /// - Parameter resourcePath: The name of the resource to decode. 20 | static func decodeFromResource(from resourcePath: String) throws -> T? { 21 | var content: T? 22 | 23 | if let bundleResourcePath = Bundle.main.path(forResource: resourcePath, ofType: "json") { 24 | let url = URL(fileURLWithPath: bundleResourcePath) 25 | let data = try Data(contentsOf: url, options: .mappedIfSafe) 26 | content = try JSONDecoder().decode(T.self, from: data) 27 | } else { 28 | content = nil 29 | } 30 | 31 | return content 32 | } 33 | 34 | /// Safely decode a JSON file located in a bundle's resources to a specified `Decodable` struct. 35 | /// - Parameter resourcePath: The name of the resource to decode. 36 | /// 37 | /// If the resource failed to be decoded, `nil` will be returned instead. To forcefully decode and throw an error, 38 | /// use ``JSONDecoder.decodeFromResource`` instead. 39 | /// 40 | /// - SeeAlso: ``JSONDecoder.decodeFromResource`` 41 | static func safeDecodeFromResource(from resourcePath: String) -> T? { 42 | var content: T? 43 | do { 44 | content = try decodeFromResource(from: resourcePath) 45 | } catch { 46 | print("An error occurred when decoding from JSON: \(error.localizedDescription)") 47 | content = nil 48 | } 49 | return content 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Utilities/Mock/Mock Data/Poll.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "6", 3 | "expires_at": "2020-09-15T03:59:02.588Z", 4 | "expired": true, 5 | "multiple": true, 6 | "votes_count": 3, 7 | "voters_count": 3, 8 | "voted": true, 9 | "own_votes": [], 10 | "options": [ 11 | { 12 | "title": "Hell yeah!", 13 | "votes_count": 3 14 | }, 15 | { 16 | "title": "Probably not...", 17 | "votes_count": 0 18 | }, 19 | { 20 | "title": "I'm not sure", 21 | "votes_count": 0 22 | } 23 | ], 24 | "emojis": [] 25 | } 26 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Utilities/Mock/Mock Data/Profile.json: -------------------------------------------------------------------------------- 1 | {"id":"1","username":"admin","acct":"admin","display_name":"Gophey","locked":false,"bot":false,"discoverable":true,"group":false,"created_at":"2019-04-10T00:00:00.000Z","note":"\u003cp\u003eThe administrator account for Gopherdon. Currently managed by \u003cspan class=\"h-card\"\u003e\u003ca href=\"https://mastodon.goucher.edu/@quivical\" class=\"u-url mention\"\u003e@\u003cspan\u003equivical\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e.\u003c/p\u003e","url":"https://mastodon.goucher.edu/@admin","avatar":"https://mastodon.goucher.edu/system/accounts/avatars/000/000/001/original/b945b7c78b18f556.png","avatar_static":"https://mastodon.goucher.edu/system/accounts/avatars/000/000/001/original/b945b7c78b18f556.png","header":"https://mastodon.goucher.edu/system/accounts/headers/000/000/001/original/Gopherdon_Server_Banner.png","header_static":"https://mastodon.goucher.edu/system/accounts/headers/000/000/001/original/Gopherdon_Server_Banner.png","followers_count":55,"following_count":3,"statuses_count":54,"last_status_at":"2022-05-09","emojis":[],"fields":[{"name":"Managed by","value":"\u003cspan class=\"h-card\"\u003e\u003ca href=\"https://mastodon.goucher.edu/@quivical\" class=\"u-url mention\"\u003e@\u003cspan\u003equivical\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e","verified_at":null}]} 2 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Utilities/Mock/Mock Data/README.md: -------------------------------------------------------------------------------- 1 | # Mock Data 2 | 3 | The JSON files loaded in this directory are intended to provide mock data when writing views or other files that rely on 4 | test data that matches the Mastodon API. 5 | 6 | The data samples have been collected from publicly accessible timelines and pertain to the Gophey account on Mastodon. 7 | No data sample should be using data from another account unless written permission has been granted to do so. 8 | 9 | ## Mock Data Contents 10 | 11 | - `Status.json`: Provides a data sample of a status in a timeline. 12 | - `Timeline.json`: Provides a data sample of a timeline. 13 | - `Context.json`: Provides a data sample of a status's context. 14 | - `Conversation.json`: Provides a data sample of a private conversation*. 15 | - `Profile.json`: Provides a data sample of a profile. 16 | - `Poll.json`: Provides a data sample of a poll. 17 | 18 | > *Note: This sample uses public statuses instead of private messages. 19 | -------------------------------------------------------------------------------- /Fedigardens/Modules/Utilities/Mock/MockData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockData.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 12/2/22. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | // swiftlint:disable force_try 16 | 17 | import Alice 18 | import Foundation 19 | 20 | /// A data structure that houses mock data used for the app. 21 | struct MockData { 22 | /// Mock data for a status's context. 23 | /// - Note: The `descendants` property of this mock context can be used for messaging as well. 24 | static let context: Context? = try! JSONDecoder.decodeFromResource(from: "Context") 25 | 26 | /// Mock data for a private conversation. 27 | static let conversation: Conversation? = try! JSONDecoder.decodeFromResource(from: "Conversation") 28 | 29 | /// Mock data for a single status. 30 | static let status: Status? = try! JSONDecoder.decodeFromResource(from: "Status") 31 | 32 | /// Mock data for a timeline (list of statuses). 33 | static let timeline: [Status]? = try! JSONDecoder.decodeFromResource(from: "Timeline") 34 | 35 | static let profile: Account? = try! JSONDecoder.decodeFromResource(from: "Profile") 36 | 37 | static let searchResults: SearchResult? = try! JSONDecoder.decodeFromResource(from: "SearchResult") 38 | 39 | static let poll: Poll? = try! JSONDecoder.decodeFromResource(from: "Poll") 40 | } 41 | 42 | // swiftlint:enable force_try 43 | -------------------------------------------------------------------------------- /Fedigardens/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyCollectedDataTypes 6 | 7 | NSPrivacyTracking 8 | 9 | NSPrivacyTrackingDomains 10 | 11 | NSPrivacyAccessedAPITypes 12 | 13 | 14 | NSPrivacyAccessedAPIType 15 | NSPrivacyAccessedAPICategoryUserDefaults 16 | NSPrivacyAccessedAPITypeReasons 17 | 18 | CA92.1 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "universal", 6 | "reference" : "systemPurpleColor" 7 | }, 8 | "idiom" : "universal" 9 | }, 10 | { 11 | "appearances" : [ 12 | { 13 | "appearance" : "luminosity", 14 | "value" : "dark" 15 | } 16 | ], 17 | "color" : { 18 | "platform" : "universal", 19 | "reference" : "systemPurpleColor" 20 | }, 21 | "idiom" : "universal" 22 | } 23 | ], 24 | "info" : { 25 | "author" : "xcode", 26 | "version" : 1 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicerunsonfedora/fedigardens/07521fd91da6a8a5363275064810ae0813433831/Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-1024.png -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-120 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicerunsonfedora/fedigardens/07521fd91da6a8a5363275064810ae0813433831/Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-120 1.png -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicerunsonfedora/fedigardens/07521fd91da6a8a5363275064810ae0813433831/Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-120.png -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicerunsonfedora/fedigardens/07521fd91da6a8a5363275064810ae0813433831/Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-152.png -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicerunsonfedora/fedigardens/07521fd91da6a8a5363275064810ae0813433831/Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-167.png -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicerunsonfedora/fedigardens/07521fd91da6a8a5363275064810ae0813433831/Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-180.png -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicerunsonfedora/fedigardens/07521fd91da6a8a5363275064810ae0813433831/Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20.png -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicerunsonfedora/fedigardens/07521fd91da6a8a5363275064810ae0813433831/Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-29.png -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicerunsonfedora/fedigardens/07521fd91da6a8a5363275064810ae0813433831/Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40 1.png -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicerunsonfedora/fedigardens/07521fd91da6a8a5363275064810ae0813433831/Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40 2.png -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicerunsonfedora/fedigardens/07521fd91da6a8a5363275064810ae0813433831/Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40.png -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-58 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicerunsonfedora/fedigardens/07521fd91da6a8a5363275064810ae0813433831/Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-58 1.png -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicerunsonfedora/fedigardens/07521fd91da6a8a5363275064810ae0813433831/Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-58.png -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicerunsonfedora/fedigardens/07521fd91da6a8a5363275064810ae0813433831/Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-60.png -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicerunsonfedora/fedigardens/07521fd91da6a8a5363275064810ae0813433831/Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-76.png -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-80 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicerunsonfedora/fedigardens/07521fd91da6a8a5363275064810ae0813433831/Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-80 1.png -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicerunsonfedora/fedigardens/07521fd91da6a8a5363275064810ae0813433831/Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-80.png -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicerunsonfedora/fedigardens/07521fd91da6a8a5363275064810ae0813433831/Fedigardens/Resources/Assets.xcassets/AppIcon.appiconset/Icon-87.png -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/GardensIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Fedigardens2-icon.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/GardensIcon.imageset/Fedigardens2-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicerunsonfedora/fedigardens/07521fd91da6a8a5363275064810ae0813433831/Fedigardens/Resources/Assets.xcassets/GardensIcon.imageset/Fedigardens2-icon.png -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/LaunchScreen.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.871", 9 | "green" : "0.322", 10 | "red" : "0.686" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/OneSec.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "one sec app icon.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/OneSec.imageset/one sec app icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicerunsonfedora/fedigardens/07521fd91da6a8a5363275064810ae0813433831/Fedigardens/Resources/Assets.xcassets/OneSec.imageset/one sec app icon.png -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/Splash.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LaunchScreen.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "original" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Fedigardens/Resources/Assets.xcassets/matrix.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "matrix.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Fedigardens/Resources/Disallow.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | gab.ai 6 | gab.com 7 | exited.eu 8 | not-develop.gab.com 9 | develop.gab.com 10 | ekrem.develop.gab.com 11 | gab.io 12 | gabble.xyz 13 | gab.polaris-1.work 14 | gabfed.com 15 | spinster.xyz 16 | djitter.com 17 | kazvam.com 18 | truthsocial.com 19 | endtimebelievers.com 20 | bitcoinhackers.org 21 | megamast.io 22 | obo.sh 23 | social.ancreport.com 24 | gearlandia.haus 25 | 26 | 27 | -------------------------------------------------------------------------------- /Fedigardens/Supporting Files/MIT-HTML2Markdown.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Matthew Flint 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Fedigardens/Tests/Shared.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Shared.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 11/2/22. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Foundation 16 | 17 | enum ShoutTestsError: Error { 18 | case notImplemented 19 | } 20 | -------------------------------------------------------------------------------- /Fedigardens/Tests/Statuses.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Statuses.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 11/2/22. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | @testable import Fedigardens 16 | import Foundation 17 | import XCTest 18 | 19 | /// Test cases for statuses/posts in Gardens. 20 | class ShoutTestStatus: XCTestCase { 21 | /// Test that a HTML-formatted string is stripped and converted to plain text. 22 | func testStatusConversionToPlainText() throws { 23 | let originalText = "

Hello, world!

" 24 | let expectedText = "Hello, world!" 25 | 26 | let performed = originalText.plainTextContents() 27 | XCTAssertEqual(performed, expectedText) 28 | } 29 | 30 | /// Test that an HTML-formatted string is stripped, converted to plain text, and ignores any HTML formatting. 31 | func testStatusConversionToPlainTextIgnoresFormatting() throws { 32 | let originalText = """ 33 |

34 | Shout, shout, let it all out
35 | These are the things I can do without
36 | Come on; I'm talking to you
37 | Come on 38 |

39 | """ 40 | let convertedText = originalText.plainTextContents() 41 | for element in ["

", "

", "
", "", "", ""] { 42 | XCTAssertTrue(!convertedText.contains(element)) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'danger' 4 | gem 'danger-gitlab' -------------------------------------------------------------------------------- /Packages/Alice/.swiftlint.alice.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - file_header 3 | - orphaned_doc_comment 4 | 5 | excluded: 6 | - Package.swift 7 | -------------------------------------------------------------------------------- /Packages/Alice/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "KeychainAccess", 6 | "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess", 7 | "state": { 8 | "branch": null, 9 | "revision": "84e546727d66f1adc5439debad16270d0fdd04e7", 10 | "version": "4.2.2" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Packages/Alice/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Alice", 8 | platforms: [.macOS(.v13), .iOS(.v16), .tvOS(.v16), .watchOS(.v9)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, and make them visible to other packages. 11 | .library( 12 | name: "Alice", 13 | targets: ["Alice"]), 14 | .library( 15 | name: "AliceMockingbird", 16 | targets: ["AliceMockingbird"]) 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | // .package(url: /* package url */, from: "1.0.0"), 21 | .package(url: "https://github.com/kishikawakatsumi/KeychainAccess", from: "4.2.2") 22 | ], 23 | targets: [ 24 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 25 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 26 | .target( 27 | name: "Alice", 28 | dependencies: ["KeychainAccess"]), 29 | .target( 30 | name: "AliceMockingbird", 31 | dependencies: ["Alice"], 32 | resources: [ 33 | .process("Resources") 34 | ]), 35 | .testTarget( 36 | name: "AliceTests", 37 | dependencies: ["Alice", "AliceMockingbird"]) 38 | ] 39 | ) 40 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Extensions/CustomStringConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomStringConvertible.swift 3 | // 4 | // 5 | // Created by Alex Modroño Vara on 14/7/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Allows to easily debug methods. 11 | public extension CustomStringConvertible { 12 | var description: String { 13 | var description = "========= \(type(of: self)) =========".uppercased() 14 | let selfMirror = Mirror(reflecting: self) 15 | for child in selfMirror.children { 16 | if let propertyName = child.label { 17 | description += "\n\(propertyName): \(child.value)" 18 | } 19 | } 20 | description += "\n====== END OF \(type(of: self)) ======".uppercased() 21 | return description 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Extensions/URL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL.swift 3 | // File 4 | // 5 | // Created by Alex Modroño Vara on 15/7/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension URL { 11 | func queryItem(_ queryItem: String, value: String?) -> URL { 12 | guard var urlComponents = URLComponents(string: absoluteString) else { return absoluteURL } 13 | var queryItems: [URLQueryItem] = urlComponents.queryItems ?? [] 14 | let queryItem = URLQueryItem(name: queryItem, value: value) 15 | queryItems.append(queryItem) 16 | urlComponents.queryItems = queryItems 17 | return urlComponents.url! 18 | } 19 | 20 | var queryParameters: [String: String]? { 21 | guard 22 | let components = URLComponents(url: self, resolvingAgainstBaseURL: true), 23 | let queryItems = components.queryItems else { return nil } 24 | return queryItems.reduce(into: [String: String]()) { result, item in 25 | result[item.name] = item.value 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Helpers/Box.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Marquis Kurt on 2/12/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A reference type that houses a wrapped element. 11 | /// This is communly used in scenarios where some value types might have recursive stored properties, such as 12 | /// ``Account``. 13 | /// 14 | /// The implementation was pulled from Sweeper on StackOverflow: 15 | /// https://stackoverflow.com/questions/73315816/swift-codable-struct-recursively-containing-itself-as-property 16 | public class Box: Codable { 17 | /// The wrapped value this box houses. 18 | public let wrappedValue: WrappedElement 19 | 20 | public required init(from decoder: Decoder) throws { 21 | wrappedValue = try WrappedElement(from: decoder) 22 | } 23 | 24 | public func encode(to encoder: Encoder) throws { 25 | try wrappedValue.encode(to: encoder) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Helpers/isOnMainThread.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alex Modroño Vara on 14/7/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Allows us to determine whether the function is running on the main thread or on a background thread, and log a brief 12 | description of the code on the console. 13 | 14 | - Parameter codeDescription: A brief description of what the code does so that it is easy to log the app. 15 | */ 16 | public func isOnMainThread(named codeDescription: String) -> Bool { 17 | if Thread.isMainThread { 18 | print("\(codeDescription) ON MAIN THREAD") 19 | return true 20 | } else { 21 | print("\(codeDescription) ON BACKGROUND THREAD") 22 | return false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Accounts/FamiliarFollowers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FamiliarFollowers.swift 3 | // 4 | // 5 | // Created by Marquis Kurt on 2/12/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A subset of accounts that two users follow in common. 11 | public struct FamiliarFollowers: Codable, Identifiable { 12 | /// The ID of the account that shares followers with the current user. 13 | public let id: String 14 | 15 | /// The list of accounts that the current user and account share. 16 | public let accounts: [Account] 17 | 18 | private enum CodingKeys: String, CodingKey { 19 | case id, accounts 20 | } 21 | } 22 | 23 | extension FamiliarFollowers: Hashable {} 24 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Accounts/Field.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Field.swift 3 | // Chica 4 | // 5 | // Created by Alejandro Modroño Vara on 07/07/2020. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Represents a profile field as a name-value pair with optional verification. 11 | public struct Field: Codable, Identifiable { 12 | /// A unique identifier generated for this field. 13 | public let id = UUID() 14 | 15 | /// The key of a given field's key-value pair. 16 | public let name: String 17 | 18 | /// The value associated with the name key. 19 | public let value: String 20 | 21 | /// An ISO-8601 date string of when the URL was verified. 22 | public let verifiedAt: String? 23 | 24 | // MARK: - Coding Keys 25 | 26 | private enum CodingKeys: String, CodingKey { 27 | case name 28 | case value 29 | case verifiedAt = "verified_at" 30 | } 31 | } 32 | 33 | /// Grants us conformance to `Hashable` for _free_ 34 | extension Field: Hashable { 35 | public static func == (lhs: Field, rhs: Field) -> Bool { 36 | return lhs.id == rhs.id 37 | } 38 | 39 | public func hash(into hasher: inout Hasher) { 40 | hasher.combine(id) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Accounts/Filters/UserFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserFilter.swift 3 | // 4 | // 5 | // Created by Marquis Kurt on 2/12/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A filter the user has generated to filter out content. 11 | public struct UserFilter: Codable, Identifiable { 12 | /// A typealias referring to the filter's keyword. 13 | public typealias Keyword = UserFilterKeyword 14 | 15 | /// An enumeration representing the context in which a filter applies. 16 | public enum Context: String, Codable { 17 | case home, notifications, `public`, thread, account 18 | } 19 | 20 | /// An enumeration representing an action that can be taken if content matches the filter. 21 | public enum Action: String, Codable { 22 | case warn, hide 23 | } 24 | 25 | /// The filter's identifier. 26 | public let id: String 27 | 28 | /// The filter's title. 29 | public let title: String 30 | 31 | /// The context in which the filter applies. 32 | public let context: [Context] 33 | 34 | /// The ISO-8601 date string at which the filter expires. 35 | public let expiresAt: String? 36 | 37 | /// The action to take when specified content matches the filter's requirements. 38 | public let filterAction: Action 39 | 40 | /// The keywords that trigger this filter. 41 | public let keywords: [Keyword] 42 | 43 | /// The statuses affected by this filter. 44 | public let statuses: [UserFilterStatus] 45 | 46 | // MARK: - Coding Keys 47 | 48 | private enum CodingKeys: String, CodingKey { 49 | case id, title, context, keywords, statuses 50 | case filterAction = "filter_action" 51 | case expiresAt = "expires_at" 52 | } 53 | } 54 | 55 | extension UserFilter: Hashable {} 56 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Accounts/Filters/UserFilterKeyword.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserFilterKeyword.swift 3 | // 4 | // 5 | // Created by Marquis Kurt on 2/12/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A keyword registered in a user filter. 11 | public struct UserFilterKeyword: Codable, Identifiable { 12 | /// The idenfitier of the keyword. 13 | public let id: String 14 | 15 | /// The keyword phrase included in the filter. 16 | public let keyword: String 17 | 18 | /// Whether the keyword phrase respects word boundaries. 19 | public let useWordBoundaries: Bool 20 | 21 | // MARK: - Coding Keys 22 | 23 | private enum CodingKeys: String, CodingKey { 24 | case id, keyword 25 | case useWordBoundaries = "whole_word" 26 | } 27 | } 28 | 29 | extension UserFilterKeyword: Hashable {} 30 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Accounts/Filters/UserFilterResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserFilterResult.swift 3 | // 4 | // 5 | // Created by Marquis Kurt on 2/12/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A filter that matches a given keyword or status. 11 | public struct UserFilterResult: Codable, Identifiable { 12 | /// A unique identifer generated for this result. 13 | public let id = UUID() 14 | 15 | /// The filter that the result matches. 16 | public let filter: UserFilter 17 | 18 | /// The keywords that matched to this filter. 19 | public let keywordMatches: [String]? 20 | 21 | /// The status IDs that matched to this filter. 22 | public let statusMatches: [String]? 23 | 24 | // MARK: - Coding Keys 25 | 26 | private enum CodingKeys: String, CodingKey { 27 | case filter 28 | case keywordMatches = "keyword_matches" 29 | case statusMatches = "status_matches" 30 | } 31 | } 32 | 33 | extension UserFilterResult: Hashable {} 34 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Accounts/Filters/UserFilterStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserFilterStatus.swift 3 | // 4 | // 5 | // Created by Marquis Kurt on 2/12/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Represents a status that, if matched, should cause its corresponding filter action to be taken. 11 | public struct UserFilterStatus: Codable, Identifiable { 12 | /// The identifier for this status filter. 13 | public let id: String 14 | 15 | /// The status ID that will be filtered. 16 | public let statusId: String 17 | 18 | // MARK: - Coding Keys 19 | 20 | private enum CodingKeys: String, CodingKey { 21 | case id 22 | case statusId = "status_id" 23 | } 24 | } 25 | 26 | extension UserFilterStatus: Hashable {} 27 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Accounts/Source.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Source.swift 3 | // Chica 4 | // 5 | // Created by Alejandro Modroño Vara on 07/07/2020. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Represents display or publishing preferences of user's own account. 11 | public struct Source: Codable, Identifiable { 12 | /// A unique identifier generated for this source. 13 | public let id = UUID() 14 | 15 | /// The profile's bio / description. 16 | public let note: String 17 | 18 | /// Metadata about the account. 19 | public let fields: [Field] 20 | 21 | /// The default post privacy to be used for new statuses. 22 | public let privacy: String? 23 | 24 | /// Whether new statuses should be marked sensitive by default. 25 | public let sensitive: Bool? 26 | 27 | /// The default posting language for new statuses. 28 | public let language: Bool? 29 | 30 | /// The number of pending follow requests. 31 | public let followRequestsCount: Int? 32 | 33 | // MARK: - Coding Keys 34 | 35 | private enum CodingKeys: String, CodingKey { 36 | case note 37 | case fields 38 | case privacy 39 | case sensitive 40 | case language 41 | case followRequestsCount = "follow_requests_count" 42 | } 43 | } 44 | 45 | /// Grants us conformance to `Hashable` for _free_ 46 | extension Source: Hashable { 47 | public static func == (lhs: Source, rhs: Source) -> Bool { 48 | return lhs.id == rhs.id 49 | } 50 | 51 | public func hash(into hasher: inout Hasher) { 52 | hasher.combine(id) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Authentication/Application.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Application.swift 3 | // Chica 4 | // 5 | // Created by Marquis Kurt on 7/7/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A registered service or app with the instance the user interacts with. 11 | public struct Application: Codable, Identifiable { 12 | /// A unidue identifier generated for this application. 13 | public let id = UUID() 14 | 15 | /// The name of the application. 16 | public let name: String 17 | 18 | /// Client ID key, to be used for obtaining OAuth tokens 19 | public let clientId: String? 20 | 21 | /// Client secret key, to be used for obtaining OAuth tokens 22 | public let clientSecret: String? 23 | 24 | /// The application's website, if applicable. 25 | public let website: String? 26 | 27 | /// The application's API key for push streaming, if applicable. 28 | public let vapidKey: String? 29 | 30 | // MARK: - Coding Keys 31 | 32 | private enum CodingKeys: String, CodingKey { 33 | case name 34 | case clientId = "client_id" 35 | case clientSecret = "client_secret" 36 | case website 37 | case vapidKey = "vapid_key" 38 | } 39 | } 40 | 41 | /// Grants us conformance to `Hashable` for _free_ 42 | extension Application: Hashable { 43 | public static func == (lhs: Application, rhs: Application) -> Bool { 44 | return lhs.id == rhs.id 45 | } 46 | 47 | public func hash(into hasher: inout Hasher) { 48 | hasher.combine(id) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Authentication/Token.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // File 4 | // 5 | // Created by Alex Modroño Vara on 16/7/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A class representation of an OAuth token used for authenticating with the API and performing actions. 11 | public struct Token: Codable, Identifiable { 12 | /// A unique identifier generated for this token. 13 | public let id = UUID() 14 | 15 | /// The OAuth token to be used for authorization. 16 | public let accessToken: String 17 | 18 | /// The OAuth token type. Mastodon uses Bearer tokens. 19 | public let tokenType: String 20 | 21 | /// The OAuth scopes granted by this token, space-separated. 22 | public let scope: String 23 | 24 | /// When the token was generated. 25 | public let createdAt: Date 26 | 27 | // MARK: - Coding Keys 28 | 29 | enum CodingKeys: String, CodingKey { 30 | case accessToken = "access_token" 31 | case tokenType = "token_type" 32 | case scope 33 | case createdAt = "created_at" 34 | } 35 | } 36 | 37 | /// Grants us conformance to `Hashable` for _free_ 38 | extension Token: Hashable { 39 | public static func == (lhs: Token, rhs: Token) -> Bool { 40 | return lhs.id == rhs.id 41 | } 42 | 43 | public func hash(into hasher: inout Hasher) { 44 | hasher.combine(id) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/General/EmptyNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyNode.swift 3 | // 4 | // 5 | // Created by Marquis Kurt on 12/31/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// An empty node. This is used when the request return type doesn't matter. 11 | public struct EmptyNode: Decodable {} 12 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/General/MastodonError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MastodonError.swift 3 | // Chica 4 | // 5 | // Created by Alex Modroño Vara on 18/7/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Represents an error message. 11 | public struct MastodonError: Codable, Identifiable { 12 | /// A unique identifier for this error. 13 | public let id = UUID() 14 | 15 | /// The error message. 16 | public let error: String 17 | 18 | /// A longer description of the error, mainly provided with the OAuth API. 19 | public let errorDescription: String? 20 | 21 | // MARK: - Coding Keys 22 | 23 | private enum CodingKeys: String, CodingKey { 24 | case error 25 | case errorDescription = "error_description" 26 | } 27 | } 28 | 29 | /// Grants us conformance to `Hashable` for _free_ 30 | extension MastodonError: Hashable { 31 | public static func == (lhs: MastodonError, rhs: MastodonError) -> Bool { 32 | return lhs.id == rhs.id 33 | } 34 | 35 | public func hash(into hasher: inout Hasher) { 36 | hasher.combine(id) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/General/SearchResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchResult.swift 3 | // Chica 4 | // 5 | // Created by Alex Modroño Vara on 19/7/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A result from a given search query. 11 | public struct SearchResult: Codable, Identifiable { 12 | /// The ID associated with this history event. 13 | public let id = UUID() 14 | 15 | /// Accounts that meet the specified query 16 | public let accounts: [Account]? 17 | 18 | /// Statuses that meet the specified query 19 | public let statuses: [Status]? 20 | 21 | /// Hashtags that meet the specified query 22 | public let hashtags: [Tag]? 23 | 24 | // MARK: - Coding Keys 25 | 26 | enum CodingKeys: String, CodingKey { 27 | case accounts 28 | case statuses 29 | case hashtags 30 | } 31 | } 32 | 33 | /// Grants us conformance to `Hashable` for _free_ 34 | extension SearchResult: Hashable { 35 | public static func == (lhs: SearchResult, rhs: SearchResult) -> Bool { 36 | return lhs.id == rhs.id 37 | } 38 | 39 | public func hash(into hasher: inout Hasher) { 40 | hasher.combine(id) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Instances/Announcements/AnnouncementReaction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnnouncementReaction.swift 3 | // Chica 4 | // 5 | // Created by Alejandro Modroño Vara on 07/07/2020. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Represents a reaction to an Announcement. 11 | public struct AnnouncementReaction: Codable, Identifiable { 12 | /// The reaction id, needed for iterating through the reactions. 13 | public let id = UUID() 14 | 15 | /// The emoji used for the reaction. Either a unicode emoji, or a custom emoji's shortcode. 16 | public let name: String 17 | 18 | /// The total number of users who have added this reaction. 19 | public let count: Int 20 | 21 | /// Whether the authorized user has added this reaction to the announcement. 22 | public let reacted: Bool 23 | 24 | /// A link to the custom emoji. 25 | public let url: String 26 | 27 | /// A link to a non-animated version of the custom emoji. 28 | public let staticUrl: String 29 | 30 | // MARK: - CodingKeys 31 | 32 | private enum CodingKeys: String, CodingKey { 33 | case name 34 | case count 35 | case reacted = "me" 36 | case url 37 | case staticUrl = "static_url" 38 | } 39 | } 40 | 41 | /// Grants us conformance to `Hashable` for _free_ 42 | extension AnnouncementReaction: Hashable { 43 | public static func == (lhs: AnnouncementReaction, rhs: AnnouncementReaction) -> Bool { 44 | return lhs.id == rhs.id 45 | } 46 | 47 | public func hash(into hasher: inout Hasher) { 48 | hasher.combine(id) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Instances/CustomEmoji.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Emoji.swift 3 | // Chica 4 | // 5 | // Created by Alejandro Modroño Vara on 07/07/2020. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Represents a custom emoji. 11 | public struct CustomEmoji: Codable, Identifiable { 12 | /// A unique identifier generated for this emoji. 13 | public let id = UUID() 14 | 15 | /// The shortcode of the emoji 16 | public let shortcode: String 17 | 18 | /// URL to the emoji static image 19 | public let staticURL: URL 20 | 21 | /// URL to the emoji image 22 | public let url: URL 23 | 24 | // Whether the emoji is visible in the custom emoji picker. 25 | public let visibleInPicker: Bool 26 | 27 | /// The category this emoji belogs to. 28 | public let category: String? 29 | 30 | // MARK: - Coding Keys 31 | 32 | private enum CodingKeys: String, CodingKey { 33 | case shortcode 34 | case category 35 | case visibleInPicker = "visible_in_picker" 36 | case staticURL = "static_url" 37 | case url 38 | } 39 | } 40 | 41 | /// Grants us conformance to `Hashable` for _free_ 42 | extension CustomEmoji: Hashable { 43 | public static func == (lhs: CustomEmoji, rhs: CustomEmoji) -> Bool { 44 | return lhs.id == rhs.id 45 | } 46 | 47 | public func hash(into hasher: inout Hasher) { 48 | hasher.combine(id) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Instances/DomainBlock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DomainBlock.swift 3 | // 4 | // 5 | // Created by Marquis Kurt on 2/12/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A domain being blocked by an instance. 11 | public struct DomainBlock: Codable, Identifiable { 12 | /// A representation of the different severity types a domain block can be registered as. 13 | public enum Severity: String, Codable { 14 | /// Users, notification, posts, etc. are silenced from timelines unless the current user follows anyone from 15 | /// this domain. 16 | case silence 17 | 18 | /// Incoming messages from this instance is dropped immediately, as if they never existed. 19 | case suspend 20 | } 21 | 22 | /// A unique identifier generated for this domain block. 23 | public let id = UUID() 24 | 25 | /// The domain that is being blocked. 26 | public let domain: String 27 | 28 | /// The SHA-256 digest of the domain string. 29 | public let digest: String 30 | 31 | /// The severity at which the domain is being blocked. 32 | public let severity: Severity 33 | 34 | /// An optional comment explaining why this instance was blocked. 35 | public let comment: String? 36 | 37 | // MARK: - Coding Keys 38 | 39 | private enum CodingKeys: String, CodingKey { 40 | case domain, digest, severity, comment 41 | } 42 | } 43 | 44 | // MARK: - Conformances 45 | 46 | extension DomainBlock: Hashable {} 47 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Instances/ExtendedDescription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtendedDescription.swift 3 | // 4 | // 5 | // Created by Marquis Kurt on 2/12/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// An extended description of an instance that is shown on the about page. 11 | public struct ExtendedDescription: Codable, Identifiable { 12 | /// A unique identifier generated for this description. 13 | public let id = UUID() 14 | 15 | /// The ISO-8601 date string for when the description was last updated. 16 | public let updatedAt: String 17 | 18 | /// The HTML content of the description. 19 | public let content: String 20 | 21 | // MARK: - Coding Keys 22 | 23 | private enum CodingKeys: String, CodingKey { 24 | case content 25 | case updatedAt = "updated_at" 26 | } 27 | } 28 | 29 | extension ExtendedDescription: Hashable {} 30 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Instances/FeaturedTag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeaturedTag.swift 3 | // 4 | // 5 | // Created by Marquis Kurt on 2/12/23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A tag that an account is currently featuring. 11 | public struct FeaturedTag: Codable, Identifiable { 12 | /// The tag's unique identifier. 13 | public let id: String 14 | 15 | /// The name of the tag. 16 | public let name: String 17 | 18 | /// A URL pointing to the tag's page on that instance. 19 | public let url: String 20 | 21 | /// The number of statuses that contain this tag. 22 | public let numberOfStatuses: String 23 | 24 | /// An ISO-8601 date string of when the last status containing this tag was created. 25 | public let lastStatusAt: String 26 | 27 | private enum CodingKeys: String, CodingKey { 28 | case id, name, url 29 | case numberOfStatuses = "statuses_count" 30 | case lastStatusAt = "last_status_at" 31 | } 32 | } 33 | 34 | extension FeaturedTag: Hashable {} 35 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Statuses/Attachments/AttachmentType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttachmentType.swift 3 | // Chica 4 | // 5 | // Created by Marquis Kurt on 7/7/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// An enumerated representation of an attachment's type. 11 | public enum AttachmentType: String, Codable { 12 | /// When the attachment's type is unknown to the server. 13 | case unknown 14 | 15 | /// When the attachment is an image or image-like object. 16 | case image 17 | 18 | /// When the attachment is a GIF or GIF video. 19 | case gifv 20 | 21 | /// When the attachment is an audio file. 22 | case audio 23 | 24 | /// When the attachment is a video. 25 | case video 26 | } 27 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Statuses/Attachments/MediaAttachment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Attachment.swift 3 | // Chica 4 | // 5 | // Created by Marquis Kurt on 7/7/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A media attachment from a post or other content. 11 | public struct MediaAttachment: Codable, Identifiable { 12 | /// The ID associated with this attachment registered with the server. 13 | public let id: String 14 | 15 | /// The type of attachment. 16 | public let type: AttachmentType 17 | 18 | /// The attachment's website URL. 19 | public let url: String 20 | 21 | /// The attachment's website URL to remote content. 22 | public let remoteURL: String? 23 | 24 | /// The attachment's website URL to the remote content's preview. 25 | public let previewURL: String? 26 | 27 | /// The attachment's website URL to text. 28 | @available(*, deprecated, message: "Removed in Mastodon v3.5.0. Use MediaAttachment.url instead.") 29 | public let textURL: String? 30 | 31 | /// The attachment's description. 32 | public let description: String? 33 | 34 | /// A hash generated by the BlurHash algorithm used for creating thumbnails. 35 | public let blurhash: String? 36 | 37 | // MARK: - Coding Keys 38 | 39 | private enum CodingKeys: String, CodingKey { 40 | case id 41 | case type 42 | case url 43 | case remoteURL = "remote_url" 44 | case previewURL = "preview_url" 45 | case textURL = "text_url" 46 | case description 47 | case blurhash 48 | } 49 | } 50 | 51 | /// Grants us conformance to `Hashable` for _free_ 52 | extension MediaAttachment: Hashable { 53 | public static func == (lhs: MediaAttachment, rhs: MediaAttachment) -> Bool { 54 | return lhs.id == rhs.id 55 | } 56 | 57 | public func hash(into hasher: inout Hasher) { 58 | hasher.combine(id) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Statuses/Cards/PreviewCardType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardType.swift 3 | // Chica 4 | // 5 | // Created by Marquis Kurt on 7/7/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// An enumerated representation of the card content type. 11 | public enum PreviewCardType: String, Codable { 12 | /// When the content type is a link. 13 | case link 14 | 15 | /// When the content type includes a photo. 16 | case photo 17 | 18 | /// When the content type includes a video. 19 | case video 20 | 21 | /// When the content type contains rich content. 22 | case rich 23 | } 24 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Statuses/Context.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Context.swift 3 | // Chica 4 | // 5 | // Created by Marquis Kurt on 7/7/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A class that contains the replies to a post as a descendant, or prior statuses leading up to the current post. 11 | public struct Context: Codable, Identifiable { 12 | /// A unique identifier generated for this context. 13 | public let id = UUID() 14 | 15 | /// The posts that precede the given post. 16 | public let ancestors: [Status] 17 | 18 | /// The posts that follow the given post. 19 | public let descendants: [Status] 20 | 21 | // MARK: - Coding Keys 22 | 23 | enum CodingKeys: String, CodingKey { 24 | case ancestors 25 | case descendants 26 | } 27 | } 28 | 29 | /// Grants us conformance to `Hashable` for _free_ 30 | extension Context: Hashable { 31 | public static func == (lhs: Context, rhs: Context) -> Bool { 32 | return lhs.id == rhs.id 33 | } 34 | 35 | public func hash(into hasher: inout Hasher) { 36 | hasher.combine(id) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Statuses/Conversation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Conversation.swift 3 | // 4 | // 5 | // Created by Marquis Kurt on 23/2/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A private conversation between a limited amount of users. 11 | public struct Conversation: Codable { 12 | /// The ID of the conversation. 13 | public let id: String 14 | 15 | /// The list of accounts that are members of the conversation. 16 | public let accounts: [Account] 17 | 18 | /// Whether the user hasn't read the latest message. 19 | public let unread: Bool 20 | 21 | /// The last status in the conversation. This is typically used for display purposes. 22 | public let lastStatus: Status? 23 | 24 | // MARK: - Coding Keys 25 | 26 | private enum CodingKeys: String, CodingKey { 27 | case id 28 | case accounts 29 | case unread 30 | case lastStatus = "last_status" 31 | } 32 | } 33 | 34 | /// Grants us conformance to `Hashable` for _free_ 35 | extension Conversation: Hashable { 36 | public static func == (lhs: Conversation, rhs: Conversation) -> Bool { 37 | return lhs.id == rhs.id 38 | } 39 | 40 | public func hash(into hasher: inout Hasher) { 41 | hasher.combine(id) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Statuses/Mention.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mention.swift 3 | // Chica 4 | // 5 | // Created by Marquis Kurt on 7/7/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A mention model that contains the data for an account that is mentioned in a given post. 11 | public struct Mention: Codable, Identifiable { 12 | /// The ID associated with this mention from the server. 13 | public let id: String 14 | 15 | /// The mentioned account's username. 16 | public let username: String 17 | 18 | /// The mentioned account's given username and domain, if applicable. 19 | public let acct: String 20 | 21 | /// The website URL to the mentioned account's profile. 22 | public let url: String 23 | } 24 | 25 | /// Grants us conformance to `Hashable` for _free_ 26 | extension Mention: Hashable { 27 | public static func == (lhs: Mention, rhs: Mention) -> Bool { 28 | return lhs.id == rhs.id 29 | } 30 | 31 | public func hash(into hasher: inout Hasher) { 32 | hasher.combine(id) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Statuses/Polls/Poll.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Poll.swift 3 | // Chica 4 | // 5 | // Created by Marquis Kurt on 7/7/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A class representation of a poll. 11 | public struct Poll: Codable, Identifiable { 12 | /// The ID for this poll registered with the server. 13 | public let id: String 14 | 15 | /// The exipration date for this poll. 16 | public let expiresAt: String? 17 | 18 | /// Whether the poll has expired. 19 | public let expired: Bool 20 | 21 | /// Whether the poll supports selecting multiple options. 22 | public let multiple: Bool 23 | 24 | /// The number of total votes on this poll. 25 | public let votesCount: Int 26 | 27 | /// The options listed for this poll. 28 | public let options: [PollOption] 29 | 30 | /// Whether the user has voted on this poll. 31 | public let voted: Bool? 32 | 33 | // MARK: - Coding Keys 34 | 35 | private enum CodingKeys: String, CodingKey { 36 | case id 37 | case expiresAt = "expires_at" 38 | case expired 39 | case multiple 40 | case votesCount = "votes_count" 41 | case options 42 | case voted 43 | } 44 | } 45 | 46 | /// Grants us conformance to `Hashable` for _free_ 47 | extension Poll: Hashable { 48 | public static func == (lhs: Poll, rhs: Poll) -> Bool { 49 | return lhs.id == rhs.id 50 | } 51 | 52 | public func hash(into hasher: inout Hasher) { 53 | hasher.combine(id) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Statuses/Polls/PollOption.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PollOption.swift 3 | // Chica 4 | // 5 | // Created by Alejandro Modroño Vara on 07/07/2020. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A class representation of a poll option. 11 | public struct PollOption: Codable, Identifiable { 12 | /// The ID for this poll option. 13 | public let id = UUID() 14 | 15 | /// The title for this poll option. 16 | public let title: String 17 | 18 | /// The number of votes for this option. 19 | public let votesCount: Int? 20 | 21 | // MARK: - Coding Keys 22 | 23 | private enum CodingKeys: String, CodingKey { 24 | case title 25 | case votesCount = "votes_count" 26 | } 27 | } 28 | 29 | /// Grants us conformance to `Hashable` for _free_ 30 | extension PollOption: Hashable { 31 | public static func == (lhs: PollOption, rhs: PollOption) -> Bool { 32 | return lhs.id == rhs.id 33 | } 34 | 35 | public func hash(into hasher: inout Hasher) { 36 | hasher.combine(id) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Statuses/PostVisibility.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostVisibility.swift 3 | // Chica 4 | // 5 | // Created by Marquis Kurt on 7/6/20. 6 | // 7 | 8 | import Foundation 9 | 10 | @available(*, deprecated, renamed: "PostVisibility") 11 | public typealias Visibility = PostVisibility 12 | 13 | /** 14 | An enumerated representation of a post's visibility. 15 | 16 | Posts can be restricted to a certain selection of people or used as a direct message. 17 | */ 18 | public enum PostVisibility: String, Codable, CaseIterable { 19 | /// When a post is meant for everyone to see. 20 | case `public` 21 | 22 | /// When a post is meant to be followers-only. 23 | case `private` 24 | 25 | /// When a post is meant to be visible to everyone, but only via link. 26 | case unlisted 27 | 28 | /// When a post is meant to be a direct message, only intended for its recipients. 29 | case direct 30 | } 31 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Statuses/Tags/History.swift: -------------------------------------------------------------------------------- 1 | // 2 | // History.swift 3 | // Chica 4 | // 5 | // Created by Marquis Kurt on 7/7/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A class representation of a tag's history. 11 | public struct History: Codable, Identifiable { 12 | /// A unique identifier generated for this history point. 13 | public let id = UUID() 14 | 15 | /// The weekday that this history event occurs on. 16 | public let day: String 17 | 18 | /// The number of times a tag was used on this day. 19 | public let uses: String 20 | 21 | /// The number of accounts that used this tag on this day. 22 | public let accounts: String 23 | 24 | // MARK: - Coding Keys 25 | 26 | enum CodingKeys: String, CodingKey { 27 | case day 28 | case uses 29 | case accounts 30 | } 31 | } 32 | 33 | /// Grants us conformance to `Hashable` for _free_ 34 | extension History: Hashable { 35 | public static func == (lhs: History, rhs: History) -> Bool { 36 | return lhs.id == rhs.id 37 | } 38 | 39 | public func hash(into hasher: inout Hasher) { 40 | hasher.combine(id) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Statuses/Tags/Tag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tag.swift 3 | // Chica 4 | // 5 | // Created by Marquis Kurt on 7/7/20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A representation of a hashtag. 11 | public struct Tag: Codable, Identifiable { 12 | /// A unique identifier generated for this tag. 13 | public let id = UUID() 14 | 15 | /// The name of the tag. 16 | public let name: String 17 | 18 | /// The URL to access posts with this tag. 19 | public let url: String 20 | 21 | /// The weekly history of this tag. 22 | public let history: [History]? 23 | 24 | /// Whether the user is currently following this tag. 25 | public let following: Bool? 26 | 27 | // MARK: - Coding Keys 28 | 29 | enum CodingKeys: String, CodingKey { 30 | case name 31 | case url 32 | case history 33 | case following 34 | } 35 | } 36 | 37 | /// Grants us conformance to `Hashable` for _free_ 38 | extension Tag: Hashable { 39 | public static func == (lhs: Tag, rhs: Tag) -> Bool { 40 | return lhs.id == rhs.id 41 | } 42 | 43 | public func hash(into hasher: inout Hasher) { 44 | hasher.combine(id) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Timelines/MastodonList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MastodonList.swift 3 | // Chica 4 | // 5 | // Created by Marquis Kurt on 12/29/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A list that contains statuses that the user collected. 11 | public struct MastodonList: Codable, Identifiable { 12 | /// Which replies should be shown in the list. 13 | public enum ReplyPolicy: String, Codable { 14 | case followed, list, none 15 | } 16 | 17 | /// The list's ID. 18 | public let id: String 19 | 20 | /// The name of the list. 21 | public let title: String 22 | 23 | /// The policy that dicatates what kinds of replies will be diplayed in the list. 24 | public let repliesPolicy: ReplyPolicy 25 | 26 | // MARK: - Coding Keys 27 | 28 | private enum CodingKeys: String, CodingKey { 29 | case id 30 | case title 31 | case repliesPolicy = "replies_policy" 32 | } 33 | } 34 | 35 | /// Grants us conformance to `Hashable` for _free_ 36 | extension MastodonList: Hashable { 37 | public static func == (lhs: MastodonList, rhs: MastodonList) -> Bool { 38 | return lhs.id == rhs.id 39 | } 40 | 41 | public func hash(into hasher: inout Hasher) { 42 | hasher.combine(id) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Models/Timelines/TimelineScope.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimelineScope.swift 3 | // Chica 4 | // 5 | // Created by Marquis Kurt on 5/8/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// An enumeration that represents the different types of timelines. 11 | public enum TimelineScope { 12 | /// The local or public timeline. 13 | /// 14 | /// This uses the same endpoint to fetch timeline data. To specify local data, include the `local` parameter in 15 | /// your request. 16 | /// - Example: 17 | /// `try await Alice.shared.get(.network, params: ["local": "true"])` 18 | case network 19 | 20 | /// The user's timeline. 21 | case home 22 | 23 | /// The user's direct messages. 24 | case messages 25 | 26 | /// A timeline from a given list. 27 | case list(id: String) 28 | 29 | /// A timeline of posts that correspond to a hashtag. 30 | case tag(tag: String) 31 | 32 | /// The full path to a timeline endpoint 33 | var path: String { 34 | switch self { 35 | case .home: 36 | return "/api/v1/timelines/home" 37 | case .network: 38 | return "/api/v1/timelines/public" 39 | case .messages: 40 | return "/api/v1/conversations" 41 | case .list(let id): 42 | return "/api/v1/timelines/list/\(id)" 43 | case .tag(let tag): 44 | return "/api/v1/timelines/tag/\(tag)" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Networking/AliceSecurityModule.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Marquis Kurt on 5/6/23. 6 | // 7 | 8 | import Foundation 9 | import KeychainAccess 10 | 11 | public protocol AliceSecurityModule { 12 | func setSecureStore(_ value: String?, forKey key: String) 13 | func getSecureStore(_ key: String) -> String? 14 | func flush() 15 | } 16 | 17 | extension Keychain: AliceSecurityModule { 18 | public func setSecureStore(_ value: String?, forKey key: String) { 19 | self[key] = value 20 | } 21 | 22 | public func getSecureStore(_ key: String) -> String? { 23 | return self[key] 24 | } 25 | 26 | public func flush() { 27 | try? self.removeAll() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/Alice/Networking/AliceSession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AliceSession.swift 3 | // Alice 4 | // 5 | // Created by Marquis Kurt on 1/29/23. 6 | // 7 | // This file is part of Alice. 8 | // 9 | // Alice is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Alice comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Foundation 16 | 17 | public protocol AliceSession { 18 | var configuration: URLSessionConfiguration { get } 19 | init(configuration: URLSessionConfiguration) 20 | func request(_ request: URLRequest, delegate: URLSessionTaskDelegate?) async throws -> (Data, URLResponse) 21 | } 22 | 23 | extension URLSession: AliceSession { 24 | public func request( 25 | _ request: URLRequest, 26 | delegate: URLSessionTaskDelegate? = nil 27 | ) async throws -> (Data, URLResponse) { 28 | return try await data(for: request, delegate: delegate) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/AliceMockingbird/Mock/MockConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockConstants.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/8/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import Foundation 17 | 18 | /// A structure that contains common mocked constants. 19 | public struct MockConstants { 20 | public static var context: Context = MockConstants.named("Context")! 21 | public static var conversation: Conversation = MockConstants.named("Conversation")! 22 | public static var poll: Poll = MockConstants.named("Poll")! 23 | public static var profile: Account = MockConstants.named("Profile")! 24 | public static var searchResult: SearchResult = MockConstants.named("SearchResult")! 25 | public static var status: Status = MockConstants.named("Status")! 26 | public static var timeline: [Status] = MockConstants.named("Timeline")! 27 | 28 | static func named(_ resource: String) -> T? { 29 | guard let path = Bundle.module.path(forResource: resource, ofType: "json") else { return nil } 30 | let url = URL(filePath: path) 31 | return try? JSONDecoder().decode(T.self, from: Data(contentsOf: url)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/AliceMockingbird/Mock/MockKeychain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockKeychain.swift 3 | // 4 | // 5 | // Created by Marquis Kurt on 5/6/23. 6 | // 7 | 8 | import Foundation 9 | import Alice 10 | 11 | public class AliceMockKeychain: AliceSecurityModule { 12 | private var keychain = [String: String]() 13 | 14 | public init() {} 15 | 16 | public func setSecureStore(_ value: String?, forKey key: String) { 17 | keychain[key] = value 18 | } 19 | 20 | public func getSecureStore(_ key: String) -> String? { 21 | return keychain[key] 22 | } 23 | 24 | public func flush() { 25 | keychain.removeAll() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/AliceMockingbird/Resources/Empty.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/AliceMockingbird/Resources/Poll.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "6", 3 | "expires_at": "2020-09-15T03:59:02.588Z", 4 | "expired": true, 5 | "multiple": true, 6 | "votes_count": 3, 7 | "voters_count": 3, 8 | "voted": true, 9 | "own_votes": [], 10 | "options": [ 11 | { 12 | "title": "Hell yeah!", 13 | "votes_count": 3 14 | }, 15 | { 16 | "title": "Probably not...", 17 | "votes_count": 0 18 | }, 19 | { 20 | "title": "I'm not sure", 21 | "votes_count": 0 22 | } 23 | ], 24 | "emojis": [] 25 | } 26 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/AliceMockingbird/Resources/Profile.json: -------------------------------------------------------------------------------- 1 | {"id":"1","username":"admin","acct":"admin","display_name":"Gophey","locked":false,"bot":false,"discoverable":true,"group":false,"created_at":"2019-04-10T00:00:00.000Z","note":"\u003cp\u003eThe administrator account for Gopherdon. Currently managed by \u003cspan class=\"h-card\"\u003e\u003ca href=\"https://mastodon.goucher.edu/@quivical\" class=\"u-url mention\"\u003e@\u003cspan\u003equivical\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e.\u003c/p\u003e","url":"https://mastodon.goucher.edu/@admin","avatar":"https://mastodon.goucher.edu/system/accounts/avatars/000/000/001/original/b945b7c78b18f556.png","avatar_static":"https://mastodon.goucher.edu/system/accounts/avatars/000/000/001/original/b945b7c78b18f556.png","header":"https://mastodon.goucher.edu/system/accounts/headers/000/000/001/original/Gopherdon_Server_Banner.png","header_static":"https://mastodon.goucher.edu/system/accounts/headers/000/000/001/original/Gopherdon_Server_Banner.png","followers_count":55,"following_count":3,"statuses_count":54,"last_status_at":"2022-05-09","emojis":[],"fields":[{"name":"Managed by","value":"\u003cspan class=\"h-card\"\u003e\u003ca href=\"https://mastodon.goucher.edu/@quivical\" class=\"u-url mention\"\u003e@\u003cspan\u003equivical\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e","verified_at":null}]} 2 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/AliceMockingbird/Resources/Registration.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Starlight", 3 | "client_id": "e3bb6b574ab4f2259eeddd2e25ae1113", 4 | "client_secret": "6610be015e88dfc7d9143f6cf0cfbbac", 5 | "website": "https://hyperspace.marquiskurt.net" 6 | } 7 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/AliceMockingbird/Resources/Token.json: -------------------------------------------------------------------------------- 1 | { 2 | "access_token": "d076cc9f3d73a31c13a4840884535755", 3 | "token_type": "Bearer", 4 | "scope": "read write follow", 5 | "created_at": 1683399916 6 | } 7 | -------------------------------------------------------------------------------- /Packages/Alice/Sources/AliceMockingbird/Resources/UnauthorizedError.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": "The access token is invalid" 3 | } -------------------------------------------------------------------------------- /Packages/Alice/Sources/AliceMockingbird/Resources/UnprocessableEntity.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": "Validation failed: Text can't be blank" 3 | } -------------------------------------------------------------------------------- /Packages/FrugalMode/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /Packages/FrugalMode/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "FrugalMode", 8 | defaultLocalization: "en", 9 | platforms: [.macOS(.v13), .iOS(.v16), .tvOS(.v16), .watchOS(.v9), .visionOS(.v1)], 10 | products: [ 11 | .library( 12 | name: "FrugalMode", 13 | targets: ["FrugalMode"]) 14 | ], 15 | dependencies: [ 16 | .package(name: "FlowKit", path: "../FlowKit") 17 | ], 18 | targets: [ 19 | .target( 20 | name: "FrugalMode", 21 | dependencies: ["FlowKit"], 22 | resources: [ 23 | .process("Resources")]), 24 | .testTarget( 25 | name: "FrugalModeTests", 26 | dependencies: [ 27 | "FrugalMode", 28 | .product(name: "FlowKitTestSupport", package: "FlowKit") 29 | ]) 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /Packages/FrugalMode/Sources/FrugalMode/FrugalModeEnvironmentKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FrugalModeEnvironmentKey.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 5/4/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import SwiftUI 16 | 17 | private struct FrugalModeEnvironmentKey: EnvironmentKey { 18 | static let defaultValue: Bool = false 19 | } 20 | 21 | public extension EnvironmentValues { 22 | /// Whether frugal mode should be enforced all the time, rather than per user defaults. 23 | /// 24 | /// Typically, this environment variable can be used to suspend expensive tasks or selectively display contents that 25 | /// aren't computationally intensive with respect to time and/or resources. 26 | /// 27 | /// Use this alongside ``FrugalModeFlow`` to automatically override frugal mode settings based on Low Power Mode: 28 | /// ```swift 29 | /// import FrugalMode 30 | /// import SwiftUI 31 | /// 32 | /// @main 33 | /// struct MainApp: App { 34 | /// private var frugalFlow = FrugalFlow() 35 | /// 36 | /// var body: some Scene { 37 | /// WindowGroup { 38 | /// ContentView() 39 | /// .environment(\.frugalMode, 40 | /// (frugalFlow.state == .overridden)) 41 | /// .onAppear { 42 | /// Task { await frugalFlow.emit(.checkOverrides) } 43 | /// } 44 | /// } 45 | /// } 46 | /// } 47 | /// ``` 48 | var enforcedFrugalMode: Bool { 49 | get { return self[FrugalModeEnvironmentKey.self] } 50 | set { self[FrugalModeEnvironmentKey.self] = newValue } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Packages/FrugalMode/Sources/FrugalMode/FrugalModeFlow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FrugalModeFlow.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 25/6/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Combine 16 | import FlowKit 17 | import Foundation 18 | 19 | /// A flow designed to assess the state of frugal mode in an app. 20 | public actor FrugalModeFlow: ObservableObject { 21 | public enum State { 22 | /// The initial state of the flow, which indicates nothing has occurred. 23 | case initial 24 | 25 | /// Frugal mode has been overridden due to environment factors, such as Low Power Mode. 26 | case overridden 27 | 28 | /// Frugal mode is dictated by what is in the user's defaults. 29 | case userDefaults 30 | } 31 | 32 | public enum Event { 33 | /// Check whether frugal mode will be overridden by environment factors such as Low Poer Mode. 34 | case checkOverrides 35 | 36 | /// Resets the state of the flow to the initial state. 37 | case reset 38 | } 39 | 40 | public var onStateChange: ((State) -> Void)? 41 | public var stateSubscribers = [((State) -> Void)]() 42 | 43 | @Published var internalState: State = .initial { 44 | didSet { 45 | stateSubscribers.forEach { callback in 46 | callback(internalState) 47 | } 48 | } 49 | } 50 | 51 | public init() {} 52 | } 53 | 54 | extension FrugalModeFlow: StatefulFlowProviding { 55 | public var state: State { internalState } 56 | public func emit(_ event: Event) async { 57 | switch event { 58 | case .checkOverrides: 59 | internalState = ProcessInfo.processInfo.isLowPowerModeEnabled ? .overridden: .userDefaults 60 | case .reset: 61 | internalState = .initial 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Packages/FrugalMode/Sources/FrugalMode/FrugalModeLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FrugalModeLabel.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 25/6/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import SwiftUI 16 | 17 | /// A label that displays "Frugal mode enabled" based on the current locales. 18 | public struct FrugalModeLabel: View { 19 | public init() {} 20 | 21 | public var body: some View { 22 | Label { 23 | Text("frugalmode.on", bundle: .module) 24 | .font(.system(.body, design: .rounded)) 25 | } icon: { 26 | Image(systemName: "leaf.fill") 27 | } 28 | .foregroundColor(.green) 29 | } 30 | } 31 | 32 | struct FrugalModeLabel_Previews: PreviewProvider { 33 | static var previews: some View { 34 | FrugalModeLabel() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Packages/FrugalMode/Sources/FrugalMode/Resources/Localizable.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "frugalmode.on" : { 5 | "comment" : "Frugal mode has been enabled on the device.", 6 | "localizations" : { 7 | "en" : { 8 | "stringUnit" : { 9 | "state" : "translated", 10 | "value" : "Frugal mode enabled." 11 | } 12 | } 13 | } 14 | } 15 | }, 16 | "version" : "1.0" 17 | } -------------------------------------------------------------------------------- /Packages/FrugalMode/Tests/FrugalModeTests/FrugalModeTests.swift: -------------------------------------------------------------------------------- 1 | import FlowKitTestSupport 2 | @testable import FrugalMode 3 | import XCTest 4 | 5 | final class FrugalModeFlowTests: XCTestCase, StatefulTestCase { 6 | typealias TestableFlow = FrugalModeFlow 7 | 8 | var flow: FrugalModeFlow? 9 | 10 | override func setUp() async throws { 11 | flow = FrugalModeFlow() 12 | } 13 | 14 | override func tearDown() async throws { 15 | flow = nil 16 | } 17 | 18 | func testEmitCheckedOverrides() async throws { 19 | await withCheckedFlow { currentFlow in 20 | await currentFlow.emit(.checkOverrides) 21 | await self.expectState(matches: .userDefaults) 22 | } 23 | } 24 | 25 | func testEmitReset() async throws { 26 | let expectation = XCTestExpectation(description: "Wait finished.") 27 | await withCheckedFlow { currentFlow in 28 | await currentFlow.emit(.checkOverrides) 29 | DispatchQueue.main.asyncAfter(deadline: .now() + 5) { 30 | expectation.fulfill() 31 | } 32 | await self.fulfillment(of: [expectation], timeout: 10) 33 | await currentFlow.emit(.reset) 34 | await self.expectState(matches: .initial) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Packages/GardenComposer/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /Packages/GardenComposer/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "GardenComposer", 8 | defaultLocalization: "en", 9 | platforms: [.iOS(.v16)], 10 | products: [ 11 | .library( 12 | name: "GardenComposer", 13 | targets: ["GardenComposer"]) 14 | ], 15 | dependencies: [ 16 | .package(name: "Alice", path: "../Alice"), 17 | .package(name: "FlowKit", path: "../FlowKit"), 18 | .package(name: "GardenSettings", path: "../GardenSettings") 19 | ], 20 | targets: [ 21 | .target( 22 | name: "GardenComposer", 23 | dependencies: ["Alice", "FlowKit", "GardenSettings"], 24 | resources: [ 25 | .process("Resources") 26 | ]), 27 | .testTarget( 28 | name: "GardenComposerTests", 29 | dependencies: [ 30 | "GardenComposer", 31 | .product(name: "AliceMockingbird", package: "Alice"), 32 | .product(name: "FlowKitTestSupport", package: "FlowKit") 33 | ]) 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /Packages/GardenComposer/Sources/GardenComposer/ComposerFlowError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComposerFlowError.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 2/8/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import Foundation 17 | 18 | /// A representation of the various errors the ``ComposerFlow`` can encounter. 19 | public enum ComposerFlowError: LocalizedError { 20 | /// A draft wasn't supplied to the flow. 21 | case noDraftSupplied 22 | 23 | /// The current contents of the draft exceed the imposed character limit. 24 | case exceedsCharacterLimit(draft: ComposerDraft) 25 | 26 | /// An error occurred server-side. 27 | case mastodonError(FetchError, draft: ComposerDraft) 28 | 29 | /// The current flow state and event being emitted doesn't match the expected chain of events. 30 | case unsupportedEventDispatch 31 | 32 | public var localizedDescription: String { 33 | switch self { 34 | case .noDraftSupplied: 35 | return String(localized: "composer.error.missing-draft") 36 | case .exceedsCharacterLimit: 37 | return String(localized: "composer.error.character-limit") 38 | case .mastodonError(let fetchError, _): 39 | return fetchError.localizedDescription 40 | case .unsupportedEventDispatch: 41 | return String(localized: "composer.error.unsupported-event") 42 | } 43 | } 44 | } 45 | 46 | extension ComposerFlowError: Equatable, Hashable {} 47 | 48 | extension FetchError: Equatable, Hashable { 49 | public static func == (lhs: FetchError, rhs: FetchError) -> Bool { 50 | lhs.description == rhs.description 51 | } 52 | 53 | public func hash(into hasher: inout Hasher) { 54 | hasher.combine(description) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Packages/GardenComposer/Sources/GardenComposer/NSTextCheckingResult+Substring.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSTextCheckingResult+Substring.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/21/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Foundation 16 | 17 | extension NSTextCheckingResult { 18 | func text(in sourceText: String) -> Substring? { 19 | guard let range = Range(range, in: sourceText) else { return nil } 20 | return sourceText[range] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Packages/GardenComposer/Sources/GardenComposer/Resources/Localizable.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "composer.error.character-limit" : { 5 | "localizations" : { 6 | "en" : { 7 | "stringUnit" : { 8 | "state" : "translated", 9 | "value" : "This draft exceeds the character limit." 10 | } 11 | } 12 | } 13 | }, 14 | "composer.error.missing-draft" : { 15 | "localizations" : { 16 | "en" : { 17 | "stringUnit" : { 18 | "state" : "translated", 19 | "value" : "A draft is required to publish a discussion." 20 | } 21 | } 22 | } 23 | }, 24 | "composer.error.unsupported-event" : { 25 | "localizations" : { 26 | "en" : { 27 | "stringUnit" : { 28 | "state" : "translated", 29 | "value" : "The flow has encountered an unsupported state-event pair." 30 | } 31 | } 32 | } 33 | } 34 | }, 35 | "version" : "1.0" 36 | } -------------------------------------------------------------------------------- /Packages/GardenComposer/Sources/GardenComposer/Status+ReplyText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Status+ReplyText.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/8/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import Foundation 17 | 18 | extension Status { 19 | static let mentionRegex = #/\@([a-zA-Z0-9\_]+)\@([a-zA-Z0-9\_\-.]+)/# 20 | 21 | func replyMentions(including existingParticipants: String = "", excluding currentUser: Account) -> String { 22 | var allMentions = mentions 23 | if let reblog { 24 | allMentions.append(contentsOf: reblog.mentions) 25 | } 26 | 27 | let otherMembers = Mention.members(of: allMentions, excluding: [account.acct, currentUser.acct]) 28 | var allParticipants = [existingParticipants, otherMembers] 29 | 30 | if account != currentUser { 31 | allParticipants.append("@\(account.acct)") 32 | } 33 | 34 | // Perform a second padd to add the reblogged author if they weren't already included. 35 | if let reblog, !allMentions.map(\.acct).contains(reblog.account.acct), reblog.account != currentUser { 36 | allParticipants.append("@\(reblog.account.acct)") 37 | } 38 | 39 | return allParticipants.filter(\.isNotPureWhitespace).joined(separator: " ") 40 | } 41 | } 42 | 43 | extension Mention { 44 | static func members(of party: [Mention], excluding respondents: [String]) -> String { 45 | party.filter { member in !respondents.contains(member.acct) } 46 | .map { member in "@\(member.acct)" } 47 | .joined(separator: " ") 48 | } 49 | } 50 | 51 | extension String { 52 | var isPureWhitespace: Bool { 53 | allSatisfy(\.isWhitespace) 54 | } 55 | 56 | var isNotPureWhitespace: Bool { 57 | !isPureWhitespace 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Packages/GardenComposer/Tests/GardenComposerTests/ComposerDraftTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComposerDraftTests.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/8/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | @testable import GardenComposer 16 | import XCTest 17 | 18 | final class ComposerDraftTests: XCTestCase { 19 | func testCountMatchesOnRegularText() throws { 20 | let draft = ComposerDraft(new: "This is some normal text.") 21 | XCTAssertEqual(draft.count, 25) 22 | } 23 | 24 | func testCountMatchesWithURL() throws { 25 | let draft = ComposerDraft(new: "Testing https://www.google.com is that kool") 26 | XCTAssertEqual(draft.count, 44) 27 | } 28 | 29 | func testCountMatchesWithAccountMention() throws { 30 | let draft = ComposerDraft(new: "Let's mention @Gargron@mastodon.social in this reply, shall we?") 31 | XCTAssertEqual(draft.count, 47) 32 | } 33 | 34 | func testCountMatchesWithMixedContent() throws { 35 | let draft = ComposerDraft(new: "Will you check this, please, @Gargron@mastodon.social https://www.google.com") 36 | XCTAssertEqual(draft.count, 61) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Packages/GardenComposer/Tests/GardenComposerTests/ReplyMentionsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReplyMentionsTests.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/8/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | @testable import GardenComposer 16 | import Alice 17 | import XCTest 18 | import AliceMockingbird 19 | 20 | final class ReplyMentionsTests: XCTestCase { 21 | let allAccounts = MockConstants.timeline.flatMap { status in 22 | var accounts = [status.account] 23 | if let reblogged = status.reblog { 24 | accounts.append(reblogged.account) 25 | } 26 | return accounts 27 | } 28 | 29 | func testReplyMentionsOnSelfThread() throws { 30 | let reply = MockConstants.status.replyMentions(excluding: MockConstants.profile) 31 | XCTAssertEqual(reply, "") 32 | } 33 | 34 | func testReplyMentionsInConversation() throws { 35 | let nonUniqueAccount = allAccounts.first { account in account != MockConstants.profile } 36 | let reply = MockConstants.status.replyMentions(excluding: nonUniqueAccount!) 37 | XCTAssertEqual(reply, "@admin") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Packages/GardenDiscussions/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /Packages/GardenDiscussions/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "GardenDiscussions", 8 | defaultLocalization: "en", 9 | platforms: [.iOS(.v16)], 10 | products: [ 11 | .library( 12 | name: "GardenDiscussions", 13 | targets: ["GardenDiscussions"]) 14 | ], 15 | dependencies: [ 16 | .package(name: "Alice", path: "../Alice"), 17 | .package(name: "FrugalMode", path: "../FrugalMode"), 18 | .package(name: "GardenProfiles", path: "../GardenProfiles"), 19 | .package(name: "GardenSettings", path: "../GardenSettings"), 20 | .package(name: "WebString", path: "../WebString"), 21 | .package(url: "https://github.com/divadretlaw/EmojiText", from: "2.8.0") 22 | ], 23 | targets: [ 24 | .target( 25 | name: "GardenDiscussions", 26 | dependencies: ["Alice", "FrugalMode", "GardenProfiles", "GardenSettings", "WebString", "EmojiText"]), 27 | .testTarget( 28 | name: "GardenDiscussionsTests", 29 | dependencies: ["GardenDiscussions"]) 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /Packages/GardenDiscussions/Sources/GardenDiscussions/Status+Authorship.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Status+Authorship.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 5/8/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import Foundation 17 | 18 | extension Status { 19 | var originalAuthor: Account { 20 | reblog?.account ?? account 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Packages/GardenDiscussions/Tests/GardenDiscussionsTests/GardenDiscussionsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import GardenDiscussions 3 | 4 | final class GardenDiscussionsTests: XCTestCase { 5 | func testExample() throws { 6 | // XCTest Documentation 7 | // https://developer.apple.com/documentation/xctest 8 | 9 | // Defining Test Cases and Test Methods 10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Packages/GardenGate/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /Packages/GardenGate/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "GardenGate", 8 | defaultLocalization: "en", 9 | platforms: [.iOS(.v16)], 10 | products: [ 11 | .library( 12 | name: "GardenGate", 13 | targets: ["GardenGate"]) 14 | ], 15 | dependencies: [ 16 | .package(name: "Alice", path: "../Alice"), 17 | .package(name: "FlowKit", path: "../FlowKit"), 18 | .package(url: "https://github.com/alicerunsonfedora/SafariView", branch: "root") 19 | ], 20 | targets: [ 21 | .target( 22 | name: "GardenGate", 23 | dependencies: ["Alice", "FlowKit", "SafariView"], 24 | resources: [ 25 | .process("Resources") 26 | ]), 27 | .testTarget( 28 | name: "GardenGateTests", 29 | dependencies: [ 30 | "GardenGate", 31 | "Alice", 32 | .product(name: "AliceMockingbird", package: "Alice"), 33 | .product(name: "FlowKitTestSupport", package: "FlowKit") 34 | ]) 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /Packages/GardenGate/Sources/GardenGate/AuthenticationGateBrowser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthenticationGateBrowser.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 24/6/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Foundation 16 | import SafariView 17 | import SwiftUI 18 | 19 | /// A browser window used in the authentication view for rendering OAuth webpages in Gopherdon. 20 | struct AuthenticationGateBrowser: View { 21 | /// The URL that the browser will open when the view is rendered. 22 | /// 23 | /// If this value is `nil`, an error screen will appear instead, informing the user that the URL could not 24 | /// be opened. 25 | @Binding var url: URL? 26 | 27 | var body: some View { 28 | Group { 29 | SafariView(url: $url) 30 | .prefersReaderMode(.constant(false)) 31 | .collapsible(.constant(false)) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Packages/GardenGate/Sources/GardenGate/AuthenticationGateHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthenticationGateHeaderView.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 1/7/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import SwiftUI 16 | 17 | struct AuthenticationGateHeaderView: View { 18 | var compact: Bool 19 | var alignment: HorizontalAlignment = .center 20 | 21 | var body: some View { 22 | VStack(alignment: alignment) { 23 | if !compact { 24 | Image("GardensIcon") 25 | .symbolRenderingMode(.palette) 26 | .resizable() 27 | .scaledToFit() 28 | .frame(width: 76, height: 76) 29 | .cornerRadius(16) 30 | } 31 | Text("auth.welcome", bundle: .module) 32 | .font(.system(.title2, design: .rounded)) 33 | .bold() 34 | Text("auth.appname", bundle: .module) 35 | .font(.system(size: 56, weight: .bold, design: .rounded)) 36 | .foregroundColor(.accentColor) 37 | } 38 | .animation(.bouncy, value: compact) 39 | } 40 | } 41 | 42 | struct AuthenticationGateHeaderView_Previews: PreviewProvider { 43 | static var previews: some View { 44 | Group { 45 | AuthenticationGateHeaderView(compact: true, alignment: .leading) 46 | AuthenticationGateHeaderView(compact: true, alignment: .center) 47 | AuthenticationGateHeaderView(compact: false, alignment: .leading) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Packages/GardenGate/Sources/GardenGate/DomainValidationError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DomainValidationError.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 24/6/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Foundation 16 | 17 | public enum DomainValidationError: Error { 18 | case invalid(domain: String) 19 | case rejected(domain: String) 20 | 21 | public var message: String { 22 | switch self { 23 | case .invalid(let domain): 24 | String(format: NSLocalizedString("auth.badurl.message", bundle: .module, comment: "Invalid URL (FGD-22)"), 25 | domain) 26 | case.rejected(let domain): 27 | String(format: NSLocalizedString("auth.disallowed.message", bundle: .module, comment: "In disallow list"), 28 | domain) 29 | } 30 | } 31 | } 32 | 33 | extension DomainValidationError: LocalizedError { 34 | public var localizedDescription: String { 35 | message 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Packages/GardenGate/Sources/GardenGate/Resources/Disallow.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | gab.ai 6 | gab.com 7 | exited.eu 8 | not-develop.gab.com 9 | develop.gab.com 10 | ekrem.develop.gab.com 11 | gab.io 12 | gabble.xyz 13 | gab.polaris-1.work 14 | gabfed.com 15 | spinster.xyz 16 | djitter.com 17 | kazvam.com 18 | truthsocial.com 19 | endtimebelievers.com 20 | bitcoinhackers.org 21 | megamast.io 22 | obo.sh 23 | social.ancreport.com 24 | gearlandia.haus 25 | 26 | 27 | -------------------------------------------------------------------------------- /Packages/GardenProfiles/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /Packages/GardenProfiles/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "GardenProfiles", 8 | defaultLocalization: "en", 9 | platforms: [.iOS(.v16)], 10 | products: [ 11 | .library( 12 | name: "GardenProfiles", 13 | targets: ["GardenProfiles"]) 14 | ], 15 | dependencies: [ 16 | .package(name: "Alice", path: "../Alice"), 17 | .package(name: "FrugalMode", path: "../FrugalMode"), 18 | .package(name: "GardenSettings", path: "../GardenSettings") 19 | ], 20 | targets: [ 21 | .target( 22 | name: "GardenProfiles", 23 | dependencies: ["Alice", "FrugalMode", "GardenSettings"]), 24 | .testTarget( 25 | name: "GardenProfilesTests", 26 | dependencies: ["GardenProfiles"]) 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /Packages/GardenProfiles/Sources/GardenProfiles/Account+Verification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Account+Verification.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 5/8/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Alice 16 | import Foundation 17 | 18 | /// An enumeration that represents an account's verified status. 19 | public enum Verification { 20 | /// The account is not verified. 21 | case unverified 22 | 23 | /// The account has verification to a specified domain. 24 | case verified(String) 25 | } 26 | 27 | public extension Account { 28 | /// The account's preferred name. 29 | /// 30 | /// This value will return the first available value it finds in the display name, username, or account. 31 | var accountName: String { 32 | if !displayName.isEmpty { return displayName } 33 | if !username.isEmpty { return "@\(username)" } 34 | return "@\(acct)" 35 | } 36 | 37 | /// The account's verification status. 38 | var verification: Verification { 39 | if let verifiedAtField = fields.first(where: { field in field.verifiedAt != nil }) { 40 | return .verified(verifiedAtField.value) 41 | } 42 | return .unverified 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Packages/GardenProfiles/Tests/GardenProfilesTests/GardenProfilesTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import GardenProfiles 3 | 4 | final class GardenProfilesTests: XCTestCase { 5 | func testExample() throws { 6 | // XCTest Documentation 7 | // https://developer.apple.com/documentation/xctest 8 | 9 | // Defining Test Cases and Test Methods 10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Packages/GardenSettings/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /Packages/GardenSettings/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "GardenSettings", 8 | defaultLocalization: "en", 9 | platforms: [.macOS(.v13), .iOS(.v16)], 10 | products: [ 11 | .library( 12 | name: "GardenSettings", 13 | targets: ["GardenSettings"]) 14 | ], 15 | dependencies: [ 16 | .package(name: "Alice", path: "../Alice") 17 | ], 18 | targets: [ 19 | .target( 20 | name: "GardenSettings", 21 | dependencies: [ 22 | .product(name: "Alice", package: "Alice") 23 | ]), 24 | .testTarget( 25 | name: "GardenSettingsTests", 26 | dependencies: ["GardenSettings"]) 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /Packages/GardenSettings/Sources/GardenSettings/UserDefaults+GardenSettingsKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaults+GardensSettingsKey.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 15/7/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Foundation 16 | 17 | extension UserDefaults { 18 | /// Retrieves a decoded value from the user defaults store and decodes it. If no value is found, the default value 19 | /// is returned. 20 | /// 21 | /// - Parameter key: The settings key to retrieve from user defaults. 22 | /// - Parameter defaultValue: The default value for this user default if the value was not found in the user 23 | /// defaults store. 24 | public func getValue(forKey key: GardenSettingsKey, default defaultValue: T) -> T { 25 | let value = value(forKey: key) 26 | if let trueValue = value as? T { 27 | return trueValue 28 | } 29 | return defaultValue 30 | } 31 | 32 | /// Retrieves a value from the user defaults store. 33 | /// - Parameter key: The settings key to retrieve from user defaults. 34 | func value(forKey key: GardenSettingsKey) -> Any? { 35 | return value(forKey: key.rawValue) 36 | } 37 | 38 | /// Sets a value in the user defaults store with a specified key. 39 | /// - Parameter value: The value to store into user defaults store. 40 | /// - Parameter key: The settings key the value will be stored at. 41 | public func setValue(_ value: Any?, forKey key: GardenSettingsKey) { 42 | setValue(value, forKey: key.rawValue) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /Packages/Interventions/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /Packages/Interventions/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Interventions", 8 | defaultLocalization: "en", 9 | platforms: [.macOS(.v13), .iOS(.v16), .tvOS(.v16), .watchOS(.v9), .visionOS(.v1)], 10 | products: [ 11 | .library( 12 | name: "Interventions", 13 | targets: ["Interventions"]) 14 | ], 15 | dependencies: [ 16 | .package(name: "FlowKit", path: "../FlowKit") 17 | ], 18 | targets: [ 19 | .target( 20 | name: "Interventions", 21 | dependencies: ["FlowKit"], 22 | resources: [ 23 | .process("Resources") 24 | ]), 25 | .testTarget( 26 | name: "InterventionsTests", 27 | dependencies: [ 28 | "Interventions", 29 | .product(name: "FlowKitTestSupport", package: "FlowKit") 30 | ]) 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /Packages/Interventions/Sources/Interventions/InterventionAllowedMechanisms.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InterventionAllowedMechanism.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 26/6/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Foundation 16 | 17 | /// The set of allowed cases for which an intervention can occur. 18 | public struct InterventionAllowedMechanisms: OptionSet { 19 | public var rawValue: UInt8 20 | 21 | public init(rawValue: UInt8) { 22 | self.rawValue = rawValue 23 | } 24 | 25 | /// No interventions can occur. 26 | public static let none = InterventionAllowedMechanisms(rawValue: 1 << 0) 27 | 28 | /// Presents an intervention when refreshing content. 29 | public static let refresh = InterventionAllowedMechanisms(rawValue: 1 << 1) 30 | 31 | /// Presents an intervention when requesting for more content. 32 | public static let fetchMore = InterventionAllowedMechanisms(rawValue: 1 << 2) 33 | 34 | /// Presents an intervention at all possible opportunities. 35 | public static let all: InterventionAllowedMechanisms = [.refresh, .fetchMore] 36 | } 37 | -------------------------------------------------------------------------------- /Packages/Interventions/Sources/Interventions/InterventionLinkOpener.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InterventionLinkOpener.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 26/6/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | #if os(macOS) 16 | import AppKit 17 | #else 18 | import UIKit 19 | #endif 20 | 21 | /// A protocol that defines an actor that can open links to call one sec. 22 | public protocol InterventionLinkOpener { 23 | /// Returns whether the specified URL can be opened. 24 | /// - Parameter url: The URL that is being requested to navigate to. 25 | func canOpenURL(_ url: URL) async -> Bool 26 | 27 | /// Opens the URL with the requested options. 28 | /// - Parameter url: The URL to open. 29 | /// - Parameter options: The options used to configure how the URL is opened. 30 | @discardableResult 31 | func open(_ url: URL) async -> Bool 32 | } 33 | 34 | #if os(macOS) 35 | extension NSWorkspace: InterventionLinkOpener { 36 | func canOpenURL(_ url: URL) { return true } 37 | } 38 | #else 39 | extension UIApplication: InterventionLinkOpener { 40 | public func open(_ url: URL) async -> Bool { 41 | await self.open(url, options: [:]) 42 | } 43 | } 44 | #endif 45 | -------------------------------------------------------------------------------- /Packages/Interventions/Sources/Interventions/InterventionRequestError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InterventionRequestError.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 26/6/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Foundation 16 | 17 | /// An enumeration representing the various errors that can occur when requesting an intervention. 18 | public enum InterventionRequestError: Error { 19 | /// one sec is not installed on the device. 20 | case oneSecNotAvailable 21 | 22 | /// A request has already been made. 23 | case requestAlreadyMade(Date) 24 | 25 | /// An authorization request was made from an invalid state. 26 | /// 27 | /// This usually occurs when the request was made in its initial state, or if the request was made after it 28 | /// encountered an error. 29 | case invalidAuthorizationFlowState 30 | 31 | /// The user has already authorized the action, and no further action is needed. 32 | case alreadyAuthorized(Date, context: InterventionAuthorizationContext) 33 | } 34 | 35 | extension InterventionRequestError: Equatable { 36 | public static func == (lhs: InterventionRequestError, rhs: InterventionRequestError) -> Bool { 37 | lhs.localizedDescription == rhs.localizedDescription 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Packages/Interventions/Sources/Interventions/Resources/Localizable.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "Click me!" : { 5 | 6 | }, 7 | "interventions.cta.disable" : { 8 | "comment" : "Turn off the interventions feature.", 9 | "localizations" : { 10 | "en" : { 11 | "stringUnit" : { 12 | "state" : "translated", 13 | "value" : "Turn Off Interventions" 14 | } 15 | } 16 | } 17 | }, 18 | "interventions.cta.dismiss" : { 19 | "comment" : "Dismiss the intervention dialog.", 20 | "localizations" : { 21 | "en" : { 22 | "stringUnit" : { 23 | "state" : "translated", 24 | "value" : "Not Now" 25 | } 26 | } 27 | } 28 | }, 29 | "interventions.cta.install" : { 30 | "comment" : "Prompt to install one sec from the App Store.", 31 | "localizations" : { 32 | "en" : { 33 | "stringUnit" : { 34 | "state" : "translated", 35 | "value" : "Install one sec…" 36 | } 37 | } 38 | } 39 | }, 40 | "interventions.missing.detail" : { 41 | "comment" : "Explains that one sec can be installed from the App Store or interventions can be disabled.", 42 | "localizations" : { 43 | "en" : { 44 | "stringUnit" : { 45 | "state" : "translated", 46 | "value" : "You can install one sec from the App Store or disable interventions." 47 | } 48 | } 49 | } 50 | }, 51 | "interventions.missing.title" : { 52 | "comment" : "Informs the user that one sec is not installed.", 53 | "localizations" : { 54 | "en" : { 55 | "stringUnit" : { 56 | "state" : "translated", 57 | "value" : "one sec is Not Installed" 58 | } 59 | } 60 | } 61 | } 62 | }, 63 | "version" : "1.0" 64 | } -------------------------------------------------------------------------------- /Packages/SeedUI/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /Packages/SeedUI/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "SeedUI", 7 | platforms: [.macOS(.v13), .iOS(.v16)], 8 | products: [ 9 | .library( 10 | name: "SeedUI", 11 | targets: ["SeedUI"]) 12 | ], 13 | targets: [ 14 | .target( 15 | name: "SeedUI") 16 | ] 17 | ) 18 | -------------------------------------------------------------------------------- /Packages/SeedUI/Sources/SeedUI/HashableType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HashableType.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 8/7/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import Foundation 16 | 17 | /// A box type that allows keying in a dictionary from a hashable type. 18 | /// 19 | /// This is used in conjunction with ``RecursiveNavigationStack`` to set recursive destinations. 20 | public struct HashableType: Hashable { 21 | public let base: T.Type 22 | 23 | public init(_ base: T.Type) { 24 | self.base = base 25 | } 26 | 27 | public static func == (lhs: HashableType, rhs: HashableType) -> Bool { 28 | lhs.base == rhs.base 29 | } 30 | 31 | public func hash(into hasher: inout Hasher) { 32 | hasher.combine(ObjectIdentifier(base)) 33 | } 34 | } 35 | 36 | public extension Dictionary { 37 | subscript(key: T.Type) -> Value? where Key == HashableType { 38 | get { return self[HashableType(key)] } 39 | set { self[HashableType(key)] = newValue } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Packages/SeedUI/Sources/SeedUI/InformationCard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InformationCard.swift 3 | // Fedigardens 4 | // 5 | // Created by Marquis Kurt on 8/7/23. 6 | // 7 | // This file is part of Fedigardens. 8 | // 9 | // Fedigardens is non-violent software: you can use, redistribute, and/or modify it under the terms of the CNPLv7+ 10 | // as found in the LICENSE file in the source code root directory or at . 11 | // 12 | // Fedigardens comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. See the CNPL for 13 | // details. 14 | 15 | import SwiftUI 16 | 17 | /// A card with a labelled heading, with multiline content. 18 | public struct InformationCard: View { 19 | var title: TextContent 20 | var systemImage: String 21 | var content: TextContent 22 | 23 | public init(title: TextContent, systemImage: String, content: TextContent) { 24 | self.title = title 25 | self.systemImage = systemImage 26 | self.content = content 27 | } 28 | 29 | public var body: some View { 30 | VStack(alignment: .leading, spacing: 6) { 31 | Label(title, systemImage: systemImage) 32 | .font(.system(.headline, design: .rounded)) 33 | .multilineTextAlignment(.leading) 34 | Text(content) 35 | .font(.system(.subheadline, design: .rounded)) 36 | .multilineTextAlignment(.leading) 37 | } 38 | .padding() 39 | .frame(maxWidth: .infinity) 40 | .background(Color(uiColor: .tertiarySystemFill).cornerRadius(10)) 41 | } 42 | } 43 | 44 | struct InformationCard_Previews: PreviewProvider { 45 | static var previews: some View { 46 | InformationCard( 47 | title: "Unable to load data", 48 | systemImage: "exclamationmark.triangle", 49 | content: "A secure connection to the server could not be made." 50 | ) 51 | .padding() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Packages/SeedUI/Sources/SeedUI/SeedUI.docc/SeedUI.md: -------------------------------------------------------------------------------- 1 | # ``SeedUI`` 2 | 3 | A package for common reusable views in the Fedigardens project. 4 | 5 | ## Overview 6 | 7 | SeedUI contains views that are commonly reused throughout the Fedigardens project. All views in SeedUI are designed with 8 | SwiftUI and are stateless. 9 | 10 | ## Topics 11 | 12 | ### Texts 13 | 14 | - ``BadgedText`` 15 | - ``InformationCard`` 16 | 17 | ### Navigation 18 | 19 | - ``RecursiveNavigationStack`` 20 | - ``HashableType`` 21 | -------------------------------------------------------------------------------- /Packages/WebString/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /Packages/WebString/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "WebString", 8 | platforms: [.macOS(.v13), .iOS(.v16), .tvOS(.v16), .watchOS(.v9)], 9 | products: [ 10 | .library( 11 | name: "WebString", 12 | targets: ["WebString"]) 13 | ], 14 | dependencies: [ 15 | .package(url: "https://gitlab.com/mflint/HTML2Markdown", from: "1.0.0") 16 | ], 17 | targets: [ 18 | .target( 19 | name: "WebString", 20 | dependencies: ["HTML2Markdown"]), 21 | .testTarget( 22 | name: "WebStringTests", 23 | dependencies: ["WebString"]) 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /Packages/WebString/Tests/WebStringTests/WebStringTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import WebString 3 | 4 | final class WebStringTests: XCTestCase { 5 | func testExample() throws { 6 | // XCTest Documentation 7 | // https://developer.apple.com/documentation/xctest 8 | 9 | // Defining Test Cases and Test Methods 10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods 11 | } 12 | } 13 | --------------------------------------------------------------------------------