├── .gitattributes ├── .github └── workflows │ ├── delete_old_workflow_runs.yml │ └── preflight.yml ├── .gitignore ├── .swift-version ├── .swiftformat ├── BuildTools ├── Empty.swift ├── Package.resolved ├── Package.swift ├── swiftgen.yml └── xcassets-colors-swiftui.stencil ├── CHANGELOG.md ├── LICENSE.md ├── Makefile ├── Prose ├── .gitignore ├── ConversationFeaturePreview │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── ContentView.swift │ ├── ConversationFeaturePreview.entitlements │ ├── ConversationFeaturePreviewApp.swift │ └── Preview Content │ │ └── Preview Assets.xcassets │ │ └── Contents.json ├── EditProfileFeaturePreview │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── EditProfileFeaturePreview.entitlements │ └── EditProfileFeaturePreviewApp.swift ├── Prose.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcshareddata │ │ └── xcschemes │ │ ├── ConversationFeaturePreview.xcscheme │ │ ├── Prose.xcscheme │ │ ├── Release.xcscheme │ │ └── UITestHost.xcscheme ├── Prose │ ├── AllTests.xctestplan │ ├── AppDelegate.swift │ ├── Components │ │ └── Sidebar │ │ │ └── SidebarPartContextComponent.swift │ ├── Package.resolved │ ├── Prose.entitlements │ ├── ProseApp.swift │ ├── Resources │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── Dock@1024.png │ │ │ │ ├── Dock@128.png │ │ │ │ ├── Dock@16.png │ │ │ │ ├── Dock@256 1.png │ │ │ │ ├── Dock@256.png │ │ │ │ ├── Dock@32 1.png │ │ │ │ ├── Dock@32.png │ │ │ │ ├── Dock@512 1.png │ │ │ │ ├── Dock@512.png │ │ │ │ └── Dock@64.png │ │ │ └── Contents.json │ │ └── Credits.rtf │ ├── UITests.xctestplan │ └── UnitTests.xctestplan ├── ProseLib │ ├── .gitignore │ ├── .swiftpm │ │ └── xcode │ │ │ └── package.xcworkspace │ │ │ └── contents.xcworkspacedata │ ├── Package.resolved │ ├── Package.swift │ ├── Sources │ │ ├── AddressBookFeature │ │ │ ├── AddressBookScreen.swift │ │ │ ├── Target+Logger.swift │ │ │ └── Toolbar.swift │ │ ├── App │ │ │ ├── Accounts │ │ │ │ ├── AccountReducer.swift │ │ │ │ ├── AccountsReducer.swift │ │ │ │ └── SelectedAccountReducer.swift │ │ │ ├── AppDelegate.swift │ │ │ ├── AppReducer.swift │ │ │ ├── AppScene.swift │ │ │ └── Target+Logger.swift │ │ ├── AppDomain │ │ │ ├── Account.swift │ │ │ ├── ConnectionStatus.swift │ │ │ ├── Connectivity.swift │ │ │ ├── Credentials.swift │ │ │ ├── Exports.swift │ │ │ ├── ProseCoreExtensions │ │ │ │ ├── Availability.swift │ │ │ │ ├── BareJid.swift │ │ │ │ ├── Contact.swift │ │ │ │ ├── FullJid.swift │ │ │ │ ├── Message.swift │ │ │ │ └── UserProfile.swift │ │ │ ├── SessionState.swift │ │ │ └── UserInfo.swift │ │ ├── AppLocalization │ │ │ ├── AppDomain+i18n.swift │ │ │ ├── Bundle+Fix.swift │ │ │ ├── Generated │ │ │ │ ├── .gitkeep │ │ │ │ └── Strings+Generated.swift │ │ │ ├── Resources │ │ │ │ └── en.lproj │ │ │ │ │ ├── Localizable.strings │ │ │ │ │ └── Localizable.stringsdict │ │ │ ├── String+Markdown.swift │ │ │ └── Target+Logger.swift │ │ ├── Assets │ │ │ ├── Bundle+Fix.swift │ │ │ ├── Generated │ │ │ │ ├── .gitkeep │ │ │ │ ├── Colors+Generated.swift │ │ │ │ └── Images+Generated.swift │ │ │ ├── ImageAsset+URL.swift │ │ │ ├── Resources │ │ │ │ ├── Colors.xcassets │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── background │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── message.colorset │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── border │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ ├── primary.colorset │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ ├── secondary.colorset │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ ├── tertiary.colorset │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── tertiaryLight.colorset │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── state │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ ├── coolGrey.colorset │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ ├── green.colorset │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ ├── greenLight.colorset │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ ├── grey.colorset │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── greyLight.colorset │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── text │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ ├── primary.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ ├── primaryLight.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ ├── secondary.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── secondaryLight.colorset │ │ │ │ │ │ └── Contents.json │ │ │ │ └── Images.xcassets │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── platform-logo.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── macos-logo.svg │ │ │ └── Target+Logger.swift │ │ ├── AuthenticationFeature │ │ │ ├── AuthenticationReducer.swift │ │ │ ├── AuthenticationScreen.swift │ │ │ ├── BasicAuth │ │ │ │ ├── BasicAuthReducer.swift │ │ │ │ └── BasicAuthView.swift │ │ │ ├── MFA │ │ │ │ ├── Authentication+TemporaryFix.swift │ │ │ │ ├── MFA6DigitsView.swift │ │ │ │ └── MFAView.swift │ │ │ ├── Profile │ │ │ │ ├── ProfileReducer.swift │ │ │ │ └── ProfileView.swift │ │ │ ├── Target+Logger.swift │ │ │ └── UserData.swift │ │ ├── ConnectivityClient │ │ │ ├── ConnectivityClient+Live.swift │ │ │ └── ConnectivityClient.swift │ │ ├── ConversationFeature │ │ │ ├── Chat │ │ │ │ ├── Chat.swift │ │ │ │ ├── ChatReducer.swift │ │ │ │ ├── ChatSessionState.swift │ │ │ │ ├── EditMessageReducer.swift │ │ │ │ ├── EditMessageView.swift │ │ │ │ ├── MessageMenu.swift │ │ │ │ ├── MessageView.swift │ │ │ │ └── ReactionPickerView.swift │ │ │ ├── ConversationScreen.swift │ │ │ ├── ConversationScreenReducer.swift │ │ │ ├── InfoSidebar │ │ │ │ ├── ConversationInfoReducer.swift │ │ │ │ ├── ConversationInfoView.swift │ │ │ │ └── Views │ │ │ │ │ ├── ActionRow.swift │ │ │ │ │ ├── EntryRowLabelStyle.swift │ │ │ │ │ ├── IdentityPopover.swift │ │ │ │ │ └── SubtitledActionButtonStyle.swift │ │ │ ├── MessageBar │ │ │ │ ├── MessageBar.swift │ │ │ │ ├── MessageBarReducer.swift │ │ │ │ ├── MessageField │ │ │ │ │ ├── MessageField.swift │ │ │ │ │ └── MessageFieldReducer.swift │ │ │ │ └── TypingIndicator.swift │ │ │ ├── README.md │ │ │ ├── Target+Logger.swift │ │ │ └── Toolbar │ │ │ │ ├── Toolbar.swift │ │ │ │ ├── ToolbarReducer.swift │ │ │ │ └── ToolbarSecurity.swift │ │ ├── CredentialsClient │ │ │ ├── CredentialsClient+Live.swift │ │ │ ├── CredentialsClient.swift │ │ │ └── Target+Logger.swift │ │ ├── EditProfileFeature │ │ │ ├── AuthenticationReducer.swift │ │ │ ├── AuthenticationView.swift │ │ │ ├── Components │ │ │ │ ├── ContentSection.swift │ │ │ │ ├── SecondaryRow.swift │ │ │ │ └── ThreeColumns.swift │ │ │ ├── EditProfileReducer.swift │ │ │ ├── EditProfileScreen.swift │ │ │ ├── EncryptionReducer.swift │ │ │ ├── EncryptionView.swift │ │ │ ├── IdentityReducer.swift │ │ │ ├── IdentityView.swift │ │ │ ├── ProfileReducer.swift │ │ │ ├── ProfileView.swift │ │ │ ├── Sidebar │ │ │ │ ├── Sidebar.swift │ │ │ │ ├── SidebarHeader.swift │ │ │ │ ├── SidebarHeaderReducer.swift │ │ │ │ ├── SidebarReducer.swift │ │ │ │ ├── SidebarRow.swift │ │ │ │ └── SidebarRowReducer.swift │ │ │ └── Target+Logger.swift │ │ ├── JoinChatFeature │ │ │ ├── AddMemberSheet.swift │ │ │ └── JoinGroupSheet.swift │ │ ├── MainScreenFeature │ │ │ ├── MainScreenReducer.swift │ │ │ ├── MainScreenView.swift │ │ │ └── Target+Logger.swift │ │ ├── Mocks │ │ │ ├── BareJid.swift │ │ │ ├── Contact.swift │ │ │ ├── Message.swift │ │ │ ├── RandomUser │ │ │ │ ├── RandomUser+JSON.swift │ │ │ │ ├── RandomUser.swift │ │ │ │ └── random_user.json │ │ │ └── SessionState.swift │ │ ├── NotificationsClient │ │ │ ├── NotificationsClient+Live.swift │ │ │ ├── NotificationsClient+Noop.swift │ │ │ ├── NotificationsClient.swift │ │ │ └── Target+Logger.swift │ │ ├── PasteboardClient │ │ │ ├── PasteboardClient+Live.swift │ │ │ └── PasteboardClient.swift │ │ ├── PreviewAssets │ │ │ ├── Bundle+Fix.swift │ │ │ ├── Generated │ │ │ │ ├── .gitkeep │ │ │ │ └── Assets+Generated.swift │ │ │ ├── ImageAsset+URL.swift │ │ │ ├── Resources │ │ │ │ └── Assets.xcassets │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── avatars │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── alexandre.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── avatar.png │ │ │ │ │ ├── antoine.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── avatar.jpg │ │ │ │ │ ├── baptiste.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── avatar.jpg │ │ │ │ │ ├── camille.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── avatag.jpg │ │ │ │ │ ├── constellation-health.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── constellation-health.jpg │ │ │ │ │ ├── eliott.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── avatar.jpg │ │ │ │ │ ├── julien.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── julien.jpg │ │ │ │ │ └── valerian.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── avatar.jpg │ │ │ │ │ ├── logo-crisp.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── logo-crisp.png │ │ │ │ │ ├── logo-makair.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── logo-makair.png │ │ │ │ │ └── webcam-valerian.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── webcam.jpg │ │ │ └── Target+Logger.swift │ │ ├── ProseCore │ │ │ ├── AccountBookmarksClient+Live.swift │ │ │ ├── AccountBookmarksClient.swift │ │ │ ├── AccountsClient+Live.swift │ │ │ ├── AccountsClient.swift │ │ │ ├── Exports.swift │ │ │ ├── ProseCoreClient+Live.swift │ │ │ └── ProseCoreClient.swift │ │ ├── ProseCoreViews │ │ │ ├── API │ │ │ │ ├── MessagingContext.swift │ │ │ │ └── MessagingStore.swift │ │ │ ├── FFI.swift │ │ │ ├── Generated │ │ │ │ ├── .gitkeep │ │ │ │ └── Files+Generated.swift │ │ │ ├── Helpers │ │ │ │ ├── Bundle+Fix.swift │ │ │ │ ├── IdentifiedArray+Difference.swift │ │ │ │ ├── JSEventError.swift │ │ │ │ ├── JSHelpers.swift │ │ │ │ ├── NSError+JavaScript.swift │ │ │ │ └── WKUserContentController+TCA.swift │ │ │ ├── Resources │ │ │ │ ├── .gitkeep │ │ │ │ └── Views │ │ │ │ │ ├── .gitkeep │ │ │ │ │ ├── action-more.77dcdfab.svg │ │ │ │ │ ├── action-reactions.7a469ec6.svg │ │ │ │ │ ├── file-other-option-get.a1a4420f.svg │ │ │ │ │ ├── messaging.3477b7db.js │ │ │ │ │ ├── messaging.36ff8472.css │ │ │ │ │ ├── messaging.html │ │ │ │ │ └── origin-attribute-insecure.aa6021b6.svg │ │ │ ├── Target+Logger.swift │ │ │ └── Types │ │ │ │ ├── ColorScheme.swift │ │ │ │ ├── EventOrigin.swift │ │ │ │ ├── MessageAction.swift │ │ │ │ └── MessageEvent.swift │ │ ├── ProseUI │ │ │ ├── AvailabilityIndicator.swift │ │ │ ├── Avatar.swift │ │ │ ├── Common │ │ │ │ ├── ColoredIconLabelStyle.swift │ │ │ │ ├── ContentCommonNameStatusComponent.swift │ │ │ │ ├── ShadowedButtonStyle.swift │ │ │ │ └── VerticalLabelStyle.swift │ │ │ ├── HelpButton.swift │ │ │ ├── Icon.swift │ │ │ ├── LEDIndicator.swift │ │ │ ├── LevelIndicator.swift │ │ │ ├── OnlineStatusIndicator.swift │ │ │ ├── ReactionPicker │ │ │ │ ├── ReactionPicker.swift │ │ │ │ └── ReactionPickerReducer.swift │ │ │ ├── SpotlightGroupBoxStyle.swift │ │ │ ├── TableFooterButton.swift │ │ │ ├── Target+Logger.swift │ │ │ ├── Toolbar │ │ │ │ ├── CommonToolbar.swift │ │ │ │ ├── CommonToolbarActions.swift │ │ │ │ ├── CommonToolbarNavigation.swift │ │ │ │ └── ToolbarDivider.swift │ │ │ ├── View+OnKeyDown.swift │ │ │ └── ViewWrap+Extension.swift │ │ ├── SettingsFeature │ │ │ ├── AccountSettings │ │ │ │ ├── AccountSettingsAccountView.swift │ │ │ │ ├── AccountSettingsFeaturesView.swift │ │ │ │ └── AccountSettingsSecurityView.swift │ │ │ ├── Atoms │ │ │ │ ├── AccountPickerRow.swift │ │ │ │ ├── ConnectionStatusIndicator.swift │ │ │ │ ├── FormGroupBoxStyle.swift │ │ │ │ └── VideoPreviewView.swift │ │ │ ├── SettingsConstants.swift │ │ │ ├── SettingsView.swift │ │ │ ├── Tabs │ │ │ │ ├── AccountsTab.swift │ │ │ │ ├── AdvancedTab.swift │ │ │ │ ├── CallsTab.swift │ │ │ │ ├── GeneralTab.swift │ │ │ │ ├── MessagesTab.swift │ │ │ │ └── NotificationsTab.swift │ │ │ └── Target+Logger.swift │ │ ├── SidebarFeature │ │ │ ├── Atoms │ │ │ │ └── Counter.swift │ │ │ ├── Footer │ │ │ │ ├── AccountSettingsMenu │ │ │ │ │ ├── AccountSettingsMenuReducer.swift │ │ │ │ │ └── AccountSettingsMenuView.swift │ │ │ │ ├── AccountSwitcherMenu │ │ │ │ │ ├── AccountSwitcherMenuReducer.swift │ │ │ │ │ └── AccountSwitcherMenuView.swift │ │ │ │ ├── FooterDetails.swift │ │ │ │ ├── FooterReducer.swift │ │ │ │ ├── FooterView.swift │ │ │ │ └── Styles │ │ │ │ │ ├── SidebarFooterPopoverButtonStyle.swift │ │ │ │ │ └── VStackGroupBoxStyle.swift │ │ │ ├── Rows │ │ │ │ ├── ActionButton.swift │ │ │ │ ├── ContactRow.swift │ │ │ │ └── IconRow.swift │ │ │ ├── SidebarReducer.swift │ │ │ ├── SidebarView.swift │ │ │ └── Target+Logger.swift │ │ ├── TestHelpers │ │ │ ├── Date+YMD.swift │ │ │ ├── UUID+Incrementing.swift │ │ │ └── XCTestCase+Combine.swift │ │ ├── TestHostApp │ │ │ ├── Helpers │ │ │ │ ├── Target+Logger.swift │ │ │ │ └── TestScene.swift │ │ │ ├── Mocks │ │ │ │ └── AppState.swift │ │ │ └── TestCases │ │ │ │ └── RosterSelection.swift │ │ ├── Toolbox │ │ │ ├── AsyncStream+Prose.swift │ │ │ ├── FocusState+Synchronize.swift │ │ │ ├── ItemProvider+Prose.swift │ │ │ ├── PlatformImage.swift │ │ │ └── Target+Logger.swift │ │ └── UnreadFeature │ │ │ ├── Target+Logger.swift │ │ │ ├── Toolbar.swift │ │ │ ├── UnreadScreen.swift │ │ │ ├── UnreadScreenReducer.swift │ │ │ └── UnreadSection.swift │ └── Tests │ │ ├── ConversationFeatureTests │ │ ├── Helpers │ │ │ └── ChatSessionState+Mock.swift │ │ └── TypingIndicatorTests.swift │ │ ├── CredentialsClientTests │ │ └── CredentialsClientTests.swift │ │ └── ProseCoreViewsTests │ │ ├── FFITests.swift │ │ └── IdentifiedArrayDifferenceTests.swift ├── ProseUITests │ ├── ConversationInfoTests.swift │ ├── Helpers │ │ └── XCUIApplication+Prose.swift │ └── RosterSelectionTests.swift └── UITestHost │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── UITestHost.entitlements │ └── UITestHostApp.swift └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | Prose/ProseLib/Sources/ProseCoreViews/Generated/** linguist-generated=true 2 | Prose/ProseLib/Sources/ProseCoreViews/Resources/Views/** linguist-generated=true 3 | Prose/ProseLib/Sources/Assets/Generated/** linguist-generated=true 4 | Prose/ProseLib/Sources/AppLocalization/Generated/** linguist-generated=true 5 | Prose/ProseLib/Sources/PreviewAssets/Generated/** linguist-generated=true 6 | -------------------------------------------------------------------------------- /.github/workflows/delete_old_workflow_runs.yml: -------------------------------------------------------------------------------- 1 | name: Delete old workflow runs 2 | on: 3 | schedule: 4 | - cron: '0 0 1 * *' 5 | # Run monthly, at 00:00 on the 1st day of month. 6 | 7 | jobs: 8 | del_runs: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Delete old workflow runs 12 | uses: Mattraks/delete-workflow-runs@v2 13 | with: 14 | token: ${{ github.token }} 15 | repository: ${{ github.repository }} 16 | retain_days: 30 17 | keep_minimum_runs: 6 18 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.6.1 2 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | --indent 2 2 | --self insert 3 | --disable enumNamespaces 4 | --enable isEmpty,blockComments 5 | --ranges no-space 6 | --decimalgrouping 3,5 7 | --wraparguments before-first 8 | --wrapcollections before-first 9 | --maxwidth 100 10 | --header \nThis file is part of prose-app-macos.\nCopyright (c) 2023 Prose Foundation\n -------------------------------------------------------------------------------- /BuildTools/Empty.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | // File intentionally left empty. 7 | // https://blog.apptekstudios.com/2019/12/spm-xcode-build-tools/ 8 | -------------------------------------------------------------------------------- /BuildTools/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.6 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "BuildTools", 6 | platforms: [.macOS(.v10_11)], 7 | dependencies: [ 8 | .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.49.9"), 9 | .package(url: "https://github.com/SwiftGen/SwiftGen", from: "6.5.1"), 10 | .package(url: "https://github.com/thii/xcbeautify", from: "0.13.0"), 11 | ], 12 | targets: [.target(name: "BuildTools", path: "")] 13 | ) 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog — Prose (macOS) 2 | 3 | ## 0.1.0 (unreleased) 4 | 5 | * Initial version. 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # ################ Assets ################ 2 | 3 | assets: swiftgen format 4 | 5 | swiftgen: 6 | @(cd BuildTools; SDKROOT=macosx; swift run -c release swiftgen config run --config ./swiftgen.yml) 7 | 8 | format: 9 | @(cd BuildTools; SDKROOT=macosx; swift run -c release swiftformat ..) 10 | 11 | XCBEAUTIFY: 12 | @(cd BuildTools; SDKROOT=macosx; echo "" | swift run -c release xcbeautify) 13 | 14 | # ################ Web Views ################ 15 | 16 | VIEWS_LIB_URL=https://github.com/prose-im/prose-core-views 17 | VIEWS_LIB_VERSION=0.17.0 18 | VIEWS_ARCHIVE_NAME=release-${VIEWS_LIB_VERSION}.tar.gz 19 | DESTINATION=Prose/ProseLib/Sources/ProseCoreViews/Resources/Views 20 | 21 | views: views-build assets 22 | 23 | views-build: 24 | rm -rf "${DESTINATION}" 25 | mkdir "${DESTINATION}" 26 | touch "${DESTINATION}/.gitkeep" 27 | 28 | @curl -Ls "${VIEWS_LIB_URL}/releases/download/${VIEWS_LIB_VERSION}/${VIEWS_ARCHIVE_NAME}" | tar -xvz -C ${DESTINATION} --strip=1 29 | 30 | # ################ Code Hygiene ################ 31 | 32 | XCBEAUTIFY = ./BuildTools/.build/release/xcbeautify 33 | XCODEBUILD = set -o pipefail && xcodebuild 34 | XCPROJ = Prose/Prose.xcodeproj 35 | XCSCHEME = Prose 36 | PREVIEW_SCHEMES = ConversationFeaturePreview EditProfileFeaturePreview 37 | 38 | preflight: lint test release_build build_preview_apps 39 | 40 | lint: 41 | @(cd BuildTools; SDKROOT=macosx; swift run -c release swiftformat --lint ..) 42 | 43 | test: XCBEAUTIFY 44 | @$(XCODEBUILD) \ 45 | -project $(XCPROJ) \ 46 | -scheme $(XCSCHEME) \ 47 | -testPlan AllTests \ 48 | test | $(XCBEAUTIFY) 49 | 50 | test-ci: XCBEAUTIFY 51 | @$(XCODEBUILD) \ 52 | -project $(XCPROJ) \ 53 | -scheme $(XCSCHEME) \ 54 | -testPlan AllTests \ 55 | -resultBundlePath TestResults \ 56 | test | $(XCBEAUTIFY) 57 | 58 | release_build: XCBEAUTIFY 59 | @(export IS_RELEASE_BUILD=1 && $(XCODEBUILD) \ 60 | -project $(XCPROJ) \ 61 | -scheme $(XCSCHEME) \ 62 | -configuration Release | $(XCBEAUTIFY)) 63 | 64 | build_preview_apps: XCBEAUTIFY $(PREVIEW_SCHEMES) 65 | 66 | $(PREVIEW_SCHEMES): XCBEAUTIFY 67 | @$(XCODEBUILD) \ 68 | -project $(XCPROJ) \ 69 | -scheme $@ | $(XCBEAUTIFY) 70 | -------------------------------------------------------------------------------- /Prose/.gitignore: -------------------------------------------------------------------------------- 1 | Prose.xcodeproj/**/*.xcuserdatad/ 2 | -------------------------------------------------------------------------------- /Prose/ConversationFeaturePreview/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Prose/ConversationFeaturePreview/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Prose/ConversationFeaturePreview/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Prose/ConversationFeaturePreview/ConversationFeaturePreview.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Prose/ConversationFeaturePreview/ConversationFeaturePreviewApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import SwiftUI 7 | 8 | @main 9 | struct ConversationFeaturePreviewApp: App { 10 | var body: some Scene { 11 | WindowGroup { 12 | ContentView() 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Prose/ConversationFeaturePreview/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Prose/EditProfileFeaturePreview/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Prose/EditProfileFeaturePreview/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Prose/EditProfileFeaturePreview/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Prose/EditProfileFeaturePreview/EditProfileFeaturePreview.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Prose/EditProfileFeaturePreview/EditProfileFeaturePreviewApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import ComposableArchitecture 7 | @testable import EditProfileFeature 8 | import Mocks 9 | import ProseCore 10 | import SwiftUI 11 | 12 | @main 13 | struct EditProfileFeaturePreviewApp: App { 14 | let store: StoreOf 15 | 16 | init() { 17 | var state = EditProfileReducer.EditProfileState() 18 | state.route = .profile(.init()) 19 | 20 | self.store = Store( 21 | initialState: .mock(state), 22 | reducer: EditProfileReducer(), 23 | prepareDependencies: { 24 | var accountsClient = AccountsClient.noop 25 | accountsClient.client = { _ in .noop } 26 | $0.accountsClient = accountsClient 27 | } 28 | ) 29 | } 30 | 31 | var body: some Scene { 32 | WindowGroup { 33 | EditProfileScreen(store: self.store) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Prose/Prose.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Prose/Prose.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Prose/Prose/AllTests.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "AA58C178-605B-4A55-B48D-7D8B07E8DD8D", 5 | "name" : "Configuration 1", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | "language" : "en", 13 | "region" : "US", 14 | "testTimeoutsEnabled" : true 15 | }, 16 | "testTargets" : [ 17 | { 18 | "skippedTests" : [ 19 | "CredentialsStoreTests" 20 | ], 21 | "target" : { 22 | "containerPath" : "container:ProseLib", 23 | "identifier" : "CredentialsClientTests", 24 | "name" : "CredentialsClientTests" 25 | } 26 | }, 27 | { 28 | "target" : { 29 | "containerPath" : "container:Prose.xcodeproj", 30 | "identifier" : "2CBFFE66287D7B0B00A53992", 31 | "name" : "ProseUITests" 32 | } 33 | }, 34 | { 35 | "target" : { 36 | "containerPath" : "container:ProseLib", 37 | "identifier" : "ProseCoreViewsTests", 38 | "name" : "ProseCoreViewsTests" 39 | } 40 | }, 41 | { 42 | "target" : { 43 | "containerPath" : "container:ProseLib", 44 | "identifier" : "ConversationFeatureTests", 45 | "name" : "ConversationFeatureTests" 46 | } 47 | } 48 | ], 49 | "version" : 1 50 | } 51 | -------------------------------------------------------------------------------- /Prose/Prose/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Foundation 7 | -------------------------------------------------------------------------------- /Prose/Prose/Components/Sidebar/SidebarPartContextComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import SwiftUI 7 | 8 | struct SidebarPartContextComponent: View { 9 | var avatar: String = "avatar-valerian" 10 | var teamName: String = "Crisp" 11 | var statusIcon: Character = "🚀" 12 | var statusMessage: String = "Building new stuff." 13 | 14 | var body: some View { 15 | VStack(spacing: 0) { 16 | Divider() 17 | 18 | HStack(spacing: 12) { 19 | // User avatar 20 | SidebarContextAvatarComponent( 21 | avatar: self.avatar, 22 | status: .online 23 | ) 24 | 25 | // Team name + user status 26 | SidebarContextCurrentComponent( 27 | teamName: self.teamName, 28 | statusIcon: self.statusIcon, 29 | statusMessage: self.statusMessage 30 | ) 31 | .layoutPriority(1) 32 | 33 | Spacer() 34 | 35 | // Quick action button 36 | SidebarContextActionsComponent() 37 | } 38 | .padding(.leading, 20.0) 39 | .padding(.trailing, 14.0) 40 | .frame(maxHeight: 64) 41 | } 42 | .frame(height: 64) 43 | } 44 | } 45 | 46 | struct SidebarPartContextComponent_Previews: PreviewProvider { 47 | static var previews: some View { 48 | SidebarPartContextComponent() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Prose/Prose/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Preferences", 6 | "repositoryURL": "https://github.com/sindresorhus/Preferences.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "ffeaaad1def45d0625720dc1adae3789cd9c167d", 10 | "version": "2.5.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Prose/Prose/Prose.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Prose/Prose/ProseApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import App 7 | import SwiftUI 8 | 9 | @main 10 | struct ProseApp: App { 11 | var body: some Scene { 12 | AppScene() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Prose/Prose/Resources/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Prose/Prose/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Dock@16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "Dock@32 1.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "Dock@32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "Dock@64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "Dock@128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "Dock@256 1.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "Dock@256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "Dock@512 1.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "Dock@512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "Dock@1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Prose/Prose/Resources/Assets.xcassets/AppIcon.appiconset/Dock@1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/Prose/Resources/Assets.xcassets/AppIcon.appiconset/Dock@1024.png -------------------------------------------------------------------------------- /Prose/Prose/Resources/Assets.xcassets/AppIcon.appiconset/Dock@128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/Prose/Resources/Assets.xcassets/AppIcon.appiconset/Dock@128.png -------------------------------------------------------------------------------- /Prose/Prose/Resources/Assets.xcassets/AppIcon.appiconset/Dock@16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/Prose/Resources/Assets.xcassets/AppIcon.appiconset/Dock@16.png -------------------------------------------------------------------------------- /Prose/Prose/Resources/Assets.xcassets/AppIcon.appiconset/Dock@256 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/Prose/Resources/Assets.xcassets/AppIcon.appiconset/Dock@256 1.png -------------------------------------------------------------------------------- /Prose/Prose/Resources/Assets.xcassets/AppIcon.appiconset/Dock@256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/Prose/Resources/Assets.xcassets/AppIcon.appiconset/Dock@256.png -------------------------------------------------------------------------------- /Prose/Prose/Resources/Assets.xcassets/AppIcon.appiconset/Dock@32 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/Prose/Resources/Assets.xcassets/AppIcon.appiconset/Dock@32 1.png -------------------------------------------------------------------------------- /Prose/Prose/Resources/Assets.xcassets/AppIcon.appiconset/Dock@32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/Prose/Resources/Assets.xcassets/AppIcon.appiconset/Dock@32.png -------------------------------------------------------------------------------- /Prose/Prose/Resources/Assets.xcassets/AppIcon.appiconset/Dock@512 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/Prose/Resources/Assets.xcassets/AppIcon.appiconset/Dock@512 1.png -------------------------------------------------------------------------------- /Prose/Prose/Resources/Assets.xcassets/AppIcon.appiconset/Dock@512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/Prose/Resources/Assets.xcassets/AppIcon.appiconset/Dock@512.png -------------------------------------------------------------------------------- /Prose/Prose/Resources/Assets.xcassets/AppIcon.appiconset/Dock@64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/Prose/Resources/Assets.xcassets/AppIcon.appiconset/Dock@64.png -------------------------------------------------------------------------------- /Prose/Prose/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Prose/Prose/Resources/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf2636 2 | \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica-Bold;\f1\fswiss\fcharset0 Helvetica;\f2\fnil\fcharset0 LucidaGrande; 3 | } 4 | {\colortbl;\red255\green255\blue255;} 5 | {\*\expandedcolortbl;;} 6 | {\*\listtable{\list\listtemplateid1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{square\}}{\leveltext\leveltemplateid1\'01\uc0\u9642 ;}{\levelnumbers;}\fi-360\li720\lin720 }{\listname ;}\listid1}} 7 | {\*\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}} 8 | \margl1440\margr1440\vieww9000\viewh8400\viewkind0 9 | \pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0 10 | 11 | \f0\b\fs24 \cf0 Developer: 12 | \f1\b0 Valerian Saliou\ 13 | \ 14 | 15 | \f0\b Open-Source libraries: 16 | \f1\b0 \ 17 | \ 18 | \pard\tx220\tx720\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5492\tx5669\tx6236\tx6803\li720\fi-720\pardirnatural\partightenfactor0 19 | \ls1\ilvl0\cf0 20 | \f2 \uc0\u9642 21 | \f1 Preferences by @sindresorhus} -------------------------------------------------------------------------------- /Prose/Prose/UITests.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "0D940990-E17C-4D24-AEBC-3F70CD687104", 5 | "name" : "Configuration 1", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | "language" : "en", 13 | "region" : "US", 14 | "testTimeoutsEnabled" : true 15 | }, 16 | "testTargets" : [ 17 | { 18 | "target" : { 19 | "containerPath" : "container:Prose.xcodeproj", 20 | "identifier" : "2CBFFE66287D7B0B00A53992", 21 | "name" : "ProseUITests" 22 | } 23 | } 24 | ], 25 | "version" : 1 26 | } 27 | -------------------------------------------------------------------------------- /Prose/Prose/UnitTests.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "AA58C178-605B-4A55-B48D-7D8B07E8DD8D", 5 | "name" : "Configuration 1", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | "language" : "en", 13 | "region" : "US", 14 | "testTimeoutsEnabled" : true 15 | }, 16 | "testTargets" : [ 17 | { 18 | "target" : { 19 | "containerPath" : "container:ProseLib", 20 | "identifier" : "CredentialsClientTests", 21 | "name" : "CredentialsClientTests" 22 | } 23 | }, 24 | { 25 | "target" : { 26 | "containerPath" : "container:ProseLib", 27 | "identifier" : "ProseCoreViewsTests", 28 | "name" : "ProseCoreViewsTests" 29 | } 30 | }, 31 | { 32 | "target" : { 33 | "containerPath" : "container:ProseLib", 34 | "identifier" : "ConversationFeatureTests", 35 | "name" : "ConversationFeatureTests" 36 | } 37 | } 38 | ], 39 | "version" : 1 40 | } 41 | -------------------------------------------------------------------------------- /Prose/ProseLib/.gitignore: -------------------------------------------------------------------------------- 1 | .env.json -------------------------------------------------------------------------------- /Prose/ProseLib/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AddressBookFeature/AddressBookScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import SwiftUI 7 | 8 | public struct AddressBookScreen: View { 9 | public init() {} 10 | 11 | public var body: some View { 12 | Text("Address book") 13 | .frame(maxWidth: .infinity, maxHeight: .infinity) 14 | .unredacted() 15 | .toolbar(content: Toolbar.init) 16 | } 17 | } 18 | 19 | internal struct AddressBookScreen_Previews: PreviewProvider { 20 | private struct Preview: View { 21 | var body: some View { 22 | NavigationView { 23 | Text("Test") 24 | AddressBookScreen() 25 | } 26 | } 27 | } 28 | 29 | static var previews: some View { 30 | Preview() 31 | Preview() 32 | .redacted(reason: .placeholder) 33 | .previewDisplayName("Placeholder") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AddressBookFeature/Target+Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import OSLog 7 | 8 | internal let logger = Logger(subsystem: "org.prose.app", category: "address-book") 9 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AddressBookFeature/Toolbar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import ProseUI 7 | import SwiftUI 8 | 9 | struct Toolbar: ToolbarContent { 10 | var body: some ToolbarContent { 11 | ToolbarItemGroup(placement: .navigation) { 12 | CommonToolbarNavigation() 13 | } 14 | 15 | ToolbarItemGroup { 16 | Self.actions() 17 | 18 | ToolbarDivider() 19 | 20 | CommonToolbarActions() 21 | } 22 | } 23 | 24 | static func actions() -> some View { 25 | Group { 26 | Button { logger.info("Add contact tapped") } label: { 27 | Label("Add contact", systemImage: "person.crop.circle.badge.plus") 28 | } 29 | Button { logger.info("Stack plus tapped") } label: { 30 | Label("Add group", systemImage: "rectangle.stack.badge.plus") 31 | } 32 | 33 | ToolbarDivider() 34 | 35 | Menu { 36 | // TODO: Add actions 37 | Text("TODO") 38 | } label: { 39 | Label("Filter", systemImage: "line.3.horizontal.decrease.circle") 40 | } 41 | } 42 | .unredacted() 43 | } 44 | } 45 | 46 | internal struct Toolbar_Previews: PreviewProvider { 47 | private struct Preview: View { 48 | var body: some View { 49 | HStack { 50 | Toolbar.actions() 51 | } 52 | } 53 | } 54 | 55 | static var previews: some View { 56 | Preview() 57 | Preview() 58 | .redacted(reason: .placeholder) 59 | .previewDisplayName("Placeholder") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/App/Accounts/SelectedAccountReducer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import ComposableArchitecture 7 | import ProseCore 8 | 9 | private extension AppReducer.State { 10 | var selectedAccount: Account? { 11 | self.selectedAccountId.flatMap { 12 | self.availableAccounts[id: $0] 13 | } 14 | } 15 | } 16 | 17 | // Observes changes to the selected account 18 | struct SelectedAccountReducer< 19 | Base: ReducerProtocol 20 | >: ReducerProtocol { 21 | let base: Base 22 | 23 | @Dependency(\.accountsClient) var accounts 24 | 25 | public var body: some ReducerProtocol { 26 | // Make sure that the selected account actually changed and not that the selection was replaced 27 | self.base 28 | .onChange(of: \.selectedAccount) { formerAccount, currentAccount, _, _ in 29 | guard 30 | let formerAccount, 31 | let currentAccount, 32 | formerAccount.jid == currentAccount.jid 33 | else { 34 | return .none 35 | } 36 | 37 | var effects = [EffectTask]() 38 | 39 | if currentAccount.availability != formerAccount.availability { 40 | effects.append(.fireAndForget { 41 | try await self.accounts.client(currentAccount.jid) 42 | .setAvailability(currentAccount.availability, nil) 43 | }) 44 | } 45 | 46 | if currentAccount.settings != formerAccount.settings { 47 | effects.append(.fireAndForget { 48 | try await self.accounts.client(currentAccount.jid) 49 | .saveAccountSettings(currentAccount.settings) 50 | }) 51 | } 52 | 53 | return .merge(effects) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppKit 7 | 8 | final class AppDelegate: NSObject, NSApplicationDelegate { 9 | func applicationDidFinishLaunching(_: Notification) { 10 | NSWindow.allowsAutomaticWindowTabbing = false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/App/Target+Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import OSLog 7 | 8 | internal let logger = Logger(subsystem: "org.prose.app", category: "app") 9 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AppDomain/Account.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Foundation 7 | import ProseCoreFFI 8 | 9 | @dynamicMemberLookup 10 | public struct Account: Hashable, Identifiable { 11 | public var jid: BareJid 12 | public var status: ConnectionStatus 13 | public var settings: AccountSettings 14 | public var profile: UserProfile? 15 | public var contacts: [BareJid: Contact] 16 | public var avatar: URL? 17 | 18 | public var id: BareJid { 19 | self.jid 20 | } 21 | 22 | public init( 23 | jid: BareJid, 24 | status: ConnectionStatus, 25 | settings: AccountSettings, 26 | profile: UserProfile? = nil, 27 | contacts: [BareJid: Contact] = [:], 28 | avatar: URL? = nil 29 | ) { 30 | self.jid = jid 31 | self.status = status 32 | self.settings = settings 33 | self.profile = profile 34 | self.contacts = contacts 35 | self.avatar = avatar 36 | } 37 | } 38 | 39 | public extension Account { 40 | subscript(dynamicMember keyPath: WritableKeyPath) -> T { 41 | get { self.settings[keyPath: keyPath] } 42 | set { self.settings[keyPath: keyPath] = newValue } 43 | } 44 | } 45 | 46 | public extension Account { 47 | var username: String { 48 | if let fullName = self.profile?.fullName { 49 | return fullName 50 | } 51 | if let nickname = self.profile?.nickname { 52 | return nickname 53 | } 54 | return (self.jid.node ?? self.jid.domain) 55 | .split(separator: ".", omittingEmptySubsequences: true) 56 | .joined(separator: " ") 57 | .localizedCapitalized 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AppDomain/ConnectionStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import BareMinimum 7 | import Foundation 8 | 9 | public enum ConnectionStatus: Hashable { 10 | case disconnected 11 | case connecting 12 | case connected 13 | case error(Error) 14 | } 15 | 16 | public extension ConnectionStatus { 17 | var isError: Bool { 18 | if case .error = self { 19 | return true 20 | } 21 | return false 22 | } 23 | } 24 | 25 | public extension ConnectionStatus { 26 | static func == (lhs: Self, rhs: Self) -> Bool { 27 | switch (lhs, rhs) { 28 | case (.disconnected, .disconnected): 29 | return true 30 | case (.connecting, .connecting): 31 | return true 32 | case (.connected, .connected): 33 | return true 34 | case let (.error(lErr), .error(rErr)): 35 | return lErr.isEqual(to: rErr) 36 | case (.disconnected, _), (.connecting, _), (.connected, _), (.error, _): 37 | return false 38 | } 39 | } 40 | 41 | func hash(into hasher: inout Hasher) { 42 | switch self { 43 | case .disconnected: 44 | hasher.combine(1) 45 | case .connecting: 46 | hasher.combine(2) 47 | case .connected: 48 | hasher.combine(3) 49 | case let .error(error): 50 | hasher.combine(EquatableError(error)) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AppDomain/Connectivity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | public enum Connectivity: Hashable { 7 | case online 8 | case offline 9 | } 10 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AppDomain/Credentials.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Foundation 7 | import ProseCoreFFI 8 | 9 | public struct Credentials: Hashable { 10 | public let jid: BareJid 11 | public let password: String 12 | 13 | public init( 14 | jid: BareJid, 15 | password: String 16 | ) { 17 | self.jid = jid 18 | self.password = password 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AppDomain/Exports.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | @_exported import ProseCoreFFI 7 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AppDomain/ProseCoreExtensions/Availability.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import ProseCoreFFI 7 | 8 | public extension Availability { 9 | static let selectableCases: [Availability] = [ 10 | .available, 11 | .away, 12 | .doNotDisturb, 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AppDomain/ProseCoreExtensions/BareJid.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Foundation 7 | import ProseCoreFFI 8 | 9 | extension BareJid: RawRepresentable { 10 | public init?(rawValue: String) { 11 | guard let jid = try? parseJid(jid: rawValue) else { 12 | return nil 13 | } 14 | self = jid 15 | } 16 | 17 | public var rawValue: String { 18 | formatJid(jid: self) 19 | } 20 | } 21 | 22 | extension BareJid: Codable { 23 | public init(from decoder: Decoder) throws { 24 | let container = try decoder.singleValueContainer() 25 | self = try parseJid(jid: container.decode(String.self)) 26 | } 27 | 28 | public func encode(to encoder: Encoder) throws { 29 | var container = encoder.singleValueContainer() 30 | try container.encode(self.rawValue) 31 | } 32 | } 33 | 34 | #if DEBUG 35 | /// Use this for testing only, since we might crash otherwise. 36 | extension BareJid: ExpressibleByStringLiteral { 37 | public init(stringLiteral value: StringLiteralType) { 38 | do { 39 | try self = parseJid(jid: value) 40 | } catch { 41 | fatalError(error.localizedDescription) 42 | } 43 | } 44 | } 45 | #endif 46 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AppDomain/ProseCoreExtensions/Contact.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import ProseCoreFFI 7 | 8 | extension Contact: Comparable { 9 | public static func < (lhs: Contact, rhs: Contact) -> Bool { 10 | switch lhs.name.localizedStandardCompare(rhs.name) { 11 | case .orderedAscending: 12 | return true 13 | case .orderedDescending: 14 | return false 15 | case .orderedSame: 16 | return lhs.jid.rawValue.caseInsensitiveCompare(rhs.jid.rawValue) == .orderedAscending 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AppDomain/ProseCoreExtensions/FullJid.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Foundation 7 | import ProseCoreFFI 8 | 9 | public extension FullJid { 10 | var bareJid: BareJid { 11 | BareJid(node: self.node, domain: self.domain) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AppDomain/ProseCoreExtensions/Message.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Foundation 7 | import ProseCoreFFI 8 | 9 | extension Message: Identifiable {} 10 | 11 | extension Message: Encodable { 12 | enum CodingKeys: String, CodingKey { 13 | case id 14 | case type 15 | case date 16 | case content 17 | case from 18 | case reactions 19 | case metas 20 | } 21 | 22 | enum UserCodingKeys: String, CodingKey { 23 | case jid, name 24 | } 25 | 26 | enum MetaCodingKeys: String, CodingKey { 27 | case encrypted, edited 28 | } 29 | 30 | fileprivate static var dateFormatter: ISO8601DateFormatter = { 31 | let formatter = ISO8601DateFormatter() 32 | formatter.formatOptions.insert(.withFractionalSeconds) 33 | return formatter 34 | }() 35 | 36 | public func encode(to encoder: Encoder) throws { 37 | var container = encoder.container(keyedBy: CodingKeys.self) 38 | 39 | try container.encode(self.id, forKey: .id) 40 | try container.encode("text", forKey: .type) 41 | try container.encode(Self.dateFormatter.string(from: self.timestamp), forKey: .date) 42 | try container.encode(self.body, forKey: .content) 43 | try container.encode(self.from, forKey: .from) 44 | 45 | do { 46 | var container = container.nestedContainer(keyedBy: MetaCodingKeys.self, forKey: .metas) 47 | try container.encode(self.isEdited, forKey: .edited) 48 | try container.encode(false, forKey: .encrypted) 49 | } 50 | 51 | try container.encode(self.reactions, forKey: .reactions) 52 | } 53 | } 54 | 55 | extension Reaction: Encodable { 56 | enum CodingKeys: String, CodingKey { 57 | case emoji = "reaction" 58 | case from = "authors" 59 | } 60 | 61 | public func encode(to encoder: Encoder) throws { 62 | var container = encoder.container(keyedBy: CodingKeys.self) 63 | try container.encode(self.emoji, forKey: .emoji) 64 | try container.encode(self.from, forKey: .from) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AppDomain/ProseCoreExtensions/UserProfile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import ProseCoreFFI 7 | 8 | public extension UserProfile { 9 | init() { 10 | self = .init( 11 | fullName: nil, 12 | nickname: nil, 13 | org: nil, 14 | title: nil, 15 | email: nil, 16 | tel: nil, 17 | url: nil, 18 | address: nil 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AppDomain/UserInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Foundation 7 | import ProseCoreFFI 8 | 9 | public struct UserInfo: Equatable { 10 | public var jid: BareJid 11 | public var name: String 12 | public var avatar: URL? 13 | 14 | public init(jid: BareJid, name: String, avatar: URL? = nil) { 15 | self.jid = jid 16 | self.name = name 17 | self.avatar = avatar 18 | } 19 | } 20 | 21 | public extension UserInfo { 22 | init(contact: Contact) { 23 | self.jid = contact.jid 24 | self.name = contact.name 25 | self.avatar = contact.avatar 26 | } 27 | } 28 | 29 | extension UserInfo: Encodable {} 30 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AppLocalization/AppDomain+i18n.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | 8 | public extension Availability { 9 | var localizedDescription: String { 10 | switch self { 11 | case .available: 12 | return L10n.Availability.available 13 | case .unavailable: 14 | return L10n.Availability.unavailable 15 | case .doNotDisturb: 16 | return L10n.Availability.doNotDisturb 17 | case .away: 18 | return L10n.Availability.away 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AppLocalization/Generated/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/ProseLib/Sources/AppLocalization/Generated/.gitkeep -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AppLocalization/Resources/en.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | content.message_bar.typing 6 | 7 | NSStringLocalizedFormatKey 8 | %#@typing@ 9 | typing 10 | 11 | NSStringFormatSpecTypeKey 12 | NSStringPluralRuleType 13 | NSStringFormatValueTypeKey 14 | u 15 | one 16 | is typing… 17 | other 18 | are typing… 19 | 20 | 21 | content.message_bar.others 22 | 23 | NSStringLocalizedFormatKey 24 | %#@others@ 25 | others 26 | 27 | NSStringFormatSpecTypeKey 28 | NSStringPluralRuleType 29 | NSStringFormatValueTypeKey 30 | u 31 | one 32 | %u other 33 | other 34 | %u others 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AppLocalization/String+Markdown.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import SwiftUI 7 | 8 | public extension String { 9 | /// Convert a `String` to a Markdown `AttributedString`. 10 | /// 11 | /// SwiftUI automatically renders Markdown, but only for static strings used in the `Text` 12 | /// initializer. 13 | /// If a string is localized, it doesn't render Markdown. 14 | /// That's why we have to use: 15 | /// 16 | /// ```swift 17 | /// Text(L10n.myString.asMarkdown) 18 | /// ``` 19 | var asMarkdown: AttributedString { 20 | do { 21 | return try AttributedString( 22 | markdown: self, 23 | options: AttributedString.MarkdownParsingOptions( 24 | // Render `\n`s as new lines. 25 | interpretedSyntax: .inlineOnlyPreservingWhitespace 26 | ) 27 | ) 28 | } catch { 29 | logger.warning("Error parsing markdown: \(error.localizedDescription)") 30 | return AttributedString(self) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AppLocalization/Target+Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import OSLog 7 | 8 | internal let logger = Logger(subsystem: "org.prose.app", category: "localization") 9 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Assets/Generated/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/ProseLib/Sources/Assets/Generated/.gitkeep -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Assets/ImageAsset+URL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Foundation 7 | 8 | /// `PreviewAssets.ImageAsset.customURL` creates a custom URL for assets, using the `asset` scheme. 9 | /// This function tries to parse an URL to check if it's an asset. 10 | /// 11 | /// ```swift 12 | /// let url: URL? = URL(string: "asset://path/to.bundle?imageName=some-image-asset") 13 | /// if let assetData = url.flatMap(Assets.assetData) { 14 | /// Image(assetData.0, bundle: assetData.1).resizable() 15 | /// } else { 16 | /// AsyncImage(url: url) { image in 17 | /// image.resizable() 18 | /// } placeholder: { 19 | /// Color.primary.opacity(0.125) 20 | /// } 21 | /// } 22 | /// ``` 23 | /// 24 | /// - Returns: If the URL references an asset, it returns the asset name (potentially namespaced) 25 | /// and the optional `Bundle`. `nil` otherwise or if any error occured. 26 | public func assetData(from url: URL) -> (String, Bundle?)? { 27 | guard url.scheme == "asset" else { return nil } 28 | guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) 29 | else { return nil } 30 | 31 | let query = components.queryItems ?? [] 32 | components.queryItems = nil 33 | 34 | guard let imageName = query.first(where: { $0.name == "imageName" })?.value else { return nil } 35 | 36 | return (imageName, Bundle(path: components.path)) 37 | } 38 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Assets/Resources/Colors.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Assets/Resources/Colors.xcassets/background/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Assets/Resources/Colors.xcassets/background/message.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.000", 27 | "green" : "0.000", 28 | "red" : "0.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Assets/Resources/Colors.xcassets/border/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Assets/Resources/Colors.xcassets/border/primary.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "extended-gray", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "white" : "0.514" 9 | } 10 | }, 11 | "idiom" : "universal" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Assets/Resources/Colors.xcassets/border/secondary.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "extended-gray", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "white" : "0.741" 9 | } 10 | }, 11 | "idiom" : "universal" 12 | }, 13 | { 14 | "appearances" : [ 15 | { 16 | "appearance" : "luminosity", 17 | "value" : "dark" 18 | } 19 | ], 20 | "color" : { 21 | "color-space" : "extended-gray", 22 | "components" : { 23 | "alpha" : "0.750", 24 | "white" : "0.502" 25 | } 26 | }, 27 | "idiom" : "universal" 28 | } 29 | ], 30 | "info" : { 31 | "author" : "xcode", 32 | "version" : 1 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Assets/Resources/Colors.xcassets/border/tertiary.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "extended-gray", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "white" : "0.902" 9 | } 10 | }, 11 | "idiom" : "universal" 12 | }, 13 | { 14 | "appearances" : [ 15 | { 16 | "appearance" : "luminosity", 17 | "value" : "dark" 18 | } 19 | ], 20 | "color" : { 21 | "color-space" : "extended-gray", 22 | "components" : { 23 | "alpha" : "0.750", 24 | "white" : "0.376" 25 | } 26 | }, 27 | "idiom" : "universal" 28 | } 29 | ], 30 | "info" : { 31 | "author" : "xcode", 32 | "version" : 1 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Assets/Resources/Colors.xcassets/border/tertiaryLight.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "extended-gray", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "white" : "0.827" 9 | } 10 | }, 11 | "idiom" : "universal" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Assets/Resources/Colors.xcassets/state/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Assets/Resources/Colors.xcassets/state/coolGrey.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x7B", 9 | "green" : "0x61", 10 | "red" : "0x54" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Assets/Resources/Colors.xcassets/state/green.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.494", 9 | "green" : "0.671", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Assets/Resources/Colors.xcassets/state/greenLight.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.329", 9 | "green" : "0.780", 10 | "red" : "0.380" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Assets/Resources/Colors.xcassets/state/grey.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "extended-gray", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "white" : "0.475" 9 | } 10 | }, 11 | "idiom" : "universal" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Assets/Resources/Colors.xcassets/state/greyLight.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "extended-gray", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "white" : "0.733" 9 | } 10 | }, 11 | "idiom" : "universal" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Assets/Resources/Colors.xcassets/text/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Assets/Resources/Colors.xcassets/text/primary.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.149", 9 | "green" : "0.145", 10 | "red" : "0.137" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "platform" : "osx", 24 | "reference" : "labelColor" 25 | }, 26 | "idiom" : "universal" 27 | } 28 | ], 29 | "info" : { 30 | "author" : "xcode", 31 | "version" : 1 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Assets/Resources/Colors.xcassets/text/primaryLight.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "extended-gray", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "white" : "0.302" 9 | } 10 | }, 11 | "idiom" : "universal" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Assets/Resources/Colors.xcassets/text/secondary.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "extended-gray", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "white" : "0.475" 9 | } 10 | }, 11 | "idiom" : "universal" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Assets/Resources/Colors.xcassets/text/secondaryLight.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "extended-gray", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "white" : "0.502" 9 | } 10 | }, 11 | "idiom" : "universal" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Assets/Resources/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Assets/Resources/Images.xcassets/platform-logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "macos-logo.svg", 5 | "idiom" : "mac" 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 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Assets/Target+Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import OSLog 7 | 8 | internal let logger = Logger(subsystem: "org.prose.app", category: "assets") 9 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AuthenticationFeature/MFA/Authentication+TemporaryFix.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppLocalization 7 | import Foundation 8 | 9 | public enum MFAError: Error, Equatable { 10 | case badCode 11 | } 12 | 13 | extension MFAError: LocalizedError { 14 | public var errorDescription: String? { 15 | switch self { 16 | case .badCode: 17 | return "Bad code" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AuthenticationFeature/MFA/MFAView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import ComposableArchitecture 7 | import CredentialsClient 8 | import SwiftUI 9 | 10 | // Let's leave this here as-is until we have MFA server-side support 11 | 12 | // struct MFAView: View { 13 | // typealias State = MFAState 14 | // typealias Action = MFAAction 15 | // 16 | // let store: Store 17 | // private var actions: ViewStore { ViewStore(self.store.stateless) } 18 | // 19 | // var body: some View { 20 | // SwitchStore(self.store) { 21 | // CaseLet( 22 | // state: CasePath(State.sixDigits).extract(from:), 23 | // action: Action.sixDigits, 24 | // then: MFA6DigitsView.init(store:) 25 | // ) 26 | // } 27 | // } 28 | // } 29 | // 30 | //// MARK: - The Composable Architecture 31 | // 32 | //// MARK: Reducer 33 | // 34 | // struct AuthenticationEnvironment { 35 | // var proseClient: ProseClient 36 | // var credentials: CredentialsClient 37 | // var mainQueue: AnySchedulerOf 38 | // } 39 | // 40 | // let mfaReducer: AnyReducer< 41 | // MFAState, 42 | // MFAAction, 43 | // AuthenticationEnvironment 44 | // > = AnyReducer.combine([ 45 | // mfa6DigitsReducer.pullback( 46 | // state: CasePath(MFAState.sixDigits), 47 | // action: CasePath(MFAAction.sixDigits), 48 | // environment: { $0 } 49 | // ), 50 | // AnyReducer { _, action, _ in 51 | // switch action { 52 | // case let .sixDigits(.verifyOneTimeCodeResult(.success(route))): 53 | // return EffectTask(value: .didPassChallenge(next: route)) 54 | // 55 | // default: 56 | // break 57 | // } 58 | // 59 | // return .none 60 | // }, 61 | // ]) 62 | // 63 | //// MARK: State 64 | // 65 | // public enum MFAState: Equatable { 66 | // case sixDigits(MFA6DigitsState) 67 | // } 68 | // 69 | //// MARK: Actions 70 | // 71 | // public enum MFAAction: Equatable { 72 | // case didPassChallenge(next: Authentication.Route) 73 | // case sixDigits(MFA6DigitsAction) 74 | // } 75 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AuthenticationFeature/Profile/ProfileView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppLocalization 7 | import ComposableArchitecture 8 | import SwiftUI 9 | 10 | struct ProfileView: View { 11 | let store: StoreOf 12 | 13 | @ObservedObject var viewStore: ViewStoreOf 14 | @FocusState private var focusedField: ProfileReducer.Field? 15 | 16 | init(store: StoreOf) { 17 | self.store = store 18 | self.viewStore = ViewStore(store) 19 | } 20 | 21 | var body: some View { 22 | VStack(spacing: 32) { 23 | VStack { 24 | TextField( 25 | L10n.Authentication.Profile.Form.FullName.placeholder, 26 | text: self.viewStore.binding(\.$fullName) 27 | ) 28 | .textContentType(.username) 29 | .focused(self.$focusedField, equals: .fullName) 30 | .onSubmit { self.viewStore.send(.fieldSubmitted(.fullName)) } 31 | 32 | TextField( 33 | L10n.Authentication.Profile.Form.Title.placeholder, 34 | text: self.viewStore.binding(\.$title) 35 | ) 36 | .focused(self.$focusedField, equals: .title) 37 | .onSubmit { self.viewStore.send(.fieldSubmitted(.title)) } 38 | }.disabled(self.viewStore.isLoading) 39 | 40 | Button(action: { self.viewStore.send(.submitButtonTapped) }) { 41 | Text( 42 | self.viewStore.isLoading 43 | ? L10n.Authentication.Profile.Form.cancel 44 | : L10n.Authentication.Profile.Form.save 45 | ) 46 | .frame(minWidth: 196) 47 | } 48 | .overlay(alignment: .leading) { 49 | if self.viewStore.isLoading { 50 | ProgressView() 51 | .scaleEffect(0.5, anchor: .center) 52 | } 53 | } 54 | } 55 | .synchronize(self.viewStore.binding(\.$focusedField), self.$focusedField) 56 | .alert(self.store.scope(state: \.alert), dismiss: .alertDismissed) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AuthenticationFeature/Target+Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import OSLog 7 | 8 | internal let logger = Logger(subsystem: "org.prose.app", category: "authentication") 9 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/AuthenticationFeature/UserData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | import Foundation 8 | 9 | public struct UserData: Equatable { 10 | let credentials: Credentials 11 | var avatar: URL? 12 | var profile: UserProfile? 13 | } 14 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ConnectivityClient/ConnectivityClient+Live.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | import Combine 8 | import ComposableArchitecture 9 | import Network 10 | 11 | extension ConnectivityClient { 12 | static let live: ConnectivityClient = { 13 | let connectivitySubject = CurrentValueSubject(Connectivity.online) 14 | let pathMonitorQueue = DispatchQueue(label: "org.prose.pathmonitor") 15 | let pathMonitor = NWPathMonitor() 16 | 17 | pathMonitor.pathUpdateHandler = { path in 18 | switch path.status { 19 | case .satisfied: 20 | connectivitySubject.send(.online) 21 | case .unsatisfied, .requiresConnection: 22 | connectivitySubject.send(.offline) 23 | @unknown default: 24 | connectivitySubject.send(.offline) 25 | } 26 | } 27 | 28 | pathMonitor.start(queue: pathMonitorQueue) 29 | 30 | return ConnectivityClient(connectivity: { 31 | AsyncStream(connectivitySubject.removeDuplicates().values) 32 | }) 33 | }() 34 | } 35 | 36 | extension ConnectivityClient: DependencyKey { 37 | public static var liveValue = ConnectivityClient.live 38 | 39 | public static var previewValue = ConnectivityClient( 40 | connectivity: { 41 | AsyncStream(unfolding: { .online }) 42 | } 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ConnectivityClient/ConnectivityClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | import ComposableArchitecture 8 | 9 | public struct ConnectivityClient { 10 | public var connectivity: () -> AsyncStream 11 | } 12 | 13 | public extension DependencyValues { 14 | var connectivityClient: ConnectivityClient { 15 | get { self[ConnectivityClient.self] } 16 | set { self[ConnectivityClient.self] = newValue } 17 | } 18 | } 19 | 20 | extension ConnectivityClient: TestDependencyKey { 21 | public static var testValue = ConnectivityClient( 22 | connectivity: unimplemented("\(Self.self).connectivity") 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ConversationFeature/Chat/EditMessageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppLocalization 7 | import ComposableArchitecture 8 | import ProseUI 9 | import SwiftUI 10 | 11 | struct EditMessageView: View { 12 | let store: StoreOf 13 | let viewStore: ViewStoreOf 14 | 15 | init(store: StoreOf) { 16 | self.store = store 17 | self.viewStore = ViewStore(store) 18 | } 19 | 20 | var body: some View { 21 | VStack(alignment: .leading) { 22 | Text(L10n.Content.EditMessage.title) 23 | .font(.title2.bold()) 24 | MessageField(store: self.store.scope( 25 | state: \.messageField, 26 | action: EditMessageReducer.Action.messageField 27 | )) 28 | } 29 | .safeAreaInset(edge: .bottom, spacing: 12) { 30 | HStack { 31 | Group { 32 | Button { print("NOT IMPLEMENTED") } label: { 33 | Image(systemName: "paperclip") 34 | } 35 | 36 | Button { self.viewStore.send(.emojiButtonTapped) } label: { 37 | Image(systemName: "face.smiling") 38 | } 39 | .reactionPicker( 40 | store: self.store.scope(state: \.emojiPicker), 41 | action: EditMessageReducer.Action.emojiPicker, 42 | dismiss: .emojiPickerDismissed 43 | ) 44 | } 45 | .buttonStyle(.plain) 46 | .font(MessageBar.buttonsFont) 47 | Spacer() 48 | WithViewStore(self.store.scope(state: \.childState.isConfirmButtonEnabled)) { viewStore in 49 | Button(L10n.Content.EditMessage.CancelAction.title, role: .cancel) { 50 | viewStore.send(.cancelTapped) 51 | } 52 | .buttonStyle(.bordered) 53 | Button(L10n.Content.EditMessage.ConfirmAction.title) { viewStore.send(.confirmTapped) } 54 | .buttonStyle(.borderedProminent) 55 | .disabled(!viewStore.state) 56 | } 57 | } 58 | .controlSize(.large) 59 | } 60 | .padding() 61 | .frame(width: 500) 62 | .frame(minHeight: 200) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ConversationFeature/Chat/MessageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | import Assets 8 | import ProseUI 9 | import SwiftUI 10 | 11 | public struct MessageView: View { 12 | let model: Message 13 | 14 | public init(model: Message) { 15 | self.model = model 16 | } 17 | 18 | public var body: some View { 19 | HStack(alignment: .top, spacing: 12) { 20 | Avatar(.placeholder, size: 32) 21 | 22 | VStack(alignment: .leading, spacing: 3) { 23 | HStack(alignment: .firstTextBaseline) { 24 | Text(self.model.from.rawValue) 25 | .font(.system(size: 13).bold()) 26 | .foregroundColor(Colors.Text.primary.color) 27 | 28 | Text(self.model.timestamp, format: .relative(presentation: .numeric)) 29 | .font(.system(size: 11.5)) 30 | .foregroundColor(Colors.Text.secondary.color) 31 | } 32 | 33 | Text(self.model.body) 34 | .font(.system(size: 12.5)) 35 | .fontWeight(.regular) 36 | .foregroundColor(Colors.Text.primary.color) 37 | } 38 | .textSelection(.enabled) 39 | 40 | Spacer() 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ConversationFeature/Chat/ReactionPickerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import ComposableArchitecture 7 | import ProseUI 8 | import SwiftUI 9 | 10 | final class ReactionPickerView: NSHostingView< 11 | ModifiedContent> 12 | > { 13 | typealias Content = ModifiedContent> 14 | 15 | var store: Store { 16 | didSet { 17 | self.rootView = Self.body(store: self.store, action: self.action, dismiss: self.dismiss) 18 | } 19 | } 20 | 21 | let action: CasePath 22 | let dismiss: Action 23 | 24 | init( 25 | store: Store, 26 | action: CasePath, 27 | dismiss: Action 28 | ) { 29 | self.store = store 30 | self.action = action 31 | self.dismiss = dismiss 32 | 33 | super.init(rootView: Self.body(store: store, action: action, dismiss: dismiss)) 34 | } 35 | 36 | @MainActor required init(rootView _: Content) { 37 | fatalError("init(rootView:) has not been implemented") 38 | } 39 | 40 | @available(*, unavailable) 41 | required init?(coder _: NSCoder) { 42 | fatalError("init(coder:) has not been implemented") 43 | } 44 | 45 | static func body( 46 | store: Store, 47 | action: CasePath, 48 | dismiss: Action 49 | ) -> Content { 50 | AnyView(Color.clear) 51 | .modifier(ReactionPickerPopup(store: store, action: action, dismiss: dismiss)) 52 | } 53 | } 54 | 55 | struct ReactionPickerPopup: ViewModifier { 56 | let store: Store 57 | let action: CasePath 58 | let dismiss: Action 59 | 60 | func body(content: Content) -> some View { 61 | content 62 | .reactionPicker( 63 | store: self.store.scope(state: { $0.pickerState }), 64 | action: self.action.embed(_:), 65 | dismiss: self.dismiss 66 | ) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ConversationFeature/ConversationScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import ComposableArchitecture 7 | import IdentifiedCollections 8 | import PasteboardClient 9 | import SwiftUI 10 | import Toolbox 11 | 12 | public struct ConversationScreen: View { 13 | private let store: StoreOf 14 | private var actions: ViewStore 15 | 16 | public init(store: StoreOf) { 17 | self.store = store 18 | self.actions = ViewStore(self.store.stateless) 19 | } 20 | 21 | public var body: some View { 22 | Chat(store: self.store.scope(state: \.chat, action: ConversationScreenReducer.Action.chat)) 23 | .safeAreaInset(edge: .bottom, spacing: 0) { 24 | MessageBar( 25 | store: self.store 26 | .scope(state: \.messageBar, action: ConversationScreenReducer.Action.messageBar) 27 | ) 28 | // Make footer have a higher priority, to be accessible over the scroll view 29 | .accessibilitySortPriority(1) 30 | } 31 | .accessibilityIdentifier("ChatWebView") 32 | .accessibilityElement(children: .contain) 33 | .onAppear { self.actions.send(.onAppear) } 34 | .onDisappear { self.actions.send(.onDisappear) } 35 | .safeAreaInset(edge: .trailing, spacing: 0) { 36 | WithViewStore(self.store.scope(state: \.toolbar.childState.isShowingInfo)) { showingInfo in 37 | HStack(spacing: 0) { 38 | ConversationInfoView( 39 | store: self.store.scope(state: \.info, action: ConversationScreenReducer.Action.info) 40 | ).frame(width: 256) 41 | } 42 | .frame(width: showingInfo.state ? 256 : 0, alignment: .leading) 43 | .clipped() 44 | } 45 | } 46 | .toolbar { 47 | Toolbar( 48 | store: self.store 49 | .scope(state: \.toolbar, action: ConversationScreenReducer.Action.toolbar) 50 | ) 51 | } 52 | // Hide the navigation title so that our contact's name and availability indicator can act 53 | // as the title instead. 54 | .navigationTitle("") 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ConversationFeature/InfoSidebar/Views/ActionRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Assets 7 | import SwiftUI 8 | 9 | struct ActionRow: View { 10 | let name: String 11 | var deployTo: Bool = false 12 | let action: () -> Void 13 | 14 | var body: some View { 15 | Button(action: self.action) { 16 | HStack(spacing: 8) { 17 | Text(self.name) 18 | .fontWeight(.medium) 19 | 20 | Spacer() 21 | 22 | if self.deployTo { 23 | Image(systemName: "chevron.right") 24 | .font(.system(size: 10)) 25 | .foregroundColor(Colors.Text.primary.color) 26 | } 27 | } 28 | // Make hit box full width 29 | .contentShape([.interaction], Rectangle()) 30 | } 31 | .buttonStyle(.plain) 32 | } 33 | } 34 | 35 | struct ActionRow_Previews: PreviewProvider { 36 | static var previews: some View { 37 | ActionRow( 38 | name: "View full profile", 39 | deployTo: true, 40 | action: {} 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ConversationFeature/InfoSidebar/Views/EntryRowLabelStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Assets 7 | import SwiftUI 8 | 9 | struct EntryRowLabelStyle: LabelStyle { 10 | static let iconFrameMinWidth: CGFloat = 16 11 | 12 | func makeBody(configuration: Configuration) -> some View { 13 | HStack(alignment: .firstTextBaseline, spacing: 8) { 14 | configuration.icon 15 | .foregroundColor(Colors.State.grey.color) 16 | .frame(width: Self.iconFrameMinWidth, alignment: .center) 17 | 18 | configuration.title 19 | .foregroundColor(Colors.Text.primary.color) 20 | } 21 | .frame(maxWidth: .infinity, alignment: .leading) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ConversationFeature/README.md: -------------------------------------------------------------------------------- 1 | Mind you that SwiftUI's `onAppear` hooks will not be called when a different conversation is 2 | selected. SwiftUI is reusing the `ConversationScreen` so we have to call it ourselves. For this 3 | reason you'll see reducers send .onAppear and .onDisappear to their child reducers, e.g. in 4 | `ConversationScreenReducer`… 5 | 6 | ```swift 7 | case .onAppear: 8 | return .merge( 9 | // … 10 | MessageBarReducer().reduce(into: &state.messageBar, action: .onAppear) 11 | .map(Action.messageBar) 12 | ) 13 | ``` 14 | 15 | `MainScreenReducer` is responsible for initiating this mechanism. 16 | 17 | Let's have a uniform symmetry here and send .onAppear to child reducers last and send .onDisappear 18 | first. 19 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ConversationFeature/Target+Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import OSLog 7 | 8 | internal let logger = Logger(subsystem: "org.prose.app", category: "conversation") 9 | internal let jsLogger = Logger(subsystem: "org.prose.app", category: "js-ffi") 10 | internal let signposter = OSSignposter(logger: logger) 11 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ConversationFeature/Toolbar/Toolbar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | import ComposableArchitecture 8 | import ProseUI 9 | import SwiftUI 10 | 11 | struct Toolbar: ToolbarContent { 12 | struct ViewState: Equatable { 13 | var contact: Contact 14 | var isShowingInfo: Bool 15 | } 16 | 17 | @ObservedObject var viewStore: ViewStore 18 | 19 | init(store: StoreOf) { 20 | self.viewStore = ViewStore(store.scope(state: ViewState.init)) 21 | } 22 | 23 | var body: some ToolbarContent { 24 | ToolbarItemGroup(placement: .navigation) { 25 | CommonToolbarNavigation() 26 | 27 | ContentCommonNameStatusComponent( 28 | name: self.viewStore.contact.name, 29 | status: self.viewStore.contact.availability 30 | ) 31 | .padding(.horizontal, 8) 32 | } 33 | 34 | ToolbarItemGroup { 35 | ToolbarSecurity( 36 | jid: self.viewStore.contact.jid, 37 | isVerified: false 38 | ) 39 | 40 | ToolbarDivider() 41 | 42 | Button { self.viewStore.send(.startVideoCallTapped) } label: { 43 | Label("Video", systemImage: "video") 44 | } 45 | .disabled(true) 46 | 47 | Toggle(isOn: self.viewStore.binding( 48 | get: \.isShowingInfo, 49 | send: ToolbarReducer.Action.toggleInfoButtonTapped 50 | ).animation()) { 51 | Label("Info", systemImage: "info.circle") 52 | } 53 | 54 | ToolbarDivider() 55 | 56 | CommonToolbarActions() 57 | } 58 | } 59 | } 60 | 61 | extension Toolbar.ViewState { 62 | init(_ state: ToolbarReducer.State) { 63 | self.contact = state.contact 64 | self.isShowingInfo = state.isShowingInfo 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ConversationFeature/Toolbar/ToolbarReducer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | import ComposableArchitecture 8 | 9 | public struct ToolbarReducer: ReducerProtocol { 10 | public typealias State = ChatSessionState 11 | 12 | public struct ToolbarState: Equatable { 13 | var isShowingInfo = false 14 | } 15 | 16 | public enum Action: Equatable { 17 | case startVideoCallTapped 18 | case toggleInfoButtonTapped 19 | } 20 | 21 | public init() {} 22 | 23 | public var body: some ReducerProtocol { 24 | Reduce { state, action in 25 | switch action { 26 | case .startVideoCallTapped: 27 | logger.info("Start video call tapped") 28 | return .none 29 | 30 | case .toggleInfoButtonTapped: 31 | state.isShowingInfo.toggle() 32 | return .none 33 | } 34 | } 35 | } 36 | } 37 | 38 | extension ToolbarReducer.State { 39 | var contact: Contact { 40 | if let contact = self.userInfos[self.chatId] { 41 | return contact 42 | } 43 | return Contact( 44 | jid: self.chatId, 45 | name: self.chatId.rawValue, 46 | avatar: nil, 47 | availability: .unavailable, 48 | status: nil, 49 | groups: [] 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ConversationFeature/Toolbar/ToolbarSecurity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | import Assets 8 | import SwiftUI 9 | 10 | /// Separated as its own view as we might need to reuse it someday. 11 | struct ToolbarSecurity: View { 12 | let jid: BareJid 13 | let isVerified: Bool 14 | 15 | var body: some View { 16 | HStack(alignment: .firstTextBaseline, spacing: 4) { 17 | Image(systemName: self.isVerified ? "checkmark.seal.fill" : "xmark.seal.fill") 18 | .foregroundColor(self.isVerified ? Colors.State.green.color : .red) 19 | .accessibilityElement() 20 | .accessibilityLabel(self.isVerified ? "Verified" : "Not verified") 21 | .accessibilitySortPriority(1) 22 | 23 | Text(verbatim: self.jid.rawValue) 24 | .foregroundColor(Colors.Text.secondary.color) 25 | .accessibilitySortPriority(2) 26 | } 27 | .padding(.horizontal, 8) 28 | .accessibilityElement(children: .combine) 29 | } 30 | } 31 | 32 | #if DEBUG 33 | struct ToolbarSecurity_Previews: PreviewProvider { 34 | static var previews: some View { 35 | VStack(alignment: .leading) { 36 | ToolbarSecurity( 37 | jid: "valerian@prose.org", 38 | isVerified: true 39 | ) 40 | ToolbarSecurity( 41 | jid: "valerian@prose.org", 42 | isVerified: false 43 | ) 44 | ToolbarSecurity( 45 | jid: "valerian@prose.org", 46 | isVerified: false 47 | ) 48 | .redacted(reason: .placeholder) 49 | .previewDisplayName("Placeholder") 50 | } 51 | .previewLayout(.sizeThatFits) 52 | } 53 | } 54 | #endif 55 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/CredentialsClient/CredentialsClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | import ComposableArchitecture 8 | 9 | public struct CredentialsClient { 10 | public var loadCredentials: (_ jid: BareJid) throws -> Credentials? 11 | public var save: (_ credentials: Credentials) throws -> Void 12 | public var deleteCredentials: (_ jid: BareJid) throws -> Void 13 | } 14 | 15 | public extension DependencyValues { 16 | var credentialsClient: CredentialsClient { 17 | get { self[CredentialsClient.self] } 18 | set { self[CredentialsClient.self] = newValue } 19 | } 20 | } 21 | 22 | extension CredentialsClient: TestDependencyKey { 23 | public static var testValue = CredentialsClient( 24 | loadCredentials: unimplemented("\(Self.self).loadCredentials"), 25 | save: unimplemented("\(Self.self).save"), 26 | deleteCredentials: unimplemented("\(Self.self).deleteCredentials") 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/CredentialsClient/Target+Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import OSLog 7 | 8 | internal let logger = Logger(subsystem: "org.prose.app", category: "credentials-client") 9 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/EditProfileFeature/AuthenticationReducer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppLocalization 7 | import ComposableArchitecture 8 | 9 | public struct AuthenticationReducer: ReducerProtocol { 10 | public struct State: Equatable { 11 | var recoveryEmail = "baptiste@jamin.me" 12 | var recoveryPhone = "+33631893345" 13 | var isMfaEnabled = false 14 | 15 | var mfaStateLabel: String { 16 | self.isMfaEnabled 17 | ? L10n.EditProfile.Authentication.MfaStatus.StateEnabled.label 18 | : L10n.EditProfile.Authentication.MfaStatus.StateDisabled.label 19 | } 20 | 21 | public init() {} 22 | } 23 | 24 | public enum Action: Equatable, BindableAction { 25 | case changePasswordTapped 26 | case editRecoveryEmailTapped 27 | case disableMFATapped 28 | case editRecoveryPhoneTapped 29 | case binding(BindingAction) 30 | } 31 | 32 | public init() {} 33 | 34 | public var body: some ReducerProtocol { 35 | BindingReducer() 36 | self.core 37 | } 38 | 39 | @ReducerBuilder 40 | private var core: some ReducerProtocol { 41 | Reduce { state, action in 42 | switch action { 43 | case .changePasswordTapped: 44 | logger.trace("Change password tapped") 45 | return .none 46 | 47 | case .editRecoveryEmailTapped: 48 | logger.trace("Edit recovery email tapped") 49 | return .none 50 | 51 | case .disableMFATapped: 52 | state.isMfaEnabled.toggle() 53 | return .none 54 | 55 | case .editRecoveryPhoneTapped: 56 | logger.trace("Edit recovery phone tapped") 57 | return .none 58 | 59 | case .binding: 60 | return .none 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/EditProfileFeature/Components/ContentSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppLocalization 7 | import SwiftUI 8 | 9 | struct ContentSection: View { 10 | let header: String 11 | let footer: String 12 | let content: () -> Content 13 | 14 | var body: some View { 15 | VStack(alignment: .leading, spacing: 16) { 16 | Text(verbatim: self.header) 17 | .font(.title3.bold()) 18 | .foregroundColor(.secondary) 19 | .multilineTextAlignment(.leading) 20 | .padding(.horizontal) 21 | // Ignore as it's already the container label 22 | .accessibilityHidden(true) 23 | self.content() 24 | .layoutPriority(1) 25 | Text(self.footer.asMarkdown) 26 | .font(.footnote) 27 | .foregroundColor(.secondary) 28 | .multilineTextAlignment(.leading) 29 | .padding(.horizontal) 30 | .fixedSize(horizontal: false, vertical: true) 31 | } 32 | .accessibilityElement(children: .contain) 33 | .accessibilityLabel(self.header) 34 | } 35 | } 36 | 37 | struct ContentSection_Previews: PreviewProvider { 38 | static var previews: some View { 39 | ScrollView(.vertical) { 40 | ContentSection( 41 | header: "Current location", 42 | footer: """ 43 | You can opt-in to automatic location updates based on your last used device location. It is handy if you travel a lot, and would like this to be auto-managed. Your current city and country will be shared, not your exact GPS location. 44 | 45 | **Note that geolocation permissions are required for automatic mode.** 46 | """ 47 | ) { 48 | Color.red 49 | .frame(height: 256) 50 | } 51 | } 52 | .frame(height: 480) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/EditProfileFeature/Components/SecondaryRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import SwiftUI 7 | 8 | struct SecondaryRow: View { 9 | let label: String 10 | let content: () -> Content 11 | 12 | init(_ label: String, @ViewBuilder content: @escaping () -> Content) { 13 | self.label = label 14 | self.content = content 15 | } 16 | 17 | var body: some View { 18 | HStack { 19 | Text(verbatim: self.label) 20 | .font(.headline.weight(.medium)) 21 | // Ignore as it's already the container label 22 | .accessibilityHidden(true) 23 | self.content() 24 | } 25 | .accessibilityElement(children: .contain) 26 | .accessibilityLabel(self.label) 27 | } 28 | } 29 | 30 | struct SecondaryRow_Previews: PreviewProvider { 31 | static var previews: some View { 32 | VStack(alignment: .leading) { 33 | SecondaryRow("Status:") { 34 | HStack(spacing: 4) { 35 | Image(systemName: "location.fill") 36 | .foregroundColor(.blue) 37 | Text(verbatim: "Automatic") 38 | } 39 | } 40 | SecondaryRow("Geolocation permission:") { 41 | Text(verbatim: "Allowed") 42 | Button("Manage") {} 43 | .controlSize(.small) 44 | } 45 | } 46 | .padding() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/EditProfileFeature/IdentityReducer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import ComposableArchitecture 7 | 8 | public struct IdentityReducer: ReducerProtocol { 9 | public struct State: Equatable { 10 | @BindingState var firstName: String 11 | @BindingState var lastName: String 12 | @BindingState var email: String 13 | @BindingState var phone: String 14 | 15 | @BindingState var isNameVerified: Bool 16 | @BindingState var isEmailVerified: Bool 17 | @BindingState var isPhoneVerified: Bool 18 | 19 | public init( 20 | firstName: String = "Baptiste", 21 | lastName: String = "Jamin", 22 | email: String = "baptiste@crisp.chat", 23 | phone: String = "+33631893345", 24 | isNameVerified: Bool = false, 25 | isEmailVerified: Bool = true, 26 | isPhoneVerified: Bool = false 27 | ) { 28 | self.firstName = firstName 29 | self.lastName = lastName 30 | self.email = email 31 | self.phone = phone 32 | self.isNameVerified = isNameVerified 33 | self.isEmailVerified = isEmailVerified 34 | self.isPhoneVerified = isPhoneVerified 35 | } 36 | } 37 | 38 | public enum Action: Equatable, BindableAction { 39 | case verifyNameTapped 40 | case verifyEmailTapped 41 | case verifyPhoneTapped 42 | case binding(BindingAction) 43 | } 44 | 45 | public var body: some ReducerProtocol { 46 | BindingReducer() 47 | self.core 48 | } 49 | 50 | @ReducerBuilder 51 | private var core: some ReducerProtocol { 52 | Reduce { state, action in 53 | switch action { 54 | case .verifyNameTapped: 55 | state.isNameVerified = true 56 | return .none 57 | 58 | case .verifyEmailTapped: 59 | state.isEmailVerified = true 60 | return .none 61 | 62 | case .verifyPhoneTapped: 63 | state.isPhoneVerified = true 64 | return .none 65 | 66 | case .binding: 67 | return .none 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/EditProfileFeature/ProfileReducer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppLocalization 7 | import ComposableArchitecture 8 | 9 | public struct ProfileReducer: ReducerProtocol { 10 | public struct State: Equatable { 11 | @BindingState var organization: String 12 | @BindingState var jobTitle: String 13 | @BindingState var autoDetectLocation: Bool 14 | @BindingState var location: String 15 | @BindingState var isLocationPermissionAllowed: Bool 16 | 17 | var locationPermissionLabel: String { 18 | self.isLocationPermissionAllowed 19 | ? L10n.EditProfile.Profile.LocationPermission.StateAllowed.label 20 | : L10n.EditProfile.Profile.LocationPermission.StateDenied.label 21 | } 22 | 23 | public init( 24 | organization: String = "Crisp", 25 | jobTitle: String = "CEO", 26 | autoDetectLocation: Bool = true, 27 | location: String = "Nantes, France", 28 | isLocationPermissionAllowed: Bool = true 29 | ) { 30 | self.organization = organization 31 | self.jobTitle = jobTitle 32 | self.autoDetectLocation = autoDetectLocation 33 | self.location = location 34 | self.isLocationPermissionAllowed = isLocationPermissionAllowed 35 | } 36 | } 37 | 38 | public enum Action: Equatable, BindableAction { 39 | case manageLocationPermissionTapped 40 | case binding(BindingAction) 41 | } 42 | 43 | public init() {} 44 | 45 | public var body: some ReducerProtocol { 46 | BindingReducer() 47 | self.core 48 | } 49 | 50 | @ReducerBuilder 51 | private var core: some ReducerProtocol { 52 | Reduce { state, action in 53 | switch action { 54 | case .manageLocationPermissionTapped: 55 | state.isLocationPermissionAllowed.toggle() 56 | return .none 57 | 58 | case .binding: 59 | return .none 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/EditProfileFeature/Sidebar/Sidebar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppLocalization 7 | import ComposableArchitecture 8 | import IdentifiedCollections 9 | import SwiftUI 10 | 11 | struct Sidebar: View { 12 | static let minWidth: CGFloat = 228 13 | 14 | let store: StoreOf 15 | 16 | var body: some View { 17 | WithViewStore(self.store, removeDuplicates: { $0.selection == $1.selection }) { viewStore in 18 | List(selection: viewStore.binding(\.$selection)) { 19 | ForEachStore( 20 | self.store.scope(state: \.rows, action: SidebarReducer.Action.row), 21 | content: SidebarRow.init(store:) 22 | ) 23 | } 24 | .listStyle(.sidebar) 25 | .frame(minWidth: Self.minWidth) 26 | } 27 | .safeAreaInset(edge: .top, spacing: 0) { 28 | SidebarHeader( 29 | store: self.store.scope(state: \.header, action: SidebarReducer.Action.header) 30 | ) 31 | .padding([.horizontal, .top], 24) 32 | .padding(.bottom, 8) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/EditProfileFeature/Sidebar/SidebarRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppLocalization 7 | import ComposableArchitecture 8 | import SwiftUI 9 | 10 | struct SidebarRow: View { 11 | let store: StoreOf 12 | 13 | var body: some View { 14 | WithViewStore(self.store) { viewStore in 15 | Self.content(viewStore: viewStore) 16 | } 17 | } 18 | 19 | @ViewBuilder 20 | static func content(viewStore: ViewStoreOf) -> some View { 21 | let foregroundColor = viewStore.foregroundColor 22 | HStack { 23 | ZStack { 24 | Circle() 25 | .fill(viewStore.isSelected ? Color.white : Color.accentColor) 26 | Image(systemName: viewStore.icon) 27 | } 28 | .symbolVariant(.fill) 29 | .foregroundColor(viewStore.isSelected ? Color.accentColor : Color.white) 30 | .frame(width: 24, height: 24) 31 | .accessibilityHidden(true) 32 | VStack(alignment: .leading, spacing: 0) { 33 | Text(verbatim: viewStore.headline) 34 | Text(verbatim: viewStore.subheadline) 35 | .font(.subheadline) 36 | .foregroundColor(foregroundColor.opacity(0.75)) 37 | } 38 | .frame(maxWidth: .infinity, alignment: .leading) 39 | .accessibilityElement(children: .ignore) 40 | .accessibilityLabel( 41 | L10n.EditProfile.Sidebar.Row 42 | .axLabel(viewStore.headline, viewStore.subheadline) 43 | ) 44 | Image(systemName: "chevron.forward.circle.fill") 45 | .symbolVariant(.fill) 46 | .foregroundColor(viewStore.isSelected ? .white : .primary) 47 | .opacity(viewStore.isSelected ? 1 : 0) 48 | .accessibilityHidden(true) 49 | } 50 | .font(.headline) 51 | .padding(.vertical, 4) 52 | .padding(.horizontal, 8) 53 | .foregroundColor(foregroundColor) 54 | .tag(viewStore.id) 55 | .accessibilityElement(children: .combine) 56 | .accessibilityAddTraits(viewStore.isSelected ? .isSelected : []) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/EditProfileFeature/Sidebar/SidebarRowReducer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import ComposableArchitecture 7 | import SwiftUI 8 | 9 | public struct SidebarRowReducer: ReducerProtocol { 10 | public struct State: Equatable, Identifiable { 11 | public let id: SidebarReducer.Selection 12 | 13 | let icon: String 14 | let headline: String 15 | let subheadline: String 16 | var isSelected = false 17 | 18 | var foregroundColor: Color { self.isSelected ? .white : .primary } 19 | } 20 | 21 | public enum Action: Equatable, BindableAction { 22 | case binding(BindingAction) 23 | } 24 | 25 | public init() {} 26 | 27 | public var body: some ReducerProtocol { 28 | BindingReducer() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/EditProfileFeature/Target+Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import OSLog 7 | 8 | internal let logger = Logger(subsystem: "org.prose.app", category: "edit-profile") 9 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/MainScreenFeature/Target+Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import OSLog 7 | 8 | internal let logger = Logger(subsystem: "org.prose.app", category: "main-window") 9 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Mocks/BareJid.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | 8 | public extension BareJid { 9 | static let janeDoe: BareJid = "jane.doe@.prose.org" 10 | static let johnDoe: BareJid = "john.doe@.prose.org" 11 | } 12 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Mocks/Contact.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | import Foundation 8 | 9 | public extension Contact { 10 | static func mock( 11 | jid: BareJid = "user@prose.org", 12 | name: String = "Prose User", 13 | avatar: URL? = nil, 14 | availability: Availability = .unavailable, 15 | status: String? = nil, 16 | groups: [String] = [] 17 | ) -> Self { 18 | .init( 19 | jid: jid, 20 | name: name, 21 | avatar: avatar, 22 | availability: availability, 23 | status: status, 24 | groups: groups 25 | ) 26 | } 27 | } 28 | 29 | public extension Contact { 30 | static let johnDoe = Contact.mock( 31 | jid: "john.doe@prose.org", 32 | name: "John Doe" 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Mocks/Message.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | import Foundation 8 | 9 | public extension Message { 10 | static func mock( 11 | id: MessageId = .init(UUID().uuidString), 12 | from: BareJid = "jane.doe@prose.org", 13 | body: String = "Hello World!", 14 | timestamp: Date = Date(), 15 | isRead: Bool = false, 16 | isEdited: Bool = false, 17 | isDelivered: Bool = false, 18 | reactions: [Reaction] = [] 19 | ) -> Self { 20 | .init( 21 | id: id, 22 | stanzaId: "stanza-id", 23 | from: from, 24 | body: body, 25 | timestamp: timestamp, 26 | isRead: isRead, 27 | isEdited: isEdited, 28 | isDelivered: isDelivered, 29 | reactions: reactions 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Mocks/RandomUser/RandomUser+JSON.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Foundation 7 | 8 | public extension RNDResult { 9 | static func all() -> [RNDResult] { 10 | let data = try! Data( 11 | contentsOf: Foundation.Bundle.module 12 | .url(forResource: "random_user", withExtension: "json")! 13 | ) 14 | return try! JSONDecoder().decode(RNDRandomUserResponse.self, from: data).results 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Mocks/SessionState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | 8 | public extension SessionState { 9 | static func mock( 10 | account: Account = .init( 11 | jid: .janeDoe, 12 | status: .connected, 13 | settings: .init(availability: .available) 14 | ), 15 | _ childState: ChildState 16 | ) -> Self { 17 | .init( 18 | selectedAccountId: account.jid, 19 | accounts: [account], 20 | childState: childState 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/NotificationsClient/NotificationsClient+Noop.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Combine 7 | import ComposableArchitecture 8 | import Foundation 9 | 10 | #if DEBUG 11 | public extension NotificationsClient { 12 | static var noop = NotificationsClient( 13 | promptForPushNotifications: {}, 14 | notificationPermission: { Empty(completeImmediately: false).eraseToEffect() }, 15 | scheduleLocalNotification: { _, _ in } 16 | ) 17 | } 18 | #endif 19 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/NotificationsClient/NotificationsClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | import ComposableArchitecture 8 | import Foundation 9 | 10 | public enum NotificationPermission: Equatable { 11 | case notDetermined 12 | case denied 13 | case authorized 14 | case provisional 15 | case ephemeral 16 | } 17 | 18 | public struct NotificationsClient { 19 | public var promptForPushNotifications: () -> Void 20 | public var notificationPermission: () -> EffectTask 21 | public var scheduleLocalNotification: (Message, UserInfo) async throws -> Void 22 | } 23 | 24 | public extension DependencyValues { 25 | var notificationsClient: NotificationsClient { 26 | get { self[NotificationsClient.self] } 27 | set { self[NotificationsClient.self] = newValue } 28 | } 29 | } 30 | 31 | extension NotificationsClient: TestDependencyKey { 32 | public static var testValue = NotificationsClient( 33 | promptForPushNotifications: unimplemented("\(Self.self).promptForPushNotifications"), 34 | notificationPermission: unimplemented("\(Self.self).notificationPermission"), 35 | scheduleLocalNotification: unimplemented("\(Self.self).scheduleLocalNotification") 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/NotificationsClient/Target+Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import OSLog 7 | 8 | internal let logger = Logger(subsystem: "org.prose.app", category: "notifications") 9 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PasteboardClient/PasteboardClient+Live.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | #if os(macOS) 7 | import Cocoa 8 | #elseif canImport(UIKit) 9 | import UIKit 10 | #endif 11 | import ComposableArchitecture 12 | 13 | extension PasteboardClient: DependencyKey { 14 | public static var liveValue = PasteboardClient.live() 15 | } 16 | 17 | public extension PasteboardClient { 18 | #if os(macOS) 19 | static func live( 20 | pasteboard: NSPasteboard = .general 21 | ) -> Self { 22 | .init( 23 | copyString: { 24 | pasteboard.clearContents() 25 | pasteboard.setString($0, forType: .string) 26 | } 27 | ) 28 | } 29 | 30 | #elseif canImport(UIKit) 31 | static func live( 32 | pasteboard: UIPasteboard = .general 33 | ) -> Self { 34 | .init( 35 | copyString: { pasteboard.string = $0 } 36 | ) 37 | } 38 | #endif 39 | } 40 | 41 | #if DEBUG 42 | public extension PasteboardClient { 43 | static var noop = PasteboardClient( 44 | copyString: { _ in () } 45 | ) 46 | } 47 | #endif 48 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PasteboardClient/PasteboardClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import ComposableArchitecture 7 | import Foundation 8 | 9 | public struct PasteboardClient { 10 | public var copyString: (String) -> Void 11 | 12 | public init(copyString: @escaping (String) -> Void) { 13 | self.copyString = copyString 14 | } 15 | } 16 | 17 | public extension DependencyValues { 18 | var pasteboardClient: PasteboardClient { 19 | get { self[PasteboardClient.self] } 20 | set { self[PasteboardClient.self] = newValue } 21 | } 22 | } 23 | 24 | extension PasteboardClient: TestDependencyKey { 25 | public static var testValue = PasteboardClient( 26 | copyString: unimplemented("\(Self.self).copyString") 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Generated/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/ProseLib/Sources/PreviewAssets/Generated/.gitkeep -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/ImageAsset+URL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Foundation 7 | 8 | public extension ImageAsset { 9 | var customURL: URL? { 10 | var components = URLComponents() 11 | components.scheme = "asset" 12 | components.path = Bundle.fixedModule.bundlePath 13 | components.queryItems = [ 14 | .init(name: "imageName", value: self.name), 15 | ] 16 | return components.url 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "properties" : { 7 | "provides-namespace" : true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/alexandre.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "avatar.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 | "properties" : { 22 | "template-rendering-intent" : "original" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/alexandre.imageset/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/alexandre.imageset/avatar.png -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/antoine.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "avatar.jpg", 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 | "properties" : { 22 | "template-rendering-intent" : "original" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/antoine.imageset/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/antoine.imageset/avatar.jpg -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/baptiste.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "avatar.jpg", 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 | "properties" : { 22 | "template-rendering-intent" : "original" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/baptiste.imageset/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/baptiste.imageset/avatar.jpg -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/camille.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "avatag.jpg", 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 | "properties" : { 22 | "template-rendering-intent" : "original" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/camille.imageset/avatag.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/camille.imageset/avatag.jpg -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/constellation-health.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "constellation-health.jpg", 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 | "properties" : { 22 | "template-rendering-intent" : "original" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/constellation-health.imageset/constellation-health.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/constellation-health.imageset/constellation-health.jpg -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/eliott.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "avatar.jpg", 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 | "properties" : { 22 | "template-rendering-intent" : "original" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/eliott.imageset/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/eliott.imageset/avatar.jpg -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/julien.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "julien.jpg", 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 | "properties" : { 22 | "template-rendering-intent" : "original" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/julien.imageset/julien.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/julien.imageset/julien.jpg -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/valerian.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "avatar.jpg", 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 | "properties" : { 22 | "template-rendering-intent" : "original" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/valerian.imageset/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/avatars/valerian.imageset/avatar.jpg -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/logo-crisp.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "logo-crisp.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 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/logo-crisp.imageset/logo-crisp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/logo-crisp.imageset/logo-crisp.png -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/logo-makair.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "logo-makair.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 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/logo-makair.imageset/logo-makair.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/logo-makair.imageset/logo-makair.png -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/webcam-valerian.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "webcam.jpg", 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 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/webcam-valerian.imageset/webcam.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/ProseLib/Sources/PreviewAssets/Resources/Assets.xcassets/webcam-valerian.imageset/webcam.jpg -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/PreviewAssets/Target+Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import OSLog 7 | 8 | internal let logger = Logger(subsystem: "org.prose.app", category: "preview-assets") 9 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseCore/AccountBookmarksClient+Live.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import ComposableArchitecture 7 | import Foundation 8 | import ProseCoreFFI 9 | 10 | extension AccountBookmarksClient { 11 | static func live( 12 | bookmarksURL: URL = URL.documentsDirectory 13 | .appending(component: "accounts.json") 14 | ) -> Self { 15 | let client = ProseCoreFFI.AccountBookmarksClient(bookmarksPath: bookmarksURL) 16 | 17 | return .init( 18 | loadBookmarks: { 19 | try client.loadBookmarks() 20 | }, 21 | addBookmark: { jid in 22 | try await Task { 23 | try client.addBookmark(jid: jid, selectBookmark: true) 24 | }.result.get() 25 | }, 26 | removeBookmark: { jid in 27 | try await Task { 28 | try client.removeBookmark(jid: jid) 29 | }.result.get() 30 | }, 31 | selectBookmark: { jid in 32 | try await Task { 33 | try client.selectBookmark(jid: jid) 34 | }.result.get() 35 | } 36 | ) 37 | } 38 | } 39 | 40 | extension AccountBookmarksClient: DependencyKey { 41 | public static var liveValue: AccountBookmarksClient = .live() 42 | } 43 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseCore/AccountBookmarksClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | import ComposableArchitecture 8 | import Foundation 9 | 10 | public struct AccountBookmarksClient { 11 | public var loadBookmarks: () throws -> [AccountBookmark] 12 | public var addBookmark: (BareJid) async throws -> Void 13 | public var removeBookmark: (BareJid) async throws -> Void 14 | public var selectBookmark: (BareJid) async throws -> Void 15 | } 16 | 17 | public extension DependencyValues { 18 | var accountBookmarksClient: AccountBookmarksClient { 19 | get { self[AccountBookmarksClient.self] } 20 | set { self[AccountBookmarksClient.self] = newValue } 21 | } 22 | } 23 | 24 | extension AccountBookmarksClient: TestDependencyKey { 25 | public static var testValue = AccountBookmarksClient( 26 | loadBookmarks: unimplemented("\(Self.self).loadBookmarks"), 27 | addBookmark: unimplemented("\(Self.self).saveBookmark"), 28 | removeBookmark: unimplemented("\(Self.self).removeBookmark"), 29 | selectBookmark: unimplemented("\(Self.self).selectBookmark") 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseCore/Exports.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | @_exported import AppDomain 7 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseCoreViews/API/MessagingContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | import Foundation 8 | 9 | public struct MessagingContext { 10 | public let setAccountJID: JSFunc1 11 | public let setStyleTheme: JSFunc1 12 | 13 | public init(evaluator: @escaping JSEvaluator) { 14 | let cls = JSClass(name: "MessagingContext", evaluator: evaluator) 15 | self.setAccountJID = cls.setAccountJID 16 | self.setStyleTheme = cls.setStyleTheme 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseCoreViews/API/MessagingStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | import Foundation 8 | import IdentifiedCollections 9 | 10 | public struct MessagingStore { 11 | private let signpostID = signposter.makeSignpostID() 12 | 13 | public let insertMessages: JSRestFunc1<[Message], Void> 14 | public let retractMessage: JSFunc1 15 | public let highlightMessage: JSFunc1 16 | public let interact: JSFunc3 17 | public let identify: JSFunc2 18 | 19 | private let update: JSFunc2 20 | 21 | init(evaluator: @escaping JSEvaluator) { 22 | let cls = JSClass(name: "MessagingStore", evaluator: evaluator) 23 | self.insertMessages = cls.insert 24 | self.update = cls.update 25 | self.retractMessage = cls.retract 26 | self.highlightMessage = cls.highlight 27 | self.identify = cls.identify 28 | self.interact = cls.interact 29 | } 30 | 31 | public func updateMessages( 32 | to messages: [Message], 33 | oldMessages: inout IdentifiedArrayOf 34 | ) { 35 | let interval = signposter.beginInterval(#function, id: self.signpostID) 36 | 37 | let messages = IdentifiedArrayOf(uniqueElements: messages) 38 | defer { oldMessages = messages } 39 | 40 | let diff = messages.difference(from: oldMessages) 41 | 42 | for messageId in diff.removedIds { 43 | self.retractMessage(messageId) 44 | } 45 | for messageId in diff.updatedIds { 46 | if let message = messages[id: messageId] { 47 | self.updateMessage(message) 48 | } 49 | } 50 | self.insertMessages(diff.insertedIds.compactMap { messages[id: $0] }) 51 | 52 | signposter.endInterval(#function, interval) 53 | } 54 | 55 | public func updateMessage(_ message: Message) { 56 | self.update(message.id, message) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseCoreViews/FFI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Foundation 7 | 8 | public struct FFI { 9 | public let messagingContext: MessagingContext 10 | public let messagingStore: MessagingStore 11 | 12 | public init(evaluator: @escaping JSEvaluator) { 13 | self.messagingContext = MessagingContext(evaluator: evaluator) 14 | self.messagingStore = MessagingStore(evaluator: evaluator) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseCoreViews/Generated/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/ProseLib/Sources/ProseCoreViews/Generated/.gitkeep -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseCoreViews/Generated/Files+Generated.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | // swiftlint:disable all 7 | // Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen 8 | 9 | import Foundation 10 | 11 | // swiftlint:disable superfluous_disable_command file_length line_length implicit_return 12 | 13 | // MARK: - Files 14 | 15 | // swiftlint:disable explicit_type_interface identifier_name 16 | // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces 17 | public enum Files { 18 | /// messaging.html 19 | public static let messagingHtml = File( 20 | name: "messaging", 21 | ext: "html", 22 | relativePath: "", 23 | mimeType: "text/html" 24 | ) 25 | } 26 | 27 | // swiftlint:enable explicit_type_interface identifier_name 28 | // swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces 29 | 30 | // MARK: - Implementation Details 31 | 32 | public struct File { 33 | public let name: String 34 | public let ext: String? 35 | public let relativePath: String 36 | public let mimeType: String 37 | 38 | public var url: URL { 39 | url(locale: nil) 40 | } 41 | 42 | public func url(locale: Locale?) -> URL { 43 | let bundle = Bundle.fixedModule 44 | let url = bundle.url( 45 | forResource: self.name, 46 | withExtension: self.ext, 47 | subdirectory: self.relativePath, 48 | localization: locale?.identifier 49 | ) 50 | guard let result = url else { 51 | let file = self.name + (self.ext.flatMap { ".\($0)" } ?? "") 52 | fatalError("Could not locate file named \(file)") 53 | } 54 | return result 55 | } 56 | 57 | public var path: String { 58 | path(locale: nil) 59 | } 60 | 61 | public func path(locale: Locale?) -> String { 62 | self.url(locale: locale).path 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseCoreViews/Helpers/IdentifiedArray+Difference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import struct IdentifiedCollections.IdentifiedArray 7 | import struct OrderedCollections.OrderedSet 8 | 9 | extension IdentifiedArray where Element: Equatable { 10 | struct Difference: Equatable { 11 | let removedIds, insertedIds: OrderedSet 12 | let updatedIds: Set 13 | } 14 | 15 | func difference(from other: Self) -> Difference { 16 | let ids: OrderedSet = self.ids 17 | let otherIds: OrderedSet = other.ids 18 | 19 | return Difference( 20 | removedIds: otherIds.subtracting(ids), 21 | insertedIds: ids.subtracting(otherIds), 22 | updatedIds: Set(ids.intersection(otherIds)).filter { self[id: $0] != other[id: $0] } 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseCoreViews/Helpers/JSEventError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Foundation 7 | 8 | public enum JSEventError: Error, Equatable { 9 | case badSerialization, decodingError(String) 10 | } 11 | 12 | extension JSEventError: CustomDebugStringConvertible { 13 | public var debugDescription: String { 14 | switch self { 15 | case .badSerialization: 16 | return "JS message body should be serialized as a String" 17 | case let .decodingError(debugDescription): 18 | return debugDescription 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseCoreViews/Helpers/NSError+JavaScript.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Foundation 7 | 8 | public extension NSError { 9 | var crisp_javaScriptExceptionMessage: String { 10 | (self.userInfo["WKJavaScriptExceptionMessage"] as? String) ?? self.localizedDescription 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseCoreViews/Resources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/ProseLib/Sources/ProseCoreViews/Resources/.gitkeep -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseCoreViews/Resources/Views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prose-im/prose-app-macos/54c76806ff2f24d35cfdd2598cd8f2816c0a5655/Prose/ProseLib/Sources/ProseCoreViews/Resources/Views/.gitkeep -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseCoreViews/Resources/Views/action-more.77dcdfab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseCoreViews/Resources/Views/action-reactions.7a469ec6.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseCoreViews/Resources/Views/file-other-option-get.a1a4420f.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseCoreViews/Resources/Views/origin-attribute-insecure.aa6021b6.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseCoreViews/Target+Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import OSLog 7 | 8 | internal let logger = Logger(subsystem: "org.prose.app", category: "views") 9 | internal let signposter = OSSignposter(logger: logger) 10 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseCoreViews/Types/ColorScheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Foundation 7 | 8 | public enum StyleTheme: String, Encodable { 9 | case light 10 | case dark 11 | } 12 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseCoreViews/Types/EventOrigin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Foundation 7 | 8 | public struct Point: Equatable, Decodable { 9 | public let x, y: Double 10 | public var cgPoint: CGPoint { CGPoint(x: self.x, y: self.y) } 11 | } 12 | 13 | public struct Frame: Equatable, Decodable { 14 | public let x, y, width, height: Double 15 | public var cgRect: CGRect { CGRect(x: self.x, y: self.y, width: self.width, height: self.height) } 16 | } 17 | 18 | public struct EventOrigin: Equatable, Decodable { 19 | public let anchor: Point 20 | public let parent: Frame? 21 | public var cgRect: CGRect { 22 | self.parent?.cgRect ?? CGRect(origin: self.anchor.cgPoint, size: .zero) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseCoreViews/Types/MessageAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Foundation 7 | 8 | public enum MessageAction: String, Encodable { 9 | case reactions, actions 10 | } 11 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseCoreViews/Types/MessageEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | 8 | public enum MessageEvent: Equatable { 9 | case showMenu(MessageMenuHandlerPayload) 10 | case toggleReaction(ToggleReactionHandlerPayload) 11 | case showReactions(ShowReactionsHandlerPayload) 12 | case reachedEndOfList(ReachedEndOfListPayload) 13 | } 14 | 15 | public struct MessageMenuHandlerPayload: Equatable, Decodable { 16 | public let id: Message.ID? 17 | public let origin: EventOrigin 18 | } 19 | 20 | public struct ShowReactionsHandlerPayload: Equatable, Decodable { 21 | public let id: Message.ID? 22 | public let origin: EventOrigin 23 | } 24 | 25 | public struct ToggleReactionHandlerPayload: Equatable, Decodable { 26 | public let id: Message.ID? 27 | public let reaction: Emoji 28 | } 29 | 30 | public struct ReachedEndOfListPayload: Equatable, Decodable { 31 | public enum Direction: String, Decodable { 32 | case forwards 33 | case backwards 34 | } 35 | 36 | public let direction: Direction 37 | } 38 | 39 | public extension MessageEvent { 40 | enum Kind: String { 41 | case showMenu = "message:actions:view" 42 | case toggleReaction = "message:reactions:react" 43 | case showReactions = "message:reactions:view" 44 | case reachedEndOfList = "message:history:seek" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseUI/AvailabilityIndicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | import Assets 8 | import SwiftUI 9 | 10 | public struct AvailabilityIndicator: View { 11 | @Environment(\.redactionReasons) private var redactionReasons 12 | 13 | private let availability: Availability 14 | private let size: CGFloat 15 | 16 | public init( 17 | availability: Availability, 18 | size: CGFloat = 11.0 19 | ) { 20 | self.availability = availability 21 | self.size = size 22 | } 23 | 24 | public init(_ availability: Availability) { 25 | self.init(availability: availability) 26 | } 27 | 28 | public var body: some View { 29 | // Having a `ZStack` with the background circle always present allows animations. 30 | // Conditional views (aka `if`, `switch`…) break identity, and thus animations. 31 | ZStack { 32 | Circle() 33 | .fill(Color.white) 34 | Circle() 35 | .fill(self.redactionReasons.contains(.placeholder) ? .gray : self.availability.fillColor) 36 | .padding(2) 37 | Circle() 38 | .strokeBorder(Color(nsColor: .separatorColor), lineWidth: 0.5) 39 | } 40 | .frame(width: self.size, height: self.size) 41 | .drawingGroup() 42 | .accessibilityElement(children: .ignore) 43 | .accessibilityLabel(String(describing: self.availability)) 44 | } 45 | } 46 | 47 | private extension Availability { 48 | var fillColor: Color { 49 | switch self { 50 | case .available: 51 | return .green 52 | case .doNotDisturb, .away, .unavailable: 53 | return .orange 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseUI/Common/ColoredIconLabelStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import SwiftUI 7 | 8 | public struct ColoredIconLabelStyle: LabelStyle { 9 | public func makeBody(configuration: Configuration) -> some View { 10 | Label { 11 | configuration.title 12 | } icon: { 13 | configuration.icon 14 | .foregroundColor(.accentColor) 15 | } 16 | } 17 | } 18 | 19 | public extension LabelStyle where Self == ColoredIconLabelStyle { 20 | static var coloredIcon: Self { ColoredIconLabelStyle() } 21 | } 22 | 23 | struct ColoredIconLabelStyle_Previews: PreviewProvider { 24 | static var previews: some View { 25 | Label("Valerian Saliou", systemImage: "message") 26 | .labelStyle(.coloredIcon) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseUI/Common/ContentCommonNameStatusComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | import SwiftUI 8 | 9 | public struct ContentCommonNameStatusComponent: View { 10 | var name: String 11 | var status: Availability = .unavailable 12 | 13 | public init(name: String, status: Availability = .unavailable) { 14 | self.name = name 15 | self.status = status 16 | } 17 | 18 | public var body: some View { 19 | HStack { 20 | OnlineStatusIndicator(self.status) 21 | .offset(x: 3, y: 1) 22 | .accessibilitySortPriority(1) 23 | 24 | Text(verbatim: self.name) 25 | .font(.system(size: 14).bold()) 26 | .accessibilitySortPriority(2) 27 | } 28 | .accessibilityElement(children: .combine) 29 | } 30 | } 31 | 32 | struct ContentCommonNameStatusComponent_Previews: PreviewProvider { 33 | static var previews: some View { 34 | ContentCommonNameStatusComponent( 35 | name: "Valerian Saliou", 36 | status: .available 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseUI/Common/ShadowedButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import SwiftUI 7 | 8 | public struct ShadowedButtonStyle: ButtonStyle { 9 | public func makeBody(configuration: Configuration) -> some View { 10 | configuration.label 11 | .opacity(configuration.isPressed ? 0.5 : 1) 12 | .padding(.vertical, 6) 13 | .padding(.horizontal, 12) 14 | .background { 15 | RoundedRectangle(cornerRadius: 6) 16 | .fill(.background) 17 | .shadow(color: .gray.opacity(0.5), radius: 2) 18 | } 19 | } 20 | } 21 | 22 | public extension ButtonStyle where Self == ShadowedButtonStyle { 23 | static var shadowed: Self { ShadowedButtonStyle() } 24 | } 25 | 26 | struct ShadowedButtonStyle_Previews: PreviewProvider { 27 | static var previews: some View { 28 | VStack { 29 | Button(action: {}) { 30 | Label("Reply", systemImage: "arrowshape.turn.up.right") 31 | .frame(maxWidth: .infinity) 32 | } 33 | .foregroundColor(.accentColor) 34 | Button(action: {}) { 35 | Text("Mark read") 36 | .frame(maxWidth: .infinity) 37 | } 38 | } 39 | .frame(width: 96) 40 | .labelStyle(.vertical) 41 | .buttonStyle(.shadowed) 42 | .padding() 43 | .background(.background) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseUI/Common/VerticalLabelStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import SwiftUI 7 | 8 | public struct VerticalLabelStyle: LabelStyle { 9 | public func makeBody(configuration: Configuration) -> some View { 10 | VStack { 11 | configuration.icon 12 | .font(.system(size: 24)) 13 | configuration.title 14 | } 15 | } 16 | } 17 | 18 | public extension LabelStyle where Self == VerticalLabelStyle { 19 | static var vertical: Self { VerticalLabelStyle() } 20 | } 21 | 22 | struct VerticalLabelStyle_Previews: PreviewProvider { 23 | static var previews: some View { 24 | Label("Reply", systemImage: "arrowshape.turn.up.right") 25 | .labelStyle(.vertical) 26 | Button(action: {}) { 27 | Label("Reply", systemImage: "arrowshape.turn.up.right") 28 | } 29 | // .buttonStyle(.plain) 30 | .labelStyle(.vertical) 31 | .padding() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseUI/HelpButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import SwiftUI 7 | 8 | // MARK: - View 9 | 10 | /// A pre-styled button that presents a popover with the provided content when tapped. 11 | public struct HelpButton: View { 12 | @Environment(\.redactionReasons) private var redactionReasons 13 | 14 | @State private var isShowingPopover: Bool = false 15 | 16 | let content: () -> Content 17 | 18 | public init(content: @escaping () -> Content) { 19 | self.content = content 20 | } 21 | 22 | public var body: some View { 23 | Button { self.isShowingPopover = true } label: { 24 | Image(systemName: "questionmark") 25 | } 26 | .buttonStyle(CircleButtonStyle()) 27 | .unredacted() 28 | .disabled(self.redactionReasons.contains(.placeholder)) 29 | .popover(isPresented: self.$isShowingPopover, content: self.content) 30 | } 31 | } 32 | 33 | private struct CircleButtonStyle: ButtonStyle { 34 | @Environment(\.isEnabled) private var isEnabled 35 | func makeBody(configuration: Self.Configuration) -> some View { 36 | configuration.label 37 | .font(.system(size: 12, weight: .semibold, design: .rounded)) 38 | .frame(width: 20, height: 20) 39 | .contentShape(Circle()) 40 | .background { 41 | Circle() 42 | .fill(Color(nsColor: .controlColor)) 43 | .shadow(color: Color.black.opacity(0.25), radius: 0, y: 0.5) 44 | } 45 | .overlay { 46 | Circle() 47 | .strokeBorder(Color.black.opacity(0.15), lineWidth: 0.5) 48 | } 49 | .opacity((configuration.isPressed || !self.isEnabled) ? 0.5 : 1) 50 | } 51 | } 52 | 53 | // MARK: - Previews 54 | 55 | struct HelpButton_Previews: PreviewProvider { 56 | static var previews: some View { 57 | HelpButton { 58 | Text("Test") 59 | .padding() 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseUI/Icon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import SwiftUI 7 | 8 | public enum Icon: String, CaseIterable { 9 | case unread = "tray.2" 10 | case reply = "arrowshape.turn.up.left.2" 11 | case directMessage = "message" 12 | case addressBook = "text.book.closed" 13 | case group = "circle.grid.2x2" 14 | 15 | public var image: Image { 16 | Image(systemName: self.rawValue) 17 | } 18 | } 19 | 20 | struct Icon_Previews: PreviewProvider { 21 | static var previews: some View { 22 | List { 23 | ForEach(Icon.allCases, id: \.rawValue) { icon in 24 | Label(String(describing: icon), systemImage: icon.rawValue) 25 | } 26 | } 27 | .frame(width: 200) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseUI/LevelIndicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Cocoa 7 | import SwiftUI 8 | 9 | public struct LevelIndicator: View { 10 | private let minimumValue, maximumValue: Double 11 | private let warningValue, criticalValue: Double 12 | private let tickMarkFactor: Double 13 | private let currentValue: Double 14 | 15 | public init( 16 | currentValue: Double, 17 | minimumValue: Double = 0.0, 18 | maximumValue: Double = 1.0, 19 | warningValue: Double = 0.5, 20 | criticalValue: Double = 0.75, 21 | tickMarkFactor: Double = 6.0 22 | ) { 23 | self.minimumValue = minimumValue 24 | self.maximumValue = maximumValue 25 | self.warningValue = warningValue 26 | self.criticalValue = criticalValue 27 | self.tickMarkFactor = tickMarkFactor 28 | self.currentValue = currentValue 29 | } 30 | 31 | public var body: some View { 32 | let indicator = NSLevelIndicator() 33 | 34 | // Configure bounds 35 | indicator.minValue = self.minimumValue * self.tickMarkFactor 36 | indicator.maxValue = self.maximumValue * self.tickMarkFactor 37 | indicator.warningValue = self.warningValue * self.tickMarkFactor 38 | indicator.criticalValue = self.criticalValue * self.tickMarkFactor 39 | 40 | // Apply value 41 | indicator.doubleValue = self.currentValue * self.tickMarkFactor 42 | 43 | return ViewWrap(indicator) 44 | } 45 | } 46 | 47 | struct LevelIndicator_Previews: PreviewProvider { 48 | private struct Preview: View { 49 | var body: some View { 50 | VStack { 51 | ForEach([-2, 0, 0.2, 0.4, 0.6, 0.8, 1, 2], id: \.self) { value in 52 | LevelIndicator( 53 | currentValue: value 54 | ) 55 | } 56 | } 57 | .padding() 58 | } 59 | } 60 | 61 | static var previews: some View { 62 | Preview() 63 | .preferredColorScheme(.light) 64 | .previewDisplayName("Light") 65 | 66 | Preview() 67 | .preferredColorScheme(.dark) 68 | .previewDisplayName("Dark") 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseUI/OnlineStatusIndicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | import Assets 8 | import SwiftUI 9 | 10 | public struct OnlineStatusIndicator: View { 11 | let availability: Availability 12 | 13 | public init(_ availability: Availability) { 14 | self.availability = availability 15 | } 16 | 17 | public var body: some View { 18 | LEDIndicator(isOn: self.availability != .unavailable) 19 | .fillColor(self.availability == .available ? Colors.State.green.color : Color.orange) 20 | // Override `LEDIndicator` accessibility label as it says "on"/"off" 21 | .accessibilityElement(children: .ignore) 22 | // FIXME: Localize ProseCoreFFI.Availability 23 | .accessibilityLabel(String(describing: self.availability)) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseUI/ReactionPicker/ReactionPickerReducer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | import ComposableArchitecture 8 | import Foundation 9 | 10 | public struct ReactionPickerReducer: ReducerProtocol { 11 | public struct State: Equatable { 12 | let reactions: [Emoji] = "👋👉👍😂😢😭😍😘😊🤯❤️🙏😛🚀⚠️😀😌😇🙃🙂🤩🥳🤨🙁😳🤔😐👀✅❌" 13 | .map { Emoji(String($0)) } 14 | var selected: Set 15 | 16 | let columnCount = 5 17 | let fontSize: CGFloat = 24 18 | let spacing: CGFloat = 4 19 | 20 | var width: CGFloat { self.fontSize * 1.5 } 21 | var height: CGFloat { self.width } 22 | 23 | public init(selected: Set = []) { 24 | self.selected = selected 25 | } 26 | } 27 | 28 | public enum Action: Equatable { 29 | case select(Emoji) 30 | case deselect(Emoji) 31 | } 32 | 33 | public init() {} 34 | 35 | public var body: some ReducerProtocol { 36 | Reduce { state, action in 37 | switch action { 38 | case let .select(reaction): 39 | state.selected.insert(reaction) 40 | return .none 41 | case let .deselect(reaction): 42 | state.selected.remove(reaction) 43 | return .none 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseUI/SpotlightGroupBoxStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import SwiftUI 7 | 8 | public extension GroupBoxStyle where Self == SpotlightGroupBoxStyle { 9 | static var spotlight: Self { SpotlightGroupBoxStyle() } 10 | } 11 | 12 | public struct SpotlightGroupBoxStyle: GroupBoxStyle { 13 | public func makeBody(configuration: Configuration) -> some View { 14 | VStack(spacing: 12) { 15 | configuration.label 16 | .padding(.horizontal, 4) 17 | VStack(spacing: 6) { 18 | configuration.content 19 | .modifier(SpotlightItemBackground()) 20 | } 21 | } 22 | } 23 | } 24 | 25 | private struct SpotlightItemBackground: ViewModifier { 26 | var shape: RoundedRectangle { 27 | RoundedRectangle(cornerRadius: 3) 28 | } 29 | 30 | func body(content: Content) -> some View { 31 | content 32 | .padding(.vertical, 8) 33 | .padding(.horizontal, 16) 34 | .background { 35 | self.shape 36 | .fill(.background) 37 | .shadow(color: .gray.opacity(0.5), radius: 1) 38 | } 39 | } 40 | } 41 | 42 | #if DEBUG 43 | import PreviewAssets 44 | 45 | struct SpotlightGroupBoxStyle_Previews: PreviewProvider { 46 | static var previews: some View { 47 | GroupBox { 48 | Text("GroupBox Content goes here") 49 | } label: { 50 | HStack { 51 | Label("support", systemImage: "circle.grid.2x2") 52 | .labelStyle(.coloredIcon) 53 | .font(.title2.bold()) 54 | Spacer() 55 | Text("dummy") 56 | .foregroundColor(.secondary) 57 | } 58 | } 59 | .groupBoxStyle(.spotlight) 60 | } 61 | } 62 | 63 | struct SpotlightItemBackground_Previews: PreviewProvider { 64 | static var previews: some View { 65 | Text("GroupBox Content goes here") 66 | .modifier(SpotlightItemBackground()) 67 | .padding() 68 | } 69 | } 70 | #endif 71 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseUI/Target+Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import OSLog 7 | 8 | internal let logger = Logger(subsystem: "org.prose.app", category: "prose-ui") 9 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseUI/Toolbar/CommonToolbar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import SwiftUI 7 | 8 | public struct CommonToolbar: ToolbarContent { 9 | public init() {} 10 | 11 | public var body: some ToolbarContent { 12 | ToolbarItemGroup(placement: .navigation) { 13 | CommonToolbarNavigation() 14 | } 15 | 16 | ToolbarItemGroup { 17 | CommonToolbarActions() 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseUI/Toolbar/CommonToolbarActions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import SwiftUI 7 | 8 | public struct CommonToolbarActions: View { 9 | @Environment(\.redactionReasons) private var redactionReasons 10 | 11 | public init() {} 12 | 13 | public var body: some View { 14 | Button { logger.info("Search tapped") } label: { 15 | Label("Search", systemImage: "magnifyingglass") 16 | } 17 | .unredacted() 18 | .disabled(self.redactionReasons.contains(.placeholder)) 19 | // https://github.com/prose-im/prose-app-macos/issues/48 20 | .disabled(true) 21 | } 22 | } 23 | 24 | // MARK: - Previews 25 | 26 | struct CommonToolbarActions_Previews: PreviewProvider { 27 | private struct Preview: View { 28 | var body: some View { 29 | CommonToolbarActions() 30 | .padding() 31 | .previewLayout(.sizeThatFits) 32 | } 33 | } 34 | 35 | static var previews: some View { 36 | Preview() 37 | Preview() 38 | .preferredColorScheme(.dark) 39 | .previewDisplayName("Dark mode") 40 | Preview() 41 | .redacted(reason: .placeholder) 42 | .previewDisplayName("Placeholder") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseUI/Toolbar/CommonToolbarNavigation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import SwiftUI 7 | 8 | public struct CommonToolbarNavigation: View { 9 | @Environment(\.redactionReasons) private var redactionReasons 10 | 11 | public init() {} 12 | 13 | public var body: some View { 14 | Group { 15 | Button { logger.info("Navigation back tapped") } label: { 16 | Label("Back", systemImage: "chevron.backward") 17 | } 18 | Button { logger.info("Navigation forward tapped") } label: { 19 | Label("Forward", systemImage: "chevron.forward") 20 | } 21 | Menu { 22 | // TODO: Add actions 23 | Text("TODO") 24 | } label: { 25 | Label("History", systemImage: "clock") 26 | } 27 | } 28 | .unredacted() 29 | .disabled(self.redactionReasons.contains(.placeholder)) 30 | // https://github.com/prose-im/prose-app-macos/issues/48 31 | .disabled(true) 32 | } 33 | } 34 | 35 | // MARK: - Previews 36 | 37 | struct CommonToolbarNavigation_Previews: PreviewProvider { 38 | private struct Preview: View { 39 | var body: some View { 40 | HStack { 41 | CommonToolbarNavigation() 42 | } 43 | .padding() 44 | .previewLayout(.sizeThatFits) 45 | } 46 | } 47 | 48 | static var previews: some View { 49 | Preview() 50 | Preview() 51 | .preferredColorScheme(.dark) 52 | .previewDisplayName("Dark mode") 53 | Preview() 54 | .redacted(reason: .placeholder) 55 | .previewDisplayName("Placeholder") 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseUI/Toolbar/ToolbarDivider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import SwiftUI 7 | 8 | public struct ToolbarDivider: View { 9 | public init() {} 10 | 11 | public var body: some View { 12 | HStack { 13 | Divider() 14 | .frame(height: 24) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/ProseUI/ViewWrap+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Cocoa 7 | import Foundation 8 | import SwiftUI 9 | 10 | // This wraps any AppKit (NS[..]) component and transforms it into a SwiftUI-compatible View 11 | extension ViewWrap { 12 | init( 13 | _ makeView: @escaping @autoclosure () -> Wrapped, 14 | updater update: @escaping (Wrapped) -> Void 15 | ) { 16 | self.makeView = makeView 17 | self.update = { view, _ in update(view) } 18 | } 19 | 20 | init(_ makeView: @escaping @autoclosure () -> Wrapped) { 21 | self.makeView = makeView 22 | self.update = { _, _ in } 23 | } 24 | } 25 | 26 | struct ViewWrap: NSViewRepresentable { 27 | typealias Updater = (Wrapped, Context) -> Void 28 | 29 | var makeView: () -> Wrapped 30 | var update: (Wrapped, Context) -> Void 31 | 32 | init( 33 | _ makeView: @escaping @autoclosure () -> Wrapped, 34 | updater update: @escaping Updater 35 | ) { 36 | self.makeView = makeView 37 | self.update = update 38 | } 39 | 40 | func makeNSView(context _: Context) -> Wrapped { 41 | self.makeView() 42 | } 43 | 44 | func updateNSView(_ view: Wrapped, context: Context) { 45 | self.update(view, context) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/SettingsFeature/AccountSettings/AccountSettingsAccountView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppLocalization 7 | import SwiftUI 8 | 9 | private let l10n = L10n.Settings.Accounts.self 10 | 11 | struct AccountSettingsAccountView: View { 12 | @AppStorage("settings.accounts.x.account.enabled") var enabled = true 13 | @AppStorage("settings.accounts.x.account.username") var username = "" 14 | @State var password = "" 15 | 16 | var body: some View { 17 | VStack(spacing: 24) { 18 | GroupBox(l10n.enabledLabel) { 19 | Toggle("", isOn: self.$enabled) 20 | .toggleStyle(.switch) 21 | .labelsHidden() 22 | .disabled(true) 23 | } 24 | 25 | GroupBox(l10n.statusLabel) { 26 | HStack(spacing: 4) { 27 | ConnectionStatusIndicator(status: .connected) 28 | Text(l10n.statusConnected) 29 | .font(.system(size: 13)) 30 | .fontWeight(.semibold) 31 | } 32 | } 33 | 34 | VStack { 35 | GroupBox(l10n.addressLabel) { 36 | TextField("", text: self.$username, prompt: Text(l10n.addressPlaceholder)) 37 | .textContentType(.username) 38 | .disableAutocorrection(true) 39 | } 40 | 41 | GroupBox(l10n.passwordLabel) { 42 | SecureField("", text: self.$password, prompt: Text(l10n.passwordPlaceholder)) 43 | .textContentType(.password) 44 | } 45 | } 46 | 47 | Spacer() 48 | } 49 | .groupBoxStyle(FormGroupBoxStyle( 50 | firstColumnWidth: SettingsConstants 51 | .firstFormColumnConstrainedWidth 52 | )) 53 | .padding() 54 | } 55 | } 56 | 57 | struct AccountSettingsAccountView_Previews: PreviewProvider { 58 | static var previews: some View { 59 | AccountSettingsAccountView() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/SettingsFeature/AccountSettings/AccountSettingsFeaturesView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import SwiftUI 7 | 8 | struct AccountSettingsFeaturesView: View { 9 | var body: some View { 10 | VStack(spacing: 24) { 11 | // TODO: Add content here 12 | Text("(Server features automated check, i.e. no setting here.)") 13 | Spacer() 14 | } 15 | .padding() 16 | } 17 | } 18 | 19 | struct AccountSettingsFeaturesView_Previews: PreviewProvider { 20 | static var previews: some View { 21 | AccountSettingsFeaturesView() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/SettingsFeature/AccountSettings/AccountSettingsSecurityView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import SwiftUI 7 | 8 | struct AccountSettingsSecurityView: View { 9 | var body: some View { 10 | VStack(spacing: 24) { 11 | // TODO: Add content here 12 | Text("(Server security automated check, i.e. no setting here.)") 13 | Spacer() 14 | } 15 | .padding() 16 | } 17 | } 18 | 19 | struct AccountSettingsSecurityView_Previews: PreviewProvider { 20 | static var previews: some View { 21 | AccountSettingsSecurityView() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/SettingsFeature/Atoms/AccountPickerRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import ProseUI 7 | import SwiftUI 8 | 9 | struct AccountPickerRow: View { 10 | struct ViewModel: Equatable, Identifiable { 11 | let teamLogo: String 12 | let teamDomain: String 13 | let userName: String 14 | var id: String { self.teamDomain } 15 | } 16 | 17 | let viewModel: ViewModel 18 | 19 | var body: some View { 20 | HStack { 21 | Avatar(.placeholder, size: 32) 22 | VStack(alignment: .leading, spacing: 2) { 23 | Text(verbatim: self.viewModel.userName) 24 | .font(.headline) 25 | .foregroundColor(.primary) 26 | 27 | Text(verbatim: self.viewModel.teamDomain) 28 | .font(.subheadline) 29 | .foregroundColor(.secondary) 30 | } 31 | .frame(maxWidth: .infinity, alignment: .leading) 32 | } 33 | } 34 | } 35 | 36 | struct AccountPickerRow_Previews: PreviewProvider { 37 | static var previews: some View { 38 | AccountPickerRow(viewModel: .init( 39 | teamLogo: "logo-crisp", 40 | teamDomain: "crisp.chat", 41 | userName: "Baptiste" 42 | )) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/SettingsFeature/Atoms/ConnectionStatusIndicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Assets 7 | import SwiftUI 8 | 9 | enum ConnectionStatus: Hashable, CaseIterable { 10 | case disconnected, connected 11 | 12 | var fillColor: Color { 13 | switch self { 14 | case .connected: 15 | return Colors.State.greenLight.color 16 | case .disconnected: 17 | return Colors.State.greyLight.color 18 | } 19 | } 20 | } 21 | 22 | struct ConnectionStatusIndicator: View { 23 | private let status: ConnectionStatus 24 | private let size: CGFloat 25 | 26 | init( 27 | status: ConnectionStatus = .disconnected, 28 | size: CGFloat = 10.0 29 | ) { 30 | self.status = status 31 | self.size = size 32 | } 33 | 34 | init(_ status: ConnectionStatus) { 35 | self.init(status: status) 36 | } 37 | 38 | var body: some View { 39 | Circle() 40 | .fill(self.status.fillColor) 41 | .frame(width: self.size, height: self.size) 42 | } 43 | } 44 | 45 | struct ConnectionStatusIndicator_Previews: PreviewProvider { 46 | private struct Preview: View { 47 | var body: some View { 48 | HStack { 49 | ForEach( 50 | ConnectionStatus.allCases, 51 | id: \.self, 52 | content: ConnectionStatusIndicator.init(_:) 53 | ) 54 | } 55 | .padding() 56 | } 57 | } 58 | 59 | static var previews: some View { 60 | Preview() 61 | .preferredColorScheme(.light) 62 | .previewDisplayName("Light") 63 | 64 | Preview() 65 | .preferredColorScheme(.dark) 66 | .previewDisplayName("Dark") 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/SettingsFeature/Atoms/FormGroupBoxStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import SwiftUI 7 | 8 | /// Because it is cross-platform, SwiftUI doesn't really like the macOS form style with two columns. 9 | /// It supports it, but we cannot put labels in the first column if they're not attached to a single 10 | /// control. 11 | /// To work around it, this `GroupBoxStyle` mimics the macOS form style. 12 | /// 13 | /// - Note: This style might break some layout, accessibility and readability benefits of the system 14 | /// style. 15 | struct FormGroupBoxStyle: GroupBoxStyle { 16 | let firstColumnWidth: CGFloat 17 | init(firstColumnWidth: CGFloat = SettingsConstants.firstFormColumnWidth) { 18 | self.firstColumnWidth = firstColumnWidth 19 | } 20 | 21 | func makeBody(configuration: Configuration) -> some View { 22 | HStack(alignment: .top) { 23 | VStack(alignment: .trailing) { 24 | configuration.label 25 | } 26 | .frame(width: self.firstColumnWidth, alignment: .trailing) 27 | VStack(alignment: .leading) { 28 | configuration.content 29 | } 30 | .textFieldStyle(.roundedBorder) 31 | .frame(maxWidth: .infinity, alignment: .leading) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/SettingsFeature/Atoms/VideoPreviewView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import SwiftUI 7 | 8 | struct VideoPreviewView: View { 9 | @State var streamPath: String 10 | @State var sizeWidth: CGFloat = 180 11 | @State var sizeHeight: CGFloat = 100 12 | 13 | var body: some View { 14 | Image(self.streamPath) 15 | .resizable() 16 | .aspectRatio(contentMode: .fit) 17 | .frame(width: self.sizeWidth, height: self.sizeHeight) 18 | .background(.black) 19 | .cornerRadius(4) 20 | .clipped() 21 | } 22 | } 23 | 24 | struct VideoPreviewView_Previews: PreviewProvider { 25 | static var previews: some View { 26 | VideoPreviewView( 27 | streamPath: "webcam-valerian", 28 | sizeWidth: 260, 29 | sizeHeight: 180 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/SettingsFeature/SettingsConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import CoreGraphics 7 | 8 | enum SettingsConstants { 9 | static let contentWidth: CGFloat = 640 10 | static let firstFormColumnWidth: CGFloat = 192 11 | static let firstFormColumnConstrainedWidth: CGFloat = 128 12 | static let selectLargeWidth: CGFloat = 256 13 | static let selectNormalWidth: CGFloat = 192 14 | static let selectSmallWidth: CGFloat = 96 15 | } 16 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/SettingsFeature/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppLocalization 7 | import SwiftUI 8 | 9 | public struct SettingsView: View { 10 | enum Tabs: Hashable { 11 | case general, accounts, notifications, messages, calls, advanced 12 | } 13 | 14 | @State private var tab: Tabs = .general 15 | 16 | public init() {} 17 | public var body: some View { 18 | TabView(selection: self.$tab) { 19 | GeneralTab() 20 | .tabItem { Label(L10n.Settings.Tabs.general, systemImage: "gearshape") } 21 | .tag(Tabs.general) 22 | AccountsTab() 23 | .tabItem { Label(L10n.Settings.Tabs.accounts, systemImage: "person.2") } 24 | .tag(Tabs.accounts) 25 | NotificationsTab() 26 | .tabItem { Label(L10n.Settings.Tabs.notifications, systemImage: "bell") } 27 | .tag(Tabs.notifications) 28 | MessagesTab() 29 | .tabItem { 30 | Label(L10n.Settings.Tabs.messages, systemImage: "bubble.left.and.bubble.right") 31 | } 32 | .tag(Tabs.messages) 33 | CallsTab() 34 | .tabItem { Label(L10n.Settings.Tabs.calls, systemImage: "phone.arrow.up.right") } 35 | .tag(Tabs.calls) 36 | AdvancedTab() 37 | .tabItem { Label(L10n.Settings.Tabs.advanced, systemImage: "dial.min") } 38 | .tag(Tabs.advanced) 39 | } 40 | .frame(width: SettingsConstants.contentWidth) 41 | .fixedSize(horizontal: true, vertical: false) 42 | } 43 | } 44 | 45 | struct SettingsView_Previews: PreviewProvider { 46 | static var previews: some View { 47 | SettingsView() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/SettingsFeature/Tabs/AdvancedTab.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppLocalization 7 | import SwiftUI 8 | 9 | private let l10n = L10n.Settings.Advanced.self 10 | 11 | enum AdvancedSettingsUpdateChannel: String, Equatable, CaseIterable { 12 | case stable 13 | case beta 14 | 15 | var localizedDescription: String { 16 | switch self { 17 | case .stable: 18 | return l10n.UpdateChannel.optionStable 19 | case .beta: 20 | return l10n.UpdateChannel.optionBeta 21 | } 22 | } 23 | } 24 | 25 | struct AdvancedTab: View { 26 | @AppStorage( 27 | "settings.advanced.updateChannel" 28 | ) var updateChannel: AdvancedSettingsUpdateChannel = 29 | .stable 30 | @AppStorage("settings.advanced.reportsUsage") var reportsUsage = true 31 | @AppStorage("settings.advanced.reportsCrash") var reportsCrash = true 32 | 33 | var body: some View { 34 | VStack(spacing: 24) { 35 | // "Update channel" 36 | GroupBox(l10n.UpdateChannel.label) { 37 | Picker("", selection: self.$updateChannel) { 38 | ForEach(AdvancedSettingsUpdateChannel.allCases, id: \.self) { value in 39 | Text(value.localizedDescription) 40 | .tag(value) 41 | } 42 | } 43 | .labelsHidden() 44 | .frame(width: SettingsConstants.selectNormalWidth) 45 | } 46 | 47 | // "Reports" 48 | GroupBox(l10n.Reports.label) { 49 | Toggle(l10n.Reports.usageToggle, isOn: self.$reportsUsage) 50 | Toggle(l10n.Reports.crashToggle, isOn: self.$reportsCrash) 51 | } 52 | } 53 | .groupBoxStyle(FormGroupBoxStyle()) 54 | .padding() 55 | .disabled(true) 56 | } 57 | } 58 | 59 | struct AdvancedTab_Previews: PreviewProvider { 60 | static var previews: some View { 61 | AdvancedTab() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/SettingsFeature/Target+Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import OSLog 7 | 8 | internal let logger = Logger(subsystem: "org.prose.app", category: "settings") 9 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/SidebarFeature/Atoms/Counter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import SwiftUI 7 | 8 | struct Counter: View { 9 | var count = 0 10 | 11 | var body: some View { 12 | // We're enabling the isEmpty rule due to a bug in Swiftformat where it thinks that we're 13 | // accessing the count property of a Collection. 14 | // swiftformat:disable isEmpty 15 | if self.count > 0 { 16 | Text(self.count, format: .number) 17 | .font(.system(size: 11, weight: .semibold)) 18 | .padding(.vertical, 2) 19 | .padding(.horizontal, 5) 20 | .foregroundColor(.secondary) 21 | .background { 22 | Capsule() 23 | .fill(.quaternary) 24 | } 25 | // swiftformat:enable isEmpty 26 | } 27 | } 28 | } 29 | 30 | struct Counter_Previews: PreviewProvider { 31 | private struct Preview: View { 32 | private static let values = [0, 2, 10, 1000] 33 | 34 | var body: some View { 35 | VStack { 36 | ForEach(Self.values, id: \.self) { count in 37 | HStack { 38 | Text(count.description) 39 | .unredacted() 40 | Spacer() 41 | Counter(count: count) 42 | } 43 | } 44 | } 45 | .frame(width: 128) 46 | .padding() 47 | } 48 | } 49 | 50 | static var previews: some View { 51 | Preview() 52 | .preferredColorScheme(.light) 53 | .previewDisplayName("Light") 54 | Preview() 55 | .preferredColorScheme(.dark) 56 | .previewDisplayName("Dark") 57 | Preview() 58 | .redacted(reason: .placeholder) 59 | .previewDisplayName("Placeholder") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/SidebarFeature/Footer/AccountSettingsMenu/AccountSettingsMenuReducer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | import ComposableArchitecture 8 | import CredentialsClient 9 | import Foundation 10 | import ProseCore 11 | 12 | public struct AccountSettingsMenuReducer: ReducerProtocol { 13 | public typealias State = SessionState 14 | 15 | public struct AccountSettingsMenuState: Equatable { 16 | var statusIcon: Character 17 | } 18 | 19 | public enum Action: Equatable { 20 | case accountSettingsTapped 21 | case changeAvailabilityTapped(Availability) 22 | case editProfileTapped 23 | case offlineModeTapped 24 | case pauseNotificationsTapped 25 | case signOutTapped 26 | case updateMoodTapped 27 | } 28 | 29 | @Dependency(\.accountBookmarksClient) var accountBookmarks 30 | @Dependency(\.accountsClient) var accounts 31 | @Dependency(\.credentialsClient) var credentials 32 | 33 | public var body: some ReducerProtocol { 34 | Reduce { state, action in 35 | switch action { 36 | case .signOutTapped: 37 | return .fireAndForget { [jid = state.selectedAccountId] in 38 | try? await self.accountBookmarks.removeBookmark(jid) 39 | try? self.credentials.deleteCredentials(jid) 40 | self.accounts.removeAccount(jid) 41 | } 42 | 43 | case .accountSettingsTapped: 44 | return .none 45 | 46 | case let .changeAvailabilityTapped(availability): 47 | state.selectedAccount.availability = availability 48 | return .none 49 | 50 | case .editProfileTapped: 51 | return .none 52 | 53 | case .offlineModeTapped: 54 | return .none 55 | 56 | case .pauseNotificationsTapped: 57 | return .none 58 | 59 | case .updateMoodTapped: 60 | return .none 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/SidebarFeature/Footer/AccountSwitcherMenu/AccountSwitcherMenuReducer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | import ComposableArchitecture 8 | 9 | public struct AccountSwitcherMenuReducer: ReducerProtocol { 10 | public typealias State = SessionState 11 | 12 | public struct AccountSwitcherMenuState: Equatable { 13 | public init() {} 14 | } 15 | 16 | public enum Action: Equatable { 17 | case showMenuTapped 18 | case accountSelected(BareJid) 19 | case connectAccountTapped 20 | /// Only here for accessibility 21 | case manageServerTapped 22 | } 23 | 24 | public init() {} 25 | 26 | public var body: some ReducerProtocol { 27 | EmptyReducer() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/SidebarFeature/Footer/FooterDetails.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppLocalization 7 | import Assets 8 | import SwiftUI 9 | 10 | struct FooterDetails: View { 11 | let teamName: String 12 | let statusIcon: Character 13 | let statusMessage: String 14 | 15 | var body: some View { 16 | VStack(alignment: .leading, spacing: 4) { 17 | Text(self.teamName) 18 | .font(.system(size: 12, weight: .bold)) 19 | .foregroundColor(Colors.Text.primary.color) 20 | 21 | Text("\(String(self.statusIcon)) “\(self.statusMessage)”") 22 | .font(.system(size: 11)) 23 | .foregroundColor(Colors.Text.secondary.color) 24 | .layoutPriority(1) 25 | } 26 | // Make hit box full width 27 | .frame(maxWidth: .infinity, alignment: .leading) 28 | .contentShape([.interaction, .focusEffect], Rectangle()) 29 | .accessibilityElement(children: .ignore) 30 | .accessibilityLabel("\(L10n.Server.ConnectedTo.label(self.teamName)), \(self.statusMessage)") 31 | } 32 | } 33 | 34 | struct FooterDetails_Previews: PreviewProvider { 35 | private struct Preview: View { 36 | var body: some View { 37 | FooterDetails( 38 | teamName: "Crisp", 39 | statusIcon: "🚀", 40 | statusMessage: "Building new stuff." 41 | ) 42 | .frame(maxWidth: 256) 43 | .padding() 44 | .previewLayout(.sizeThatFits) 45 | } 46 | } 47 | 48 | static var previews: some View { 49 | Preview() 50 | .preferredColorScheme(.light) 51 | .previewDisplayName("Light") 52 | Preview() 53 | .preferredColorScheme(.dark) 54 | .previewDisplayName("Dark") 55 | Preview() 56 | .redacted(reason: .placeholder) 57 | .previewDisplayName("Placeholder") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/SidebarFeature/Footer/Styles/SidebarFooterPopoverButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import SwiftUI 7 | 8 | struct SidebarFooterPopoverButtonStyle: ButtonStyle { 9 | @Environment(\.isEnabled) private var isEnabled 10 | func makeBody(configuration: Configuration) -> some View { 11 | configuration.label 12 | .opacity((configuration.isPressed || !self.isEnabled) ? 0.5 : 1) 13 | .foregroundColor(Self.color(for: configuration.role)) 14 | // Make hit box full width 15 | // NOTE: [Rémi Bardon] We could avoid making the hit box full width for destructive actions. 16 | .frame(maxWidth: .infinity, alignment: .leading) 17 | // Allow hits in the transparent areas 18 | .contentShape(Rectangle()) 19 | } 20 | 21 | static func color(for role: ButtonRole?) -> Color? { 22 | switch role { 23 | case .some(.destructive): 24 | return .red 25 | default: 26 | return nil 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/SidebarFeature/Footer/Styles/VStackGroupBoxStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import SwiftUI 7 | 8 | struct VStackGroupBoxStyle: GroupBoxStyle { 9 | let alignment: HorizontalAlignment 10 | let spacing: CGFloat? 11 | 12 | func makeBody(configuration: Configuration) -> some View { 13 | VStack(alignment: self.alignment, spacing: self.spacing) { configuration.content } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/SidebarFeature/Rows/ActionButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import SwiftUI 7 | 8 | struct ActionButton: View { 9 | let title: String 10 | let action: () -> Void 11 | 12 | var body: some View { 13 | Button(action: self.action) { 14 | Label(self.title, systemImage: "plus.square.fill") 15 | .symbolVariant(.fill) 16 | // Make hit box full width 17 | .frame(maxWidth: .infinity, alignment: .leading) 18 | .contentShape(.interaction, Rectangle()) 19 | } 20 | .buttonStyle(.plain) 21 | } 22 | } 23 | 24 | struct ActionRow_Previews: PreviewProvider { 25 | static var previews: some View { 26 | ActionButton( 27 | title: "sidebar_groups_add", 28 | action: {} 29 | ) 30 | .frame(width: 196) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/SidebarFeature/Rows/ContactRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | import ProseUI 8 | import SwiftUI 9 | 10 | struct ContactRow: View { 11 | var title: String 12 | var avatar: AvatarImage 13 | var count = 0 14 | var status: Availability = .unavailable 15 | 16 | var body: some View { 17 | HStack { 18 | Avatar(self.avatar, size: 18) 19 | 20 | HStack(alignment: .firstTextBaseline, spacing: 4) { 21 | Text(self.title) 22 | 23 | OnlineStatusIndicator(self.status) 24 | } 25 | 26 | Spacer() 27 | 28 | Counter(count: self.count) 29 | } 30 | } 31 | } 32 | 33 | #if DEBUG 34 | import PreviewAssets 35 | 36 | struct ContactRow_Previews: PreviewProvider { 37 | private struct Preview: View { 38 | var body: some View { 39 | ContactRow( 40 | title: "Valerian", 41 | avatar: AvatarImage(url: PreviewAsset.Avatars.valerian.customURL), 42 | count: 3 43 | ) 44 | .frame(width: 196) 45 | .padding() 46 | } 47 | } 48 | 49 | static var previews: some View { 50 | Preview() 51 | .preferredColorScheme(.light) 52 | .previewDisplayName("Light") 53 | 54 | Preview() 55 | .preferredColorScheme(.dark) 56 | .previewDisplayName("Dark") 57 | } 58 | } 59 | #endif 60 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/SidebarFeature/Rows/IconRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import ProseUI 7 | import SwiftUI 8 | 9 | struct IconRow: View { 10 | var title: String 11 | var icon: Icon 12 | var count = 0 13 | 14 | var body: some View { 15 | HStack { 16 | Label(self.title, systemImage: self.icon.rawValue) 17 | .frame(maxWidth: .infinity, alignment: .leading) 18 | Counter(count: self.count) 19 | } 20 | } 21 | } 22 | 23 | struct NavigationRow_Previews: PreviewProvider { 24 | static var previews: some View { 25 | IconRow( 26 | title: "Label", 27 | icon: .unread, 28 | count: 10 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/SidebarFeature/Target+Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import OSLog 7 | 8 | internal let logger = Logger(subsystem: "org.prose.app", category: "sidebar") 9 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/TestHelpers/Date+YMD.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Foundation 7 | 8 | public extension Date { 9 | static func ymd( 10 | _ year: Int, 11 | _ month: Int, 12 | _ day: Int, 13 | _ hour: Int = 0, 14 | _ minute: Int = 0, 15 | _ second: Int = 0, 16 | timeZone: TimeZone? = nil 17 | ) -> Date { 18 | let components = DateComponents( 19 | timeZone: timeZone, 20 | year: year, 21 | month: month, 22 | day: day, 23 | hour: hour, 24 | minute: minute, 25 | second: second 26 | ) 27 | return Calendar.current.date(from: components)! 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/TestHelpers/UUID+Incrementing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Foundation 7 | 8 | public extension UUID { 9 | /// A deterministic, auto-incrementing "UUID" generator for testing. 10 | static var incrementing: () -> UUID { 11 | var uuid = 0 12 | return { 13 | defer { uuid += 1 } 14 | return UUID(uuidString: "00000000-0000-0000-0000-\(String(format: "%012x", uuid))")! 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/TestHostApp/Helpers/Target+Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import OSLog 7 | 8 | internal let logger = Logger(subsystem: "org.prose.app", category: "test-host-app") 9 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/TestHostApp/Helpers/TestScene.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Foundation 7 | import SwiftUI 8 | 9 | public struct TestScene: Scene { 10 | public init() { 11 | if ProcessInfo.processInfo.environment["is-running-ui-test"] == "1" { 12 | NSScrollView.prose_prepareForUITest() 13 | } 14 | } 15 | 16 | public var body: some Scene { 17 | WindowGroup { 18 | Group { 19 | switch ProcessInfo.processInfo.environment["test-case"] { 20 | case "roster-selection": 21 | RosterSelection() 22 | 23 | case "conversation-info": 24 | RosterSelection() 25 | 26 | default: 27 | Text(""" 28 | Missing or unknown test case. 29 | 30 | If you're trying to run a UI Test, make sure to specify the desired testcase in \ 31 | your XCUIApplication with: 32 | 33 | app.launchEnvironment = ["test-case": "roster-selection"] 34 | 35 | If you're trying to run the app manually, add a Environment Variable "test-case" to \ 36 | the current scheme. 37 | """) 38 | } 39 | } 40 | .ignoresSafeArea() 41 | .preferredColorScheme( 42 | ProcessInfo.processInfo.environment["dark-mode-enabled"] == "1" ? .dark : .light 43 | ) 44 | } 45 | } 46 | } 47 | 48 | private func SwizzleImplementations( 49 | in obj: AnyClass, 50 | originalSelector: Selector, 51 | swizzledSelector: Selector 52 | ) { 53 | if 54 | let originalMethod = class_getInstanceMethod(obj, originalSelector), 55 | let swizzledMethod = class_getInstanceMethod(obj, swizzledSelector) 56 | { 57 | method_exchangeImplementations(originalMethod, swizzledMethod) 58 | } 59 | } 60 | 61 | extension NSScrollView { 62 | public static func prose_prepareForUITest() { 63 | SwizzleImplementations( 64 | in: NSScrollView.self, 65 | originalSelector: #selector(flashScrollers), 66 | swizzledSelector: #selector(prose_flashScrollers) 67 | ) 68 | } 69 | 70 | @objc func prose_flashScrollers() { 71 | // Do nothing… 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/TestHostApp/Mocks/AppState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | @testable import App 7 | import AppDomain 8 | import Foundation 9 | import Mocks 10 | 11 | extension AppReducer.State { 12 | /// Returns an `AppState` with jane.doe@prose.org logged in. 13 | static let authenticated: AppReducer.State = { 14 | var state = AppReducer.State() 15 | state.initialized = true 16 | state.selectedAccountId = .janeDoe 17 | state.availableAccounts = [ 18 | .init( 19 | jid: .janeDoe, 20 | status: .connected, 21 | settings: .init(availability: .available), 22 | contacts: { 23 | let contacts = Contact.random() 24 | return Dictionary( 25 | zip(contacts.map(\.jid), contacts), 26 | uniquingKeysWith: { _, last in last } 27 | ) 28 | }() 29 | ), 30 | ] 31 | state.mainState = .init() 32 | return state 33 | }() 34 | } 35 | 36 | extension Contact { 37 | static func random(count: Int = 5) -> [Contact] { 38 | RNDResult.all() 39 | .prefix(count) 40 | .map { user in 41 | Contact( 42 | jid: BareJid(stringLiteral: user.email), 43 | name: "\(user.name.first) \(user.name.last)", 44 | avatar: nil, 45 | availability: .available, 46 | status: nil, 47 | groups: ["Contacts"] 48 | ) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/TestHostApp/TestCases/RosterSelection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | @testable import App 7 | import Combine 8 | import ComposableArchitecture 9 | @testable import ProseCore 10 | import SwiftUI 11 | import Toolbox 12 | 13 | struct RosterSelection: View { 14 | let store: StoreOf 15 | 16 | init() { 17 | var coreClient = ProseCoreClient.noop 18 | coreClient.loadLatestMessages = { jid, _, _ in 19 | // Only take the node so that the WebView doesn't highlight the URL which creates a separate 20 | // accessiblity element (a link). 21 | let name = String(jid.rawValue.prefix(while: { $0 != "@" })) 22 | return [Message.mock(from: jid, body: "Hello from \(name)")] 23 | } 24 | 25 | var accountsClient = AccountsClient.noop 26 | accountsClient.client = { _ in coreClient } 27 | 28 | self.store = .init( 29 | initialState: .authenticated, 30 | reducer: AppReducer(), 31 | prepareDependencies: { deps in 32 | deps.accountBookmarksClient = .testValue 33 | deps.accountsClient = accountsClient 34 | deps.connectivityClient = .previewValue 35 | deps.credentialsClient = .noop 36 | deps.openURL = .init(handler: { url in 37 | logger.info("Not opening url \(url.absoluteString) in UITest.") 38 | return true 39 | }) 40 | } 41 | ) 42 | } 43 | 44 | public var body: some View { 45 | AppView(store: self.store) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Toolbox/AsyncStream+Prose.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | // Source: https://github.com/tgrapperon/swift-dependencies-additions/blob/bcb5e934a1b9a7d661ab5a9dce026015d5b03db4/Sources/DependenciesAdditionsBasics/ConcurrencySupport/AsyncStream%2BAdditions.swift 7 | 8 | public extension AsyncStream { 9 | /// Produces an `AsyncStream` from an async `AsyncSequence` by awaiting and then consuming the 10 | /// sequence till it terminates, ignoring any failure. 11 | /// 12 | /// Useful as a kind of type eraser for actor-isolated live `AsyncSequence`-based dependencies, 13 | /// that also erases the `async` extraction. 14 | init(_ sequence: @escaping () async throws -> S) rethrows 15 | where S.Element == Element 16 | { 17 | var iterator: S.AsyncIterator? 18 | self.init { 19 | if iterator == nil { 20 | iterator = try? await sequence().makeAsyncIterator() 21 | } 22 | return try? await iterator?.next() 23 | } 24 | } 25 | } 26 | 27 | public extension AsyncStream { 28 | static func empty() -> Self { 29 | .init(unfolding: { nil }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Toolbox/FocusState+Synchronize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import SwiftUI 7 | 8 | public extension View { 9 | /// Synchronizes a `Binding` and a `FocusState.Binding`. 10 | /// 11 | /// Comes from . 12 | func synchronize( 13 | _ first: Binding, 14 | _ second: FocusState.Binding 15 | ) -> some View { 16 | self 17 | .onChange(of: first.wrappedValue) { second.wrappedValue = $0 } 18 | .onChange(of: second.wrappedValue) { first.wrappedValue = $0 } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Toolbox/ItemProvider+Prose.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Combine 7 | import Foundation 8 | 9 | public extension NSItemProvider { 10 | func prose_loadObject(ofClass: T.Type) async throws -> T? where T: NSItemProviderReading { 11 | try await withCheckedThrowingContinuation { continuation in 12 | _ = loadObject(ofClass: ofClass) { data, error in 13 | if let error { 14 | continuation.resume(throwing: error) 15 | return 16 | } 17 | 18 | guard let image = data as? T else { 19 | continuation.resume(returning: nil) 20 | return 21 | } 22 | 23 | continuation.resume(returning: image) 24 | } 25 | } 26 | } 27 | 28 | func prose_loadImage() async throws -> PlatformImage? { 29 | try await self.prose_loadObject(ofClass: PlatformImage.self) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Toolbox/PlatformImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Foundation 7 | 8 | #if os(macOS) 9 | import AppKit 10 | 11 | public typealias PlatformImage = NSImage 12 | 13 | public extension NSImage { 14 | var cgImage: CGImage? { 15 | self.cgImage(forProposedRect: nil, context: nil, hints: nil) 16 | } 17 | 18 | func jpegData(compressionQuality: CGFloat) -> Data? { 19 | self.cgImage.flatMap { cgImage in 20 | NSBitmapImageRep(cgImage: cgImage).representation( 21 | using: NSBitmapImageRep.FileType.jpeg, 22 | properties: [.compressionFactor: compressionQuality] 23 | ) 24 | } 25 | } 26 | } 27 | 28 | #elseif os(iOS) || os(tvOS) || os(watchOS) 29 | import UIKit 30 | 31 | public typealias PlatformImage = UIImage 32 | #endif 33 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/Toolbox/Target+Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import OSLog 7 | 8 | internal let logger = Logger(subsystem: "org.prose.app", category: "toolbox") 9 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/UnreadFeature/Target+Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import OSLog 7 | 8 | internal let logger = Logger(subsystem: "org.prose.app", category: "unread") 9 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/UnreadFeature/Toolbar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import ProseUI 7 | import SwiftUI 8 | 9 | // MARK: - View 10 | 11 | struct Toolbar: ToolbarContent { 12 | @Environment(\.redactionReasons) private var redactionReasons 13 | 14 | var body: some ToolbarContent { 15 | ToolbarItemGroup(placement: .navigation) { 16 | CommonToolbarNavigation() 17 | } 18 | ToolbarItemGroup { 19 | Self.actions(redactionReasons: self.redactionReasons) 20 | ToolbarDivider() 21 | CommonToolbarActions() 22 | } 23 | } 24 | 25 | @ViewBuilder 26 | static func actions(redactionReasons: RedactionReasons) -> some View { 27 | Button { logger.info("Mark as read tapped") } label: { 28 | Label("Mark as read", systemImage: "envelope.open") 29 | } 30 | .unredacted() 31 | .disabled(redactionReasons.contains(.placeholder)) 32 | // https://github.com/prose-im/prose-app-macos/issues/48 33 | .disabled(true) 34 | 35 | ToolbarDivider() 36 | 37 | Menu { 38 | // TODO: Add actions 39 | Text("TODO") 40 | } label: { 41 | Label("Filter", systemImage: "line.3.horizontal.decrease.circle") 42 | } 43 | .unredacted() 44 | .disabled(redactionReasons.contains(.placeholder)) 45 | // https://github.com/prose-im/prose-app-macos/issues/48 46 | .disabled(true) 47 | } 48 | } 49 | 50 | // MARK: - Previews 51 | 52 | internal struct Toolbar_Previews: PreviewProvider { 53 | private struct Preview: View { 54 | @Environment(\.redactionReasons) private var redactionReasons 55 | 56 | var body: some View { 57 | HStack { 58 | Toolbar.actions(redactionReasons: self.redactionReasons) 59 | } 60 | .padding() 61 | .previewLayout(.sizeThatFits) 62 | } 63 | } 64 | 65 | static var previews: some View { 66 | Preview() 67 | Preview() 68 | .redacted(reason: .placeholder) 69 | .previewDisplayName("Placeholder") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/UnreadFeature/UnreadScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Assets 7 | import ComposableArchitecture 8 | import ConversationFeature 9 | import ProseUI 10 | import SwiftUI 11 | 12 | public struct UnreadScreen: View { 13 | private let store: StoreOf 14 | private var actions: ViewStore 15 | 16 | public init(store: StoreOf) { 17 | self.store = store 18 | self.actions = ViewStore(store.stateless) 19 | } 20 | 21 | public var body: some View { 22 | self.content() 23 | .frame(maxWidth: .infinity, maxHeight: .infinity) 24 | .background(Colors.Background.message.color) 25 | .toolbar(content: Toolbar.init) 26 | .onAppear { self.actions.send(.onAppear) } 27 | .groupBoxStyle(.spotlight) 28 | } 29 | 30 | private func content() -> some View { 31 | WithViewStore(self.store.scope(state: \.messages.isEmpty)) { noMessage in 32 | if noMessage.state { 33 | self.nothing() 34 | } else { 35 | self.list() 36 | } 37 | } 38 | } 39 | 40 | private func nothing() -> some View { 41 | Text("Looks like you read everything 🎉") 42 | .font(.largeTitle.bold()) 43 | .foregroundColor(.secondary) 44 | .padding() 45 | .unredacted() 46 | } 47 | 48 | private func list() -> some View { 49 | ScrollView { 50 | VStack(spacing: 24) { 51 | WithViewStore(self.store.scope(state: \.messages)) { messages in 52 | ForEach(messages.state, id: \.chatId, content: UnreadSection.init(model:)) 53 | } 54 | } 55 | .padding(24) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/UnreadFeature/UnreadScreenReducer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | import ComposableArchitecture 8 | 9 | public struct UnreadScreenReducer: ReducerProtocol { 10 | public typealias State = SessionState 11 | 12 | public struct UnreadScreenState: Equatable { 13 | var messages = [UnreadSectionModel]() 14 | 15 | public init() {} 16 | } 17 | 18 | public enum Action: Equatable { 19 | case onAppear 20 | } 21 | 22 | public init() {} 23 | 24 | public var body: some ReducerProtocol { 25 | Reduce { _, action in 26 | switch action { 27 | case .onAppear: 28 | return .none 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Prose/ProseLib/Sources/UnreadFeature/UnreadSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | import ConversationFeature 8 | import ProseUI 9 | import SwiftUI 10 | 11 | public struct UnreadSectionModel: Equatable { 12 | let chatId: BareJid 13 | let chatTitle: String 14 | var messages: [Message] 15 | 16 | public init( 17 | chatId: BareJid, 18 | chatTitle: String, 19 | messages: [Message] 20 | ) { 21 | self.chatId = chatId 22 | self.chatTitle = chatTitle 23 | self.messages = messages 24 | } 25 | } 26 | 27 | struct UnreadSection: View { 28 | @Environment(\.redactionReasons) private var redactionReasons 29 | 30 | let model: UnreadSectionModel 31 | 32 | var body: some View { 33 | GroupBox { 34 | HStack { 35 | VStack { 36 | ForEach(self.model.messages, content: MessageView.init(model:)) 37 | } 38 | VStack { 39 | VStack { 40 | Button { logger.info("Reply tapped") } label: { 41 | // FIXME: Localize 42 | Label("Reply", systemImage: "arrowshape.turn.up.right") 43 | .frame(maxWidth: .infinity) 44 | } 45 | .foregroundColor(.accentColor) 46 | .unredacted() 47 | Button { logger.info("Mark read tapped") } label: { 48 | // FIXME: Localize 49 | Text("Mark read") 50 | .frame(maxWidth: .infinity) 51 | } 52 | .unredacted() 53 | } 54 | .frame(width: 96) 55 | .labelStyle(.vertical) 56 | .buttonStyle(.shadowed) 57 | } 58 | } 59 | } label: { 60 | HStack { 61 | Label(self.model.chatTitle, systemImage: Icon.directMessage.rawValue) 62 | .labelStyle(.coloredIcon) 63 | .font(.title3.bold()) 64 | Spacer() 65 | Text(self.model.messages.last!.timestamp, format: .relative(presentation: .named)) 66 | .foregroundColor(.secondary) 67 | } 68 | } 69 | .disabled(self.redactionReasons.contains(.placeholder)) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Prose/ProseLib/Tests/ConversationFeatureTests/Helpers/ChatSessionState+Mock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | import ConversationFeature 8 | import Mocks 9 | 10 | public extension ChatSessionState { 11 | static func mock( 12 | selectedAccountId: BareJid = .janeDoe, 13 | chatId: BareJid = .johnDoe, 14 | userInfos: [BareJid: Contact] = [.johnDoe: .johnDoe], 15 | composingUsers: [BareJid] = [], 16 | _ childState: ChildState 17 | ) -> Self { 18 | .init( 19 | selectedAccountId: selectedAccountId, 20 | chatId: chatId, 21 | userInfos: userInfos, 22 | composingUsers: composingUsers, 23 | childState: childState 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Prose/ProseLib/Tests/CredentialsClientTests/CredentialsClientTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppDomain 7 | @testable import CredentialsClient 8 | import XCTest 9 | 10 | final class CredentialsStoreTests: XCTestCase { 11 | func testSavesUpdatesAndDeletesCredentials() throws { 12 | let store = CredentialsClient.live(service: "org.prose.app.tests") 13 | 14 | let jid: BareJid = "tests@prose.org" 15 | 16 | try XCTAssertNil(store.loadCredentials(jid)) 17 | 18 | let initialCredentials = Credentials(jid: jid, password: "initial-password") 19 | 20 | try store.save(initialCredentials) 21 | 22 | try XCTAssertEqual(store.loadCredentials(jid), initialCredentials) 23 | 24 | let updatedCredentials = Credentials(jid: jid, password: "updated-passowrd") 25 | 26 | try store.save(updatedCredentials) 27 | 28 | try XCTAssertEqual(store.loadCredentials(jid), updatedCredentials) 29 | 30 | try store.deleteCredentials(jid) 31 | 32 | try XCTAssertNil(store.loadCredentials(jid)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Prose/ProseLib/Tests/ProseCoreViewsTests/IdentifiedArrayDifferenceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import typealias IdentifiedCollections.IdentifiedArrayOf 7 | @testable import ProseCoreViews 8 | import XCTest 9 | 10 | final class IdentifiedArrayDifferenceTests: XCTestCase { 11 | func testDifference() { 12 | struct Item: Equatable, Identifiable, ExpressibleByIntegerLiteral { 13 | let id: Int 14 | var content = UUID() 15 | init(id: Int) { 16 | self.id = id 17 | } 18 | 19 | init(integerLiteral value: IntegerLiteralType) { 20 | self.init(id: Int(value)) 21 | } 22 | } 23 | 24 | let items: [Item] = [0, 1, 2, 3] 25 | 26 | let arrayBefore: IdentifiedArrayOf = [items[0], items[3], items[1]] 27 | var item3 = items[3] 28 | item3.content = UUID() 29 | let arrayAfter: IdentifiedArrayOf = [items[0], item3, items[2]] 30 | 31 | let diff = arrayAfter.difference(from: arrayBefore) 32 | XCTAssertEqual(diff.insertedIds, [2]) 33 | XCTAssertEqual(diff.removedIds, [1]) 34 | XCTAssertEqual(diff.updatedIds, [3]) 35 | 36 | let oppositeDiff = arrayBefore.difference(from: arrayAfter) 37 | XCTAssertEqual(oppositeDiff.insertedIds, diff.removedIds) 38 | XCTAssertEqual(oppositeDiff.removedIds, diff.insertedIds) 39 | XCTAssertEqual(oppositeDiff.updatedIds, diff.updatedIds) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Prose/ProseUITests/ConversationInfoTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Foundation 7 | import XCTest 8 | 9 | final class ConversationInfoTests: XCTestCase { 10 | override func setUp() { 11 | self.continueAfterFailure = false 12 | } 13 | 14 | func testInfoChangesWhenSwitchingConversation() { 15 | let app = XCUIApplication.launching(testCase: "conversation-info") 16 | 17 | app.sidebar.cells 18 | .containing(.staticText, identifier: "Oya Karaböcek").element.tap() 19 | 20 | app.toolbars.checkBoxes["Info"].tap() 21 | 22 | XCTAssertTrue( 23 | app.conversationInfo.otherElements["Oya Karaböcek, available"] 24 | .waitForExistence(timeout: 5) 25 | ) 26 | 27 | app.sidebar.cells 28 | .containing(.staticText, identifier: "Donna Reed").element.tap() 29 | 30 | app.toolbars.checkBoxes["Info"].tap() 31 | 32 | XCTAssertTrue( 33 | app.conversationInfo.otherElements["Donna Reed, available"] 34 | .waitForExistence(timeout: 5) 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Prose/ProseUITests/Helpers/XCUIApplication+Prose.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Foundation 7 | import XCTest 8 | 9 | extension XCUIApplication { 10 | static func launching( 11 | testCase: String, 12 | isDarkModeEnabled: Bool = false, 13 | animationsEnabled: Bool = false 14 | ) -> XCUIApplication { 15 | let app = XCUIApplication() 16 | app.launchEnvironment = [ 17 | "test-case": testCase, 18 | "is-running-ui-test": "1", 19 | "dark-mode-enabled": isDarkModeEnabled ? "1" : "0", 20 | "animations-enabled": animationsEnabled ? "1" : "0", 21 | ] 22 | app.launch() 23 | return app 24 | } 25 | 26 | func wait(for seconds: TimeInterval) { 27 | Thread.sleep(forTimeInterval: seconds) 28 | } 29 | } 30 | 31 | extension XCUIApplication { 32 | /// Matches content in the left split group. 33 | var sidebar: XCUIElement { 34 | self.groups["Sidebar"] 35 | } 36 | 37 | /// Matches content in the right split group. 38 | var mainContent: XCUIElement { 39 | self.groups["MainContent"] 40 | } 41 | 42 | var chatWebView: XCUIElement { 43 | self.groups["ChatWebView"].webViews.firstMatch 44 | } 45 | 46 | var conversationInfo: XCUIElement { 47 | self.scrollViews["ConversationInfo"] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Prose/ProseUITests/RosterSelectionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import Foundation 7 | import XCTest 8 | 9 | final class RosterSelectionTests: XCTestCase { 10 | override func setUp() { 11 | self.continueAfterFailure = false 12 | } 13 | 14 | func testSwitchesConversationWhenSelectingRosterItem() { 15 | let app = XCUIApplication.launching(testCase: "roster-selection") 16 | 17 | app.sidebar.cells 18 | .containing(.staticText, identifier: "Oya Karaböcek").element.tap() 19 | 20 | XCTAssertTrue( 21 | app.chatWebView.staticTexts["Hello from oya.karabocek"] 22 | .waitForExistence(timeout: 5) 23 | ) 24 | 25 | // Check that the MessageField placeholder is displayed correctly 26 | XCTAssertTrue(app.mainContent.staticTexts["Message Oya Karaböcek"].exists) 27 | 28 | app.sidebar.cells 29 | .containing(.staticText, identifier: "Donna Reed").element.tap() 30 | 31 | XCTAssertTrue( 32 | app.chatWebView.staticTexts["Donna Reed"] 33 | .waitForExistence(timeout: 5) 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Prose/UITestHost/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Prose/UITestHost/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Prose/UITestHost/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Prose/UITestHost/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Prose/UITestHost/UITestHost.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Prose/UITestHost/UITestHostApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of prose-app-macos. 3 | // Copyright (c) 2023 Prose Foundation 4 | // 5 | 6 | import AppKit 7 | import SwiftUI 8 | import TestHostApp 9 | 10 | @main 11 | struct UITestHostApp: App { 12 | var body: some Scene { 13 | TestScene() 14 | } 15 | } 16 | --------------------------------------------------------------------------------