├── .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 | [](https://matrix.to/#/#seaglass:matrix.org)
4 | [](https://circleci.com/gh/neilalexander/seaglass)
5 | [](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 | 
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 |
--------------------------------------------------------------------------------