├── .ci └── ExportOptions.plist ├── .circleci ├── config.yml ├── signing.key └── signing.pub ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── Gemfile ├── LICENSE ├── Podfile ├── Podfile.lock ├── README.md ├── Seaglass.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ └── Seaglass.xcscheme └── xcuserdata │ └── neilalexander.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ ├── Seaglass.xcscheme │ └── xcschememanagement.plist ├── Seaglass.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Seaglass ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── seaglass-128-1.png │ │ ├── seaglass-128-2.png │ │ ├── seaglass-16-1.png │ │ ├── seaglass-16-2.png │ │ ├── seaglass-256-1.png │ │ ├── seaglass-256-2.png │ │ ├── seaglass-32-1.png │ │ ├── seaglass-32-2.png │ │ ├── seaglass-512-1.png │ │ └── seaglass-512-2.png │ ├── Contents.json │ ├── EmojiPicker.imageset │ │ ├── Contents.json │ │ ├── emojix1.png │ │ └── emojix2.png │ ├── MadeForMatrix.imageset │ │ ├── Contents.json │ │ ├── Made for Matrix - Preferred.pdf │ │ └── Made for Matrix — Inverted.pdf │ └── PlaceholderGradient.imageset │ │ ├── Contents.json │ │ ├── gradient-1.png │ │ └── gradient.png ├── Base.lproj │ └── Main.storyboard ├── Controller │ ├── AboutViewController.swift │ ├── AppDefaults.swift │ ├── Embedded Views │ │ └── MemberListController.swift │ ├── Login Logout Views │ │ ├── LoginViewController.swift │ │ ├── LoginViewSettingsController.swift │ │ └── LogoutViewController.swift │ ├── Main View │ │ ├── MainViewController.swift │ │ ├── MainViewEncryptionController.swift │ │ ├── MainViewInviteController.swift │ │ ├── MainViewJoinController.swift │ │ ├── MainViewKeyRequestController.swift │ │ ├── MainViewMessageInfoController.swift │ │ ├── MainViewPartController.swift │ │ ├── MainViewRoomController.swift │ │ ├── MainViewRoomsController.swift │ │ └── MainViewSendErrorController.swift │ ├── MenuController.swift │ ├── Popover Views │ │ ├── PopoverEncryptionDevice.swift │ │ ├── PopoverRoomActions.swift │ │ ├── PopoverRoomList.swift │ │ └── PopoverUserList.swift │ ├── Room Settings Views │ │ ├── RoomAliasesController.swift │ │ ├── RoomPowerLevelsController.swift │ │ └── RoomSettingsController.swift │ ├── Segues │ │ └── LoginSuccessfulSegue.swift │ ├── User Settings Views │ │ ├── UserSettingsEncryptionController.swift │ │ ├── UserSettingsProfileController.swift │ │ ├── UserSettingsTabController.swift │ │ └── UserSettingsTabViewController.swift │ └── Windows │ │ ├── LoginWindowController.swift │ │ ├── MainWindowController.swift │ │ └── UserSettingsWindowController.swift ├── Extension │ ├── NSImage.swift │ ├── NSImageView.swift │ └── String.swift ├── Info.plist ├── Model │ ├── MatrixRoomCache.swift │ └── MatrixServices.swift ├── Seaglass.entitlements ├── Seaglass.xcdatamodeld │ ├── .xccurrentversion │ └── Matrix.xcdatamodel │ │ └── contents └── Subclass │ ├── Image Views │ ├── AvatarImageView.swift │ ├── ContextImageView.swift │ └── InlineImageView.swift │ ├── Member Lists │ ├── MemberListEntry.swift │ ├── MemberListTableView.swift │ └── MembersCacheEntry.swift │ ├── Room Lists │ ├── RoomAliasEntry.swift │ ├── RoomListEntry.swift │ └── RoomsCacheEntry.swift │ ├── Room Message Types │ ├── RoomMessage.swift │ ├── RoomMessageIncoming.swift │ ├── RoomMessageIncomingCoalesced.swift │ ├── RoomMessageInline.swift │ ├── RoomMessageOutgoing.swift │ └── RoomMessageOutgoingCoalesced.swift │ └── Room View │ ├── AutoGrowingTextField.swift │ ├── MainViewTableView.swift │ ├── MessageInputField.swift │ ├── MessageInputField.xib │ ├── RoomMessageEntry.swift │ └── VibrancyArea.swift ├── SeaglassTests ├── Info.plist └── SeaglassTests.swift ├── SeaglassUITests ├── Info.plist └── SeaglassUITests.swift ├── appcast.xml ├── dsa_pub.pem ├── fastlane ├── Fastfile ├── Gymfile ├── README.md └── actions │ └── sparkle_add_update.rb ├── image.png └── scripts ├── set_build_number.sh └── version.sh /.ci/ExportOptions.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | method 6 | mac-application 7 | 8 | 9 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | macos: 5 | xcode: "10.2.1" 6 | # https://circleci.com/docs/2.0/testing-ios/#supported-xcode-versions 7 | working_directory: ~/seaglass 8 | shell: /bin/bash --login -o pipefail 9 | environment: 10 | LC_ALL: en_US.UTF-8 11 | LANG: en_US.UTF-8 12 | steps: 13 | - checkout 14 | - run: 15 | name: Create Upload Directory 16 | command: mkdir -p /tmp/seaglass/upload 17 | - run: 18 | name: Update Fastlane 19 | command: "sudo gem update fastlane" 20 | - run: 21 | name: Decrypt signing key 22 | command: | 23 | if [ "${CIRCLE_BRANCH}" == "release" ]; then 24 | openssl aes-256-cbc -d -in .circleci/signing.key -k $SPARKLE_EDDSA_SECRET >> /tmp/dsa_priv.pem || true 25 | fi 26 | - run: 27 | name: Install AWS CLI 28 | command: | 29 | if [ "${CIRCLE_BRANCH}" == "release" ]; then 30 | pip install awscli --upgrade --user 31 | ~/Library/Python/2.7/bin/aws configure set aws_access_key_id ${AWS_ACCESS_KEY_ID} 32 | ~/Library/Python/2.7/bin/aws configure set aws_secret_access_key ${AWS_SECRET_ACCESS_KEY} 33 | ~/Library/Python/2.7/bin/aws configure set region eu-west-2 34 | fi 35 | - run: 36 | name: Retrieve appcast.xml 37 | command: | 38 | if [ "${CIRCLE_BRANCH}" == "release" ]; then 39 | ~/Library/Python/2.7/bin/aws s3 cp s3://seaglass-ci/appcast.xml ~/seaglass/ --acl public-read || true; 40 | fi 41 | - run: 42 | name: Fetch CocoaPods Specs 43 | command: curl https://cocoapods-specs.circleci.com/fetch-cocoapods-repo-from-s3.sh | bash -s cf 44 | - run: 45 | name: Install CocoaPods 46 | command: | 47 | pod install --verbose 48 | - run: 49 | name: Build Seaglass 50 | command: | 51 | if [ "${CIRCLE_BRANCH}" == "release" ]; then 52 | fastlane build_and_release; 53 | else 54 | fastlane build; 55 | fi 56 | - run: 57 | name: Upload appcast.xml to S3 58 | command: | 59 | if [ "${CIRCLE_BRANCH}" == "release" ]; then 60 | ~/Library/Python/2.7/bin/aws s3 cp ~/seaglass/appcast.xml s3://seaglass-ci/ --acl public-read; 61 | fi 62 | - store_artifacts: 63 | path: /tmp/seaglass/upload 64 | destination: / 65 | -------------------------------------------------------------------------------- /.circleci/signing.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neilalexander/seaglass/dc9789334340753c64131f9dace677be6d16bc49/.circleci/signing.key -------------------------------------------------------------------------------- /.circleci/signing.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIGRzCCBDoGByqGSM44BAEwggQtAoICAQDuCMNJthX1wyW+PcCve2sPeUUO9xxJ 3 | MhY5u/Rzd8+v+MsAVlASA8YMBJq+PMjFZknhiYG5dUEcMCJUxpv0zRtO+WKkS8ZW 4 | 8fIf2DvBnQiYT70y+F8798+OvEX++1+UVfhNac07ImZZvcTHrYvI3Yxwj4H/u/0M 5 | dcslJudxd+EmhXFX5zxQvn1x9sLGVnn8/c08T9PsfkceNW1+SPs/eSzHANHUVwUS 6 | vqwX+ecLYZJhCCND7FvWpa+I/I6LKO6t8J/tsSRjvfkeYN/MvbhHSaRC5yF5m41Y 7 | TjLWxDsIKVT/GumqLuHxHxhsb1EXEqu2ae3VIlmTYM+dDdKZlNOHOqwKXlaobo3U 8 | QxeBSxqB3JXtie/hvgo0L1iWKKuiouwVHMLF1P+atVFmdJNS3Ultb6TcNOLfd6Az 9 | jsSGImbDuSUZJWG15wMQhWwpcbr9j+xw4LYcvef5TWsLWsHa3Gn/RqmhXtl5piFe 10 | gvAQjcjIYzS+s5M0O86R+ODaOQwlAU+sMIpbo5stWq9MCwS7b1dsMw8AjOAgoWCF 11 | 2danDt28wdw/jz7iFbu3T3TSD55nwWodGpmkIrB1gyfC3D83RfRdd+cmw3vnTgcs 12 | G+iYnkiS9zkFeGzMYd24EcYutssKEf4egduCs2IKEy+LsKygXf4nPngntJ8OQ0Pk 13 | q3tTnUOx+j9nRwIhAMTehjdQwNHB8xFzKKvKXDw2LoUiSe3uAdtC/ERkuq/pAoIC 14 | AQDCGTZhHfxCOuTNCbWyqqSV60SY/4AAzz+sw2dzQAt/3sbP7oWzWPsqgD3zPE5B 15 | zso/DBNWhX3x5DEev/MlD5mu6OWMxxA5r02RqXQfIIQEOybS525+y3QNE6K6kSO7 16 | GU7nsheg3e3G/WbMQC/zPxUBphSdvWUXNprDvp583eqJqKca+66M1TNFx/PxQ6Dk 17 | J6DsPbeBhqVHKSptigjxZ69Dl27ptH/bz5ntiOFNWALPX6+wqrzeKTIgT2iE0KHv 18 | zWNCQO0xF37STJMLCV0W7oTi0yiOFeGzE6KTkNW8is8Oa58NFy1Ek4Yk1d4DXUSS 19 | dg4K5dCO5vDI33FPKNogcvH8YspLO6cIZYiA2iryPXoCNnbYP2DLJbugjerQG1DU 20 | 7VeCIgcSEOyCKWOFRPKDZox9dQ01grddYYxrUIxqnjDqDJGh7oeVaG/2L72nucbW 21 | 8lJPP0r8/PJ+yWbAhS6mme7aCIixk9m0GGqr5soo3HGMUayJoHdNbR9QZ2co+u37 22 | sX6cSUa7RkdM+4BPlL+NSe0YP8L6Nii4xbfZFQTsFWdglSjJTXlGBJ+3/lS1VGWV 23 | qLmk+oCxnsKcCWttfxx6HtmyLLt7NMXDSAxaPOaQCwmC2g8O+BhtSgJfkZ8N1vMh 24 | lORr3gXh7y/FRmkdtmKTTp857IjUwAbDUAGi633ajw4atwOCAgUAAoICAFP5tj7G 25 | rlweIVN+Y/Qs5UvAgUG/VFSp6E5aJZG5LhA1GMmftiTJwJYV9nWC7VMEu8iC/RIB 26 | MuthWs4bJbPkeV2p+hD0edGB+lxBvwi8xnCMxt7J/hLPJ5C7RQ802w8JLDTMsMmh 27 | HR7z+oBosGAsWeEclDHU6HCVuecteKPS9LLxEgHZ4engIsJ84+31ZT0bmEMjH9kw 28 | DTVIZnX8mXp0Z+QGu0K+LpTWkghZtAI7Yrs4sTAI+d0HTOyr31svs9PPK3nx0INS 29 | LSUjb/jVBzdnSHZuI33s5ljXg2ZCOKfDxwXRD/RBz+Eq37TI0WMj77cJrWJ14eRo 30 | c9+dqlt++ueSMba3Za+f4kaMSGiMXIW9RSxbbBXJDETs8tlCqhbQQ4vNau6Qq3GD 31 | BXQ4aNuqgtq/9PkS3Hf5LO3kgtNd26I8zIkzmuAA/CPCAsXC0i1n2FiK3k9mzGif 32 | iiJcIog/gUiPpcXsItN9WAYizgFxnLr5CI76q1mUXCkyOCDcbzuaqNLvt9Tp/tS7 33 | /NKKAtah8Sk+Z9o75boWMyWLe0+aVFDOz8e2CbkJnUaDEoEY4GNPKOtcWVS7bOKH 34 | mqNZAl+kxB/dmDwLkvQrX7VkuRjdT1tc8wj2I1IxXfOcd/XV4+AUxzci3fJfN8x9 35 | LcGU6BcWTtXM5+yiSuMkE0EXsnAD7JzeNCOP 36 | -----END PUBLIC KEY----- 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 21 | 22 | ## Specifications 23 | 24 | - Seaglass Version: 25 | - macOS Version: 26 | 27 | ## Expected Behavior 28 | 29 | 30 | 31 | ## Actual Behavior 32 | 33 | 34 | 35 | ## Steps to Reproduce the Problem 36 | 37 | 1. 38 | 2. 39 | 3. 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/xcode,cocoapods 2 | 3 | ### CocoaPods ### 4 | ## CocoaPods GitIgnore Template 5 | 6 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing 7 | # - Also handy if you have a large number of dependant pods 8 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE 9 | Pods/ 10 | 11 | ### Xcode ### 12 | # Xcode 13 | # 14 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 15 | 16 | ## User settings 17 | xcuserdata/ 18 | 19 | ### Xcode Patch ### 20 | *.xcodeproj/* 21 | !*.xcodeproj/project.pbxproj 22 | !*.xcodeproj/xcshareddata/ 23 | !*.xcworkspace/contents.xcworkspacedata 24 | /*.gcno 25 | 26 | # End of https://www.gitignore.io/api/xcode,cocoapods 27 | 28 | build/ 29 | 30 | fastlane/report.xml 31 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: osx 2 | language: swift 3 | cache: cocoapods 4 | install: 5 | - git config remote.origin.fetch +refs/heads/*:refs/remotes/origin/* 6 | - git fetch --unshallow --tags 7 | before_script: 8 | - set -o pipefail 9 | - pod install --repo-update 10 | script: 11 | - xcodebuild -workspace Seaglass.xcworkspace -scheme Seaglass archive -archivePath 12 | $PWD/build/Seaglass.xcarchive | xcpretty 13 | - xcodebuild -exportArchive -archivePath $PWD/build/Seaglass.xcarchive -exportOptionsPlist 14 | .ci/ExportOptions.plist -exportPath $PWD/build CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO 15 | before_deploy: 16 | - cd build && tar -zcf Seaglass-$(cd $TRAVIS_BUILD_DIR && sh scripts/version.sh).tar.gz Seaglass.app && cd .. 17 | - sed -i -e "s/VERSION_NAME_VALUE/$(cd $TRAVIS_BUILD_DIR && sh scripts/version.sh)/g" .ci/bintray-release.json 18 | - sed -i -e "s/BINTRAY_USER/$BINTRAY_USER/g" .ci/bintray-release.json 19 | deploy: 20 | - provider: bintray 21 | file: .ci/bintray-release.json 22 | user: $BINTRAY_USER 23 | key: 24 | secure: $BINTRAY_TOKEN 25 | skip_cleanup: true 26 | on: master 27 | notifications: 28 | webhooks: 29 | urls: 30 | - "$MATRIX_WEBHOOK_URL" 31 | on_success: change 32 | on_failure: always 33 | on_start: never 34 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.13' 2 | inhibit_all_warnings! 3 | 4 | target 'Seaglass' do 5 | use_frameworks! 6 | 7 | pod 'SwiftMatrixSDK', '0.10.12' 8 | pod 'Down' 9 | pod 'TSMarkdownParser' 10 | pod 'Sparkle' 11 | pod 'LetsMove' 12 | end 13 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - AFNetworking (3.2.1): 3 | - AFNetworking/NSURLSession (= 3.2.1) 4 | - AFNetworking/Reachability (= 3.2.1) 5 | - AFNetworking/Security (= 3.2.1) 6 | - AFNetworking/Serialization (= 3.2.1) 7 | - AFNetworking/UIKit (= 3.2.1) 8 | - AFNetworking/NSURLSession (3.2.1): 9 | - AFNetworking/Reachability 10 | - AFNetworking/Security 11 | - AFNetworking/Serialization 12 | - AFNetworking/Reachability (3.2.1) 13 | - AFNetworking/Security (3.2.1) 14 | - AFNetworking/Serialization (3.2.1) 15 | - Down (0.5.2) 16 | - GZIP (1.2.2) 17 | - LetsMove (1.24) 18 | - OLMKit (2.2.2): 19 | - OLMKit/olmc (= 2.2.2) 20 | - OLMKit/olmcpp (= 2.2.2) 21 | - OLMKit/olmc (2.2.2) 22 | - OLMKit/olmcpp (2.2.2) 23 | - Realm (3.6.0): 24 | - Realm/Headers (= 3.6.0) 25 | - Realm/Headers (3.6.0) 26 | - Sparkle (1.20.0) 27 | - SwiftMatrixSDK (0.10.12): 28 | - AFNetworking (~> 3.2.0) 29 | - GZIP (~> 1.2.1) 30 | - OLMKit (~> 2.2.2) 31 | - Realm (~> 3.6.0) 32 | - TSMarkdownParser (2.1.5) 33 | 34 | DEPENDENCIES: 35 | - Down 36 | - LetsMove 37 | - Sparkle 38 | - SwiftMatrixSDK (= 0.10.12) 39 | - TSMarkdownParser 40 | 41 | SPEC REPOS: 42 | https://github.com/cocoapods/specs.git: 43 | - AFNetworking 44 | - Down 45 | - GZIP 46 | - LetsMove 47 | - OLMKit 48 | - Realm 49 | - Sparkle 50 | - SwiftMatrixSDK 51 | - TSMarkdownParser 52 | 53 | SPEC CHECKSUMS: 54 | AFNetworking: b6f891fdfaed196b46c7a83cf209e09697b94057 55 | Down: 587371ad58002b06023d9c602519216862c3acbc 56 | GZIP: 12374d285e3b5d46cfcd480700fcfc7e16caf4f1 57 | LetsMove: fefe56bc7bc7fb7d37049e28a14f297961229fc5 58 | OLMKit: b9d8c0ffee9ea8c45bc0aaa9afb47f93fba7efbd 59 | Realm: 08b464b462d4f31bbd4ba5f5a1c8722ef0a700b7 60 | Sparkle: 48999e7ee032f05ca05e28451eadf4af8ede6b44 61 | SwiftMatrixSDK: 793d7505afe6cb8aa563c2c4c94613ef279bca87 62 | TSMarkdownParser: 3224ea196ecc7148fa7e03bc438f9390d4edfb6d 63 | 64 | PODFILE CHECKSUM: 3d7addb7b20fc302245d8951a9721bc1542434b1 65 | 66 | COCOAPODS: 1.7.1 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Seaglass 2 | 3 | [![#seaglass:matrix.org](https://img.shields.io/matrix/seaglass:matrix.org.svg?label=%23seaglass:matrix.org)](https://matrix.to/#/#seaglass:matrix.org) 4 | [![CircleCI Build Status](https://circleci.com/gh/neilalexander/seaglass.svg?style=shield)](https://circleci.com/gh/neilalexander/seaglass) 5 | [![Stable Version](https://img.shields.io/badge/download-stable-green.svg)](https://github.com/neilalexander/seaglass/releases/latest) 6 | 7 | Seaglass is a truly native macOS client for Matrix. It is written in Swift and 8 | uses the Cocoa user interface framework. 9 | 10 | ![Screenshot of Seaglass](image.png) 11 | 12 | ## Install Seaglass 13 | 14 | You can [find the latest release on GitHub](https://github.com/neilalexander/seaglass/releases) 15 | or you can install Seaglass from Homebrew Cask. Either way, you'll be able to use the built 16 | in auto updating feature to ensure you have the latest version. 17 | 18 | ``` 19 | brew cask install seaglass 20 | ``` 21 | 22 | ## Building from source 23 | 24 | Use Xcode 9.4 or Xcode 10.0 on macOS 10.13. Seaglass may require macOS 10.13 as a 25 | result of using auto-layout for some table views, which seems to have been introduced 26 | with High Sierra. I hope to find an alternate way to relax this requirement. 27 | 28 | If you do not already have CocoaPods installed, then install it: 29 | ``` 30 | sudo gem install cocoapods 31 | ``` 32 | 33 | Clone the Seaglass repository and install dependencies: 34 | ``` 35 | git clone https://github.com/neilalexander/seaglass 36 | cd seaglass 37 | pod install 38 | ``` 39 | Open up `Seaglass.xcworkspace` in Xcode and build! 40 | 41 | ## Current features 42 | 43 | - Logging in to a homeserver you are already registered with 44 | - Creating and leaving rooms and direct chats 45 | - Joining and parting rooms 46 | - Inviting users to rooms (through `/invite`) 47 | - Emotes (using `/me`) 48 | - Message redaction 49 | - Posting text to rooms with Markdown formatting 50 | - Changing some room settings (history visibility, join rules, name, topic, aliases) 51 | - Message coalescing 52 | - End-to-end encryption 53 | - Enabling end-to-end encryption in rooms 54 | - Marking devices as verified or blacklisted 55 | - Exporting and importing encryption keys (compatible with Riot) 56 | - Requesting (and re-requesting) keys from other Matrix clients 57 | - Choosing whether to send encrypted messages to unverified devices 58 | - Viewing inline images and stickers 59 | - Links to non-image attachments 60 | 61 | ## Disclaimer 62 | 63 | At this stage it is early in development and stands a good chance of being buggy 64 | and unreliable. I'm also not a Swift expert - I only started using Swift three 65 | or four days before my initial commit - and this code is probably awful. You've 66 | been warned. :-) 67 | -------------------------------------------------------------------------------- /Seaglass.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Seaglass.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Seaglass.xcodeproj/xcuserdata/neilalexander.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /Seaglass.xcodeproj/xcuserdata/neilalexander.xcuserdatad/xcschemes/Seaglass.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 59 | 60 | 61 | 62 | 63 | 64 | 74 | 76 | 82 | 83 | 84 | 85 | 86 | 87 | 93 | 95 | 101 | 102 | 103 | 104 | 106 | 107 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /Seaglass.xcodeproj/xcuserdata/neilalexander.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Seaglass.xcscheme 8 | 9 | orderHint 10 | 6 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Seaglass.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Seaglass.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Seaglass/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | @NSApplicationMain 22 | class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { 23 | 24 | @IBOutlet var MenuSeaglass: NSMenu! 25 | 26 | func menuWillOpen(_ menu: NSMenu) { 27 | if menu == MenuSeaglass { 28 | let loggedIn = MatrixServices.inst.state == .started 29 | for menuItem in menu.items { 30 | if menuItem.tag == 1 { 31 | menuItem.isEnabled = loggedIn 32 | menuItem.state = .off 33 | } 34 | } 35 | } 36 | } 37 | 38 | func applicationDidFinishLaunching(_ aNotification: Notification) { 39 | AppDefaults.init() 40 | } 41 | 42 | func applicationWillTerminate(_ aNotification: Notification) { 43 | MatrixServices.inst.close() 44 | } 45 | 46 | func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { 47 | guard !flag else { return false } 48 | 49 | let identifier = "MainWindowController" 50 | let controller = NSStoryboard.main?.instantiateController(withIdentifier: identifier) as! MainWindowController 51 | 52 | if let window = controller.window { 53 | window.makeKeyAndOrderFront(self) 54 | window.setFrameAutosaveName("seaglass-main") 55 | return true 56 | } 57 | 58 | return false 59 | } 60 | 61 | @IBAction func logoutMenuItemSelected(_ sender: Any) { 62 | MatrixServices.inst.logout() 63 | } 64 | 65 | lazy var persistentContainer: NSPersistentContainer = { 66 | /* 67 | The persistent container for the application. This implementation 68 | creates and returns a container, having loaded the store for the 69 | application to it. This property is optional since there are legitimate 70 | error conditions that could cause the creation of the store to fail. 71 | */ 72 | let container = NSPersistentContainer(name: "Matrix") 73 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in 74 | if let error = error { 75 | // Replace this implementation with code to handle the error appropriately. 76 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 77 | 78 | /* 79 | Typical reasons for an error here include: 80 | * The parent directory does not exist, cannot be created, or disallows writing. 81 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 82 | * The device is out of space. 83 | * The store could not be migrated to the current model version. 84 | Check the error message to determine what the actual problem was. 85 | */ 86 | fatalError("Unresolved error \(error)") 87 | } 88 | }) 89 | return container 90 | }() 91 | 92 | @IBAction func saveAction(_ sender: AnyObject?) { 93 | let context = persistentContainer.viewContext 94 | 95 | if !context.commitEditing() { 96 | NSLog("\(NSStringFromClass(type(of: self))) unable to commit editing before saving") 97 | } 98 | if context.hasChanges { 99 | do { 100 | try context.save() 101 | } catch { 102 | let nserror = error as NSError 103 | NSApplication.shared.presentError(nserror) 104 | } 105 | } 106 | } 107 | 108 | func windowWillReturnUndoManager(window: NSWindow) -> UndoManager? { 109 | return persistentContainer.viewContext.undoManager 110 | } 111 | 112 | func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { 113 | return .terminateNow 114 | } 115 | 116 | func applicationWillFinishLaunching(_ notification: Notification) { 117 | print("Seaglass version: " + (Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String)) 118 | 119 | // if you checked the box that said "Do not show this message again" but want see the Move to Applications dialog box again, run: 120 | // defaults write eu.neilalexander.seaglass moveToApplicationsFolderAlertSuppress NO 121 | #if RELEASE 122 | PFMoveToApplicationsFolderIfNecessary(); 123 | #endif 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /Seaglass/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "seaglass-16-1.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "seaglass-16-2.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "seaglass-32-1.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "seaglass-32-2.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "seaglass-128-1.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "seaglass-128-2.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "seaglass-256-1.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "seaglass-256-2.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "seaglass-512-1.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "seaglass-512-2.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /Seaglass/Assets.xcassets/AppIcon.appiconset/seaglass-128-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neilalexander/seaglass/dc9789334340753c64131f9dace677be6d16bc49/Seaglass/Assets.xcassets/AppIcon.appiconset/seaglass-128-1.png -------------------------------------------------------------------------------- /Seaglass/Assets.xcassets/AppIcon.appiconset/seaglass-128-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neilalexander/seaglass/dc9789334340753c64131f9dace677be6d16bc49/Seaglass/Assets.xcassets/AppIcon.appiconset/seaglass-128-2.png -------------------------------------------------------------------------------- /Seaglass/Assets.xcassets/AppIcon.appiconset/seaglass-16-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neilalexander/seaglass/dc9789334340753c64131f9dace677be6d16bc49/Seaglass/Assets.xcassets/AppIcon.appiconset/seaglass-16-1.png -------------------------------------------------------------------------------- /Seaglass/Assets.xcassets/AppIcon.appiconset/seaglass-16-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neilalexander/seaglass/dc9789334340753c64131f9dace677be6d16bc49/Seaglass/Assets.xcassets/AppIcon.appiconset/seaglass-16-2.png -------------------------------------------------------------------------------- /Seaglass/Assets.xcassets/AppIcon.appiconset/seaglass-256-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neilalexander/seaglass/dc9789334340753c64131f9dace677be6d16bc49/Seaglass/Assets.xcassets/AppIcon.appiconset/seaglass-256-1.png -------------------------------------------------------------------------------- /Seaglass/Assets.xcassets/AppIcon.appiconset/seaglass-256-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neilalexander/seaglass/dc9789334340753c64131f9dace677be6d16bc49/Seaglass/Assets.xcassets/AppIcon.appiconset/seaglass-256-2.png -------------------------------------------------------------------------------- /Seaglass/Assets.xcassets/AppIcon.appiconset/seaglass-32-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neilalexander/seaglass/dc9789334340753c64131f9dace677be6d16bc49/Seaglass/Assets.xcassets/AppIcon.appiconset/seaglass-32-1.png -------------------------------------------------------------------------------- /Seaglass/Assets.xcassets/AppIcon.appiconset/seaglass-32-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neilalexander/seaglass/dc9789334340753c64131f9dace677be6d16bc49/Seaglass/Assets.xcassets/AppIcon.appiconset/seaglass-32-2.png -------------------------------------------------------------------------------- /Seaglass/Assets.xcassets/AppIcon.appiconset/seaglass-512-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neilalexander/seaglass/dc9789334340753c64131f9dace677be6d16bc49/Seaglass/Assets.xcassets/AppIcon.appiconset/seaglass-512-1.png -------------------------------------------------------------------------------- /Seaglass/Assets.xcassets/AppIcon.appiconset/seaglass-512-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neilalexander/seaglass/dc9789334340753c64131f9dace677be6d16bc49/Seaglass/Assets.xcassets/AppIcon.appiconset/seaglass-512-2.png -------------------------------------------------------------------------------- /Seaglass/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Seaglass/Assets.xcassets/EmojiPicker.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "emojix1.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "emojix2.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /Seaglass/Assets.xcassets/EmojiPicker.imageset/emojix1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neilalexander/seaglass/dc9789334340753c64131f9dace677be6d16bc49/Seaglass/Assets.xcassets/EmojiPicker.imageset/emojix1.png -------------------------------------------------------------------------------- /Seaglass/Assets.xcassets/EmojiPicker.imageset/emojix2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neilalexander/seaglass/dc9789334340753c64131f9dace677be6d16bc49/Seaglass/Assets.xcassets/EmojiPicker.imageset/emojix2.png -------------------------------------------------------------------------------- /Seaglass/Assets.xcassets/MadeForMatrix.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Made for Matrix - Preferred.pdf" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "Made for Matrix — Inverted.pdf", 10 | "appearances" : [ 11 | { 12 | "appearance" : "luminosity", 13 | "value" : "dark" 14 | } 15 | ] 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | }, 22 | "properties" : { 23 | "preserves-vector-representation" : true 24 | } 25 | } -------------------------------------------------------------------------------- /Seaglass/Assets.xcassets/MadeForMatrix.imageset/Made for Matrix - Preferred.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neilalexander/seaglass/dc9789334340753c64131f9dace677be6d16bc49/Seaglass/Assets.xcassets/MadeForMatrix.imageset/Made for Matrix - Preferred.pdf -------------------------------------------------------------------------------- /Seaglass/Assets.xcassets/MadeForMatrix.imageset/Made for Matrix — Inverted.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neilalexander/seaglass/dc9789334340753c64131f9dace677be6d16bc49/Seaglass/Assets.xcassets/MadeForMatrix.imageset/Made for Matrix — Inverted.pdf -------------------------------------------------------------------------------- /Seaglass/Assets.xcassets/PlaceholderGradient.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "filename" : "gradient.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "filename" : "gradient-1.png", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | } 18 | } -------------------------------------------------------------------------------- /Seaglass/Assets.xcassets/PlaceholderGradient.imageset/gradient-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neilalexander/seaglass/dc9789334340753c64131f9dace677be6d16bc49/Seaglass/Assets.xcassets/PlaceholderGradient.imageset/gradient-1.png -------------------------------------------------------------------------------- /Seaglass/Assets.xcassets/PlaceholderGradient.imageset/gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neilalexander/seaglass/dc9789334340753c64131f9dace677be6d16bc49/Seaglass/Assets.xcassets/PlaceholderGradient.imageset/gradient.png -------------------------------------------------------------------------------- /Seaglass/Controller/AboutViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | class AboutViewController: NSViewController { 22 | 23 | @IBOutlet weak var versionTextField: NSTextField! 24 | 25 | @IBAction func viewSourceCodeButtonPressed(_: NSButton) { 26 | guard let sourceURL = URL(string: "https://github.com/neilalexander/seaglass") else { return } 27 | NSWorkspace.shared.open(sourceURL) 28 | } 29 | 30 | override func viewDidLoad() { 31 | super.viewDidLoad() 32 | 33 | (self.view as! NSVisualEffectView).material = .menu 34 | (self.view as! NSVisualEffectView).isEmphasized = true 35 | 36 | let appVersionString: String = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String 37 | 38 | versionTextField.stringValue = "Version " + appVersionString 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /Seaglass/Controller/AppDefaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Foundation 20 | 21 | final class AppDefaults { 22 | 23 | static let shared = AppDefaults() 24 | 25 | struct Key { 26 | static let showMostRecentMessageInSidebar = "showMostRecentMessageInSidebar" 27 | } 28 | 29 | @discardableResult init() { 30 | let defaults: [String: Any] = [ 31 | Key.showMostRecentMessageInSidebar: true 32 | ] 33 | 34 | UserDefaults.standard.register(defaults: defaults) 35 | } 36 | 37 | var showMostRecentMessageInSidebar: Bool { 38 | get { 39 | return bool(for: Key.showMostRecentMessageInSidebar) 40 | } 41 | set { 42 | setBool(for: Key.showMostRecentMessageInSidebar, newValue) 43 | } 44 | } 45 | } 46 | 47 | private extension AppDefaults { 48 | 49 | func bool(for key: String) -> Bool { 50 | return UserDefaults.standard.bool(forKey: key) 51 | } 52 | 53 | func setBool(for key: String, _ flag: Bool) { 54 | UserDefaults.standard.set(flag, forKey: key) 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /Seaglass/Controller/Embedded Views/MemberListController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | import SwiftMatrixSDK 21 | 22 | class MemberListController: NSViewController, NSTableViewDelegate, NSTableViewDataSource { 23 | @IBOutlet var membersCacheController: NSArrayController! 24 | 25 | @IBOutlet var MemberSearch: NSSearchField! 26 | 27 | public var roomId: String = "" 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | 32 | if roomId == "" { 33 | return 34 | } 35 | 36 | for member in MatrixServices.inst.session.room(withRoomId: roomId).state.members { 37 | membersCacheController.insert(MembersCacheEntry(member), atArrangedObjectIndex: 0) 38 | } 39 | 40 | let membercount = (membersCacheController.arrangedObjects as! [MXRoomMember]).count 41 | 42 | MemberSearch.placeholderString = "Search \(membercount) member" 43 | if membercount != 1 { 44 | MemberSearch.placeholderString?.append(contentsOf: "s") 45 | } 46 | } 47 | 48 | func numberOfRows(in tableView: NSTableView) -> Int { 49 | if roomId == "" { 50 | return 0 51 | } 52 | return (membersCacheController.arrangedObjects as! [MXRoomMember]).count 53 | } 54 | 55 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { 56 | let room = MatrixServices.inst.session.room(withRoomId: roomId) 57 | let member: MembersCacheEntry = (membersCacheController.arrangedObjects as! [MembersCacheEntry])[row] 58 | var powerlevel = 0 59 | if room?.state.powerLevels != nil { 60 | powerlevel = room?.state.powerLevels.powerLevelOfUser(withUserID: member.userId) ?? 0 61 | } 62 | 63 | let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "MemberListEntry"), owner: self) as? MemberListEntry 64 | 65 | cell?.MemberName.stringValue = member.name() 66 | cell?.MemberDescription.stringValue = "Power level \(powerlevel)" 67 | cell?.MemberIcon.setAvatar(forUserId: member.userId) 68 | 69 | cell?.identifier = nil 70 | return cell 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Seaglass/Controller/Login Logout Views/LoginViewSettingsController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | class LoginViewSettingsController: NSViewController { 22 | 23 | @IBOutlet var HomeserverURLField: NSTextField! 24 | @IBOutlet var DisableCacheCheckbox: NSButton! 25 | 26 | let defaults = UserDefaults.standard 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | 31 | if defaults.string(forKey: "Homeserver") != nil { 32 | HomeserverURLField.stringValue = defaults.string(forKey: "Homeserver")! 33 | } 34 | 35 | if defaults.bool(forKey: "DisableCache") { 36 | DisableCacheCheckbox.state = .on 37 | } 38 | } 39 | 40 | override func viewWillDisappear() { 41 | homeserverURLFieldEdited(sender: HomeserverURLField) 42 | } 43 | 44 | @IBAction func homeserverURLFieldEdited(sender: NSTextField) { 45 | if URL(string: HomeserverURLField.stringValue) == nil { 46 | defaults.setValue("https://matrix.org", forKey: "Homeserver") 47 | } else { 48 | defaults.setValue(HomeserverURLField.stringValue, forKey: "Homeserver") 49 | } 50 | } 51 | 52 | @IBAction func disableCacheCheckboxEdited(sender: NSButton) { 53 | defaults.setValue(DisableCacheCheckbox.state == .on, forKey: "DisableCache") 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Seaglass/Controller/Login Logout Views/LogoutViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | class LogoutViewController: NSViewController { 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | MatrixServices.inst.logout() 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Seaglass/Controller/Main View/MainViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | import SwiftMatrixSDK 21 | 22 | class MainViewController: NSSplitViewController, MatrixServicesDelegate { 23 | 24 | let defaults = UserDefaults.standard 25 | 26 | weak var roomsController: MainViewRoomsController? 27 | weak var channelController: MainViewRoomController? 28 | 29 | weak var servicesDelegate: MatrixServicesDelegate? 30 | weak var roomsDelegate: MatrixRoomsDelegate? 31 | weak var channelDelegate: MatrixRoomDelegate? 32 | 33 | var keyRequests: [MainViewKeyRequestController] = [] 34 | 35 | required init?(coder: NSCoder) { 36 | super.init(coder: coder) 37 | } 38 | 39 | override func viewDidLoad() { 40 | MatrixServices.inst.mainController = self 41 | 42 | roomsController = children.compactMap({ return $0 as? MainViewRoomsController }).first 43 | channelController = children.compactMap({ return $0 as? MainViewRoomController }).first 44 | 45 | roomsController?.mainController = self 46 | channelController?.mainController = self 47 | 48 | servicesDelegate = self 49 | roomsDelegate = roomsController 50 | channelDelegate = channelController 51 | 52 | super.viewDidLoad() 53 | } 54 | 55 | func matrixNetworkConnectivityChanged(wifi: Bool, wwan: Bool) { 56 | 57 | } 58 | 59 | func matrixDidLogin(_ session: MXSession) { 60 | } 61 | 62 | func matrixWillLogout() { 63 | defaults.set(false, forKey: "LoginAutomatically") 64 | defaults.removeObject(forKey: "AccessToken") 65 | defaults.removeObject(forKey: "HomeServer") 66 | defaults.removeObject(forKey: "UserID") 67 | } 68 | 69 | func matrixDidLogout() { 70 | view.window?.close() 71 | NSAnimationContext.runAnimationGroup({ (context) in 72 | context.duration = 0.5 73 | view.window?.animator().alphaValue = 0 74 | }, completionHandler: { 75 | NSApplication.shared.terminate(self) 76 | }) 77 | } 78 | 79 | func matrixDidReceiveKeyRequest(_ request: MXIncomingRoomKeyRequest) { 80 | guard request.userId == MatrixServices.inst.session.myUser.userId else { return } 81 | guard (request.requestBody["sender_key"] as? String) != nil else { return } 82 | guard !keyRequests.contains(where: { $0.request!.deviceId == request.deviceId }) else { return } 83 | 84 | MatrixServices.inst.session.crypto.deviceList.downloadKeys([request.userId], forceDownload: false, success: { (devicemap) in 85 | if MatrixServices.inst.session.crypto.deviceList.storedDevice(request.userId, deviceId: request.deviceId) != nil { 86 | DispatchQueue.main.async { 87 | let sheet = self.storyboard?.instantiateController(withIdentifier: "KeyRequest") as! MainViewKeyRequestController 88 | sheet.request = request 89 | self.presentAsSheet(sheet) 90 | self.keyRequests.append(sheet) 91 | } 92 | } 93 | }) { (error) in 94 | } 95 | } 96 | 97 | func matrixDidReceiveKeyRequestCancellation(_ cancellation: MXIncomingRoomKeyRequestCancellation) { 98 | for (index, controller) in keyRequests.enumerated() { 99 | guard cancellation.deviceId == controller.request!.deviceId else { continue } 100 | guard cancellation.userId == controller.request!.userId else { continue } 101 | if controller.request!.requestId == cancellation.requestId { 102 | controller.dismiss(self) 103 | keyRequests.remove(at: index) 104 | } 105 | } 106 | } 107 | 108 | func matrixDidCompleteKeyRequest(_ request: MXIncomingRoomKeyRequest) { 109 | for (index, controller) in keyRequests.enumerated() { 110 | guard request.deviceId == controller.request!.deviceId else { continue } 111 | guard request.userId == controller.request!.userId else { continue } 112 | if controller.request!.requestId == request.requestId { 113 | keyRequests.remove(at: index) 114 | } 115 | } 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /Seaglass/Controller/Main View/MainViewEncryptionController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | class MainViewEncryptionController: NSViewController { 22 | 23 | @IBOutlet weak var EnableEncryptionCheckbox: NSButton! 24 | @IBOutlet weak var PreventUnverifiedCheckbox: NSButton! 25 | @IBOutlet weak var ConfirmButton: NSButton! 26 | @IBOutlet weak var ConfirmSpinner: NSProgressIndicator! 27 | 28 | var roomId: String = "" 29 | 30 | override func viewDidLoad() { 31 | super.viewDidLoad() 32 | 33 | ConfirmButton.isEnabled = false 34 | ConfirmButton.alphaValue = 1 35 | ConfirmSpinner.alphaValue = 0 36 | 37 | if let room = MatrixServices.inst.session.room(withRoomId: roomId) { 38 | EnableEncryptionCheckbox.state = room.state.isEncrypted ? .on : .off 39 | EnableEncryptionCheckbox.isEnabled = EnableEncryptionCheckbox.state == .off 40 | } else { 41 | EnableEncryptionCheckbox.isEnabled = false 42 | } 43 | 44 | PreventUnverifiedCheckbox.state = MatrixServices.inst.session.crypto.warnOnUnknowDevices ? .on : .off 45 | } 46 | 47 | 48 | @IBAction func allowUnverifiedCheckboxChanged(_ sender: NSButton) { 49 | guard sender == PreventUnverifiedCheckbox else { return } 50 | 51 | MatrixServices.inst.session.crypto.warnOnUnknowDevices = PreventUnverifiedCheckbox.state == .on 52 | UserDefaults.standard.set(MatrixServices.inst.session.crypto.warnOnUnknowDevices, forKey: "CryptoParanoid") 53 | } 54 | 55 | @IBAction func enableEncryptionCheckboxChanged(_ sender: NSButton) { 56 | guard sender == EnableEncryptionCheckbox else { return } 57 | 58 | ConfirmButton.isEnabled = 59 | EnableEncryptionCheckbox.isEnabled && 60 | EnableEncryptionCheckbox.state == .on 61 | } 62 | 63 | 64 | @IBAction func confirmButtonPressed(_ sender: NSButton) { 65 | guard sender == ConfirmButton else { return } 66 | guard EnableEncryptionCheckbox.isEnabled else { return } 67 | guard roomId != "" else { return } 68 | 69 | ConfirmButton.isEnabled = false 70 | ConfirmSpinner.startAnimation(self) 71 | 72 | NSAnimationContext.runAnimationGroup({ (context) in 73 | context.duration = 0.5 74 | ConfirmButton.animator().alphaValue = 0 75 | ConfirmSpinner.animator().alphaValue = 1 76 | }, completionHandler: { 77 | if let room = MatrixServices.inst.session.room(withRoomId: self.roomId) { 78 | room.enableEncryption(withAlgorithm: "m.megolm.v1.aes-sha2") { (response) in 79 | if response.isSuccess { 80 | self.dismiss(self) 81 | return 82 | } 83 | 84 | print("Failed to enable encryption: \(response.error!.localizedDescription)") 85 | self.dismiss(self) 86 | } 87 | } 88 | }) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Seaglass/Controller/Main View/MainViewInviteController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | import SwiftMatrixSDK 21 | 22 | class MainViewInviteController: NSViewController { 23 | 24 | @IBOutlet weak var InviteeMatrixID: NSTextField! 25 | @IBOutlet weak var InviteButton: NSButton! 26 | @IBOutlet weak var CancelButton: NSButton! 27 | @IBOutlet weak var InviteSpinner: NSProgressIndicator! 28 | 29 | var roomId: String? 30 | 31 | override func viewDidLoad() { 32 | super.viewDidLoad() 33 | 34 | for control in [ InviteeMatrixID, InviteButton, CancelButton ] as [NSControl] { 35 | control.isEnabled = true 36 | } 37 | InviteSpinner.isHidden = true 38 | } 39 | 40 | @IBAction func inviteButtonClicked(_ sender: NSButton) { 41 | guard sender == InviteButton else { return } 42 | guard roomId != "" else { return } 43 | 44 | let invitee = MXRoomInvitee.userId(String(InviteeMatrixID.stringValue).trimmingCharacters(in: .whitespacesAndNewlines)) 45 | 46 | for control in [ InviteeMatrixID, InviteButton, CancelButton ] as [NSControl] { 47 | control.isEnabled = false 48 | } 49 | InviteSpinner.isHidden = false 50 | InviteSpinner.startAnimation(self) 51 | 52 | let group = DispatchGroup() 53 | 54 | group.enter() 55 | MatrixServices.inst.session.room(withRoomId: roomId).invite(invitee) { (response) in 56 | if response.isFailure { 57 | let alert = NSAlert() 58 | alert.messageText = "Failed to invite user" 59 | alert.informativeText = response.error!.localizedDescription 60 | alert.alertStyle = .warning 61 | alert.addButton(withTitle: "OK") 62 | alert.runModal() 63 | } 64 | group.leave() 65 | } 66 | 67 | group.notify(queue: .main, execute: { 68 | self.InviteSpinner.isHidden = true 69 | self.InviteSpinner.stopAnimation(self) 70 | 71 | sender.window?.contentViewController?.dismiss(sender) 72 | }) 73 | } 74 | 75 | @IBAction func cancelButtonClicked(_ sender: NSButton) { 76 | guard sender == CancelButton else { return } 77 | self.dismiss(sender) 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /Seaglass/Controller/Main View/MainViewJoinController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | import SwiftMatrixSDK 21 | 22 | class MainViewJoinController: NSViewController { 23 | @IBOutlet weak var CreateRoomButton: NSButton! 24 | @IBOutlet weak var CreateRoomSpinner: NSProgressIndicator! 25 | @IBOutlet weak var CreateRoomName: NSTextField! 26 | @IBOutlet weak var JoinRoomButton: NSButton! 27 | @IBOutlet weak var JoinRoomSpinner: NSProgressIndicator! 28 | @IBOutlet weak var JoinRoomText: NSTextField! 29 | 30 | var controls: [NSControl] = [] 31 | 32 | override func viewDidLoad() { 33 | controls = [ CreateRoomButton, JoinRoomButton, 34 | JoinRoomText, CreateRoomName ] 35 | for control in controls { 36 | control.isEnabled = true 37 | } 38 | super.viewDidLoad() 39 | } 40 | 41 | @IBAction func createRoomButtonClicked(_ sender: NSButton) { 42 | let roomNameField = self.CreateRoomName.stringValue.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 43 | var roomName: String? = nil 44 | if roomNameField.count > 0 { 45 | roomName = roomNameField 46 | } 47 | for control in controls { 48 | control.isEnabled = false 49 | } 50 | CreateRoomSpinner.startAnimation(sender) 51 | NSAnimationContext.runAnimationGroup({ (context) in 52 | context.duration = 0.5 53 | CreateRoomButton.animator().alphaValue = 0 54 | CreateRoomSpinner.alphaValue = 1 55 | }, completionHandler: { 56 | MatrixServices.inst.session.createRoom(name: roomName, visibility: nil, alias: nil, topic: nil, preset: MXRoomPreset.publicChat, completion: { (response) in 57 | if response.isFailure, let error = response.error { 58 | let alert = NSAlert() 59 | alert.messageText = "Failed to create room" 60 | alert.informativeText = error.localizedDescription 61 | alert.alertStyle = .warning 62 | alert.addButton(withTitle: "OK") 63 | alert.runModal() 64 | } 65 | sender.window?.contentViewController?.dismiss(sender) 66 | }) 67 | }) 68 | } 69 | 70 | @IBAction func joinRoomButtonClicked(_ sender: NSButton) { 71 | let room = self.JoinRoomText.stringValue 72 | if !(room.starts(with: "#") || room.starts(with: "!") || room.starts(with: "@")) || !room.contains(":") { 73 | return 74 | } 75 | for control in controls { 76 | control.isEnabled = false 77 | } 78 | JoinRoomSpinner.startAnimation(sender) 79 | 80 | NSAnimationContext.runAnimationGroup({ (context) in 81 | context.duration = 0.5 82 | JoinRoomButton.animator().alphaValue = 0 83 | JoinRoomSpinner.alphaValue = 1 84 | }, completionHandler: { 85 | if room.starts(with: "#") || room.starts(with: "!") { 86 | MatrixServices.inst.session.joinRoom(room, completion: { (response) in 87 | if response.isFailure, let error = response.error { 88 | let alert = NSAlert() 89 | alert.messageText = "Failed to join room \(room)" 90 | alert.informativeText = error.localizedDescription 91 | alert.alertStyle = .warning 92 | alert.addButton(withTitle: "OK") 93 | alert.runModal() 94 | } 95 | sender.window?.contentViewController?.dismiss(sender) 96 | }) 97 | } else if room.starts(with: "@") { 98 | MatrixServices.inst.session.createRoom(name: nil, visibility: nil, alias: nil, topic: nil, preset: MXRoomPreset.trustedPrivateChat, completion: { (response) in 99 | if response.isFailure, let error = response.error { 100 | let alert = NSAlert() 101 | alert.messageText = "Failed to create room" 102 | alert.informativeText = error.localizedDescription 103 | alert.alertStyle = .warning 104 | alert.addButton(withTitle: "OK") 105 | alert.runModal() 106 | sender.window?.contentViewController?.dismiss(sender) 107 | } else { 108 | if let roomId = response.value?.roomId { 109 | let invitee = MXRoomInvitee.userId(room) 110 | MatrixServices.inst.session.room(withRoomId: roomId).invite(invitee, completion: { (response) in 111 | if response.isFailure, let error = response.error { 112 | let alert = NSAlert() 113 | alert.messageText = "Failed to invite \(room)" 114 | alert.informativeText = error.localizedDescription 115 | alert.alertStyle = .warning 116 | alert.addButton(withTitle: "OK") 117 | alert.runModal() 118 | } 119 | sender.window?.contentViewController?.dismiss(sender) 120 | }) 121 | } 122 | } 123 | }) 124 | } 125 | }) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Seaglass/Controller/Main View/MainViewKeyRequestController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | import SwiftMatrixSDK 21 | 22 | class MainViewKeyRequestController: NSViewController { 23 | 24 | @IBOutlet weak var UserIDField: NSTextField! 25 | @IBOutlet weak var DeviceNameField: NSTextField! 26 | @IBOutlet weak var DeviceIDField: NSTextField! 27 | @IBOutlet weak var DeviceKeyField: NSTextField! 28 | @IBOutlet weak var ConfirmationCheckbox: NSButton! 29 | @IBOutlet weak var ShareButton: NSButton! 30 | @IBOutlet weak var IgnoreButton: NSButton! 31 | 32 | var request: MXIncomingRoomKeyRequest? 33 | 34 | override func viewWillAppear() { 35 | guard request != nil else { return } 36 | 37 | if let storedDevice = MatrixServices.inst.session.crypto.deviceList.storedDevice(request!.userId, deviceId: request!.deviceId) { 38 | 39 | if let edkey = storedDevice.fingerprint { 40 | DeviceKeyField.stringValue = String(edkey.enumerated().map { $0 > 0 && $0 % 4 == 0 ? [" ", $1] : [$1]}.joined()) 41 | } 42 | 43 | DeviceNameField.stringValue = storedDevice.displayName ?? "" 44 | 45 | UserIDField.stringValue = request!.userId 46 | DeviceIDField.stringValue = request!.deviceId 47 | 48 | ConfirmationCheckbox.state = .off 49 | } else { 50 | let alert = NSAlert() 51 | alert.messageText = "Incoming keyshare request failed" 52 | alert.informativeText = "The device information was not available." 53 | alert.alertStyle = .warning 54 | alert.addButton(withTitle: "OK") 55 | alert.runModal() 56 | self.dismiss(self) 57 | } 58 | } 59 | 60 | @IBAction func shareButtonPressed(_ sender: NSButton) { 61 | guard request != nil else { return } 62 | guard sender == ShareButton else { return } 63 | guard ConfirmationCheckbox.state == .on else { return } 64 | 65 | MatrixServices.inst.session.crypto.acceptAllPendingKeyRequests(fromUser: request!.userId, andDevice: request!.deviceId) { 66 | MatrixServices.inst.session.crypto.setDeviceVerification(MXDeviceVerified, forDevice: self.self.request!.deviceId, ofUser: self.request!.userId, success: { 67 | MatrixServices.inst.mainController?.channelDelegate?.uiRoomNeedsCryptoReload() 68 | }, failure: { (error) in 69 | let alert = NSAlert() 70 | alert.messageText = "Failed to verify device" 71 | alert.informativeText = error!.localizedDescription 72 | alert.alertStyle = .warning 73 | alert.addButton(withTitle: "OK") 74 | alert.runModal() 75 | }) 76 | self.dismiss(sender) 77 | MatrixServices.inst.mainController?.matrixDidCompleteKeyRequest(self.request!) 78 | } 79 | } 80 | 81 | @IBAction func ignoreButtonPressed(_ sender: NSButton) { 82 | guard sender == IgnoreButton else { return } 83 | 84 | MatrixServices.inst.session.crypto.ignoreAllPendingKeyRequests(fromUser: request!.userId, andDevice: request!.deviceId) { 85 | self.dismiss(sender) 86 | MatrixServices.inst.mainController?.matrixDidCompleteKeyRequest(self.request!) 87 | } 88 | } 89 | 90 | @IBAction func confirmationCheckboxPressed(_ sender: NSButton) { 91 | guard sender == ConfirmationCheckbox else { return } 92 | ShareButton.isEnabled = sender.state == .on 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /Seaglass/Controller/Main View/MainViewMessageInfoController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | import SwiftMatrixSDK 21 | 22 | class MainViewMessageInfoController: NSViewController { 23 | 24 | @IBOutlet var EventSourceView: NSTextView! 25 | @IBOutlet weak var EventTimestamp: NSTextField! 26 | 27 | var event: MXEvent? 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | 32 | guard event != nil else { return } 33 | 34 | let eventTime = Date(timeIntervalSince1970: TimeInterval(event!.originServerTs / 1000)) 35 | let eventTimeFormatter = DateFormatter() 36 | eventTimeFormatter.timeZone = TimeZone.current 37 | eventTimeFormatter.timeStyle = .long 38 | eventTimeFormatter.dateStyle = .long 39 | 40 | EventTimestamp.stringValue = eventTimeFormatter.string(from: eventTime) 41 | do { 42 | let str = NSString(data: try JSONSerialization.data(withJSONObject: event!.jsonDictionary(), options: JSONSerialization.WritingOptions.prettyPrinted), encoding: String.Encoding.utf8.rawValue)! as String 43 | EventSourceView.string = str.replacingOccurrences(of: "\\/", with: "/") 44 | } catch { 45 | EventSourceView.string = "Exception caught" 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Seaglass/Controller/Main View/MainViewPartController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | class MainViewPartController: NSViewController { 22 | @IBOutlet weak var LeaveButton: NSButton! 23 | @IBOutlet weak var LeaveSpinner: NSProgressIndicator! 24 | 25 | var roomId: String = "" 26 | 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | 30 | LeaveButton.isEnabled = MatrixServices.inst.session.room(withRoomId: roomId) != nil 31 | } 32 | 33 | @IBAction func leaveButtonClicked(_ sender: NSButton) { 34 | if roomId == "" { 35 | return 36 | } 37 | 38 | LeaveButton.isEnabled = false 39 | LeaveSpinner.startAnimation(sender) 40 | 41 | NSAnimationContext.runAnimationGroup({ (context) in 42 | context.duration = 0.5 43 | LeaveButton.animator().alphaValue = 0 44 | LeaveSpinner.animator().alphaValue = 1 45 | }, completionHandler: { 46 | MatrixServices.inst.session.leaveRoom(self.roomId) { (response) in 47 | if response.isFailure, let error = response.error { 48 | let alert = NSAlert() 49 | alert.messageText = "Failed to leave room \(self.roomId)" 50 | alert.informativeText = error.localizedDescription 51 | alert.alertStyle = .warning 52 | alert.addButton(withTitle: "OK") 53 | alert.runModal() 54 | } 55 | sender.window?.contentViewController?.dismiss(sender) 56 | } 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Seaglass/Controller/Main View/MainViewSendErrorController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | import SwiftMatrixSDK 21 | 22 | class MainViewSendErrorController: NSViewController { 23 | @IBOutlet var ApplyAllCheckbox: NSButton! 24 | @IBOutlet var ErrorDescription: NSTextField! 25 | 26 | public var roomId: String? 27 | public var eventId: String? 28 | 29 | required init?(coder: NSCoder) { 30 | super.init(coder: coder) 31 | } 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | } 36 | 37 | @IBAction func ignoreButtonClicked(_ sender: NSButton) { 38 | self.dismiss(sender) 39 | } 40 | 41 | @IBAction func deleteButtonClicked(_ sender: NSButton) { 42 | if let room = MatrixServices.inst.session.room(withRoomId: roomId) { 43 | // TODO: FIX THIS 44 | /* if ApplyAllCheckbox.state == .off { 45 | if let index = (self.mainController?.channelDelegate?.roomCache.content as! [MXEvent]).index(where: { $0.eventId == eventId }) { 46 | self.mainController?.channelDelegate?.roomCache.remove(atArrangedObjectIndex: index) 47 | } 48 | 49 | } else { 50 | for (index, event) in MatrixServices.inst.eventCache[roomId!]!.enumerated() { 51 | if event.sentState == MXEventSentStateFailed { 52 | let event = MatrixServices.inst.eventCache[roomId!]![index] 53 | MatrixServices.inst.mainController?.channelDelegate?.matrixDidRoomMessage(event: event.prune(), direction: .forwards, roomState: room.state, replaces: eventId, removeOnReplace: true) 54 | MatrixServices.inst.eventCache[roomId!]![index] = event.prune() 55 | } 56 | } 57 | } */ 58 | } 59 | self.dismiss(sender) 60 | } 61 | 62 | @IBAction func sendAgainButtonClicked(_ sender: NSButton) { 63 | self.dismiss(sender) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Seaglass/Controller/MenuController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | class MenuController: NSMenu { 22 | 23 | @IBAction func focusOnRoomSearchField(_ sender: NSMenuItem) { 24 | sender.target = self 25 | 26 | let mainWindow = NSApplication.shared.mainWindow 27 | let splitViewController = mainWindow?.contentViewController as? NSSplitViewController 28 | let mainRoomsViewController = splitViewController?.splitViewItems.first?.viewController as? MainViewRoomsController 29 | 30 | if let searchField = mainRoomsViewController?.RoomSearch { 31 | searchField.selectText(sender) 32 | 33 | let lengthOfInput = NSString(string: searchField.stringValue).length 34 | searchField.currentEditor()?.selectedRange = NSMakeRange(lengthOfInput, 0) 35 | } 36 | } 37 | 38 | @IBAction func inviteButtonClicked(_ sender: NSMenuItem) { 39 | MatrixServices.inst.mainController?.channelDelegate?.uiRoomStartInvite() 40 | } 41 | 42 | @IBAction func gotoOldestLoaded(_ sender: NSMenuItem) { 43 | let mainWindow = NSApplication.shared.mainWindow 44 | let splitViewController = mainWindow?.contentViewController as? NSSplitViewController 45 | let mainRoomViewController = splitViewController?.splitViewItems.last?.viewController as? MainViewRoomController 46 | 47 | mainRoomViewController?.RoomMessageTableView.scrollToBeginningOfDocument(self) 48 | } 49 | 50 | @IBAction func gotoNewest(_ sender: NSMenuItem) { 51 | let mainWindow = NSApplication.shared.mainWindow 52 | let splitViewController = mainWindow?.contentViewController as? NSSplitViewController 53 | let mainRoomViewController = splitViewController?.splitViewItems.last?.viewController as? MainViewRoomController 54 | 55 | mainRoomViewController?.RoomMessageTableView.scrollToEndOfDocument(self) 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /Seaglass/Controller/Popover Views/PopoverEncryptionDevice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | import SwiftMatrixSDK 21 | 22 | class PopoverEncryptionDevice: NSViewController { 23 | 24 | @IBOutlet weak var DownloadSpinner: NSProgressIndicator! 25 | 26 | @IBOutlet weak var DeviceName: NSTextField! 27 | @IBOutlet weak var DeviceID: NSTextField! 28 | @IBOutlet weak var DeviceFingerprint: NSTextField! 29 | 30 | @IBOutlet weak var MessageAlgorithm: NSTextField! 31 | 32 | @IBOutlet weak var DeviceVerified: NSButton! 33 | @IBOutlet weak var DeviceBlacklisted: NSButton! 34 | 35 | var event: MXEvent? 36 | 37 | override func viewDidLoad() { 38 | super.viewDidLoad() 39 | 40 | DownloadSpinner.isHidden = true 41 | 42 | guard event != nil else { 43 | DeviceVerified.isEnabled = false 44 | DeviceBlacklisted.isEnabled = false 45 | return 46 | } 47 | 48 | if let deviceInfo = MatrixServices.inst.session.crypto.eventDeviceInfo(self.event) { 49 | if MatrixServices.inst.client.credentials.deviceId == deviceInfo.deviceId { 50 | MatrixServices.inst.client.device(withId: MatrixServices.inst.client.credentials.deviceId, completion: { (response) in 51 | if response.isSuccess { 52 | if let device = response.value { 53 | self.DeviceName.stringValue = device.displayName ?? "" 54 | self.DeviceID.stringValue = device.deviceId ?? "" 55 | } 56 | } 57 | }) 58 | } else { 59 | DeviceName.stringValue = deviceInfo.displayName ?? "" 60 | DeviceID.stringValue = deviceInfo.deviceId ?? "" 61 | } 62 | 63 | DeviceFingerprint.stringValue = String(deviceInfo.fingerprint.enumerated().map { $0 > 0 && $0 % 4 == 0 ? [" ", $1] : [$1]}.joined()) 64 | 65 | if deviceInfo.userId == MatrixServices.inst.session.myUser.userId { 66 | DeviceVerified.isEnabled = deviceInfo.deviceId != MatrixServices.inst.client.credentials.deviceId 67 | DeviceBlacklisted.isEnabled = DeviceVerified.isEnabled 68 | } 69 | 70 | DeviceVerified.state = deviceInfo.verified == MXDeviceVerified ? .on : .off 71 | DeviceBlacklisted.state = deviceInfo.verified == MXDeviceBlocked ? .on : .off 72 | } else { 73 | DeviceVerified.isEnabled = false 74 | DeviceBlacklisted.isEnabled = false 75 | 76 | DownloadSpinner.isHidden = false 77 | DownloadSpinner.startAnimation(self) 78 | 79 | MatrixServices.inst.session.crypto.downloadKeys([event!.sender], forceDownload: false, success: { (devicemap) in 80 | self.DownloadSpinner.stopAnimation(self) 81 | self.DownloadSpinner.isHidden = true 82 | 83 | if MatrixServices.inst.session.crypto.eventDeviceInfo(self.event) != nil { 84 | self.viewDidLoad() 85 | } 86 | }) { (error) in 87 | OperationQueue.main.addOperation { 88 | self.DownloadSpinner.stopAnimation(self) 89 | self.DownloadSpinner.isHidden = true 90 | } 91 | } 92 | } 93 | 94 | MessageAlgorithm.stringValue = event!.wireContent["algorithm"] as? String ?? "" 95 | } 96 | 97 | @IBAction func deviceVerificationChanged(_ sender: NSButton) { 98 | guard sender == DeviceVerified || sender == DeviceBlacklisted else { return } 99 | guard event != nil else { return } 100 | 101 | if let deviceInfo = MatrixServices.inst.session.crypto.eventSenderDevice(of: event) { 102 | if sender == DeviceVerified { 103 | if DeviceVerified.state == .on { 104 | DeviceBlacklisted.state = .off 105 | } 106 | } 107 | 108 | if sender == DeviceBlacklisted { 109 | if DeviceBlacklisted.state == .on { 110 | DeviceVerified.state = .off 111 | } 112 | } 113 | 114 | let verificationState = 115 | DeviceBlacklisted.state == .on ? MXDeviceBlocked : 116 | DeviceVerified.state == .on ? MXDeviceVerified : MXDeviceUnverified 117 | 118 | DeviceVerified.isEnabled = false 119 | DeviceBlacklisted.isEnabled = false 120 | 121 | MatrixServices.inst.session.crypto.setDeviceVerification(verificationState, forDevice: deviceInfo.deviceId, ofUser: deviceInfo.userId, success: { 122 | self.DeviceVerified.isEnabled = true 123 | self.DeviceBlacklisted.isEnabled = true 124 | 125 | MatrixServices.inst.mainController?.channelDelegate?.uiRoomNeedsCryptoReload() 126 | }) { (error) in 127 | self.DeviceVerified.state = deviceInfo.verified == MXDeviceVerified ? .on : .off 128 | self.DeviceBlacklisted.state = deviceInfo.verified == MXDeviceBlocked ? .on : .off 129 | 130 | self.DeviceVerified.isEnabled = true 131 | self.DeviceBlacklisted.isEnabled = true 132 | } 133 | } 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /Seaglass/Controller/Popover Views/PopoverRoomActions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | import SwiftMatrixSDK 21 | 22 | class PopoverRoomActions: NSViewController { 23 | 24 | @IBOutlet weak var InviteButton: NSButton! 25 | @IBOutlet weak var AttachButton: NSButton! 26 | @IBOutlet weak var CallButton: NSButton! 27 | @IBOutlet weak var VideoButton: NSButton! 28 | 29 | var roomId: String = "" 30 | 31 | override func viewDidLoad() { 32 | super.viewDidLoad() 33 | } 34 | 35 | @IBAction func inviteButtonClicked(_ sender: NSButton) { 36 | MatrixServices.inst.mainController?.channelDelegate?.uiRoomStartInvite() 37 | self.dismiss(sender) 38 | } 39 | 40 | @IBAction func attachButtonClicked(_ sender: NSButton) { 41 | let panel = NSOpenPanel() 42 | panel.allowsMultipleSelection = false 43 | panel.canChooseFiles = true 44 | panel.canChooseDirectories = false 45 | panel.canCreateDirectories = false 46 | panel.runModal() 47 | 48 | if let path = panel.url { 49 | if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, path.pathExtension as NSString, nil)?.takeRetainedValue() { 50 | if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() { 51 | var event: MXEvent? 52 | if let room = MatrixServices.inst.session.room(withRoomId: roomId) { 53 | if UTTypeConformsTo(uti, kUTTypeImage) { 54 | // room.sendImage(data: <#T##Data#>, size: <#T##CGSize#>, mimeType: <#T##String#>, thumbnail: <#T##MXImage?#>, localEcho: &<#T##MXEvent?#>) { (<#MXResponse#>) in 55 | // print(response) 56 | // } 57 | } else { 58 | // room.sendFile(localURL: path, mimeType: mimetype as String, localEcho: &event) { (response) in 59 | // print(response) 60 | // } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Seaglass/Controller/Popover Views/PopoverRoomList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | @objcMembers class PopoverRoomList: NSViewController { 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Seaglass/Controller/Popover Views/PopoverUserList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | class PopoverUserList: NSViewController { 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Seaglass/Controller/Segues/LoginSuccessfulSegue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | class LoginSuccessfulSegue: NSStoryboardSegue { 22 | override func perform() { 23 | if let src = self.sourceController as? LoginViewController, let dest = self.destinationController as? MainWindowController { 24 | NSAnimationContext.runAnimationGroup({ (context) in 25 | context.duration = 0.5 26 | src.view.window!.animator().alphaValue = 0 27 | }, completionHandler: { 28 | src.view.window!.close() 29 | dest.showWindow(src) 30 | }) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Seaglass/Controller/User Settings Views/UserSettingsEncryptionController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | import SwiftMatrixSDK 21 | 22 | class UserSettingsEncryptionController: UserSettingsTabController { 23 | 24 | @IBOutlet weak var DeviceName: NSTextField! 25 | @IBOutlet weak var DeviceID: NSTextField! 26 | @IBOutlet weak var DeviceKey: NSTextField! 27 | 28 | @IBOutlet weak var DeviceNameSpinner: NSProgressIndicator! 29 | 30 | @IBOutlet weak var ParanoidMode: NSButton! 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | 35 | resizeToSize = NSSize(width: 450, height: 359) 36 | 37 | DeviceNameSpinner.isHidden = false 38 | DeviceNameSpinner.startAnimation(self) 39 | 40 | MatrixServices.inst.client.device(withId: MatrixServices.inst.client.credentials.deviceId, completion: { (response) in 41 | if response.isSuccess { 42 | if let device = response.value { 43 | self.DeviceName.stringValue = device.displayName ?? "" 44 | } 45 | 46 | self.DeviceNameSpinner.stopAnimation(self) 47 | self.DeviceNameSpinner.isHidden = true 48 | } 49 | }) 50 | self.DeviceID.stringValue = MatrixServices.inst.client.credentials.deviceId 51 | self.DeviceKey.stringValue = String(MatrixServices.inst.session.crypto.deviceEd25519Key.enumerated().map { $0 > 0 && $0 % 4 == 0 ? [" ", $1] : [$1]}.joined()) 52 | 53 | ParanoidMode.state = MatrixServices.inst.session.crypto.warnOnUnknowDevices ? .on : .off 54 | } 55 | 56 | @IBAction func paranoidModeChanged(_ sender: NSButton) { 57 | guard sender == ParanoidMode else { return } 58 | 59 | MatrixServices.inst.session.crypto.warnOnUnknowDevices = ParanoidMode.state == .on 60 | UserDefaults.standard.set(MatrixServices.inst.session.crypto.warnOnUnknowDevices, forKey: "CryptoParanoid") 61 | } 62 | 63 | @IBAction func importKeysPressed(_ sender: NSButton) { 64 | let panel = NSOpenPanel() 65 | panel.title = "Export Keys" 66 | panel.allowedFileTypes = ["txt"] 67 | panel.allowsOtherFileTypes = true 68 | 69 | let openresponse = panel.runModal() 70 | guard openresponse != .cancel else { return } 71 | 72 | var data: Data? 73 | do { 74 | data = try Data(contentsOf: panel.url!) 75 | } catch { 76 | let alert = NSAlert() 77 | alert.messageText = "Failed to import room keys" 78 | alert.informativeText = error.localizedDescription 79 | alert.alertStyle = .warning 80 | alert.addButton(withTitle: "OK") 81 | alert.runModal() 82 | 83 | return 84 | } 85 | 86 | let alert = NSAlert() 87 | alert.addButton(withTitle: "OK") 88 | alert.addButton(withTitle: "Cancel") 89 | alert.messageText = "Enter import password" 90 | alert.informativeText = "Enter the password that you used when exporting your encryption keys." 91 | 92 | let password = NSSecureTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 24)) 93 | password.placeholderString = "Password" 94 | 95 | alert.accessoryView = password 96 | var response: NSApplication.ModalResponse = .cancel 97 | while password.stringValue == "" { 98 | response = alert.runModal() 99 | if response == .alertSecondButtonReturn { 100 | break 101 | } 102 | } 103 | 104 | MatrixServices.inst.session.crypto.importRoomKeys(data!, withPassword: password.stringValue, success: { 105 | let alert = NSAlert() 106 | alert.messageText = "Room keys imported" 107 | alert.informativeText = "Your encryption keys have been imported." 108 | alert.addButton(withTitle: "OK") 109 | alert.runModal() 110 | }) { (error) in 111 | let alert = NSAlert() 112 | alert.messageText = "Failed to import room keys" 113 | alert.informativeText = error!.localizedDescription 114 | alert.alertStyle = .warning 115 | alert.addButton(withTitle: "OK") 116 | alert.runModal() 117 | } 118 | } 119 | 120 | @IBAction func exportKeysPressed(_ sender: NSButton) { 121 | let alert = NSAlert() 122 | alert.addButton(withTitle: "OK") 123 | alert.addButton(withTitle: "Cancel") 124 | alert.messageText = "Enter export password" 125 | alert.informativeText = "This password will protect your encryption keys. You will need to use the same password to import the keys later." 126 | 127 | let password = NSSecureTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 24)) 128 | password.placeholderString = "Password" 129 | 130 | alert.accessoryView = password 131 | var response: NSApplication.ModalResponse = .cancel 132 | while password.stringValue == "" { 133 | response = alert.runModal() 134 | if response == .alertSecondButtonReturn { 135 | break 136 | } 137 | } 138 | 139 | switch response { 140 | case NSApplication.ModalResponse.alertFirstButtonReturn: 141 | let panel = NSSavePanel() 142 | panel.title = "Export Keys" 143 | panel.allowedFileTypes = ["txt"] 144 | panel.allowsOtherFileTypes = true 145 | panel.nameFieldStringValue = "keys.txt" 146 | 147 | let saveresponse = panel.runModal() 148 | guard saveresponse != .cancel else { return } 149 | 150 | MatrixServices.inst.session.crypto.exportRoomKeys(withPassword: password.stringValue, success: { (data) in 151 | do { 152 | try data!.write(to: panel.url!) 153 | } catch { 154 | let alert = NSAlert() 155 | alert.messageText = "Failed to export room keys" 156 | alert.informativeText = error.localizedDescription 157 | alert.alertStyle = .warning 158 | alert.addButton(withTitle: "OK") 159 | alert.runModal() 160 | 161 | return 162 | } 163 | 164 | let alert = NSAlert() 165 | alert.messageText = "Room keys exported" 166 | alert.informativeText = "Your encryption keys have been exported." 167 | alert.addButton(withTitle: "OK") 168 | alert.runModal() 169 | }) { (error) in 170 | let alert = NSAlert() 171 | alert.messageText = "Failed to export room keys" 172 | alert.informativeText = error!.localizedDescription 173 | alert.alertStyle = .warning 174 | alert.addButton(withTitle: "OK") 175 | alert.runModal() 176 | 177 | return 178 | } 179 | 180 | break 181 | default: 182 | print("Cancel") 183 | } 184 | } 185 | 186 | } 187 | -------------------------------------------------------------------------------- /Seaglass/Controller/User Settings Views/UserSettingsProfileController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | class UserSettingsProfileController: UserSettingsTabController { 22 | 23 | @IBOutlet weak var showMostRecentMessageButton: NSButton! 24 | @IBOutlet weak var showRoomTopicButton: NSButton! 25 | 26 | override func viewDidLoad() { 27 | super.viewDidLoad() 28 | 29 | resizeToSize = NSSize(width: 450, height: 75) 30 | } 31 | 32 | @IBAction func sidebarPreferenceChanged(_ sender: NSButton) { 33 | // the buttons need to be set to the same action so they act as a group 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Seaglass/Controller/User Settings Views/UserSettingsTabController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | class UserSettingsTabController: NSViewController { 22 | 23 | var resizeToSize: NSSize? = NSSize(width: 450, height: 450) 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Seaglass/Controller/User Settings Views/UserSettingsTabViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | class UserSettingsTabViewController: NSTabViewController { 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | self.tabView.tabViewType = .noTabsNoBorder 27 | self.selectedTabViewItemIndex = 0 28 | } 29 | 30 | override func viewWillAppear() { 31 | if let item = self.tabView.selectedTabViewItem { 32 | resizeWindowToFit(tabViewItem: item) 33 | } 34 | } 35 | 36 | override func tabView(_ tabView: NSTabView, didSelect tabViewItem: NSTabViewItem?) { 37 | if let item = tabViewItem { 38 | resizeWindowToFit(tabViewItem: item) 39 | } 40 | } 41 | 42 | private func resizeWindowToFit(tabViewItem: NSTabViewItem?) { 43 | guard let window = view.window else { return } 44 | 45 | if let controller = tabViewItem?.viewController as? UserSettingsTabController, let size = controller.resizeToSize { 46 | let rect = NSRect(x: 0, y: 0, width: size.width, height: size.height) 47 | let frame = window.frameRect(forContentRect: rect) 48 | let toolbar = window.frame.size.height - frame.size.height 49 | let origin = NSPoint(x: window.frame.origin.x, y: window.frame.origin.y + toolbar) 50 | 51 | window.setFrame(NSRect(origin: origin, size: frame.size), display: false, animate: true) 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Seaglass/Controller/Windows/LoginWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | class LoginWindowController: NSWindowController, NSWindowDelegate { 22 | 23 | override func windowDidLoad() { 24 | super.windowDidLoad() 25 | } 26 | 27 | func windowShouldClose(_ sender: NSWindow) -> Bool { 28 | NSApplication.shared.hide(nil) 29 | return false 30 | } 31 | 32 | 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Seaglass/Controller/Windows/MainWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | class MainWindowController: NSWindowController, NSWindowDelegate { 22 | 23 | override func windowDidLoad() { 24 | super.windowDidLoad() 25 | } 26 | 27 | func windowShouldClose(_ sender: NSWindow) -> Bool { 28 | print("Main window should close?") 29 | NSApplication.shared.hide(nil) 30 | return false 31 | } 32 | 33 | func windowWillClose(_ notification: Notification) { 34 | print("Main window will close") 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Seaglass/Controller/Windows/UserSettingsWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | class UserSettingsWindowController: NSWindowController, NSToolbarDelegate { 22 | 23 | @IBOutlet var toolbar: NSToolbar! 24 | 25 | override func windowDidLoad() { 26 | super.windowDidLoad() 27 | 28 | if let window = self.window { 29 | if toolbar.items.count == 0 { 30 | toolbar.insertItem(withItemIdentifier: NSToolbarItem.Identifier.flexibleSpace, at: 0) 31 | toolbar.insertItem(withItemIdentifier: NSToolbarItem.Identifier.init("SeaglassPreferences"), at: 1) 32 | toolbar.insertItem(withItemIdentifier: NSToolbarItem.Identifier.flexibleSpace, at: 2) 33 | } 34 | 35 | window.toolbar = toolbar 36 | } 37 | } 38 | 39 | @IBAction func didChangeTabs(_ sender: NSSegmentedControl) { 40 | if let controller = self.window!.contentViewController as? UserSettingsTabViewController { 41 | controller.tabView.selectTabViewItem(at: sender.selectedSegment) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Seaglass/Extension/NSImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | extension NSImage { 22 | func tint(with tintColor: NSColor) -> NSImage { 23 | guard let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return self } 24 | 25 | return NSImage(size: size, flipped: false) { bounds in 26 | guard let context = NSGraphicsContext.current?.cgContext else { return false } 27 | 28 | tintColor.set() 29 | context.clip(to: bounds, mask: cgImage) 30 | context.fill(bounds) 31 | 32 | return true 33 | } 34 | } 35 | 36 | static func create(withLetterString: String = "?") -> NSImage 37 | { 38 | let startImage = #imageLiteral(resourceName: "PlaceholderGradient") 39 | let letterSize: CGFloat = 72 40 | 41 | let imageRect = CGRect(x: 0, y: 0, width: startImage.size.width, height: startImage.size.height) 42 | let textRect = CGRect(x: startImage.size.width / 2 - letterSize / 2, y: startImage.size.height / 2 - letterSize / 1.5, width: letterSize, height: letterSize * 1.5) 43 | let textPara = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle 44 | textPara.alignment = .center 45 | 46 | let textFontAttributes: [NSAttributedString.Key: Any] = [ 47 | .font: NSFont(name: "Arial Rounded MT Bold", size: letterSize)!, 48 | .paragraphStyle: textPara, 49 | .foregroundColor: NSColor.white 50 | ] 51 | 52 | let image: NSImage = NSImage(size: startImage.size) 53 | let representation: NSBitmapImageRep = NSBitmapImageRep( 54 | bitmapDataPlanes: nil, 55 | pixelsWide: Int(image.size.width), 56 | pixelsHigh: Int(image.size.height), 57 | bitsPerSample: 8, 58 | samplesPerPixel: 4, 59 | hasAlpha: true, 60 | isPlanar: false, 61 | colorSpaceName: .calibratedRGB, 62 | bytesPerRow: 0, 63 | bitsPerPixel: 0)! 64 | 65 | image.addRepresentation(representation) 66 | image.lockFocus() 67 | 68 | let letter = withLetterString.first { (character) -> Bool in 69 | return CharacterSet.alphanumerics.contains(String(character).unicodeScalars.first!) 70 | } 71 | 72 | startImage.draw(in: imageRect) 73 | String(letter ?? "?").uppercased().draw(in: textRect, withAttributes: textFontAttributes) 74 | 75 | image.unlockFocus() 76 | return image 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Seaglass/Extension/NSImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | extension NSImageView { 22 | func isVisible(inView: NSView?) -> Bool { 23 | guard let inView = inView else { return true } 24 | let viewFrame = inView.convert(bounds, from: self) 25 | if viewFrame.intersects(inView.bounds) { 26 | return isVisible(inView: inView.superview) 27 | } 28 | return false 29 | } 30 | 31 | func isVisible() -> Bool { 32 | return isVisible(inView: self.superview) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Seaglass/Extension/String.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import AppKit 20 | import TSMarkdownParser 21 | 22 | extension String { 23 | func toAttributedStringFromMarkdown(justify: NSTextAlignment, color: NSColor) -> NSAttributedString{ 24 | if self.count == 0 { 25 | return NSAttributedString() 26 | } 27 | guard let data = data(using: .utf16, allowLossyConversion: true) else { return NSAttributedString() } 28 | if data.isEmpty { 29 | return NSAttributedString() 30 | } 31 | 32 | let parser = TSMarkdownParser.standard() 33 | parser.defaultAttributes = [:] 34 | parser.defaultAttributes!["NSColor"] = color 35 | parser.defaultAttributes!["NSFont"] = NSFont.systemFont(ofSize: 12) 36 | parser.monospaceAttributes["NSColor"] = NSColor(calibratedRed: 0.53, green: 0.54, blue: 0.58, alpha: 1.00) 37 | parser.monospaceAttributes["NSFont"] = NSFont(name: "Menlo", size: NSFont.smallSystemFontSize) 38 | parser.quoteAttributes[0]["NSColor"] = NSColor.gray 39 | parser.quoteAttributes[0]["NSFont"] = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize) 40 | 41 | let str: NSMutableAttributedString = parser.attributedString(fromMarkdown: self) as! NSMutableAttributedString 42 | let range = NSRange(location: 0, length: str.length) 43 | let paragraphStyle = NSMutableParagraphStyle() 44 | paragraphStyle.alignment = justify 45 | 46 | str.beginEditing() 47 | str.removeAttribute(.paragraphStyle, range: range) 48 | str.addAttribute(.paragraphStyle, value: paragraphStyle, range: range) 49 | str.endEditing() 50 | 51 | return str 52 | } 53 | 54 | func toAttributedStringFromHTML(justify: NSTextAlignment) -> NSAttributedString{ 55 | if self.count == 0 { 56 | return NSAttributedString() 57 | } 58 | guard let data = data(using: .utf16, allowLossyConversion: true) else { return NSAttributedString() } 59 | if data.isEmpty { 60 | return NSAttributedString() 61 | } 62 | do { 63 | 64 | let str: NSMutableAttributedString = try NSMutableAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) 65 | let range = NSRange(location: 0, length: str.length) 66 | let paragraphStyle = NSMutableParagraphStyle() 67 | paragraphStyle.alignment = justify 68 | 69 | str.beginEditing() 70 | str.removeAttribute(.paragraphStyle, range: range) 71 | 72 | str.enumerateAttributes(in: range, options: [], using: { attr, attrRange, _ in 73 | if let font = attr[.font] as? NSFont { 74 | if font.familyName == "Times" { 75 | let newFont = NSFontManager.shared.convert(font, toFamily: NSFont.systemFont(ofSize: NSFont.systemFontSize).familyName!) 76 | str.addAttribute(.font, value: newFont, range: attrRange) 77 | } 78 | } 79 | }) 80 | 81 | str.addAttribute(.foregroundColor, value: NSColor.textColor, range: range) 82 | str.addAttribute(.paragraphStyle, value: paragraphStyle, range: range) 83 | str.endEditing() 84 | 85 | return str 86 | } catch { 87 | return NSAttributedString() 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Seaglass/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 0.0.0 21 | CFBundleVersion 22 | Set automatically by scripts/set_build_number.sh 23 | LSApplicationCategoryType 24 | public.app-category.social-networking 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSHumanReadableCopyright 28 | Copyright © 2018 Neil Alexander. All rights reserved. 29 | NSMainStoryboardFile 30 | Main 31 | NSPrincipalClass 32 | NSApplication 33 | NSRequiresAquaSystemAppearance 34 | 35 | SUPublicDSAKeyFile 36 | dsa_pub.pem 37 | SUFeedURL 38 | https://s3.eu-west-2.amazonaws.com/seaglass-ci/appcast.xml 39 | SUShowReleaseNotes 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Seaglass/Model/MatrixRoomCache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | import SwiftMatrixSDK 21 | 22 | @objcMembers class MatrixRoomCache: NSObject { 23 | 24 | static let allowedTypes = [ "m.room.create", "m.room.message", "m.room.name", "m.room.member", "m.room.topic", "m.room.avatar", "m.room.canonical_alias", "m.sticker", "m.room.encryption", "m.room.encrypted" ] 25 | 26 | private var _managedTable: MainViewTableView? 27 | private var _filteredContent: [MXEvent] = [] 28 | private var _unfilteredContent: [MXEvent] = [] 29 | 30 | private let lock = DispatchSemaphore(value: 1) 31 | 32 | var managedTable: MainViewTableView? { 33 | get { return _managedTable } 34 | set { 35 | _managedTable = newValue 36 | if let table = _managedTable { 37 | table.reloadData() 38 | } 39 | } 40 | } 41 | 42 | @objc dynamic var unfilteredContent: [MXEvent] { 43 | get { return _unfilteredContent } 44 | set { 45 | _unfilteredContent = newValue 46 | 47 | lock.wait() 48 | _filteredContent = _unfilteredContent.filter(filter) 49 | lock.signal() 50 | } 51 | } 52 | 53 | dynamic var filteredContent: [MXEvent] { 54 | get { 55 | lock.wait() 56 | defer { lock.signal() } 57 | return _filteredContent 58 | } 59 | } 60 | 61 | var filter = { (event: MXEvent) -> Bool in 62 | return MatrixRoomCache.allowedTypes.contains(event.type) && 63 | ((event.isState() && !NSDictionary(dictionary: event.content).isEqual(to: event.prevContent)) || !event.isState()) && 64 | !event.isRedactedEvent() 65 | } 66 | 67 | func reset(_ content: [MXEvent] = []) { 68 | self.unfilteredContent = content 69 | if let table = _managedTable { 70 | table.reloadData() 71 | } 72 | } 73 | 74 | func append(_ newElement: MXEvent) { 75 | guard !self.unfilteredContent.contains(where: { $0.eventId == newElement.eventId }) else { return } 76 | DispatchQueue.main.async { 77 | self.unfilteredContent.append(newElement) 78 | if let table = self._managedTable { 79 | guard table.roomId == newElement.roomId else { return } 80 | if self.filter(newElement) { 81 | table.beginUpdates() 82 | table.noteNumberOfRowsChanged() 83 | //table.insertRows(at: IndexSet([self.filteredContent.count-1]), withAnimation: []) 84 | table.endUpdates() 85 | } 86 | } 87 | } 88 | } 89 | 90 | func insert(_ newElement: MXEvent, at: Int) { 91 | guard !self.unfilteredContent.contains(where: { $0.eventId == newElement.eventId }) else { return } 92 | DispatchQueue.main.async { 93 | self.unfilteredContent.insert(newElement, at: at) 94 | if let table = self._managedTable { 95 | guard table.roomId == newElement.roomId else { return } 96 | if self.filter(newElement) { 97 | table.beginUpdates() 98 | table.insertRows(at: IndexSet([at]), withAnimation: []) 99 | table.endUpdates() 100 | } 101 | } 102 | } 103 | } 104 | 105 | func replace(_ newElement: MXEvent, at: Int) { 106 | guard self.unfilteredContent[at].eventId == newElement.eventId else { return } 107 | DispatchQueue.main.async { 108 | if let table = self._managedTable { 109 | if table.roomId == newElement.roomId { 110 | table.beginUpdates() 111 | if self.filter(self.unfilteredContent[at]) { 112 | if let index = self.filteredContent.firstIndex(of: self.unfilteredContent[at]) { 113 | table.removeRows(at: IndexSet([index]), withAnimation: []) 114 | } 115 | } 116 | } 117 | } 118 | self.unfilteredContent[at] = newElement 119 | if let table = self._managedTable { 120 | if table.roomId == newElement.roomId { 121 | if self.filter(newElement) { 122 | if let index = self.filteredContent.firstIndex(of: newElement) { 123 | table.insertRows(at: IndexSet([index]), withAnimation: []) 124 | } 125 | } 126 | table.endUpdates() 127 | } 128 | } 129 | } 130 | } 131 | 132 | func update(_ newElement: MXEvent) { 133 | guard self.unfilteredContent.contains(where: { $0.eventId == newElement.eventId }) else { return } 134 | if let at = self.unfilteredContent.firstIndex(where: { $0.eventId == newElement.eventId }) { 135 | self.replace(newElement, at: at) 136 | } 137 | } 138 | 139 | func remove(at: Int) { 140 | let rowindex = filteredContent.firstIndex(of: unfilteredContent[at]) 141 | DispatchQueue.main.async { 142 | self.unfilteredContent.remove(at: at) 143 | if let table = self._managedTable { 144 | guard table.roomId == self.unfilteredContent[at].roomId else { return } 145 | if rowindex != nil { 146 | table.beginUpdates() 147 | table.removeRows(at: IndexSet([rowindex!]), withAnimation: []) 148 | table.endUpdates() 149 | } 150 | } 151 | } 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /Seaglass/Seaglass.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Seaglass/Seaglass.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | Matrix.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /Seaglass/Seaglass.xcdatamodeld/Matrix.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Seaglass/Subclass/Image Views/AvatarImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | import SwiftMatrixSDK 21 | 22 | class AvatarImageView: ContextImageView { 23 | 24 | var mxcUrl: String? 25 | var url: String? 26 | 27 | required init?(coder: NSCoder) { 28 | super.init(coder: coder) 29 | 30 | if layer == nil { 31 | layer = CALayer() 32 | } 33 | 34 | wantsLayer = true 35 | canDrawSubviewsIntoLayer = true 36 | 37 | layer?.masksToBounds = true 38 | layer?.contentsGravity = CALayerContentsGravity.resizeAspectFill 39 | layer?.cornerRadius = (frame.width)/2 40 | } 41 | 42 | override func layout() { 43 | layer?.cornerRadius = (frame.width)/2 44 | super.layout() 45 | } 46 | 47 | override var image: NSImage? { 48 | set { 49 | layer?.contents = newValue 50 | super.image = newValue 51 | } 52 | get { 53 | return super.image 54 | } 55 | } 56 | 57 | func setAvatar(forMxcUrl: String?, defaultImage: NSImage, useCached: Bool = true) { 58 | guard mxcUrl != forMxcUrl else { return } 59 | if let cacheUrl = forMxcUrl { 60 | if useCached && MatrixServices.inst.avatarCache.keys.contains(cacheUrl) { 61 | if image != MatrixServices.inst.avatarCache[cacheUrl] { 62 | image = MatrixServices.inst.avatarCache[cacheUrl] 63 | return 64 | } 65 | } 66 | } 67 | image = defaultImage 68 | mxcUrl = forMxcUrl 69 | guard mxcUrl != nil else { return } 70 | 71 | if mxcUrl!.hasPrefix("mxc://") { 72 | url = MatrixServices.inst.client.url(ofContentThumbnail: forMxcUrl, toFitViewSize: CGSize(width: 96, height: 96), with: MXThumbnailingMethodScale) 73 | guard url != nil else { return } 74 | 75 | if url!.hasPrefix("http://") || url!.hasPrefix("https://") { 76 | guard let path = MXMediaManager.cachePathForMedia(withURL: url, andType: nil, inFolder: kMXMediaManagerAvatarThumbnailFolder) else { return } 77 | 78 | if FileManager.default.fileExists(atPath: path) && useCached { 79 | { [weak self] in 80 | if let image = MXMediaManager.loadThroughCache(withFilePath: path) { 81 | self?.image = image 82 | self?.layout() 83 | if let cacheUrl = forMxcUrl { 84 | MatrixServices.inst.avatarCache[cacheUrl] = image 85 | } 86 | } 87 | }() 88 | } else { 89 | DispatchQueue.main.async { 90 | let previousPath = path 91 | MXMediaManager.downloadMedia(fromURL: self.url!, andSaveAtFilePath: path, success: { [weak self] in 92 | if let image = MXMediaManager.loadThroughCache(withFilePath: path) { 93 | guard previousPath == path else { return } 94 | self?.image = image 95 | if let cacheUrl = forMxcUrl { 96 | MatrixServices.inst.avatarCache[cacheUrl] = image 97 | } 98 | self?.layout() 99 | } 100 | }) { [weak self] (error) in 101 | guard previousPath == path else { return } 102 | self?.image = defaultImage 103 | self?.layout() 104 | } 105 | } 106 | } 107 | } 108 | } 109 | } 110 | 111 | func setAvatar(forUserId userId: String, useCached: Bool = true) { 112 | guard let user = MatrixServices.inst.session.user(withUserId: userId) else { 113 | setAvatar(forText: "?") 114 | return 115 | } 116 | 117 | if user.avatarUrl != "" { 118 | setAvatar(forMxcUrl: user.avatarUrl, defaultImage: NSImage.create(withLetterString: user.displayname ?? "?"), useCached: useCached) 119 | } else { 120 | setAvatar(forText: user.displayname) 121 | } 122 | } 123 | 124 | func setAvatar(forRoomId roomId: String, useCached: Bool = true) { 125 | guard let room = MatrixServices.inst.session.room(withRoomId: roomId) else { 126 | setAvatar(forText: "?") 127 | return 128 | } 129 | 130 | if room.summary.avatar != "" { 131 | setAvatar(forMxcUrl: room.summary.avatar, defaultImage: NSImage.create(withLetterString: room.summary.displayname ?? "?"), useCached: useCached) 132 | } else { 133 | setAvatar(forText: room.summary.displayname) 134 | } 135 | } 136 | 137 | func setAvatar(forText: String) { 138 | let letter = forText.first { (character) -> Bool in 139 | return CharacterSet.alphanumerics.contains(String(character).unicodeScalars.first!) 140 | } 141 | 142 | if MatrixServices.inst.avatarCache.keys.contains(String(letter ?? "?")) { 143 | image = MatrixServices.inst.avatarCache[String(letter ?? "?")] 144 | } else { 145 | image = NSImage.create(withLetterString: forText) 146 | MatrixServices.inst.avatarCache[String(letter ?? "?")] = image 147 | } 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /Seaglass/Subclass/Image Views/ContextImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | import SwiftMatrixSDK 21 | 22 | class ContextImageView: NSImageView { 23 | var handler: ((_: NSView, _: MXRoom?, _: MXEvent?, _: String?) -> ())? 24 | 25 | var room: MXRoom? 26 | var event: MXEvent? 27 | var userId: String? 28 | 29 | init(handler: @escaping (_: NSView, _: MXRoom?, _: MXEvent?, _: String?) -> ()?) { 30 | self.handler = handler as? ((NSView, MXRoom?, _: MXEvent?, String?) -> ()) ?? nil 31 | super.init(frame: NSRect()) 32 | } 33 | 34 | required init?(coder: NSCoder) { 35 | super.init(coder: coder) 36 | } 37 | 38 | override func draw(_ dirtyRect: NSRect) { 39 | super.draw(dirtyRect) 40 | } 41 | 42 | override func mouseDown(with nsevent: NSEvent) { 43 | guard handler != nil else { return } 44 | guard !self.isHidden else { return } 45 | 46 | self.handler!(self, self.room, self.event, self.userId) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Seaglass/Subclass/Image Views/InlineImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | import SwiftMatrixSDK 21 | import Quartz 22 | 23 | class InlineImageView: ContextImageView, QLPreviewItem, QLPreviewPanelDelegate, QLPreviewPanelDataSource { 24 | var previewItemURL: URL! 25 | 26 | var realurl: String? 27 | 28 | var maxDimensionWidth: CGFloat = 256 29 | var maxDimensionHeight: CGFloat = 256 30 | 31 | func numberOfPreviewItems(in panel: QLPreviewPanel!) -> Int { 32 | return previewItemURL.isFileURL ? 1 : 0 33 | } 34 | 35 | func previewPanel(_ panel: QLPreviewPanel!, previewItemAt index: Int) -> QLPreviewItem! { 36 | return previewItemURL as QLPreviewItem 37 | } 38 | 39 | override func draw(_ dirtyRect: NSRect) { 40 | super.draw(dirtyRect) 41 | } 42 | 43 | func resetImage() { 44 | self.image = nil 45 | if let constraint = self.constraints.first(where: { $0.identifier! == "height" }) { 46 | constraint.constant = 16 47 | } 48 | self.handler = nil 49 | self.setNeedsDisplay() 50 | } 51 | 52 | func setImage(forMxcUrl: String?, withMimeType: String?, useCached: Bool = true, enableQuickLook: Bool = true) { 53 | guard let mxcURL = forMxcUrl else { return } 54 | 55 | if mxcURL.hasPrefix("mxc://") { 56 | guard let url = MatrixServices.inst.client.url(ofContentThumbnail: forMxcUrl, toFitViewSize: CGSize(width: 256, height: 256), with: MXThumbnailingMethodScale) else { return } 57 | guard let realurl = MatrixServices.inst.client.url(ofContent: forMxcUrl) else { return } 58 | 59 | if url.hasPrefix("http://") || url.hasPrefix("https://") { 60 | guard let path = MXMediaManager.cachePathForMedia(withURL: url, andType: withMimeType, inFolder: kMXMediaManagerDefaultCacheFolder) else { return } 61 | 62 | if enableQuickLook { 63 | self.handler = { (sender, roomId, eventId, userId) in 64 | guard let realpath = MXMediaManager.cachePathForMedia(withURL: realurl, andType: withMimeType, inFolder: kMXMediaManagerDefaultCacheFolder) else { return } 65 | if !FileManager.default.fileExists(atPath: realpath) || !useCached { 66 | MXMediaManager.downloadMedia(fromURL: realurl, andSaveAtFilePath: realpath, success: { [weak self] in 67 | self?.previewItemURL = URL(fileURLWithPath: realpath) 68 | if self?.previewItemURL.isFileURL ?? false { 69 | QLPreviewPanel.shared().delegate = self 70 | QLPreviewPanel.shared().dataSource = self 71 | QLPreviewPanel.shared().makeKeyAndOrderFront(self) 72 | } 73 | }, failure: {[weak self] (error) in 74 | self?.previewItemURL = URL(fileURLWithPath: path) 75 | if self?.previewItemURL.isFileURL ?? false { 76 | QLPreviewPanel.shared().delegate = self 77 | QLPreviewPanel.shared().dataSource = self 78 | QLPreviewPanel.shared().makeKeyAndOrderFront(self) 79 | } 80 | }) 81 | } else { 82 | self.previewItemURL = URL(fileURLWithPath: realpath) 83 | if self.previewItemURL.isFileURL { 84 | QLPreviewPanel.shared().delegate = self 85 | QLPreviewPanel.shared().dataSource = self 86 | QLPreviewPanel.shared().makeKeyAndOrderFront(self) 87 | } 88 | } 89 | } as (_: NSView, _: MXRoom?, _: MXEvent?, _: String?) -> () 90 | } else { 91 | self.handler = nil 92 | } 93 | 94 | if FileManager.default.fileExists(atPath: path) && useCached { 95 | { [weak self] in 96 | if let image = MXMediaManager.loadThroughCache(withFilePath: path) { 97 | self?.image = image 98 | var width = self?.image?.size.width 99 | var height = self?.image?.size.height 100 | if width! > maxDimensionWidth { 101 | let factor = 1 / width! * maxDimensionWidth 102 | width = maxDimensionWidth 103 | height = height! * factor 104 | } 105 | if height! > maxDimensionHeight { 106 | let factor = 1 / height! * maxDimensionHeight 107 | height = maxDimensionHeight 108 | width = width! * factor 109 | } 110 | if let constraint = self?.constraints.first(where: { $0.identifier! == "height" }) { 111 | constraint.constant = height! 112 | } 113 | self?.setNeedsDisplay() 114 | } 115 | }() 116 | } else { 117 | DispatchQueue.main.async { 118 | let previousPath = path 119 | MXMediaManager.downloadMedia(fromURL: url, andSaveAtFilePath: path, success: { [weak self] in 120 | if let image = MXMediaManager.loadThroughCache(withFilePath: path) { 121 | guard previousPath == path else { return } 122 | self?.image = image 123 | var width = self?.image?.size.width ?? 128 124 | var height = self?.image?.size.height ?? 128 125 | if width > 256 { 126 | let factor = 1 / width * 256 127 | width = 256 128 | height = height * factor 129 | } 130 | if height > 256 { 131 | let factor = 1 / height * 256 132 | height = 256 133 | width = width * factor 134 | } 135 | if let constraint = self?.constraints.first(where: { $0.identifier! == "height" }) { 136 | constraint.constant = height 137 | } 138 | self?.setNeedsDisplay() 139 | } 140 | }) { [weak self] (error) in 141 | guard previousPath == path else { return } 142 | self?.image = NSImage(named: NSImage.invalidDataFreestandingTemplateName) 143 | self?.sizeToFit() 144 | } 145 | } 146 | } 147 | } 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Seaglass/Subclass/Member Lists/MemberListEntry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | class MemberListEntry: NSTableCellView { 22 | @IBOutlet var MemberName: NSTextField! 23 | @IBOutlet var MemberDescription: NSTextField! 24 | @IBOutlet var MemberIcon: AvatarImageView! 25 | } 26 | -------------------------------------------------------------------------------- /Seaglass/Subclass/Member Lists/MemberListTableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | import SwiftMatrixSDK 21 | 22 | class MemberListTableView: NSTableView { 23 | 24 | public var roomID: String = "" 25 | 26 | override func draw(_ dirtyRect: NSRect) { 27 | super.draw(dirtyRect) 28 | 29 | // Drawing code here. 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Seaglass/Subclass/Member Lists/MembersCacheEntry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | import SwiftMatrixSDK 21 | 22 | class MembersCacheEntry: NSObject { 23 | var member: MXRoomMember 24 | 25 | @objc dynamic var displayName: String 26 | @objc dynamic var userId: String 27 | 28 | init(_ member: MXRoomMember) { 29 | self.member = member 30 | 31 | userId = member.userId 32 | displayName = member.displayname ?? "" 33 | 34 | super.init() 35 | } 36 | 37 | func name() -> String { 38 | guard displayName != "" else { return userId } 39 | return displayName 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Seaglass/Subclass/Room Lists/RoomAliasEntry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | class RoomAliasEntry: NSTableCellView { 22 | @IBOutlet var RoomAliasName: NSTextField! 23 | @IBOutlet var RoomAliasPrimary: NSButton! 24 | @IBOutlet var RoomAliasDelete: NSButton! 25 | 26 | public var parent: RoomAliasesController? 27 | 28 | @IBAction func makePrimary(_ sender: NSButton) { 29 | parent?.primaryButtonClicked(sender: self) 30 | } 31 | 32 | @IBAction func delete(_ sender: NSButton) { 33 | parent?.deleteButtonClicked(sender: self) 34 | } 35 | 36 | override func draw(_ dirtyRect: NSRect) { 37 | super.draw(dirtyRect) 38 | RoomAliasName.isEditable = true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Seaglass/Subclass/Room Lists/RoomListEntry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | class RoomListEntry: NSTableCellView { 22 | @IBOutlet var RoomListEntryName: NSTextField! 23 | @IBOutlet var RoomListEntryTopic: NSTextField! 24 | @IBOutlet var RoomListEntryIcon: AvatarImageView! 25 | @IBOutlet var RoomListEntryUnread: NSImageView! 26 | 27 | var roomsCacheEntry: RoomsCacheEntry? 28 | 29 | required init?(coder: NSCoder) { 30 | super.init(coder: coder) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Seaglass/Subclass/Room Lists/RoomsCacheEntry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | import SwiftMatrixSDK 21 | 22 | class RoomsCacheEntry: NSObject { 23 | var room: MXRoom 24 | 25 | @objc dynamic var roomId: String { 26 | return room.roomId 27 | } 28 | @objc dynamic var roomName: String { 29 | return room.state.name ?? "" 30 | } 31 | @objc dynamic var roomAlias: String { 32 | return room.state.canonicalAlias ?? "" 33 | } 34 | @objc dynamic var roomTopic: String { 35 | return room.state.topic ?? "" 36 | } 37 | @objc dynamic var roomAvatar: String { 38 | return room.state.avatar ?? "" 39 | } 40 | @objc dynamic var roomSortWeight: Int { 41 | if isInvite() { 42 | return 0 43 | } 44 | if room.isDirect { 45 | return 70 46 | } 47 | if room.summary.isEncrypted || room.state.isEncrypted { 48 | return 60 49 | } 50 | if room.state.name == "" { 51 | if room.state.topic == "" { 52 | return 52 53 | } 54 | return 51 55 | } 56 | return 50 57 | } 58 | @objc dynamic var roomDisplayName: String { 59 | let count = members.count 60 | if roomName != "" { 61 | return roomName 62 | } else if roomAlias != "" { 63 | return roomAlias 64 | } else if count > 0 { 65 | var memberNames: String = "" 66 | for m in 0.. String { 89 | return room.state.topic 90 | } 91 | 92 | func unread() -> Bool { 93 | return room.summary.localUnreadEventCount > 0 94 | } 95 | 96 | func highlights() -> Int { 97 | let highlights: Int = 0 98 | /* if !MatrixServices.inst.eventCache.keys.contains(self.roomId) { 99 | return 0 100 | } 101 | let eventCache = MatrixServices.inst.eventCache[self.roomId]! 102 | let filtered = eventCache.filter({ 103 | $0.type == "m.room.message" && 104 | $0.content.keys.contains("msgtype") && ($0.content["msgtype"] as! String) == "m.text" && 105 | $0.content.keys.contains("body") && ($0.content["body"] as! String).contains(MatrixServices.inst.session.myUser.displayname) 106 | }) 107 | highlights += filtered.count */ 108 | return highlights 109 | } 110 | 111 | func encrypted() -> Bool { 112 | return room.summary.isEncrypted || room.state.isEncrypted 113 | } 114 | 115 | func isInvite() -> Bool { 116 | return MatrixServices.inst.session.invitedRooms().contains(where: { $0.roomId == room.roomId }) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Seaglass/Subclass/Room Message Types/RoomMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | import SwiftMatrixSDK 21 | 22 | class RoomMessage: NSTableCellView { 23 | var event: MXEvent? 24 | var drawnEvent: MXEvent? 25 | var drawnEventHash: Int = 0 26 | 27 | override func draw(_ dirtyRect: NSRect) { 28 | super.draw(dirtyRect) 29 | } 30 | 31 | override func viewWillDraw() { 32 | } 33 | 34 | func updateIcon() { 35 | 36 | } 37 | 38 | func encryptionIsBlacklisted() -> Bool { 39 | guard event != nil && event!.isEncrypted else { return false } 40 | if let deviceInfo = MatrixServices.inst.session.crypto.eventSenderDevice(of: event) { 41 | return deviceInfo.verified == MXDeviceBlocked 42 | } 43 | return false 44 | } 45 | 46 | func encryptionIsEncrypted() -> Bool { 47 | guard event != nil else { return false } 48 | return event!.sentState == MXEventSentStateEncrypting || event!.isEncrypted 49 | } 50 | 51 | func encryptionIsPending() -> Bool { 52 | guard event != nil else { return false } 53 | return event!.type == "m.room.encrypted" || event!.decryptionError != nil 54 | } 55 | 56 | func encryptionIsVerified() -> Bool { 57 | guard event != nil else { return false } 58 | if let deviceInfo = MatrixServices.inst.session.crypto.eventSenderDevice(of: event!) { 59 | return self.encryptionIsEncrypted() && deviceInfo.verified == MXDeviceVerified 60 | } else { 61 | return false 62 | } 63 | } 64 | 65 | func encryptionIsSending() -> Bool { 66 | guard event != nil else { return false } 67 | return event!.sentState != MXEventSentStateSent && event!.isLocalEvent() 68 | } 69 | 70 | func icon() -> (image: NSImage, width: CGFloat, height: CGFloat) { 71 | let padlockWidth: CGFloat = 16 72 | let padlockHeight: CGFloat = 12 73 | let padlockColor: NSColor = 74 | self.encryptionIsPending() ? NSColor.systemGray.withAlphaComponent(0.5) : 75 | self.encryptionIsSending() ? NSColor(deviceRed: 0.38, green: 0.65, blue: 0.53, alpha: 0.75) : 76 | self.encryptionIsBlacklisted() ? NSColor.systemRed : 77 | (self.encryptionIsEncrypted() ? 78 | (self.encryptionIsVerified() ? 79 | NSColor(deviceRed: 0.38, green: 0.65, blue: 0.53, alpha: 0.75) : 80 | NSColor(deviceRed: 0.89, green: 0.75, blue: 0.33, alpha: 0.75) 81 | ) : NSColor(deviceRed: 0.79, green: 0.31, blue: 0.27, alpha: 0.75)) 82 | let padlockImage: NSImage = self.encryptionIsEncrypted() ? 83 | NSImage(named: NSImage.lockLockedTemplateName)!.tint(with: padlockColor) : 84 | NSImage(named: NSImage.lockUnlockedTemplateName)!.tint(with: padlockColor) 85 | return (padlockImage, padlockWidth, padlockHeight) 86 | } 87 | 88 | func from() -> String { 89 | if event == nil { 90 | return "" 91 | } 92 | if let room = MatrixServices.inst.session.room(withRoomId: event!.roomId) { 93 | return room.state.memberName(event!.sender) ?? event!.sender as String 94 | } 95 | return "" 96 | } 97 | 98 | func timestamp(_ timeStyle: DateFormatter.Style = .short, andDate dateStyle: DateFormatter.Style = .none) -> String { 99 | if event == nil { 100 | return "XX:XX" 101 | } 102 | 103 | let eventTime = Date(timeIntervalSince1970: TimeInterval(event!.originServerTs / 1000)) 104 | let eventTimeFormatter = DateFormatter() 105 | eventTimeFormatter.timeZone = TimeZone.current 106 | eventTimeFormatter.timeStyle = timeStyle 107 | eventTimeFormatter.dateStyle = dateStyle 108 | 109 | return eventTimeFormatter.string(from: eventTime) 110 | } 111 | 112 | func textContent() -> (string: String?, attributedString: NSAttributedString?) { 113 | if event == nil { 114 | return (nil, nil) 115 | } 116 | 117 | var cellAttributedStringValue: NSAttributedString? = nil 118 | var cellStringValue: String? = nil 119 | 120 | /* 121 | if event!.content["formatted_body"] != nil { 122 | let justification = event!.sender == MatrixServices.inst.client?.credentials.userId ? NSTextAlignment.right : NSTextAlignment.left 123 | cellAttributedStringValue = (event!.content["formatted_body"] as! String).trimmingCharacters(in: .whitespacesAndNewlines).toAttributedStringFromHTML(justify: justification) 124 | cellStringValue = (event!.content["formatted_body"] as! String).trimmingCharacters(in: .whitespacesAndNewlines) 125 | } else if event!.content["body"] != nil { 126 | cellStringValue = (event!.content["body"] as! String).trimmingCharacters(in: .whitespacesAndNewlines) 127 | cellAttributedStringValue = NSAttributedString(string: cellStringValue!) 128 | } 129 | */ 130 | 131 | if event!.content["body"] != nil { 132 | let justification = event!.sender == MatrixServices.inst.client?.credentials.userId ? NSTextAlignment.right : NSTextAlignment.left 133 | let color = event!.content["msgtype"] as? String ?? "m.text" == "m.notice" ? NSColor.headerColor : NSColor.textColor 134 | cellStringValue = (event!.content["body"] as! String) 135 | cellAttributedStringValue = (event!.content["body"] as! String).trimmingCharacters(in: .whitespacesAndNewlines).toAttributedStringFromMarkdown(justify: justification, color: color) 136 | } 137 | 138 | return (cellStringValue, cellAttributedStringValue) 139 | } 140 | 141 | func emoteContent(align: NSTextAlignment = .left) -> NSAttributedString { 142 | let para = NSMutableParagraphStyle() 143 | para.alignment = align 144 | 145 | let text = NSMutableAttributedString(string: "* ", attributes: [ .paragraphStyle: para, .foregroundColor: NSColor.headerColor ]) 146 | text.append(self.textContent().attributedString!) 147 | text.append(NSMutableAttributedString(string: " *", attributes: [ .foregroundColor: NSColor.headerColor ])) 148 | 149 | return text 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Seaglass/Subclass/Room Message Types/RoomMessageInline.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | import SwiftMatrixSDK 21 | 22 | class RoomMessageInline: RoomMessage { 23 | @IBOutlet var Text: NSTextField! 24 | 25 | override func draw(_ dirtyRect: NSRect) { 26 | super.draw(dirtyRect) 27 | } 28 | 29 | override func viewWillDraw() { 30 | guard let event = event else { return } 31 | guard let roomId = event.roomId else { return } 32 | guard let room = MatrixServices.inst.session.room(withRoomId: roomId) else { return } 33 | guard event != drawnEvent || event.hash != drawnEventHash else { return } 34 | 35 | Text.allowsEditingTextAttributes = true 36 | 37 | Text.toolTip = super.timestamp(.medium, andDate: .medium) 38 | switch event.type { 39 | case "m.room.encrypted": 40 | Text.stringValue = "" 41 | Text.placeholderString = "Unable to decrypt (waiting for keys)" 42 | break 43 | case "m.room.member": 44 | let current = event.content ?? [:] 45 | let previous = event.prevContent ?? [:] 46 | var changes: [String] = [] 47 | 48 | for key in current.keys { 49 | if previous.keys.contains(key) { 50 | guard let previousKey = previous[key] as? String, 51 | let currentKey = current[key] as? String else { 52 | continue 53 | } 54 | 55 | if previousKey != currentKey { 56 | changes.append(key) 57 | } 58 | } else { 59 | changes.append(key) 60 | } 61 | } 62 | 63 | if changes.contains("membership") { 64 | changes = ["membership"] 65 | } 66 | 67 | let senderDisplayName = room.state.memberName(event.sender) ?? event.sender as String 68 | let prevDisplayName = 69 | event.prevContent != nil && event.prevContent.keys.contains("displayname") ? 70 | event.prevContent["displayname"] as! String? : 71 | event.stateKey as String 72 | let newDisplayName = 73 | event.content != nil && event.content.keys.contains("displayname") ? 74 | event.content["displayname"] as! String? : 75 | event.stateKey as String 76 | 77 | Text.stringValue = "" 78 | for change in changes { 79 | if Text.stringValue != "" { 80 | Text.stringValue.append("\n") 81 | } 82 | switch change { 83 | case "membership": 84 | switch current["membership"] as! String { 85 | case "join": 86 | Text.stringValue.append(contentsOf: "\(newDisplayName!) joined the room") 87 | break 88 | case "invite": 89 | Text.stringValue.append(contentsOf: "\(senderDisplayName) invited \(newDisplayName!)") 90 | break 91 | case "leave": 92 | Text.stringValue.append(contentsOf: "\(prevDisplayName!) left the room") 93 | break 94 | case "ban": 95 | Text.stringValue.append(contentsOf: "\(senderDisplayName) banned \(newDisplayName!)") 96 | break 97 | default: 98 | Text.stringValue.append(contentsOf: "\(newDisplayName!) unknown membership state: \(current["membership"] as! String)") 99 | break 100 | } 101 | break 102 | case "displayname": 103 | Text.stringValue.append(contentsOf: "\(prevDisplayName!) is now \(newDisplayName!)") 104 | break 105 | case "avatar_url": 106 | Text.stringValue.append(contentsOf: "\(newDisplayName!) changed their avatar") 107 | break 108 | default: 109 | Text.stringValue.append(contentsOf: "\(newDisplayName!) unknown change: \(change)") 110 | break 111 | } 112 | } 113 | break 114 | case "m.room.name": 115 | guard let name = event.content["name"] as? String else { break } 116 | if name != "" { 117 | Text.stringValue = "Room renamed: \(name)" 118 | } else { 119 | Text.stringValue = "Room name removed" 120 | } 121 | break 122 | case "m.room.topic": 123 | guard let topic = event.content["topic"] as? String else { break } 124 | if topic != "" { 125 | Text.stringValue = "Room topic changed: \(topic)" 126 | } else { 127 | Text.stringValue = "Room topic removed" 128 | } 129 | break 130 | case "m.room.avatar": 131 | guard let avatarUrl = event.content["url"] as? String else { break } 132 | if avatarUrl != "" { 133 | Text.stringValue = "Room avatar changed" 134 | } else { 135 | Text.stringValue = "Room avatar removed" 136 | } 137 | break 138 | case "m.room.canonical_alias": 139 | guard let roomAlias = event.content["alias"] as? String else { break } 140 | if roomAlias != "" { 141 | Text.stringValue = "Primary room alias set to \(roomAlias)" 142 | } else { 143 | Text.stringValue = "Primary room alias removed" 144 | } 145 | break 146 | case "m.room.create": 147 | guard let roomCreator = event.content["creator"] as? String else { break } 148 | let displayName = MatrixServices.inst.session.room(withRoomId: roomId).state.memberName(roomCreator) ?? roomCreator 149 | Text.stringValue = "Room created by \(displayName)" 150 | break 151 | case "m.room.encryption": 152 | let displayName = MatrixServices.inst.session.room(withRoomId: roomId).state.memberName(event.sender) ?? event.sender ?? "Room participant" 153 | Text.stringValue = "\(displayName) enabled room encryption (\(event.content["algorithm"] ?? "unknown") algorithm)" 154 | break 155 | default: 156 | guard let eventType = event.type else { break } 157 | Text.stringValue = "(unknown event \(eventType))" 158 | } 159 | if Text.stringValue == "" { 160 | Text.stringValue = "(no message)" 161 | } 162 | 163 | drawnEvent = event 164 | drawnEventHash = event.content.count 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /Seaglass/Subclass/Room Message Types/RoomMessageOutgoingCoalesced.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | import SwiftMatrixSDK 21 | 22 | class RoomMessageOutgoingCoalesced: RoomMessage { 23 | @IBOutlet var Text: NSTextField! 24 | @IBOutlet var TextConstraint: NSLayoutConstraint! 25 | @IBOutlet var Icon: ContextImageView! 26 | @IBOutlet var InlineImage: InlineImageView! 27 | @IBOutlet var InlineImageConstraint: NSLayoutConstraint! 28 | @IBOutlet var Time: NSTextField! 29 | @IBOutlet var RequestKeys: NSButton! 30 | 31 | override func draw(_ dirtyRect: NSRect) { 32 | super.draw(dirtyRect) 33 | } 34 | 35 | override func viewWillDraw() { 36 | guard event != nil else { return } 37 | guard let roomId = event!.roomId else { return } 38 | guard let room = MatrixServices.inst.session.room(withRoomId: roomId) else { return } 39 | guard event != drawnEvent || event!.hash != drawnEventHash else { return } 40 | 41 | RequestKeys.isHidden = !super.encryptionIsPending() 42 | 43 | Text.allowsEditingTextAttributes = true 44 | 45 | Time.stringValue = super.timestamp() 46 | Time.toolTip = super.timestamp(.medium, andDate: .medium) 47 | 48 | let icon = super.icon() 49 | Icon.isHidden = !room.state.isEncrypted 50 | Icon.image = icon.image 51 | Icon.setFrameSize(room.state.isEncrypted ? NSMakeSize(icon.width, icon.height) : NSMakeSize(0, icon.height)) 52 | 53 | var finalTextColor = NSColor.textColor 54 | 55 | switch event!.type { 56 | case "m.room.encrypted": 57 | Text.stringValue = "" 58 | Text.placeholderString = "Unable to decrypt (waiting for keys)" 59 | break 60 | case "m.sticker": 61 | if let info = event!.content["info"] as? [String: Any] { 62 | if let thumbnailUrl = info["thumbnail_url"] as? String { 63 | let mimetype = info["mimetype"] as? String? ?? "application/octet-stream" 64 | InlineImage.setImage(forMxcUrl: thumbnailUrl, withMimeType: mimetype, useCached: true, enableQuickLook: false) 65 | InlineImage.isHidden = false 66 | } else { 67 | Text.stringValue = event!.content["body"] as? String ?? "Sticker failed to load" 68 | InlineImage.isHidden = true 69 | } 70 | Text.isHidden = !InlineImage.isHidden 71 | } 72 | break 73 | default: 74 | if let msgtype = event!.content["msgtype"] as? String? { 75 | InlineImage.isHidden = ["m.emote", "m.notice", "m.text"].contains(where: { $0 == msgtype }) 76 | if InlineImage.isHidden { 77 | InlineImage.resetImage() 78 | } 79 | Text.isHidden = !InlineImage.isHidden 80 | Text.layer?.cornerRadius = 6 81 | Text.wantsLayer = true 82 | 83 | switch msgtype { 84 | case "m.image": 85 | if let info = event!.content["info"] as? [String: Any] { 86 | let mimetype = info["mimetype"] as? String? ?? "application/octet-stream" 87 | InlineImage.setImage(forMxcUrl: event!.content["url"] as? String, withMimeType: mimetype, useCached: true) 88 | } else { 89 | InlineImage.setImage(forMxcUrl: event!.content["url"] as? String, withMimeType: "application/octet-stream", useCached: true) 90 | } 91 | break 92 | case "m.emote": 93 | Text.attributedStringValue = super.emoteContent(align: .right) 94 | break 95 | case "m.notice": 96 | finalTextColor = NSColor.headerColor 97 | fallthrough 98 | case "m.text": 99 | let text = super.textContent() 100 | if text.attributedString != nil { 101 | Text.attributedStringValue = text.attributedString! 102 | } else if text.string != "" { 103 | Text.stringValue = text.string! 104 | } 105 | break 106 | default: 107 | InlineImage.isHidden = true 108 | Text.isHidden = false 109 | if event!.isMediaAttachment() { 110 | if let filename = event?.content["filename"] as? String ?? event?.content["body"] as? String { 111 | if let mxcUrl = event!.content["url"] as? String { 112 | let httpUrl = MatrixServices.inst.client.url(ofContent: mxcUrl) 113 | let link: NSMutableAttributedString = NSMutableAttributedString(string: filename) 114 | link.addAttribute(NSAttributedString.Key.link, value: httpUrl as Any, range: NSMakeRange(0, filename.count)) 115 | link.setAlignment(NSTextAlignment.right, range: NSMakeRange(0, filename.count)) 116 | Text.attributedStringValue = link 117 | } else { 118 | Text.placeholderString = filename 119 | } 120 | } else { 121 | Text.placeholderString = "Unnamed attachment" 122 | } 123 | } else { 124 | Text.placeholderString = "Message type '\(msgtype ?? "(none)")' not supported" 125 | } 126 | break 127 | } 128 | } else { 129 | Text.stringValue = "No content type" 130 | } 131 | } 132 | 133 | switch event!.sentState { 134 | case MXEventSentStateSending: 135 | Text.textColor = NSColor.gridColor 136 | break 137 | case MXEventSentStateFailed: 138 | Text.textColor = NSColor.red 139 | break 140 | default: 141 | Text.textColor = finalTextColor 142 | } 143 | 144 | self.updateIcon() 145 | drawnEvent = event 146 | drawnEventHash = event!.content.count 147 | } 148 | 149 | override func updateIcon() { 150 | if event == nil { 151 | return 152 | } 153 | 154 | let roomId = event!.roomId 155 | let room = MatrixServices.inst.session.room(withRoomId: roomId) 156 | let icon = super.icon() 157 | 158 | Icon.room = room 159 | Icon.event = event! 160 | switch event!.sentState { 161 | case MXEventSentStateFailed: 162 | if !room!.state.isEncrypted { 163 | Icon.isHidden = false 164 | Icon.setFrameSize(NSMakeSize(icon.width, icon.height)) 165 | } 166 | Icon.image = NSImage(named: NSImage.refreshTemplateName)!.tint(with: NSColor.red) 167 | break 168 | default: 169 | break 170 | } 171 | TextConstraint.constant = 48 + Icon.frame.size.width 172 | InlineImageConstraint.constant = 48 + Icon.frame.size.width 173 | } 174 | 175 | @IBAction func requestKeysPressed(_ sender: NSButton) { 176 | guard sender == RequestKeys && RequestKeys.isHidden == false else { return } 177 | 178 | MatrixServices.inst.session.crypto.reRequestRoomKey(for: super.event) 179 | 180 | let alert = NSAlert() 181 | alert.messageText = "Encryption keys requested" 182 | alert.informativeText = "Encryption keys have been requested from your other Matrix clients. If your device is not verified, you may see a key sharing request." 183 | alert.alertStyle = .informational 184 | alert.addButton(withTitle: "OK") 185 | alert.beginSheetModal(for: super.window!) { (response) in 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /Seaglass/Subclass/Room View/AutoGrowingTextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | // Based on: https://gist.github.com/entotsu/ddc136832a87a0fd2f9a0a6d4cf754ea 22 | // https://github.com/DouglasHeriot/AutoGrowingNSTextField 23 | 24 | class AutoGrowingTextField: NSTextField { 25 | 26 | let bottomSpace: CGFloat = 2 27 | // magic number! (the field editor TextView is offset within the NSTextField. It’s easy to get the space above (it’s origin), but it’s difficult to get the default spacing for the bottom, as we may be changing the height 28 | 29 | var heightLimit: CGFloat? 30 | var lastSize: NSSize? 31 | var isEditing = false 32 | 33 | override func textDidBeginEditing(_ notification: Notification) { 34 | super.textDidBeginEditing(notification) 35 | isEditing = true 36 | } 37 | 38 | override func textDidEndEditing(_ notification: Notification) { 39 | super.textDidEndEditing(notification) 40 | isEditing = false 41 | } 42 | 43 | override func textDidChange(_ notification: Notification) { 44 | super.textDidChange(notification) 45 | self.invalidateIntrinsicContentSize() 46 | } 47 | 48 | override var intrinsicContentSize: NSSize { 49 | let minSize: NSSize = super.intrinsicContentSize 50 | 51 | // Only update the size if we’re editing the text, or if we’ve not set it yet 52 | // If we try and update it while another text field is selected, it may shrink back down to only the size of one line (for some reason?) 53 | if isEditing || lastSize == nil { 54 | guard let 55 | // If we’re being edited, get the shared NSTextView field editor, so we can get more info 56 | textView = self.window?.fieldEditor(false, for: self) as? NSTextView, 57 | let container = textView.textContainer, 58 | let newHeight = container.layoutManager?.usedRect(for: container).height 59 | else { 60 | return lastSize ?? minSize 61 | } 62 | var newSize = super.intrinsicContentSize 63 | newSize.height = newHeight + bottomSpace 64 | 65 | if let 66 | heightLimit = heightLimit, 67 | let lastSize = lastSize, newSize.height > heightLimit { 68 | newSize = lastSize 69 | } 70 | 71 | lastSize = newSize 72 | return newSize 73 | } 74 | else { 75 | return lastSize ?? minSize 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Seaglass/Subclass/Room View/MainViewTableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | class MainViewTableView: NSTableView { 22 | 23 | var roomId: String! = "" 24 | 25 | override func draw(_ dirtyRect: NSRect) { 26 | super.draw(dirtyRect) 27 | 28 | // Drawing code here. 29 | } 30 | 31 | func scrollRowToVisible(row: Int, animated: Bool) { 32 | if animated { 33 | guard let clipView = superview as? NSClipView, 34 | let _ = clipView.superview as? NSScrollView else { 35 | return 36 | } 37 | 38 | let rowRect = rect(ofRow: row) 39 | var scrollOrigin = rowRect.origin 40 | 41 | let tableHalfHeight = clipView.frame.height * 0.5 42 | let rowRectHalfHeight = rowRect.height * 0.5 43 | 44 | scrollOrigin.y = (scrollOrigin.y - tableHalfHeight) + rowRectHalfHeight 45 | 46 | clipView.animator().setBoundsOrigin(scrollOrigin) 47 | } else { 48 | scrollRowToVisible(row) 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Seaglass/Subclass/Room View/MessageInputField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | @IBDesignable class MessageInputField: NSControl, NSTextFieldDelegate { 22 | @IBOutlet var contentView: NSView? 23 | @IBOutlet var textField: AutoGrowingTextField! 24 | @IBOutlet var delegate: NSObject? 25 | @IBOutlet var emojiButton: NSButton! 26 | 27 | required init?(coder: NSCoder) { 28 | super.init(coder: coder) 29 | 30 | var topLevelObjects : NSArray? 31 | if Bundle.main.loadNibNamed("MessageInputField", owner: self, topLevelObjects: &topLevelObjects) { 32 | contentView = topLevelObjects!.first(where: { $0 is NSView }) as? NSView 33 | self.addSubview(contentView!) 34 | contentView?.frame = self.bounds 35 | 36 | textField.focusRingType = .none 37 | textField.delegate = self 38 | } 39 | } 40 | 41 | @IBAction func emojiButtonClicked(_ sender: NSButton) { 42 | textField.selectText(self) 43 | 44 | let lengthOfInput = NSString(string: textField.stringValue).length 45 | textField.currentEditor()?.selectedRange = NSMakeRange(lengthOfInput, 0) 46 | 47 | NSApplication.shared.orderFrontCharacterPalette(nil) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Seaglass/Subclass/Room View/MessageInputField.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /Seaglass/Subclass/Room View/RoomMessageEntry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import Cocoa 20 | 21 | class RoomMessageEntry: NSTableCellView { 22 | @IBOutlet var RoomMessageEntryInboundFrom: NSTextField! 23 | @IBOutlet var RoomMessageEntryInboundText: NSTextField! 24 | @IBOutlet var RoomMessageEntryInboundIcon: AvatarImageView! 25 | @IBOutlet var RoomMessageEntryInboundPadlock: ContextImageView! 26 | @IBOutlet var RoomMessageEntryInboundTextConstraint: NSLayoutConstraint! 27 | @IBOutlet var RoomMessageEntryInboundTime: NSTextField! 28 | 29 | @IBOutlet var RoomMessageEntryInboundCoalescedText: NSTextField! 30 | @IBOutlet var RoomMessageEntryInboundCoalescedPadlock: ContextImageView! 31 | @IBOutlet var RoomMessageEntryInboundCoalescedTextConstraint: NSLayoutConstraint! 32 | @IBOutlet var RoomMessageEntryInboundCoalescedTime: NSTextField! 33 | 34 | @IBOutlet var RoomMessageEntryOutboundFrom: NSTextField! 35 | @IBOutlet var RoomMessageEntryOutboundText: NSTextField! 36 | @IBOutlet var RoomMessageEntryOutboundIcon: AvatarImageView! 37 | @IBOutlet var RoomMessageEntryOutboundPadlock: ContextImageView! 38 | @IBOutlet var RoomMessageEntryOutboundTextConstraint: NSLayoutConstraint! 39 | @IBOutlet var RoomMessageEntryOutboundTime: NSTextField! 40 | 41 | @IBOutlet var RoomMessageEntryOutboundMediaFrom: NSTextField! 42 | @IBOutlet var RoomMessageEntryOutboundMediaCollection: NSCollectionView! 43 | @IBOutlet var RoomMessageEntryOutboundMediaIcon: AvatarImageView! 44 | @IBOutlet var RoomMessageEntryOutboundMediaPadlock: ContextImageView! 45 | @IBOutlet var RoomMessageEntryOutboundMediaTime: NSTextField! 46 | 47 | @IBOutlet var RoomMessageEntryOutboundCoalescedText: NSTextField! 48 | @IBOutlet var RoomMessageEntryOutboundCoalescedPadlock: ContextImageView! 49 | @IBOutlet var RoomMessageEntryOutboundCoalescedTextConstraint: NSLayoutConstraint! 50 | @IBOutlet var RoomMessageEntryOutboundCoalescedTime: NSTextField! 51 | 52 | 53 | 54 | @IBOutlet var RoomMessageEntryInlineText: NSTextField! 55 | } 56 | -------------------------------------------------------------------------------- /Seaglass/Subclass/Room View/VibrancyArea.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VibrancyBox.swift 3 | // Seaglass 4 | // 5 | // Created by Neil Alexander on 10/09/2018. 6 | // Copyright © 2018 Neil Alexander. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class VibrancyArea: NSVisualEffectView { 12 | 13 | required init?(coder decoder: NSCoder) { 14 | super.init(coder: decoder) 15 | self.blendingMode = .withinWindow 16 | } 17 | 18 | override func draw(_ dirtyRect: NSRect) { 19 | self.blendingMode = .withinWindow 20 | super.draw(dirtyRect) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /SeaglassTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /SeaglassTests/SeaglassTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import XCTest 20 | @testable import Seaglass 21 | 22 | class SeaglassTests: XCTestCase { 23 | 24 | override func setUp() { 25 | super.setUp() 26 | // Put setup code here. This method is called before the invocation of each test method in the class. 27 | } 28 | 29 | override func tearDown() { 30 | // Put teardown code here. This method is called after the invocation of each test method in the class. 31 | super.tearDown() 32 | } 33 | 34 | func testExample() { 35 | // This is an example of a functional test case. 36 | // Use XCTAssert and related functions to verify your tests produce the correct results. 37 | } 38 | 39 | func testPerformanceExample() { 40 | // This is an example of a performance test case. 41 | self.measure { 42 | // Put the code you want to measure the time of here. 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /SeaglassUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /SeaglassUITests/SeaglassUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Seaglass, a native macOS Matrix client 3 | // Copyright © 2018, Neil Alexander 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | // 18 | 19 | import XCTest 20 | 21 | class SeaglassUITests: XCTestCase { 22 | 23 | override func setUp() { 24 | super.setUp() 25 | 26 | // Put setup code here. This method is called before the invocation of each test method in the class. 27 | 28 | // In UI tests it is usually best to stop immediately when a failure occurs. 29 | continueAfterFailure = false 30 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 31 | XCUIApplication().launch() 32 | 33 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 34 | } 35 | 36 | override func tearDown() { 37 | // Put teardown code here. This method is called after the invocation of each test method in the class. 38 | } 39 | 40 | func testLogin() { 41 | let defaultServer = "matrix.org" 42 | 43 | let loginWindowController = XCUIApplication().windows["LoginWindowController"] 44 | loginWindowController.staticTexts["Seaglass"].click() 45 | 46 | let advancedButton = loginWindowController/*@START_MENU_TOKEN@*/.buttons["LoginAdvancedButton"]/*[[".buttons[\" matrix.org\"]",".buttons[\"action\"]",".buttons[\"LoginAdvancedButton\"]"],[[[-1,2],[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/ 47 | advancedButton.click() 48 | 49 | let homeserverURLTextField = loginWindowController.popovers.textFields["https://" + defaultServer] 50 | homeserverURLTextField.typeKey(.delete, modifierFlags:[]) 51 | homeserverURLTextField.typeText("\r") 52 | 53 | 54 | let accountNameField = loginWindowController/*@START_MENU_TOKEN@*/.textFields["AccountName"]/*[[".textFields[\"matrix.org username\"]",".textFields[\"AccountName\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/ 55 | //let accountPasswordField = loginWindowController.textFields["AccountPassword"] 56 | // doesn't find AccountPassword for unknown reason? 57 | accountNameField.click() 58 | XCTAssertEqual(accountNameField.placeholderValue, defaultServer + " username") 59 | //XCTAssertEqual(accountPasswordField.placeholderValue, defaultServer + " password") 60 | 61 | advancedButton.click() 62 | XCTAssertEqual(homeserverURLTextField.value as! String, "https://" + defaultServer) 63 | 64 | var newServer = "kde.modular.im" 65 | homeserverURLTextField.typeText("https://" + newServer) 66 | accountNameField.click() 67 | XCTAssertEqual(accountNameField.placeholderValue, newServer + " username") 68 | 69 | newServer = "not\\a\\url" 70 | advancedButton.click() 71 | homeserverURLTextField.typeText("https://" + newServer) 72 | accountNameField.click() 73 | XCTAssertEqual(accountNameField.placeholderValue, defaultServer + " username") 74 | advancedButton.click() 75 | XCTAssertEqual(homeserverURLTextField.value as! String, "https://" + defaultServer) 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Seaglass 5 | 6 | 0.0.477-automated-sparkle-f02ae61 7 | Thu, 20 Sep 2018 21:25:15 -0500 8 | 10.13 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /dsa_pub.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIGRzCCBDoGByqGSM44BAEwggQtAoICAQDuCMNJthX1wyW+PcCve2sPeUUO9xxJ 3 | MhY5u/Rzd8+v+MsAVlASA8YMBJq+PMjFZknhiYG5dUEcMCJUxpv0zRtO+WKkS8ZW 4 | 8fIf2DvBnQiYT70y+F8798+OvEX++1+UVfhNac07ImZZvcTHrYvI3Yxwj4H/u/0M 5 | dcslJudxd+EmhXFX5zxQvn1x9sLGVnn8/c08T9PsfkceNW1+SPs/eSzHANHUVwUS 6 | vqwX+ecLYZJhCCND7FvWpa+I/I6LKO6t8J/tsSRjvfkeYN/MvbhHSaRC5yF5m41Y 7 | TjLWxDsIKVT/GumqLuHxHxhsb1EXEqu2ae3VIlmTYM+dDdKZlNOHOqwKXlaobo3U 8 | QxeBSxqB3JXtie/hvgo0L1iWKKuiouwVHMLF1P+atVFmdJNS3Ultb6TcNOLfd6Az 9 | jsSGImbDuSUZJWG15wMQhWwpcbr9j+xw4LYcvef5TWsLWsHa3Gn/RqmhXtl5piFe 10 | gvAQjcjIYzS+s5M0O86R+ODaOQwlAU+sMIpbo5stWq9MCwS7b1dsMw8AjOAgoWCF 11 | 2danDt28wdw/jz7iFbu3T3TSD55nwWodGpmkIrB1gyfC3D83RfRdd+cmw3vnTgcs 12 | G+iYnkiS9zkFeGzMYd24EcYutssKEf4egduCs2IKEy+LsKygXf4nPngntJ8OQ0Pk 13 | q3tTnUOx+j9nRwIhAMTehjdQwNHB8xFzKKvKXDw2LoUiSe3uAdtC/ERkuq/pAoIC 14 | AQDCGTZhHfxCOuTNCbWyqqSV60SY/4AAzz+sw2dzQAt/3sbP7oWzWPsqgD3zPE5B 15 | zso/DBNWhX3x5DEev/MlD5mu6OWMxxA5r02RqXQfIIQEOybS525+y3QNE6K6kSO7 16 | GU7nsheg3e3G/WbMQC/zPxUBphSdvWUXNprDvp583eqJqKca+66M1TNFx/PxQ6Dk 17 | J6DsPbeBhqVHKSptigjxZ69Dl27ptH/bz5ntiOFNWALPX6+wqrzeKTIgT2iE0KHv 18 | zWNCQO0xF37STJMLCV0W7oTi0yiOFeGzE6KTkNW8is8Oa58NFy1Ek4Yk1d4DXUSS 19 | dg4K5dCO5vDI33FPKNogcvH8YspLO6cIZYiA2iryPXoCNnbYP2DLJbugjerQG1DU 20 | 7VeCIgcSEOyCKWOFRPKDZox9dQ01grddYYxrUIxqnjDqDJGh7oeVaG/2L72nucbW 21 | 8lJPP0r8/PJ+yWbAhS6mme7aCIixk9m0GGqr5soo3HGMUayJoHdNbR9QZ2co+u37 22 | sX6cSUa7RkdM+4BPlL+NSe0YP8L6Nii4xbfZFQTsFWdglSjJTXlGBJ+3/lS1VGWV 23 | qLmk+oCxnsKcCWttfxx6HtmyLLt7NMXDSAxaPOaQCwmC2g8O+BhtSgJfkZ8N1vMh 24 | lORr3gXh7y/FRmkdtmKTTp857IjUwAbDUAGi633ajw4atwOCAgUAAoICAFP5tj7G 25 | rlweIVN+Y/Qs5UvAgUG/VFSp6E5aJZG5LhA1GMmftiTJwJYV9nWC7VMEu8iC/RIB 26 | MuthWs4bJbPkeV2p+hD0edGB+lxBvwi8xnCMxt7J/hLPJ5C7RQ802w8JLDTMsMmh 27 | HR7z+oBosGAsWeEclDHU6HCVuecteKPS9LLxEgHZ4engIsJ84+31ZT0bmEMjH9kw 28 | DTVIZnX8mXp0Z+QGu0K+LpTWkghZtAI7Yrs4sTAI+d0HTOyr31svs9PPK3nx0INS 29 | LSUjb/jVBzdnSHZuI33s5ljXg2ZCOKfDxwXRD/RBz+Eq37TI0WMj77cJrWJ14eRo 30 | c9+dqlt++ueSMba3Za+f4kaMSGiMXIW9RSxbbBXJDETs8tlCqhbQQ4vNau6Qq3GD 31 | BXQ4aNuqgtq/9PkS3Hf5LO3kgtNd26I8zIkzmuAA/CPCAsXC0i1n2FiK3k9mzGif 32 | iiJcIog/gUiPpcXsItN9WAYizgFxnLr5CI76q1mUXCkyOCDcbzuaqNLvt9Tp/tS7 33 | /NKKAtah8Sk+Z9o75boWMyWLe0+aVFDOz8e2CbkJnUaDEoEY4GNPKOtcWVS7bOKH 34 | mqNZAl+kxB/dmDwLkvQrX7VkuRjdT1tc8wj2I1IxXfOcd/XV4+AUxzci3fJfN8x9 35 | LcGU6BcWTtXM5+yiSuMkE0EXsnAD7JzeNCOP 36 | -----END PUBLIC KEY----- 37 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | fastlane_version "2.125.0" 2 | default_platform(:mac) 3 | 4 | platform :mac do 5 | GITHUB_REMOTE = "origin" 6 | RELEASE_BRANCH = "release" 7 | GITHUB_REPOSITORY = "neilalexander/seaglass" 8 | 9 | VERSION = sh "bash ../scripts/version.sh" 10 | ZIP_PATH = "/tmp/seaglass/upload/Seaglass-#{VERSION}.zip" 11 | 12 | desc "Build and release Seaglass, this is the lane you probably want to use" 13 | lane :build_and_release do 14 | build 15 | release 16 | end 17 | 18 | desc "Build Seaglass" 19 | lane :build do 20 | # builds Seaglass and creates a .xcarchive 21 | gym 22 | 23 | # exports Seaglass.app from the .xcarchive 24 | xcexport( 25 | archivePath: "/tmp/seaglass/build/Seaglass.xcarchive", 26 | export_options_plist: ".ci/ExportOptions.plist", 27 | export_xcargs: "CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO", 28 | export_path: "/tmp/seaglass/", 29 | output_name: "Seaglass.app" 30 | ) 31 | 32 | zip( 33 | path: "/tmp/seaglass/Seaglass.app", 34 | output_path: ZIP_PATH 35 | ) 36 | end 37 | 38 | desc "Release Seaglass on GitHub" 39 | lane :release do 40 | set_github_release( 41 | repository_name: GITHUB_REPOSITORY, 42 | name: VERSION, 43 | tag_name: VERSION, 44 | description: "", 45 | upload_assets: [ZIP_PATH], 46 | "is_prerelease": true, 47 | api_token: ENV["GITHUB_API_TOKEN"] 48 | ) 49 | 50 | sparkle_add_version 51 | end 52 | 53 | desc "Updates Sparkle RSS file" 54 | lane :sparkle_add_version do 55 | app_download_url = "https://github.com/neilalexander/seaglass/releases/download/#{VERSION}/Seaglass-#{VERSION}.zip" 56 | 57 | sparkle_add_update( 58 | feed_file: "appcast.xml", 59 | app_download_url: app_download_url, 60 | app_size: "#{File.size(ZIP_PATH)}", 61 | machine_version: get_info_plist_value(path: "/tmp/seaglass/Seaglass.app/Contents/Info.plist", key: "CFBundleVersion"), 62 | human_version: VERSION, 63 | title: VERSION, 64 | release_notes_link: "https://s3.eu-west-2.amazonaws.com/seaglass-ci/releasenotes.html", 65 | eddsa_signature: sh("../Pods/Sparkle/bin/sign_update", ZIP_PATH, "/tmp/dsa_priv.pem") 66 | ) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /fastlane/Gymfile: -------------------------------------------------------------------------------- 1 | clean(false) 2 | silent(true) 3 | 4 | workspace("Seaglass.xcworkspace") 5 | scheme("Seaglass") 6 | #configuration("release") 7 | archive_path("/tmp/seaglass/build/Seaglass.xcarchive") 8 | -------------------------------------------------------------------------------- /fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ================ 3 | # Installation 4 | 5 | Make sure you have the latest version of the Xcode command line tools installed: 6 | 7 | ``` 8 | xcode-select --install 9 | ``` 10 | 11 | Install _fastlane_ using 12 | ``` 13 | [sudo] gem install fastlane -NV 14 | ``` 15 | or alternatively using `brew cask install fastlane` 16 | 17 | # Available Actions 18 | ## Mac 19 | ### mac build_and_release 20 | ``` 21 | fastlane mac build_and_release 22 | ``` 23 | Build and release Seaglass, this is the lane you probably want to use 24 | ### mac build 25 | ``` 26 | fastlane mac build 27 | ``` 28 | Build Seaglass 29 | ### mac release 30 | ``` 31 | fastlane mac release 32 | ``` 33 | Release Seaglass on GitHub 34 | ### mac sparkle_add_version 35 | ``` 36 | fastlane mac sparkle_add_version 37 | ``` 38 | Updates Sparkle RSS file 39 | 40 | ---- 41 | 42 | This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. 43 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). 44 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 45 | -------------------------------------------------------------------------------- /fastlane/actions/sparkle_add_update.rb: -------------------------------------------------------------------------------- 1 | module Fastlane 2 | module Actions 3 | module SharedValues 4 | end 5 | 6 | class SparkleAddUpdateAction < Action 7 | 8 | # inspired by https://github.com/CocoaPods/CocoaPods-app/blob/578555e8e86a5f7fe3e56deeb5406ffb24e1541e/Rakefile#L1069 9 | 10 | def self.run(params) 11 | 12 | # UI.message "#{params[:feed_file]}" 13 | # UI.message "#{params[:app_download_url]}" 14 | # UI.message "#{params[:app_size]}" 15 | # UI.message "#{params[:machine_version]}" 16 | # UI.message "#{params[:human_version]}" 17 | # UI.message "#{params[:title]}" 18 | # UI.message "#{params[:release_notes_link]}" 19 | # UI.message "#{params[:deployment_target]}" 20 | # UI.message "#{params[:eddsa_signature]}" 21 | 22 | xml_file = params[:feed_file] 23 | 24 | # Load existing sparkle xml feed file 25 | require 'rexml/document' 26 | doc = REXML::Document.new(File.read(xml_file)) 27 | channel = doc.elements['/rss/channel'] 28 | 29 | # Verify that the new version is strictly greater than the last one in the list 30 | last_version = channel.elements.select { |e| e.name == 'item' }.last.get_elements('enclosure').first.attributes['version'] 31 | raise "You must update the machine version to be above #{last_version}!" unless params[:machine_version] > last_version 32 | 33 | # Add a new item to the Appcast feed 34 | item = channel.add_element('item') 35 | item.add_element("title").add_text(params[:title]) 36 | item.add_element("sparkle:minimumSystemVersion").add_text(params[:deployment_target]) if params[:deployment_target] 37 | item.add_element("sparkle:releaseNotesLink").add_text(params[:release_notes_link]) 38 | item.add_element("pubDate").add_text(DateTime.now.strftime("%a, %d %h %Y %H:%M:%S %z")) 39 | 40 | enclosure = item.add_element("enclosure") 41 | enclosure.attributes["type"] = "application/octet-stream" 42 | enclosure.attributes["sparkle:version"] = params[:machine_version] 43 | enclosure.attributes["sparkle:shortVersionString"] = params[:human_version] 44 | enclosure.attributes["sparkle:dsaSignature"] = params[:eddsa_signature].tr("\n", "") if params[:eddsa_signature] 45 | enclosure.attributes["length"] = params[:app_size] 46 | enclosure.attributes["url"] = params[:app_download_url] 47 | 48 | # Write it back out 49 | formatter = REXML::Formatters::Pretty.new(2) 50 | formatter.compact = true 51 | new_xml = "" 52 | formatter.write(doc, new_xml) 53 | File.open(xml_file, 'w') { |file| file.write new_xml } 54 | 55 | end 56 | 57 | ##################################################### 58 | # @!group Documentation 59 | #########################################s############ 60 | 61 | def self.description 62 | "Adds a new version entry into your Sparkle XML feed file" 63 | end 64 | 65 | def self.details 66 | "" 67 | end 68 | 69 | def self.available_options 70 | [ 71 | FastlaneCore::ConfigItem.new(key: :feed_file, 72 | env_name: "FL_SPARKLE_ADD_UPDATE_FEED_FILE", 73 | description: "Path to the xml feed file", 74 | default_value: "sparkle.xml", 75 | verify_block: proc do |value| 76 | raise "Couldn't find file at path '#{value}'".red unless File.exist?(value) 77 | end), 78 | FastlaneCore::ConfigItem.new(key: :app_download_url, 79 | env_name: "FL_SPARKLE_ADD_UPDATE_APP_DOWNLOAD_URL", 80 | description: "Download URL of the app update", 81 | verify_block: proc do |value| 82 | raise "Invalid URL '#{value}'".red unless (value and !value.empty?) 83 | end), 84 | FastlaneCore::ConfigItem.new(key: :app_size, 85 | env_name: "FL_SPARKLE_ADD_UPDATE_APP_SIZE", 86 | description: "App's size in bytes"), 87 | FastlaneCore::ConfigItem.new(key: :title, 88 | env_name: "FL_SPARKLE_ADD_UPDATE_TITLE", 89 | description: "Update title", 90 | verify_block: proc do |value| 91 | raise "Invalid title '#{value}'".red unless (value and !value.empty?) 92 | end), 93 | FastlaneCore::ConfigItem.new(key: :release_notes_link, 94 | env_name: "FL_SPARKLE_ADD_UPDATE_RELEASE_NOTES_LINK", 95 | description: "Update release notes link", 96 | verify_block: proc do |value| 97 | raise "Invalid release notes link '#{value}'".red unless (value and !value.empty?) 98 | end), 99 | FastlaneCore::ConfigItem.new(key: :machine_version, 100 | env_name: "FL_SPARKLE_ADD_UPDATE_MACHINE_VERSION", 101 | description: "Machine version, must be strictly greater than the previous one", 102 | verify_block: proc do |value| 103 | raise "Invalid machine version '#{value}'".red unless (value and !value.empty?) 104 | end), 105 | FastlaneCore::ConfigItem.new(key: :human_version, 106 | env_name: "FL_SPARKLE_ADD_UPDATE_HUMAN_VERSION", 107 | description: "Human version string, defaults to machine version", 108 | verify_block: proc do |value| 109 | raise "Invalid human version '#{value}'".red unless (value and !value.empty?) 110 | end), 111 | FastlaneCore::ConfigItem.new(key: :deployment_target, 112 | env_name: "FL_SPARKLE_ADD_UPDATE_DEPLOYMENT_TARGET", 113 | description: "Update's deployment target", 114 | optional: true), 115 | FastlaneCore::ConfigItem.new(key: :eddsa_signature, 116 | env_name: "FL_SPARKLE_ADD_EDDSA_SIGNATURE", 117 | description: "Adds an EdDSA signature to the enclosure", 118 | optional: true) 119 | ] 120 | end 121 | 122 | def self.output 123 | [] 124 | end 125 | 126 | def self.return_value 127 | end 128 | 129 | def self.authors 130 | ["czechboy0"] 131 | end 132 | 133 | def self.is_supported?(platform) 134 | platform == :mac 135 | end 136 | end 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neilalexander/seaglass/dc9789334340753c64131f9dace677be6d16bc49/image.png -------------------------------------------------------------------------------- /scripts/set_build_number.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | version=$(sh scripts/version.sh) 4 | bundleversion=$(git rev-list HEAD --count 2>/dev/null) 5 | 6 | target_plist="$TARGET_BUILD_DIR/$INFOPLIST_PATH" 7 | dsym_plist="$DWARF_DSYM_FOLDER_PATH/$DWARF_DSYM_FILE_NAME/Contents/Info.plist" 8 | 9 | for plist in "$target_plist" "$dsym_plist"; do 10 | if [ -f "$plist" ]; then 11 | /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $bundleversion" "$plist" 12 | /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $version" "$plist" 13 | fi 14 | done 15 | -------------------------------------------------------------------------------- /scripts/version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TAG=$(git describe --abbrev=0 --tags --match="v[0-9]*\.[0-9]*" 2>/dev/null) 4 | 5 | # Is there a tag? 6 | if [ $? != 0 ]; then 7 | # No tag was found, go from initial commit 8 | PATCH=$(git rev-list HEAD --count 2>/dev/null) 9 | TAG=v0.0 10 | else 11 | # Tag was found, go from there 12 | PATCH=$(git rev-list $TAG..HEAD --count 2>/dev/null) 13 | fi 14 | 15 | # Split out tag into major, minor and patch numbers 16 | MAJOR=$(echo $TAG | cut -c 2- | cut -d "." -f 1) 17 | MINOR=$(echo $TAG | cut -c 2- | cut -d "." -f 2) 18 | 19 | # Output version number in the desired format 20 | if [ $PATCH = 0 ]; then 21 | printf '%d.%d' "$MAJOR" "$MINOR" 22 | else 23 | printf '%d.%d.%d' "$MAJOR" "$MINOR" "$PATCH" 24 | fi 25 | 26 | # Get the current checked out branch 27 | BRANCH=$(git rev-parse --abbrev-ref HEAD) 28 | BRANCHTAG=$(echo $BRANCH | tr -cd [:alnum:]) 29 | 30 | # Add the build tag on non-release branches 31 | if [ $BRANCH != "release" ] && [ $BRANCH != "HEAD" ]; then 32 | # Get the number of merges on the current branch since that tag 33 | BUILD=$(git rev-list master...$BRANCH --count) 34 | 35 | # Append builds since last branch, if appropriate 36 | if [ $BUILD != 0 ]; then 37 | printf -- "-$BRANCHTAG-%04d" "$BUILD" 38 | else 39 | printf -- "-$BRANCHTAG" 40 | fi 41 | fi 42 | 43 | # Append the short commit hash for now 44 | printf -- "-$(git rev-parse --short HEAD)" 45 | --------------------------------------------------------------------------------