├── .gitignore
├── Circles.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcshareddata
│ └── xcschemes
│ │ └── Circles.xcscheme
└── xcuserdata
│ ├── cvwright.xcuserdatad
│ ├── xcdebugger
│ │ └── Breakpoints_v2.xcbkptlist
│ └── xcschemes
│ │ └── xcschememanagement.plist
│ └── ramius.xcuserdatad
│ └── xcschemes
│ └── xcschememanagement.plist
├── Circles
├── .DS_Store
├── Assets.xcassets
│ ├── .DS_Store
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── Circles-app-icon-1024.png
│ │ └── Contents.json
│ ├── Contents.json
│ ├── LaunchScreen
│ │ ├── Contents.json
│ │ ├── LaunchColor.colorset
│ │ │ └── Contents.json
│ │ ├── LoginColor.colorset
│ │ │ └── Contents.json
│ │ ├── launch-circle-logo.imageset
│ │ │ ├── Contents.json
│ │ │ └── launch-circle-logo.svg
│ │ ├── launch-logo-purple.imageset
│ │ │ ├── Contents.json
│ │ │ └── launch-logo-purple.svg
│ │ └── launchcirclebackground.imageset
│ │ │ ├── Contents.json
│ │ │ └── launchcirclebackground.png
│ ├── MColor.colorset
│ │ └── Contents.json
│ ├── RegistrationScreen
│ │ ├── Contents.json
│ │ ├── IconFilledArrowBack.imageset
│ │ │ ├── Contents.json
│ │ │ └── IconFilledArrowBack.svg
│ │ ├── Page.imageset
│ │ │ ├── Contents.json
│ │ │ └── Page.svg
│ │ ├── acceptedCircle.imageset
│ │ │ ├── Contents.json
│ │ │ └── acceptedCircle.svg
│ │ └── forwardArrow.imageset
│ │ │ ├── Contents.json
│ │ │ └── forwardArrow.svg
│ ├── Stock Photos
│ │ ├── .DS_Store
│ │ ├── Contents.json
│ │ ├── iStock-1176559812.imageset
│ │ │ ├── Contents.json
│ │ │ └── iStock-1176559812-10.jpg
│ │ ├── iStock-1225782571.imageset
│ │ │ ├── Contents.json
│ │ │ └── iStock-1225782571-400.jpeg
│ │ ├── iStock-1304744459.imageset
│ │ │ ├── Contents.json
│ │ │ └── iStock-1304744459-400.jpeg
│ │ ├── iStock-1356527683.imageset
│ │ │ ├── Contents.json
│ │ │ └── iStock-1356527683-400.jpeg
│ │ └── iStock-640313068.imageset
│ │ │ ├── Contents.json
│ │ │ └── iStock-640313068-400.jpeg
│ ├── circles-logo-dark.imageset
│ │ ├── Contents.json
│ │ └── circles-logo-dark.png
│ └── circles-logo-light.imageset
│ │ ├── Contents.json
│ │ └── circles-logo-light.png
├── Backend
│ ├── AppStoreInterface.swift
│ ├── CirclesAppDelegate.swift
│ ├── CirclesApplicationSession.swift
│ └── CirclesStore.swift
├── Changelogs
│ ├── CHANGELOG_Full.md
│ └── CHANGELOG_LastUpdates.md
├── Circles-Bridging-Header.h
├── Circles.entitlements
├── CirclesApp.swift
├── CirclesError.swift
├── CirclesTabbedInterface.swift
├── Constants.swift
├── ContentView.swift
├── DebugMode.swift
├── Info.plist
├── Launch Screen.storyboard
├── Matrix
│ ├── Account Data
│ │ ├── mDirectContent.swift
│ │ └── mIgnoredUserListContent.swift
│ └── MatrixAccountDataType.swift
├── Models
│ ├── ApplicationState.swift
│ ├── CirclesConfigV1.swift
│ ├── CirclesConfigV2.swift
│ ├── ContainerRoom.swift
│ ├── GalleryRoom.swift
│ ├── GroupRoom.swift
│ ├── PersonRoom.swift
│ ├── PowerLevel.swift
│ ├── ProfileSpace.swift
│ ├── Room+URL.swift
│ └── TimelineSpace.swift
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── PrivacyInfo.xcprivacy
├── Utilities
│ ├── Array+Extension.swift
│ ├── Array+RawRepresentable.swift
│ ├── Array+Sample.swift
│ ├── AsyncButton.swift
│ ├── BCrypt
│ │ ├── PerfectBCrypt.swift
│ │ ├── bcrypt.c
│ │ ├── blf.c
│ │ ├── blf.h
│ │ ├── portable_endian.h
│ │ └── pycabcrypt.h
│ ├── BlurHashDecode.swift
│ ├── BlurHashEncode.swift
│ ├── Color+extensions.swift
│ ├── CustomFonts.swift
│ ├── Data+hex.swift
│ ├── DeleteAndPurge.swift
│ ├── EmojiPicker.swift
│ ├── HtmlView.swift
│ ├── ImageSaver.swift
│ ├── Keychain.swift
│ ├── LegacySecretStoreKeygen.swift
│ ├── Movie.swift
│ ├── QR.swift
│ ├── RelativeTimestampFormatter.swift
│ ├── SaveImage.swift
│ ├── SecureFieldWithEye.swift
│ ├── Sequence+asyncMap.swift
│ ├── SystemImages.swift
│ ├── ToastPresenter.swift
│ ├── Utils.swift
│ ├── WebView.swift
│ └── zxcvbn
│ │ ├── DBMatcher.h
│ │ ├── DBMatcher.m
│ │ ├── DBPasswordStrengthMeterView.h
│ │ ├── DBPasswordStrengthMeterView.m
│ │ ├── DBScorer.h
│ │ ├── DBScorer.m
│ │ ├── DBZxcvbn.h
│ │ ├── DBZxcvbn.m
│ │ ├── Zxcvbn.h
│ │ ├── Zxcvbn.swift
│ │ └── generated
│ │ ├── adjacency_graphs.json
│ │ └── frequency_lists.json
├── Views
│ ├── Circles
│ │ ├── ChangelogSheet.swift
│ │ ├── CircleCreationSheet.swift
│ │ ├── CircleFollowingRow.swift
│ │ ├── CircleInvitationsIndicator.swift
│ │ ├── CircleInvitationsView.swift
│ │ ├── CirclePicker.swift
│ │ ├── CircleRenameView.swift
│ │ ├── CirclesOverviewScreen.swift
│ │ ├── FollowingTimelineDetailsView.swift
│ │ ├── InvitedCircleCard.swift
│ │ ├── InvitedCircleDetailView.swift
│ │ ├── SingleTimelineSettingsView.swift
│ │ ├── SingleTimelineView.swift
│ │ ├── TimelineOverviewCard.swift
│ │ ├── UnifiedTimelineComposerSheet.swift
│ │ ├── UnifiedTimelineSettingsView.swift
│ │ └── UnifiedTimelineView.swift
│ ├── CirclesLogoView.swift
│ ├── Composer
│ │ ├── CloudImagePicker.swift
│ │ ├── ImagePicker.swift
│ │ ├── PhotoPicker.swift
│ │ ├── PostComposer.swift
│ │ ├── PostComposerScreen.swift
│ │ └── VideoPicker.swift
│ ├── Groups
│ │ ├── GenericPersonDetailView.swift
│ │ ├── GroupCreationSheet.swift
│ │ ├── GroupHeader.swift
│ │ ├── GroupInvitationsIndicator.swift
│ │ ├── GroupInvitationsView.swift
│ │ ├── GroupOverviewRow.swift
│ │ ├── GroupSettingsView.swift
│ │ ├── GroupTimelineScreen.swift
│ │ ├── GroupsOverviewScreen.swift
│ │ ├── InvitedGroupCard.swift
│ │ ├── InvitedGroupDetailView.swift
│ │ └── LikedEmojiView.swift
│ ├── Help
│ │ └── CirclesHelpView.swift
│ ├── Home
│ │ └── SystemNoticesView.swift
│ ├── Login
│ │ ├── ForgotPasswordView.swift
│ │ ├── LegacyLoginScreen.swift
│ │ ├── SecretStorageCreationScreen.swift
│ │ ├── SecretStoragePasswordScreen.swift
│ │ ├── UiaLoginScreen.swift
│ │ └── WelcomeScreen.swift
│ ├── People
│ │ ├── FollowersView.swift
│ │ ├── FollowingView.swift
│ │ ├── FriendsOfFriendsView.swift
│ │ ├── InviteToFollowMeView.swift
│ │ ├── MutualFriendsSection.swift
│ │ ├── PeopleInvitationsIndicator.swift
│ │ ├── PeopleInvitationsView.swift
│ │ ├── PeopleOverviewScreen.swift
│ │ ├── PersonDetailView.swift
│ │ ├── PersonHeaderRow.swift
│ │ ├── PersonsCircleRow.swift
│ │ └── SelfDetailView.swift
│ ├── Photo Galleries
│ │ ├── GalleryGridView.swift
│ │ ├── GalleryInvitationsIndicator.swift
│ │ ├── GalleryInvitationsView.swift
│ │ ├── GalleryInviteCard.swift
│ │ ├── GallerySettingsView.swift
│ │ ├── PhotoContextMenu.swift
│ │ ├── PhotoDetailView.swift
│ │ ├── PhotoGalleryCard.swift
│ │ ├── PhotoGalleryCreationSheet.swift
│ │ ├── PhotoGalleryView.swift
│ │ ├── PhotoThumbnailCard.swift
│ │ ├── PhotosOverviewScreen.swift
│ │ ├── PhotosUploadView.swift
│ │ ├── VideoDetailView.swift
│ │ └── VideoThumbnailCard.swift
│ ├── Rooms
│ │ ├── BannedRoomMemberRow.swift
│ │ ├── KnockOnRoomView.swift
│ │ ├── KnockingUserCard.swift
│ │ ├── RoomAvatarView.swift
│ │ ├── RoomCryptoInfoView.swift
│ │ ├── RoomDebugDetailsSection.swift
│ │ ├── RoomDefaultPowerLevelPicker.swift
│ │ ├── RoomInviteOneUserSheet.swift
│ │ ├── RoomInviteSheet.swift
│ │ ├── RoomInvitedMemberRow.swift
│ │ ├── RoomKnockDetailsView.swift
│ │ ├── RoomKnockIndicator.swift
│ │ ├── RoomMemberDetailView.swift
│ │ ├── RoomMemberRow.swift
│ │ ├── RoomMembersSection.swift
│ │ ├── RoomMembersSheet.swift
│ │ ├── RoomRenameView.swift
│ │ ├── RoomSecurityInfoSheet.swift
│ │ ├── RoomShareSheet.swift
│ │ ├── RoomTopicEditorView.swift
│ │ ├── ScanQrCodeAndKnockSheet.swift
│ │ ├── ScanQrCodeView.swift
│ │ └── UsersToInviteView.swift
│ ├── Settings
│ │ ├── AcknowledgementsView.swift
│ │ ├── DeactivateAccountCustomAlertView.swift
│ │ ├── DeactivateAccountView.swift
│ │ ├── DeviceDetailsView.swift
│ │ ├── DeviceInfoView.swift
│ │ ├── DevicesScreen.swift
│ │ ├── EmailSettingsView.swift
│ │ ├── IgnoredUsersView.swift
│ │ ├── NotificationsSettingsView.swift
│ │ ├── ProfileSettingsView.swift
│ │ ├── SecuritySettingsView.swift
│ │ ├── SettingsScreen.swift
│ │ ├── StorageSettingsView.swift
│ │ ├── SubscriptionSettingsView.swift
│ │ ├── UpdateDisplaynameView.swift
│ │ └── UpdateStatusMessageView.swift
│ ├── Setup
│ │ ├── CircleSetupInfo.swift
│ │ ├── SetupAddNewCircleSheet.swift
│ │ ├── SetupAvatarView.swift
│ │ ├── SetupCircleCard.swift
│ │ ├── SetupCirclesView.swift
│ │ ├── SetupIntroToCircles.swift
│ │ └── SetupScreen.swift
│ ├── Signup
│ │ ├── AccountInfoForm.swift
│ │ ├── SignUpScreen.swift
│ │ ├── SignupFinishedView.swift
│ │ ├── SignupStartForm.swift
│ │ └── TokenForm.swift
│ ├── Timeline
│ │ ├── CommentCard.swift
│ │ ├── CommentsView.swift
│ │ ├── LikeButton.swift
│ │ ├── MessageAuthorHeader.swift
│ │ ├── MessageCard.swift
│ │ ├── MessageContextMenu.swift
│ │ ├── MessageReportingSheet.swift
│ │ ├── MessageSheetType.swift
│ │ ├── MessageThumbnail.swift
│ │ ├── MessageView.swift
│ │ ├── StateEventView.swift
│ │ ├── ThreadView.swift
│ │ ├── TimelineView.swift
│ │ ├── TimelineViewModel.swift
│ │ ├── UnifiedTimeline.swift
│ │ ├── UserAvatarView.swift
│ │ ├── UserNameView.swift
│ │ └── VideoContentView.swift
│ ├── UIA
│ │ ├── BsspekeEnrollOprfForm.swift
│ │ ├── BsspekeEnrollSaveForm.swift
│ │ ├── BsspekeLoginForm.swift
│ │ ├── EmailEnrollRequestTokenForm.swift
│ │ ├── EmailEnrollSubmitTokenForm.swift
│ │ ├── EmailLoginRequestTokenForm.swift
│ │ ├── EmailLoginSubmitTokenForm.swift
│ │ ├── FreeSubscriptionForm.swift
│ │ ├── LegacyPasswordUiaForm.swift
│ │ ├── SubscriptionUIaForm.swift
│ │ ├── TermsOfServiceForm.swift
│ │ ├── UiaInProgressView.swift
│ │ ├── UiaOverlayView.swift
│ │ ├── UiaView.swift
│ │ └── UsernameEnrollForm.swift
│ └── View+Extensions
│ │ ├── ButtonStyle+Extensions.swift
│ │ ├── UIDevice+Extensions.swift
│ │ ├── View+Extentions.swift
│ │ └── ViewModifier+Extensions.swift
└── fonts
│ ├── Inter-VariableFont.ttf
│ └── Nunito-VariableFont.ttf
├── FUTO Circles.storekit
├── LICENSE
├── NSE
├── Constants.swift
├── Info.plist
├── NSE.entitlements
└── NotificationService.swift
├── Products.storekit
├── README.md
└── assets
├── .DS_Store
└── images
├── circles-and-groups.jpeg
├── circles-screenshots.jpeg
├── groups-screenshots.jpeg
├── kickstarter-logo-green.png
└── photogallery-screenshots.jpeg
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## Internal
9 | *Package.resolved
10 | *UserInterfaceState.xcuserstate
11 | *xcschememanagement.plist
12 |
13 | ## Obj-C/Swift specific
14 | *.hmap
15 |
16 | ## App packaging
17 | *.ipa
18 | *.dSYM.zip
19 | *.dSYM
20 |
21 | ## Playgrounds
22 | timeline.xctimeline
23 | playground.xcworkspace
24 |
25 | # Swift Package Manager
26 | #
27 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
28 | # Packages/
29 | # Package.pins
30 | # Package.resolved
31 | # *.xcodeproj
32 | #
33 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
34 | # hence it is not needed unless you have added a package configuration file to your project
35 | # .swiftpm
36 |
37 | .build/
38 |
39 | # CocoaPods
40 | #
41 | # We recommend against adding the Pods directory to your .gitignore. However
42 | # you should judge for yourself, the pros and cons are mentioned at:
43 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
44 | #
45 | # Pods/
46 | #
47 | # Add this line if you want to avoid checking in source code from the Xcode workspace
48 | # *.xcworkspace
49 |
50 | # Carthage
51 | #
52 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
53 | # Carthage/Checkouts
54 |
55 | Carthage/Build/
56 |
57 | # fastlane
58 | #
59 | # It is recommended to not store the screenshots in the git repo.
60 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
61 | # For more information about the recommended setup visit:
62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
63 |
64 | fastlane/report.xml
65 | fastlane/Preview.html
66 | fastlane/screenshots/**/*.png
67 | fastlane/test_output
68 |
69 | # Others
70 | # MacOS
71 | .DS_Store
72 | # Vim
73 | *.swo
74 | *.swp
75 |
--------------------------------------------------------------------------------
/Circles.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Circles.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Circles.xcodeproj/xcuserdata/cvwright.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/Circles.xcodeproj/xcuserdata/ramius.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | Circles.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 9
11 |
12 |
13 | SuppressBuildableAutocreation
14 |
15 | 404C72CC265DDF35000473DF
16 |
17 | primary
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Circles/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/circles-project/circles-ios/5aec0b5cdb379fe13113ec55f4475a020b9c1834/Circles/.DS_Store
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/circles-project/circles-ios/5aec0b5cdb379fe13113ec55f4475a020b9c1834/Circles/Assets.xcassets/.DS_Store
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "1.000",
9 | "green" : "0.161",
10 | "red" : "0.404"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "1.000",
27 | "green" : "0.161",
28 | "red" : "0.404"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/AppIcon.appiconset/Circles-app-icon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/circles-project/circles-ios/5aec0b5cdb379fe13113ec55f4475a020b9c1834/Circles/Assets.xcassets/AppIcon.appiconset/Circles-app-icon-1024.png
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Circles-app-icon-1024.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/LaunchScreen/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/LaunchScreen/LaunchColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "1.000",
9 | "green" : "0.161",
10 | "red" : "0.404"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "1.000",
27 | "green" : "0.161",
28 | "red" : "0.404"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/LaunchScreen/LoginColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.902",
9 | "green" : "0.882",
10 | "red" : "0.871"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.902",
27 | "green" : "0.882",
28 | "red" : "0.871"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/LaunchScreen/launch-circle-logo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "launch-circle-logo.svg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/LaunchScreen/launch-logo-purple.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "launch-logo-purple.svg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/LaunchScreen/launchcirclebackground.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "launchcirclebackground.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/LaunchScreen/launchcirclebackground.imageset/launchcirclebackground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/circles-project/circles-ios/5aec0b5cdb379fe13113ec55f4475a020b9c1834/Circles/Assets.xcassets/LaunchScreen/launchcirclebackground.imageset/launchcirclebackground.png
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/MColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "1.000",
9 | "green" : "1.000",
10 | "red" : "1.000"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.000",
27 | "green" : "0.000",
28 | "red" : "0.000"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/RegistrationScreen/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/RegistrationScreen/IconFilledArrowBack.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "IconFilledArrowBack.svg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/RegistrationScreen/IconFilledArrowBack.imageset/IconFilledArrowBack.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/RegistrationScreen/Page.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Page.svg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/RegistrationScreen/Page.imageset/Page.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/RegistrationScreen/acceptedCircle.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "acceptedCircle.svg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/RegistrationScreen/acceptedCircle.imageset/acceptedCircle.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/RegistrationScreen/forwardArrow.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "forwardArrow.svg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/RegistrationScreen/forwardArrow.imageset/forwardArrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/Stock Photos/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/circles-project/circles-ios/5aec0b5cdb379fe13113ec55f4475a020b9c1834/Circles/Assets.xcassets/Stock Photos/.DS_Store
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/Stock Photos/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/Stock Photos/iStock-1176559812.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "iStock-1176559812-10.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/Stock Photos/iStock-1176559812.imageset/iStock-1176559812-10.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/circles-project/circles-ios/5aec0b5cdb379fe13113ec55f4475a020b9c1834/Circles/Assets.xcassets/Stock Photos/iStock-1176559812.imageset/iStock-1176559812-10.jpg
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/Stock Photos/iStock-1225782571.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "iStock-1225782571-400.jpeg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/Stock Photos/iStock-1225782571.imageset/iStock-1225782571-400.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/circles-project/circles-ios/5aec0b5cdb379fe13113ec55f4475a020b9c1834/Circles/Assets.xcassets/Stock Photos/iStock-1225782571.imageset/iStock-1225782571-400.jpeg
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/Stock Photos/iStock-1304744459.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "iStock-1304744459-400.jpeg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/Stock Photos/iStock-1304744459.imageset/iStock-1304744459-400.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/circles-project/circles-ios/5aec0b5cdb379fe13113ec55f4475a020b9c1834/Circles/Assets.xcassets/Stock Photos/iStock-1304744459.imageset/iStock-1304744459-400.jpeg
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/Stock Photos/iStock-1356527683.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "iStock-1356527683-400.jpeg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/Stock Photos/iStock-1356527683.imageset/iStock-1356527683-400.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/circles-project/circles-ios/5aec0b5cdb379fe13113ec55f4475a020b9c1834/Circles/Assets.xcassets/Stock Photos/iStock-1356527683.imageset/iStock-1356527683-400.jpeg
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/Stock Photos/iStock-640313068.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "iStock-640313068-400.jpeg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/Stock Photos/iStock-640313068.imageset/iStock-640313068-400.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/circles-project/circles-ios/5aec0b5cdb379fe13113ec55f4475a020b9c1834/Circles/Assets.xcassets/Stock Photos/iStock-640313068.imageset/iStock-640313068-400.jpeg
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/circles-logo-dark.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "circles-logo-dark.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/circles-logo-dark.imageset/circles-logo-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/circles-project/circles-ios/5aec0b5cdb379fe13113ec55f4475a020b9c1834/Circles/Assets.xcassets/circles-logo-dark.imageset/circles-logo-dark.png
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/circles-logo-light.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "circles-logo-light.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Circles/Assets.xcassets/circles-logo-light.imageset/circles-logo-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/circles-project/circles-ios/5aec0b5cdb379fe13113ec55f4475a020b9c1834/Circles/Assets.xcassets/circles-logo-light.imageset/circles-logo-light.png
--------------------------------------------------------------------------------
/Circles/Changelogs/CHANGELOG_LastUpdates.md:
--------------------------------------------------------------------------------
1 | # v1.0.7
2 | * New app icon
3 | * Remove FUTO branding
4 | * Add notice that FUTO servers will be closing down
5 | * Fix crash when downgrading from Circles 1.1.x
6 |
--------------------------------------------------------------------------------
/Circles/Circles-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // Use this file to import your target's public headers that you would like to expose to Swift.
3 | //
4 |
5 | #include "pycabcrypt.h"
6 |
7 | #import "Zxcvbn.h"
8 |
9 |
--------------------------------------------------------------------------------
/Circles/Circles.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | aps-environment
6 | development
7 | com.apple.developer.associated-domains
8 |
9 | webcredentials:circu.li
10 | webcredentials:eu.circu.li
11 | webcredentials:*.circles-dev.net
12 | webcredentials:circles.futo.org
13 | applinks:circu.li
14 | applinks:*.circles-dev.net
15 | applinks:circles.futo.org
16 |
17 | com.apple.developer.authentication-services.autofill-credential-provider
18 |
19 | com.apple.security.app-sandbox
20 |
21 | com.apple.security.application-groups
22 |
23 | group.RX3RM99NR6.org.futo.circles
24 |
25 | com.apple.security.device.camera
26 |
27 | com.apple.security.network.client
28 |
29 | com.apple.security.personal-information.photos-library
30 |
31 | keychain-access-groups
32 |
33 | $(AppIdentifierPrefix)org.futo.circles
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/Circles/CirclesApp.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2020, 2021 Kombucha Digital Privacy Systems LLC
2 | //
3 | // CirclesApp.swift
4 | // Circles for iOS
5 | //
6 | // Created by Charles Wright on 5/25/21.
7 | //
8 |
9 | import SwiftUI
10 | import StoreKit
11 | import os
12 | import Matrix
13 |
14 | @main
15 | struct CirclesApp: App {
16 | @UIApplicationDelegateAdaptor(CirclesAppDelegate.self) var appDelegate
17 |
18 | @StateObject private var store = CirclesStore()
19 | private var paymentQueue = SKPaymentQueue.default()
20 | private var countryCode = SKPaymentQueue.default().storefront?.countryCode
21 |
22 | public static var logger = os.Logger(subsystem: "Circles", category: "Circles")
23 |
24 | init() {
25 | // We need to register all of our custom types with the Matrix library, so it can decode them for us
26 | Matrix.registerAccountDataType(EVENT_TYPE_CIRCLES_CONFIG_V1, CirclesConfigContentV1.self)
27 | Matrix.registerAccountDataType(EVENT_TYPE_CIRCLES_CONFIG_V2, CirclesConfigContentV2.self)
28 |
29 | print("CirclesApp: Done with init()")
30 | }
31 |
32 | var body: some Scene {
33 | WindowGroup {
34 | ContentView(store: store)
35 | .environmentObject(store)
36 | .onAppear {
37 | print("CirclesApp: onAppear")
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Circles/CirclesError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CirclesError.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 4/27/22.
6 | //
7 |
8 | import Foundation
9 |
10 | struct CirclesError: Error {
11 | var message: String
12 |
13 | init(_ message: String) {
14 | self.message = message
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Circles/DebugMode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DebugMode.swift
3 | // Circles
4 | //
5 | // Created by Dmytro Ryshchuk on 6/2/24.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | class DebugModel {
12 | static let shared = DebugModel()
13 |
14 | @AppStorage("debugMode") var debugMode: Bool = false
15 |
16 | private init(debugMode: Bool = false) {
17 | self.debugMode = debugMode
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Circles/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UIAppFonts
6 |
7 | Inter-VariableFont.ttf
8 | Nunito-VariableFont.ttf
9 |
10 | CFBundleDevelopmentRegion
11 | $(DEVELOPMENT_LANGUAGE)
12 | CFBundleExecutable
13 | $(EXECUTABLE_NAME)
14 | CFBundleIdentifier
15 | $(PRODUCT_BUNDLE_IDENTIFIER)
16 | CFBundleInfoDictionaryVersion
17 | 6.0
18 | CFBundleName
19 | $(PRODUCT_NAME)
20 | CFBundlePackageType
21 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
22 | CFBundleShortVersionString
23 | $(MARKETING_VERSION)
24 | CFBundleVersion
25 | $(CURRENT_PROJECT_VERSION)
26 | ITSAppUsesNonExemptEncryption
27 |
28 | LSRequiresIPhoneOS
29 |
30 | NSAppTransportSecurity
31 |
32 | NSAllowsArbitraryLoads
33 |
34 |
35 | NSCameraUsageDescription
36 | Circles needs access to the camera if you would like to take new photos to share in the app.
37 | NSFaceIDUsageDescription
38 | Circles uses FaceID to protect your cryptographic keys
39 | NSPhotoLibraryAddUsageDescription
40 | Circles needs access to the photo library to save the images that you download.
41 | UIApplicationSceneManifest
42 |
43 | UIApplicationSupportsMultipleScenes
44 |
45 |
46 | UIApplicationSupportsIndirectInputEvents
47 |
48 | UIBackgroundModes
49 |
50 | fetch
51 | remote-notification
52 |
53 | UILaunchStoryboardName
54 | Launch Screen.storyboard
55 | UIRequiredDeviceCapabilities
56 |
57 | armv7
58 |
59 | UIRequiresFullScreen
60 |
61 | UISupportedInterfaceOrientations
62 |
63 | UIInterfaceOrientationPortrait
64 |
65 | UISupportedInterfaceOrientations~ipad
66 |
67 | UIInterfaceOrientationLandscapeLeft
68 | UIInterfaceOrientationLandscapeRight
69 | UIInterfaceOrientationPortrait
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/Circles/Matrix/Account Data/mDirectContent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // mDirectContent.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 6/27/22.
6 | //
7 |
8 | import Foundation
9 |
10 | typealias mDirectContent = [UserId: [RoomId]]
11 |
--------------------------------------------------------------------------------
/Circles/Matrix/Account Data/mIgnoredUserListContent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // mIgnoredUserList.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 7/5/22.
6 | //
7 |
8 | import Foundation
9 |
10 | struct mIgnoredUserListContent: Codable {
11 | var ignoredUsers: [UserId: [String:String]]
12 | }
13 |
--------------------------------------------------------------------------------
/Circles/Matrix/MatrixAccountDataType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MatrixAccountDataType.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 6/27/22.
6 | //
7 |
8 | import Foundation
9 |
10 | enum MatrixAccountDataType: Codable {
11 | case mIdentityServer // "m.identity_server"
12 | case mFullyRead // "m.fully_read"
13 | case mDirect // "m.direct"
14 | case mIgnoredUserList
15 | case mSecretStorageKey(String) // "m.secret_storage.key.[key ID]"
16 |
17 | init(from decoder: Decoder) throws {
18 | let string = try String(from: decoder)
19 |
20 | switch string {
21 | case "m.identity_server":
22 | self = .mIdentityServer
23 | return
24 | case "m.fully_read":
25 | self = .mFullyRead
26 | return
27 |
28 | case "m.direct":
29 | self = .mDirect
30 | return
31 |
32 | case "m.ignored_user_list":
33 | self = .mIgnoredUserList
34 | return
35 |
36 | default:
37 |
38 | // OK it's not one of the "normal" ones. Is it one of the weird ones?
39 | if string.starts(with: "m.secret_storage.key.") {
40 | guard let keyId = string.split(separator: ".").last
41 | else {
42 | let msg = "Couldn't get key id for m.secret_storage.key"
43 | print(msg)
44 | throw Matrix.Error(msg)
45 | }
46 | self = .mSecretStorageKey(String(keyId))
47 | }
48 |
49 | // If we're still here, then we have *no* idea what to do with this thing.
50 |
51 | let msg = "Failed to decode MatrixAccountDataType from string [\(string)]"
52 | print(msg)
53 | throw Matrix.Error(msg)
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Circles/Models/ApplicationState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ApplicationState.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 5/10/22.
6 | //
7 |
8 | import Foundation
9 |
10 | /*
11 | class ApplicationState: ObservableObject {
12 | enum State {
13 | case loading
14 | case none
15 | case signingUp(SignupSession)
16 | case loggingIn(UIAuthSession)
17 | case loggedInDisconnected(MatrixCredentials)
18 | case connected(MatrixInterface)
19 | }
20 |
21 | @Published var state: State
22 |
23 | init() {
24 | self.state = .loading
25 | _ = Task {
26 | await load()
27 | }
28 | }
29 |
30 | func load() async {
31 | guard let userId = UserDefaults.standard.string(forKey: "user_id"),
32 | !userId.isEmpty,
33 | let deviceId = UserDefaults.standard.string(forKey: "device_id[\(userId)]"),
34 | let accessToken = UserDefaults.standard.string(forKey: "access_token[\(userId)]"),
35 | !accessToken.isEmpty
36 | else {
37 | // Apparently we're offline, waiting for (valid) credentials to log in
38 | self.state = .none
39 | return
40 | }
41 |
42 | // Update our state so that the application knows we're working on connecting to the server
43 | self.state = .loggedInDisconnected(MatrixCredentials(accessToken: accessToken, deviceId: deviceId, userId: userId))
44 |
45 | // Now actually start trying to connect
46 | do {
47 | self.state = .connected(<#T##MatrixInterface#>)
48 | } catch {
49 | // Apparently it didn't work :(
50 | }
51 | }
52 | }
53 | */
54 |
--------------------------------------------------------------------------------
/Circles/Models/CirclesConfigV1.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CirclesConfig.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 3/30/23.
6 | //
7 |
8 | import Foundation
9 | import Matrix
10 |
11 | let EVENT_TYPE_CIRCLES_CONFIG_V1 = "org.futo.circles.config"
12 |
13 | struct CirclesConfigContentV1: Codable {
14 | var root: RoomId
15 | var circles: RoomId
16 | var groups: RoomId
17 | var galleries: RoomId
18 | var people: RoomId
19 | var profile: RoomId // aka Shared Circles
20 | }
21 |
--------------------------------------------------------------------------------
/Circles/Models/CirclesConfigV2.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CirclesConfigV2.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 7/10/24.
6 | //
7 |
8 | import Foundation
9 | import Matrix
10 |
11 | let EVENT_TYPE_CIRCLES_CONFIG_V2 = "org.futo.circles.config.v2"
12 |
13 | struct CirclesConfigContentV2: Codable {
14 | var root: RoomId
15 | var groups: RoomId
16 | var galleries: RoomId
17 | var people: RoomId
18 | var profile: RoomId // aka Shared Circles
19 | var timelines: RoomId
20 | }
21 |
--------------------------------------------------------------------------------
/Circles/Models/GalleryRoom.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GalleryRoom.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 3/23/23.
6 | //
7 |
8 | import Foundation
9 | import Matrix
10 |
11 | typealias GalleryRoom = Matrix.Room
12 |
--------------------------------------------------------------------------------
/Circles/Models/GroupRoom.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GroupRoom.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 3/23/23.
6 | //
7 |
8 | import Foundation
9 | import Matrix
10 |
11 | typealias GroupRoom = Matrix.Room
12 |
--------------------------------------------------------------------------------
/Circles/Models/PersonRoom.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PersonRoom.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 3/23/23.
6 | //
7 |
8 | import Foundation
9 | import Matrix
10 |
11 | typealias PersonRoom = Matrix.SpaceRoom
12 |
--------------------------------------------------------------------------------
/Circles/Models/PowerLevel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PowerLevel.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 4/9/24.
6 | //
7 |
8 | import Foundation
9 |
10 | struct PowerLevel: Identifiable, Equatable, Hashable {
11 | var power: Int
12 |
13 | var id: Int {
14 | power
15 | }
16 |
17 | var description: String {
18 | if power < 0 {
19 | return "Can View"
20 | } else if power < 50 {
21 | return "Can Post"
22 | } else if power < 100 {
23 | return "Moderator"
24 | } else {
25 | return "Admin"
26 | }
27 | }
28 |
29 | static func ==(lhs: PowerLevel, rhs: PowerLevel) -> Bool {
30 | lhs.power == rhs.power
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Circles/Models/ProfileSpace.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileSpace.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 7/31/23.
6 | //
7 |
8 | import Foundation
9 | import Matrix
10 |
11 | /*
12 | This is the type to use for *our* profile space room,
13 | where we know that we are a joined member of every child room.
14 | To represent other users' profile spaces, use PersonRoom instead.
15 | */
16 |
17 | typealias ProfileSpace = ContainerRoom
18 |
--------------------------------------------------------------------------------
/Circles/Models/Room+URL.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Room+URL.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 10/27/23.
6 | //
7 |
8 | import Foundation
9 | import Matrix
10 |
11 | // cvw: Putting this here rather than in Matrix.swift because these URLs are specific to Circles
12 | // For a generic Matrix chat client, you would probably want to use matrix.to instead, or the new matrix:// URL type
13 | extension Matrix.Room {
14 | var url: URL {
15 |
16 | func urlPrefix() -> String {
17 | switch self.type {
18 | case ROOM_TYPE_CIRCLE:
19 | return "timeline"
20 | case ROOM_TYPE_GROUP:
21 | return "group"
22 | case ROOM_TYPE_PHOTOS:
23 | return "gallery"
24 | case ROOM_TYPE_SPACE:
25 | return "profile"
26 | default:
27 | return "room"
28 | }
29 | }
30 |
31 | let prefix = urlPrefix()
32 |
33 | return URL(string: "https://\(CIRCLES_PRIMARY_DOMAIN)/\(prefix)/\(self.roomId)")!
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Circles/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Circles/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPrivacyAccessedAPITypes
6 |
7 |
8 | NSPrivacyAccessedAPITypeReasons
9 |
10 | E174.1
11 |
12 | NSPrivacyAccessedAPIType
13 | NSPrivacyAccessedAPICategoryDiskSpace
14 |
15 |
16 | NSPrivacyAccessedAPITypeReasons
17 |
18 | C617.1
19 |
20 | NSPrivacyAccessedAPIType
21 | NSPrivacyAccessedAPICategoryFileTimestamp
22 |
23 |
24 | NSPrivacyAccessedAPIType
25 | NSPrivacyAccessedAPICategoryActiveKeyboards
26 | NSPrivacyAccessedAPITypeReasons
27 |
28 | 54BD.1
29 |
30 |
31 |
32 | NSPrivacyAccessedAPIType
33 | NSPrivacyAccessedAPICategoryUserDefaults
34 | NSPrivacyAccessedAPITypeReasons
35 |
36 | 1C8F.1
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/Circles/Utilities/Array+Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CryptoSwift
3 | //
4 | // Copyright (C) 2014-2021 Marcin Krzyżanowski
5 | // This software is provided 'as-is', without any express or implied warranty.
6 | //
7 | // In no event will the authors be held liable for any damages arising from the use of this software.
8 | //
9 | // Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
10 | //
11 | // - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required.
12 | // - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
13 | // - This notice may not be removed or altered from any source or binary distribution.
14 | //
15 |
16 | // The following extensions were extracted from https://github.com/krzyzanowskim/CryptoSwift/blob/main/Sources/CryptoSwift/Array%2BExtension.swift
17 |
18 |
19 | extension Array {
20 | @inlinable
21 | init(reserveCapacity: Int) {
22 | self = Array()
23 | self.reserveCapacity(reserveCapacity)
24 | }
25 |
26 | @inlinable
27 | var slice: ArraySlice {
28 | self[self.startIndex ..< self.endIndex]
29 | }
30 |
31 | @inlinable
32 | subscript (safe index: Index) -> Element? {
33 | return indices.contains(index) ? self[index] : nil
34 | }
35 | }
36 |
37 | extension Array where Element == UInt8 {
38 | public init(hex: String) {
39 | self.init(reserveCapacity: hex.unicodeScalars.lazy.underestimatedCount)
40 | var buffer: UInt8?
41 | var skip = hex.hasPrefix("0x") ? 2 : 0
42 | for char in hex.unicodeScalars.lazy {
43 | guard skip == 0 else {
44 | skip -= 1
45 | continue
46 | }
47 | guard char.value >= 48 && char.value <= 102 else {
48 | removeAll()
49 | return
50 | }
51 | let v: UInt8
52 | let c: UInt8 = UInt8(char.value)
53 | switch c {
54 | case let c where c <= 57:
55 | v = c - 48
56 | case let c where c >= 65 && c <= 70:
57 | v = c - 55
58 | case let c where c >= 97:
59 | v = c - 87
60 | default:
61 | removeAll()
62 | return
63 | }
64 | if let b = buffer {
65 | append(b << 4 | v)
66 | buffer = nil
67 | } else {
68 | buffer = v
69 | }
70 | }
71 | if let b = buffer {
72 | append(b)
73 | }
74 | }
75 |
76 | public func toHexString() -> String {
77 | `lazy`.reduce(into: "") {
78 | var s = String($1, radix: 16)
79 | if s.count == 1 {
80 | s = "0" + s
81 | }
82 | $0 += s
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Circles/Utilities/Array+RawRepresentable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Array+RawRepresentable.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 11/10/23.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Array: RawRepresentable where Element: Codable {
11 | // Tried to use Data but it doesn't work... Trying String
12 | public typealias RawValue = String
13 |
14 | public init?(rawValue: String) {
15 | let decoder = JSONDecoder()
16 | guard let data = rawValue.data(using: .utf8),
17 | let array = try? decoder.decode([Element].self, from: data)
18 | else {
19 | return nil
20 | }
21 | self = array
22 | }
23 |
24 | public var rawValue: String {
25 | let encoder = JSONEncoder()
26 | guard let data = try? encoder.encode(self),
27 | let string = String(data: data, encoding: .utf8)
28 | else {
29 | return ""
30 | }
31 | return string
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Circles/Utilities/Array+Sample.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2018, Ralf Ebert
2 | // Source https://www.ralfebert.de/ios-examples/swift/array-random-sample/
3 | // License https://opensource.org/licenses/MIT
4 | // License https://creativecommons.org/publicdomain/zero/1.0/
5 |
6 | import Foundation
7 | import Darwin
8 |
9 | extension Collection {
10 |
11 | /**
12 | * Returns a random element of the Array or nil if the Array is empty.
13 | */
14 | var sample : Element? {
15 | guard !isEmpty else { return nil }
16 | let offset = arc4random_uniform(numericCast(self.count))
17 | let idx = self.index(self.startIndex, offsetBy: numericCast(offset))
18 | return self[idx]
19 | }
20 |
21 | /**
22 | * Returns `count` random elements from the array.
23 | * If there are not enough elements in the Array, a smaller Array is returned.
24 | * Elements will not be returned twice except when there are duplicate elements in the original Array.
25 | */
26 | func sample(_ count : UInt) -> [Element] {
27 | let sampleCount = Swift.min(numericCast(count), self.count)
28 |
29 | var elements = Array(self)
30 | var samples : [Element] = []
31 |
32 | while samples.count < sampleCount {
33 | let idx = (0..= 1 else { return }
50 |
51 | for i in (1..: View {
12 | var role: ButtonRole?
13 | var errorMessage: String?
14 | var action: () async throws -> Void
15 | @ViewBuilder var label: () -> Label
16 |
17 | @State private var pending = false
18 |
19 | func runAction() {
20 | pending = true
21 |
22 | Task {
23 | do {
24 | try await action()
25 | } catch {
26 | print("AsyncButton: Action failed")
27 |
28 | await ToastPresenter.shared.showToast(message: errorMessage ?? error.localizedDescription)
29 | }
30 | await MainActor.run {
31 | pending = false
32 | }
33 | }
34 | }
35 |
36 | var body: some View {
37 | Button(
38 | role: role,
39 | action: runAction,
40 | label: {
41 | label()
42 | }
43 | )
44 | .disabled(pending)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Circles/Utilities/BCrypt/pycabcrypt.h:
--------------------------------------------------------------------------------
1 | #ifndef PYCABCRYPT
2 | #define PYCABCRYPT
3 |
4 | #include
5 | #include
6 | #include
7 | #include "portable_endian.h"
8 |
9 | #if defined(_WIN32)
10 | typedef unsigned char uint8_t;
11 | typedef uint8_t u_int8_t;
12 | typedef unsigned short uint16_t;
13 | typedef uint16_t u_int16_t;
14 | typedef unsigned uint32_t;
15 | typedef uint32_t u_int32_t;
16 | typedef unsigned long long uint64_t;
17 | typedef uint64_t u_int64_t;
18 | #define snprintf _snprintf
19 | #define __attribute__(unused)
20 | #elif defined(__sun)
21 | typedef uint8_t u_int8_t;
22 | typedef uint16_t u_int16_t;
23 | typedef uint32_t u_int32_t;
24 | typedef uint64_t u_int64_t;
25 | #else
26 | #include
27 | #endif
28 |
29 | #define explicit_bzero(s,n) memset(s, 0, n)
30 | #define DEF_WEAK(f)
31 |
32 | int bcrypt_hashpass(const char *key, const char *salt, char *encrypted, size_t encryptedlen);
33 | int encode_base64(char *, const u_int8_t *, size_t);
34 | int timingsafe_bcmp(const void *b1, const void *b2, size_t n);
35 | int bcrypt_pbkdf(const char *pass, size_t passlen, const uint8_t *salt, size_t saltlen, uint8_t *key, size_t keylen, unsigned int rounds);
36 |
37 | #endif
38 |
39 |
40 |
--------------------------------------------------------------------------------
/Circles/Utilities/CustomFonts.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomFonts.swift
3 | // Circles
4 | //
5 | // Created by Dmytro Ryshchuk on 7/25/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct CustomFonts {
11 | static let inter14 = Font.custom("Inter", size: 14)
12 | static let nunito12 = Font.custom("Nunito", size: 12)
13 | static let nunito14 = Font.custom("Nunito", size: 14)
14 | static let nunito16 = Font.custom("Nunito", size: 16)
15 | static let nunito20 = Font.custom("Nunito", size: 20)
16 | static let nunito24 = Font.custom("Nunito", size: 24)
17 | static let outfit11 = Font.custom("Outfit", size: 11)
18 | static let outfit14 = Font.custom("Outfit", size: 14)
19 | }
20 |
--------------------------------------------------------------------------------
/Circles/Utilities/Data+hex.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Data+hex.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 3/6/24.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Data {
11 | var hexString: String {
12 | let string = self.map {
13 | String(format: "%02hhx", $0)
14 | }.joined()
15 |
16 | return string
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Circles/Utilities/DeleteAndPurge.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DeleteAndPurge.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 2/8/24.
6 | //
7 |
8 | import Foundation
9 | import Matrix
10 |
11 |
12 | func deleteAndPurge(message: Matrix.Message) async throws {
13 | let content = message.content
14 | let session = message.room.session
15 | try await message.room.redact(eventId: message.eventId,
16 | reason: "Deleted by \(message.room.session.whoAmI())")
17 | // Now attempt to delete media associated with this event, if possible
18 | // Since the Matrix spec has no DELETE for media, this will probably fail, so don't worry if these calls throw errors
19 | if let messageContent = content as? Matrix.MessageContent {
20 | switch messageContent.msgtype {
21 | case M_IMAGE:
22 | if let imageContent = messageContent as? Matrix.mImageContent {
23 | if let file = imageContent.file {
24 | try? await session.deleteMedia(file.url)
25 | }
26 | if let url = imageContent.url {
27 | try? await session.deleteMedia(url)
28 | }
29 | if let thumbnail_file = imageContent.thumbnail_file {
30 | try? await session.deleteMedia(thumbnail_file.url)
31 | }
32 | if let thumbnail_url = imageContent.thumbnail_url {
33 | try? await session.deleteMedia(thumbnail_url)
34 | }
35 | }
36 | case M_VIDEO:
37 | if let videoContent = messageContent as? Matrix.mVideoContent {
38 | if let file = videoContent.file {
39 | try? await session.deleteMedia(file.url)
40 | }
41 | if let url = videoContent.url {
42 | try? await session.deleteMedia(url)
43 | }
44 | if let thumbnail_file = videoContent.thumbnail_file {
45 | try? await session.deleteMedia(thumbnail_file.url)
46 | }
47 | if let thumbnail_url = videoContent.thumbnail_url {
48 | try? await session.deleteMedia(thumbnail_url)
49 | }
50 | }
51 | default:
52 | print("Not deleting any media for msgtype \(messageContent.msgtype)")
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Circles/Utilities/HtmlView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HtmlView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 3/29/23.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import UIKit
11 |
12 | // Loosely based on https://stackoverflow.com/a/62281735
13 |
14 | struct HtmlView: UIViewRepresentable {
15 | let html: String
16 |
17 | func makeUIView(context: UIViewRepresentableContext) -> UILabel {
18 | let label = UILabel()
19 | label.lineBreakMode = .byWordWrapping
20 | label.numberOfLines = 0
21 |
22 | if let data = html.data(using: .utf8),
23 | let mutable = try? NSMutableAttributedString(data: data,
24 | options: [
25 | .documentType: NSAttributedString.DocumentType.html,
26 | ],
27 | documentAttributes: nil)
28 | {
29 | //mutable.addAttribute(.font, value: UIFont.systemFont(ofSize: 24), range: NSRange(location: 0, length: mutable.length))
30 | let attributedString = NSAttributedString(attributedString: mutable)
31 | DispatchQueue.main.async {
32 | label.attributedText = attributedString
33 | }
34 | }
35 |
36 | return label
37 | }
38 |
39 | func updateUIView(_ uiView: UILabel, context: Context) {
40 | // Do nothing
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Circles/Utilities/ImageSaver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageSaver.swift
3 | // Circles for iOS
4 | //
5 | // Created by Charles Wright on 11/11/20.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | // https://www.hackingwithswift.com/books/ios-swiftui/how-to-save-images-to-the-users-photo-library
12 | // https://www.hackingwithswift.com/read/13/5/saving-to-the-ios-photo-library
13 |
14 | class ImageSaver: NSObject {
15 | @IBAction func writeToPhotoAlbum(image: UIImage) {
16 | UIImageWriteToSavedPhotosAlbum(image, self, #selector(saveError(_:didFinishSavingWithError:contextInfo:)), nil)
17 | }
18 |
19 | @objc func saveError(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
20 | if let _ = error { // let error
21 | print("Error: Save failed")
22 | } else {
23 | print("Save finished!")
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Circles/Utilities/LegacySecretStoreKeygen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LegacySecretStoreKeygen.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 7/24/23.
6 | //
7 |
8 | import Foundation
9 | import CryptoKit
10 | import Matrix
11 |
12 |
13 | func generateLegacySecretStorageKey(userId: UserId, password: String) throws -> (String, Data) {
14 | // Update 2021-06-16 - Adding my crazy scheme for doing
15 | // SSSS using only a single password
16 | //
17 | // First we bcrypt the password to get a secret that is
18 | // resistant to brute force and dictionary attack.
19 | // Then we use the symmetric ratchet to generate two keys
20 | // * ~~One for the login password~~ (This one we can now ignore)
21 | // * One for the secret "private key" for the recovery service (This is the secret storage key)
22 |
23 | let username = userId.username.trimmingCharacters(in: ["@"])
24 |
25 | print("SECRETS\tExtracted username [\(username)] from given userId [\(userId)]")
26 |
27 | guard let data = username.data(using: .utf8) else {
28 | let msg = "Failed to convert username to data"
29 | print("SECRETS\t\(msg)")
30 | throw CirclesError(msg)
31 | }
32 |
33 | let saltDigest = SHA256.hash(data: data)
34 | let saltString = saltDigest
35 | .map { String(format: "%02hhx", $0) }
36 | .prefix(16)
37 | .joined()
38 | print("SECRETS\tComputed salt string = [\(saltString)]")
39 |
40 | let numRounds = 14
41 | guard let bcrypt = try? BCrypt.Hash(password, salt: "$2a$\(numRounds)$\(saltString)") else {
42 | let msg = "BCrypt KDF failed"
43 | print("SECRETS\t\(msg)")
44 | throw CirclesError(msg)
45 | }
46 | print("SECRETS\tGot bcrypt hash = [\(bcrypt)]")
47 | print(" \t 12345678901234567890123456789012345678901234567890")
48 |
49 | // Grabbing everything after the $ gives us the salt as well as the hash
50 | //let root = String(bcrypt.suffix(from: bcrypt.lastIndex(of: "$")!).dropFirst(1))
51 | // Grabbing only the last 31 chars gives us just the hash
52 | let root = String(bcrypt.suffix(31))
53 | print("SECRETS\tRoot secret = [\(root)] (\(root.count) chars)")
54 |
55 | /*
56 | let newLoginPassword = SHA256.hash(data: "LoginPassword|\(root)".data(using: .utf8)!)
57 | .prefix(16)
58 | .map { String(format: "%02hhx", $0) }
59 | .joined()
60 | print("SECRETS\tGot new login password = [\(newLoginPassword)]")
61 | */
62 |
63 | let newPrivateKey = SHA256.hash(data: "S4Key|\(root)".data(using: .utf8)!)
64 | .withUnsafeBytes {
65 | Data(Array($0))
66 | }
67 | print("SECRETS\tGot new private key = [\(newPrivateKey)]")
68 |
69 |
70 | //let keyId = try Matrix.SecretStore.computeKeyId(key: newPrivateKey)
71 | let keyId = UUID().uuidString
72 |
73 | return (keyId, newPrivateKey)
74 | }
75 |
--------------------------------------------------------------------------------
/Circles/Utilities/QR.swift:
--------------------------------------------------------------------------------
1 | //
2 | // QR.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 10/26/23.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 | import AVFoundation
11 | import CoreImage
12 | import CoreImage.CIFilterBuiltins
13 |
14 | // A QR code of the room id
15 | func qrCode(url: URL) -> UIImage? {
16 | guard let data = url.absoluteString.data(using: .ascii)
17 | else {
18 | print("Failed to generate QR code: Couldn't get roomId as ASCII data")
19 | return nil
20 | }
21 |
22 | // Use the built-in CoreImage filter to create our QR code
23 | // https://developer.apple.com/documentation/coreimage/ciqrcodegenerator
24 | let filter = CIFilter.qrCodeGenerator()
25 | filter.setValue(data, forKey: "inputMessage")
26 | filter.setValue("Q", forKey: "inputCorrectionLevel") // 25%
27 | guard let result = filter.outputImage
28 | else {
29 | print("Failed to generate QR code: Couldn't get CIFilter output image")
30 | return nil
31 | }
32 |
33 | // Scale up the QR code by a factor of 10x
34 | let transform = CGAffineTransform(scaleX: 10, y: 10)
35 | let transformedImage = result.transformed(by: transform)
36 |
37 | // For whatever reason, we MUST convert to a CGImage here, using the CIContext.
38 | // If we do not do this (eg by trying to create a UIImage directly from the CIImage),
39 | // then we get nothing but a blank square for our QR code. :(
40 | let context = CIContext()
41 | guard let cgImg = context.createCGImage(transformedImage, from: transformedImage.extent)
42 | else {
43 | print("Failed to generate QR code: Failed to create CGImage from transformed image")
44 | return nil
45 | }
46 |
47 | let img = UIImage(cgImage: cgImg)
48 | print("QR code image is \(img.size.height) x \(img.size.width)")
49 | return img
50 | }
51 |
--------------------------------------------------------------------------------
/Circles/Utilities/RelativeTimestampFormatter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RelativeTimestampFormatter.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 7/16/24.
6 | //
7 |
8 | import Foundation
9 |
10 | enum RelativeTimestampFormatter {
11 |
12 | public static func format(date: Date) -> String {
13 |
14 | let interval = Date().timeIntervalSince(date)
15 |
16 | if interval < 60 {
17 | return "now"
18 | } else if interval < 3600 {
19 | let minutes = Int(interval) / 60
20 | return "\(minutes)m"
21 | } else if interval < 86400 {
22 | let hours = Int(interval) / 3600
23 | return "\(hours)h"
24 | } else if interval < 604800 {
25 | let days = Int(interval) / 86400
26 | return "\(days)d"
27 | } else if interval < 31449600 {
28 | let weeks = Int(interval) / 604800
29 | return "\(weeks)w"
30 | } else {
31 | let years = Int(interval) / 31449600
32 | return "\(years)y"
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Circles/Utilities/SaveImage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SaveImage.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 2/8/24.
6 | //
7 |
8 | import Foundation
9 | import Matrix
10 |
11 | func saveEncryptedImage(file: Matrix.mEncryptedFile,
12 | session: Matrix.Session
13 | ) async throws {
14 |
15 | guard let data = try? await session.downloadAndDecryptData(file),
16 | let image = UIImage(data: data)
17 | else {
18 | print("Failed to get image for encrypted URL \(file.url)")
19 | return
20 | }
21 |
22 | print("Saving image...")
23 | let imageSaver = ImageSaver()
24 | await imageSaver.writeToPhotoAlbum(image: image)
25 | print("Successfully saved image from \(file.url)")
26 | }
27 |
28 | func savePlaintextImage(url: MXC,
29 | session: Matrix.Session
30 | ) async throws {
31 | print("Trying to save image from URL \(url)")
32 | guard let data = try? await session.downloadData(mxc: url),
33 | let image = UIImage(data: data)
34 | else {
35 | print("Failed to get image for url \(url)")
36 | return
37 | }
38 |
39 | print("Saving image...")
40 | let imageSaver = ImageSaver()
41 | await imageSaver.writeToPhotoAlbum(image: image)
42 | print("Successfully saved image from \(url)")
43 | }
44 |
45 | func saveImage(content: Matrix.mImageContent,
46 | session: Matrix.Session
47 | ) async throws {
48 | // Coming in, we have no idea what this m.image content may contain
49 | // It may have any mix of encrypted / unencrypted full-res image and thumbnail
50 | // So we try to be a little bit smart
51 | // - We prefer the full-res image over the thumbnail
52 | // - When trying to find an image (either full-res or thumbnail) we prefer the encrypted version over unencrypted
53 | // In other words, our preferences are:
54 | // 1. Full-res, encrypted
55 | // 2. Full-res, non encrypted
56 | // 3. Thumbnail, encrypted
57 | // 4. Thumbnail, non encrypted
58 |
59 | if let fullResFile = content.file {
60 | try await saveEncryptedImage(file: fullResFile, session: session)
61 | }
62 | else if let fullResUrl = content.url {
63 | try await savePlaintextImage(url: fullResUrl, session: session)
64 | }
65 | else if let thumbnailFile = content.thumbnail_file {
66 | try await saveEncryptedImage(file: thumbnailFile, session: session)
67 | }
68 | else if let thumbnailUrl = content.thumbnail_url {
69 | try await savePlaintextImage(url: thumbnailUrl, session: session)
70 | }
71 | else {
72 | print("Error: Can't save image -- No encrypted file or URL")
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Circles/Utilities/SecureFieldWithEye.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EyeSecureField.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 8/2/21.
6 | //
7 |
8 | import SwiftUI
9 | import Foundation
10 |
11 | struct SecureFieldWithEye: View {
12 | @Binding
13 | var password: String
14 | var isNewPassword: Bool = false
15 | var placeholder: String = "Password"
16 | var height: CGFloat = 40
17 |
18 | @State
19 | private var showText: Bool = false
20 |
21 | private enum Focus {
22 | case secure, text
23 | }
24 |
25 | @FocusState
26 | private var focus: Focus?
27 |
28 | @Environment(\.scenePhase)
29 | private var scenePhase
30 |
31 | var body: some View {
32 | HStack {
33 | ZStack {
34 | SecureField(placeholder, text: $password)
35 | .frame(height: height)
36 | .padding([.horizontal], 12)
37 | .background(Color.background)
38 | .cornerRadius(10)
39 | .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.greyCool400))
40 | .focused($focus, equals: .secure)
41 | .opacity(showText ? 0 : 1)
42 | .textContentType(isNewPassword ? .newPassword : .password)
43 |
44 | TextField(placeholder, text: $password)
45 | .frame(height: height)
46 | .padding([.horizontal], 12)
47 | .background(Color.background)
48 | .cornerRadius(10)
49 | .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.greyCool400))
50 | .focused($focus, equals: .text)
51 | .opacity(showText ? 1 : 0)
52 | .textContentType(isNewPassword ? .newPassword : .password)
53 | }
54 |
55 | Button(action: {
56 | showText.toggle()
57 | }) {
58 | Image(systemName: showText ? "eye.slash.fill" : "eye.fill")
59 | .foregroundColor(showText ? .gray : .blue)
60 | }
61 | }
62 | .onChange(of: focus) { newValue in
63 | if newValue != nil {
64 | focus = showText ? .text : .secure
65 | }
66 | }
67 | .onChange(of: scenePhase) { newValue in
68 | if newValue != .active {
69 | showText = false
70 | }
71 | }
72 | .onChange(of: showText) { newValue in
73 | if focus != nil {
74 | DispatchQueue.main.async {
75 | focus = newValue ? .text : .secure
76 | }
77 | }
78 | }
79 | }
80 | }
81 |
82 | /*
83 | struct EyeSecureField_Previews: PreviewProvider {
84 | static var previews: some View {
85 | EyeSecureField()
86 | }
87 | }
88 | */
89 |
--------------------------------------------------------------------------------
/Circles/Utilities/Sequence+asyncMap.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Sequence+asyncMap.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 3/14/23.
6 | //
7 |
8 | import Foundation
9 |
10 | // Based on https://www.swiftbysundell.com/articles/async-and-concurrent-forEach-and-map/
11 | extension Sequence {
12 | func map(
13 | _ transform: (Element) async throws -> T
14 | ) async rethrows -> [T] {
15 | var values = [T]()
16 |
17 | for element in self {
18 | try await values.append(transform(element))
19 | }
20 |
21 | return values
22 | }
23 |
24 | func compactMap(
25 | _ transform: (Element) async throws -> T?
26 | ) async rethrows -> [T] {
27 | var values = [T]()
28 |
29 | for element in self {
30 | let t = try await transform(element)
31 | if t != nil {
32 | values.append(t!)
33 | }
34 | }
35 |
36 | return values
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Circles/Utilities/ToastPresenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ToastPresenter.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 6/7/24.
6 | //
7 |
8 | import Foundation
9 | import JDStatusBarNotification
10 |
11 | public struct CustomToastSetup {
12 | var image: UIImage? = nil
13 | var titleFont: UIFont? = UIFont.systemFont(ofSize: 12, weight: .medium)
14 | var subtitleFont: UIFont? = UIFont.systemFont(ofSize: 10)
15 | var titleColor: UIColor = .gray
16 | var subtitleColor: UIColor = .systemGray
17 | var backgroundColor: UIColor = .systemBackground
18 | }
19 |
20 | public class ToastPresenter {
21 | public static var shared = ToastPresenter()
22 | public var delay = 3.0
23 |
24 | @MainActor
25 | public func showToast(message: String,
26 | subtitle: String? = nil,
27 | customToast: CustomToastSetup? = nil) async {
28 | setupToast(with: message, subtitle: subtitle, customToast: customToast)
29 | }
30 |
31 | public func showToast(message: String,
32 | subtitle: String? = nil,
33 | customToast: CustomToastSetup? = nil) {
34 | setupToast(with: message, subtitle: subtitle, customToast: customToast)
35 | }
36 |
37 | private func setupToast(with message: String,
38 | subtitle: String? = nil,
39 | customToast: CustomToastSetup? = nil) {
40 | NotificationPresenter.shared.updateDefaultStyle { style in
41 | if let customToast {
42 | style.backgroundStyle.backgroundColor = customToast.backgroundColor
43 | style.textStyle.textColor = customToast.titleColor
44 | style.textStyle.font = customToast.titleFont
45 | style.subtitleStyle.textColor = customToast.subtitleColor
46 | style.subtitleStyle.font = customToast.subtitleFont
47 | }
48 |
49 | return style
50 | }
51 |
52 | if let subtitle {
53 | NotificationPresenter.shared.present(message, subtitle: subtitle)
54 | } else {
55 | NotificationPresenter.shared.present(message)
56 | }
57 |
58 | if let toastImage = customToast?.image {
59 | let image = UIImageView(image: toastImage)
60 | NotificationPresenter.shared.displayLeftView(image)
61 | }
62 | NotificationPresenter.shared.dismiss(animated: true, after: delay)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Circles/Utilities/Utils.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2020, 2021 Kombucha Digital Privacy Systems LLC
2 | //
3 | // Utils.swift
4 | // Circles for iOS
5 | //
6 | // Created by Charles Wright on 10/28/20.
7 | //
8 |
9 | // swiftlint:disable identifier_name
10 |
11 | import Foundation
12 | import UIKit
13 |
14 | func downscale_image(from image: UIImage, to maxSize: CGSize) -> UIImage? {
15 | let height = image.size.height
16 | let width = image.size.width
17 | let MAX_HEIGHT = maxSize.height
18 | let MAX_WIDTH = maxSize.width
19 | print("DOWNSCALE\t h = \(height)\t w = \(width)")
20 | print("DOWNSCALE\t max h = \(MAX_HEIGHT)\t max w = \(MAX_WIDTH)")
21 |
22 | if height > MAX_HEIGHT || width > MAX_WIDTH {
23 | let aspectRatio = image.size.width / image.size.height
24 | print("DOWNSCALE\tAspect ratio = \(aspectRatio)")
25 | let scale = aspectRatio > 1
26 | ? MAX_WIDTH / image.size.width
27 | : MAX_HEIGHT / image.size.height
28 | print("DOWNSCALE\tScale = \(scale)")
29 | let newSize = CGSize(width: scale*image.size.width, height: scale*image.size.height)
30 | print("DOWNSCALE\tNew size = \(newSize)")
31 | let renderer = UIGraphicsImageRenderer(size: newSize)
32 | return renderer.image { (context) in
33 | image.draw(in: CGRect(origin: .zero, size: newSize))
34 | }
35 | }
36 | return image
37 | }
38 |
39 | func b64decode(_ str: String) -> [UInt8]? {
40 | guard let data = Data(base64Encoded: str) else {
41 | return nil
42 | }
43 | let array = [UInt8](data)
44 | return array
45 | }
46 |
47 | func abbreviate(_ input: String?, textIfEmpty: String = "(none)") -> String {
48 | guard let string = input
49 | else {
50 | return textIfEmpty
51 | }
52 |
53 | if string.count < 23 {
54 | return string
55 | } else {
56 | return String("\(string.prefix(20))...")
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Circles/Utilities/zxcvbn/DBMatcher.h:
--------------------------------------------------------------------------------
1 | //
2 | // DBMatcher.h
3 | // Zxcvbn
4 | //
5 | // Created by Leah Culver on 2/9/14.
6 | // Copyright (c) 2014 Dropbox. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | @interface DBMatcher : NSObject
12 |
13 | @property (nonatomic, assign) NSUInteger keyboardAverageDegree;
14 | @property (nonatomic, assign) NSUInteger keypadAverageDegree;
15 | @property (nonatomic, assign) NSUInteger keyboardStartingPositions;
16 | @property (nonatomic, assign) NSUInteger keypadStartingPositions;
17 |
18 | - (NSArray *)omnimatch:(NSString *)password userInputs:(NSArray *)userInputs;
19 |
20 | @end
21 |
22 | @interface DBMatchResources : NSObject
23 |
24 | @property (nonatomic, strong) NSArray *dictionaryMatchers;
25 | @property (nonatomic, strong) NSDictionary *graphs;
26 |
27 | + (DBMatchResources *)sharedDBMatcherResources;
28 |
29 | @end
30 |
31 | @interface DBMatch : NSObject
32 |
33 | @property (nonatomic, assign) NSString *pattern;
34 | @property (strong, nonatomic) NSString *token;
35 | @property (nonatomic, assign) NSUInteger i;
36 | @property (nonatomic, assign) NSUInteger j;
37 | @property (nonatomic, assign) float entropy;
38 | @property (nonatomic, assign) int cardinality;
39 |
40 | // Dictionary
41 | @property (strong, nonatomic) NSString *matchedWord;
42 | @property (strong, nonatomic) NSString *dictionaryName;
43 | @property (nonatomic, assign) int rank;
44 | @property (nonatomic, assign) float baseEntropy;
45 | @property (nonatomic, assign) float upperCaseEntropy;
46 |
47 | // l33t
48 | @property (nonatomic, assign) BOOL l33t;
49 | @property (strong, nonatomic) NSDictionary *sub;
50 | @property (strong, nonatomic) NSString *subDisplay;
51 | @property (nonatomic, assign) int l33tEntropy;
52 |
53 | // Spatial
54 | @property (strong, nonatomic) NSString *graph;
55 | @property (nonatomic, assign) int turns;
56 | @property (nonatomic, assign) int shiftedCount;
57 |
58 | // Repeat
59 | @property (strong, nonatomic) NSString *repeatedChar;
60 |
61 | // Sequence
62 | @property (strong, nonatomic) NSString *sequenceName;
63 | @property (nonatomic, assign) int sequenceSpace;
64 | @property (nonatomic, assign) BOOL ascending;
65 |
66 | // Date
67 | @property (nonatomic, assign) int day;
68 | @property (nonatomic, assign) int month;
69 | @property (nonatomic, assign) int year;
70 | @property (strong, nonatomic) NSString *separator;
71 |
72 | @end
--------------------------------------------------------------------------------
/Circles/Utilities/zxcvbn/DBPasswordStrengthMeterView.h:
--------------------------------------------------------------------------------
1 | //
2 | // DBPasswordStrengthMeterView.h
3 | // Zxcvbn
4 | //
5 | // Created by Leah Culver on 2/22/14.
6 | // Copyright (c) 2014 Dropbox. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | @protocol DBPasswordStrengthMeterViewDelegate;
12 |
13 | @interface DBPasswordStrengthMeterView : UIView
14 |
15 | @property (nonatomic, assign) id delegate;
16 |
17 | - (void)setLightColor:(UIColor *)lightColor darkColor:(UIColor *)darkColor;
18 | - (void)scorePassword:(NSString *)password;
19 | - (void)scorePassword:(NSString *)password userInputs:(NSArray *)userInputs;
20 |
21 | @end
22 |
23 | @protocol DBPasswordStrengthMeterViewDelegate
24 |
25 | - (void)passwordStrengthMeterViewTapped:(DBPasswordStrengthMeterView *)passwordStrengthMeterView;
26 |
27 | @end
28 |
--------------------------------------------------------------------------------
/Circles/Utilities/zxcvbn/DBScorer.h:
--------------------------------------------------------------------------------
1 | //
2 | // DBScorer.h
3 | // Zxcvbn
4 | //
5 | // Created by Leah Culver on 2/9/14.
6 | // Copyright (c) 2014 Dropbox. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | @class DBResult;
12 |
13 | @interface DBScorer : NSObject
14 |
15 | - (DBResult *)minimumEntropyMatchSequence:(NSString *)password matches:(NSArray *)matches;
16 |
17 | @end
18 |
19 |
20 | @interface DBResult : NSObject
21 |
22 | @property (strong, nonatomic) NSString *password;
23 | @property (strong, nonatomic) NSString *entropy; // bits
24 | @property (strong, nonatomic) NSString *crackTime; // estimation of actual crack time, in seconds.
25 | @property (strong, nonatomic) NSString *crackTimeDisplay; // same crack time, as a friendlier string: "instant", "6 minutes", "centuries", etc.
26 | @property (nonatomic, assign) int score; // [0,1,2,3,4] if crack time is less than [10**2, 10**4, 10**6, 10**8, Infinity]. (useful for implementing a strength bar.)
27 | @property (strong, nonatomic) NSArray *matchSequence; // the list of patterns that zxcvbn based the entropy calculation on.
28 | @property (nonatomic, assign) float calcTime; // how long it took to calculate an answer, in milliseconds. usually only a few ms.
29 |
30 | @end
--------------------------------------------------------------------------------
/Circles/Utilities/zxcvbn/DBZxcvbn.h:
--------------------------------------------------------------------------------
1 | //
2 | // DBZxcvbn.h
3 | // Zxcvbn
4 | //
5 | // Created by Leah Culver on 2/9/14.
6 | // Copyright (c) 2014 Dropbox. All rights reserved.
7 | //
8 |
9 | #import "DBMatcher.h"
10 | #import "DBScorer.h"
11 |
12 | @interface DBZxcvbn : NSObject
13 |
14 | - (DBResult *)passwordStrength:(NSString *)password;
15 | - (DBResult *)passwordStrength:(NSString *)password userInputs:(NSArray *)userInputs;
16 |
17 | @end
18 |
--------------------------------------------------------------------------------
/Circles/Utilities/zxcvbn/DBZxcvbn.m:
--------------------------------------------------------------------------------
1 | //
2 | // DBZxcvbn.m
3 | // Zxcvbn
4 | //
5 | // Created by Leah Culver on 2/9/14.
6 | // Copyright (c) 2014 Dropbox. All rights reserved.
7 | //
8 |
9 | #import "DBZxcvbn.h"
10 | #import
11 |
12 | @interface DBZxcvbn ()
13 |
14 | @property (nonatomic, strong) DBMatcher *matcher;
15 | @property (nonatomic, strong) DBScorer *scorer;
16 |
17 | @end
18 |
19 | @implementation DBZxcvbn
20 |
21 | - (id)init
22 | {
23 | self = [super init];
24 |
25 | if (self != nil) {
26 | self.matcher = [[DBMatcher alloc] init];
27 | self.scorer = [[DBScorer alloc] init];
28 | }
29 |
30 | return self;
31 | }
32 |
33 | - (DBResult *)passwordStrength:(NSString *)password
34 | {
35 | return [self passwordStrength:password userInputs:nil];
36 | }
37 |
38 | - (DBResult *)passwordStrength:(NSString *)password userInputs:(NSArray *)userInputs
39 | {
40 | CFTimeInterval start = CACurrentMediaTime();
41 | NSArray *matches = [self.matcher omnimatch:password userInputs:userInputs];
42 | DBResult *result = [self.scorer minimumEntropyMatchSequence:password matches:matches];
43 | CFTimeInterval end = CACurrentMediaTime();
44 | result.calcTime = (end - start) * 1000.0;
45 |
46 | return result;
47 | }
48 |
49 | @end
50 |
--------------------------------------------------------------------------------
/Circles/Utilities/zxcvbn/Zxcvbn.h:
--------------------------------------------------------------------------------
1 | //
2 | // Zxcvbn.h
3 | // Zxcvbn
4 | //
5 | // Created by Leah Culver on 26 Oct 2015.
6 | // Copyright © 2015 Dropbox. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for Zxcvbn.
12 | FOUNDATION_EXPORT double ZxcvbnVersionNumber;
13 |
14 | //! Project version string for Zxcvbn.
15 | FOUNDATION_EXPORT const unsigned char ZxcvbnVersionString[];
16 |
17 | #import "DBMatcher.h"
18 | #import "DBScorer.h"
19 | #import "DBZxcvbn.h"
20 | #import "DBPasswordStrengthMeterView.h"
21 |
--------------------------------------------------------------------------------
/Circles/Views/Circles/CircleFollowingRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CircleFollowingRow.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 11/7/23.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | // This shows a row in a list representing a friend's timeline that we are following in one of our circles
12 | // This needs to be its own type so that it can keep local state `showConfirmUnfollow`
13 | // We wouldn't need this if the SwiftUI `.confirmationDialog` actually used its `presenting:` argument properly -- then we could just put the confirmation dialog in the parent view -- but oh well
14 | struct CircleFollowingRow: View {
15 | var container: ContainerRoom
16 | @ObservedObject var room: Matrix.Room
17 | @ObservedObject var user: Matrix.User
18 |
19 | var body: some View {
20 | NavigationLink(destination: FollowingTimelineDetailsView(room: room, user: user, container: container)) {
21 | RoomAvatarView(room: room, avatarText: .none)
22 | .frame(width: 40, height: 40)
23 |
24 | VStack(alignment: .leading) {
25 | Text("\(user.displayName ?? user.userId.username)")
26 | if DebugModel.shared.debugMode {
27 | Text(room.roomId.stringValue)
28 | .font(.subheadline)
29 | .foregroundColor(.red)
30 | }
31 | let followerCount = max(0, room.joinedMembers.count-1)
32 | let unit = followerCount > 1 ? "followers" : "follower"
33 | Text("\(room.name ?? "(???)") (\(followerCount) \(unit))")
34 | }
35 | .onAppear {
36 | user.refreshProfile()
37 | }
38 |
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Circles/Views/Circles/CircleInvitationsIndicator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CircleInvitationsIndicator.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 7/19/23.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct CircleInvitationsIndicator: View {
12 | //@Binding var invitations: [Matrix.InvitedRoom]
13 | @ObservedObject var session: Matrix.Session
14 | @ObservedObject var container: ContainerRoom
15 |
16 | var body: some View {
17 | VStack {
18 | let invitations = session.invitations.values.filter { $0.type == ROOM_TYPE_CIRCLE }
19 |
20 | if invitations.count > 0 {
21 |
22 | NavigationLink(destination: CircleInvitationsView(session: session, container: container)) {
23 | HStack {
24 | Spacer()
25 | Label("You have \(invitations.count) pending invitation(s)", systemImage: "star")
26 | .fontWeight(.bold)
27 | .padding()
28 | Spacer()
29 | Image(systemName: SystemImages.chevronRight.rawValue)
30 | .font(.system(size: 24))
31 | .padding()
32 | }
33 | .foregroundColor(.white)
34 | .background(Color.accentColor)
35 | .frame(maxHeight: 60)
36 | }
37 |
38 | }
39 | if DebugModel.shared.debugMode {
40 | Text("Debug: \(invitations.count) invitations here; \(session.invitations.count) total in the session")
41 | .foregroundColor(.red)
42 | .padding()
43 | }
44 | }
45 |
46 | }
47 | }
48 |
49 | /*
50 | struct CircleInvitationsIndicator_Previews: PreviewProvider {
51 | static var previews: some View {
52 | CircleInvitationsIndicator()
53 | }
54 | }
55 | */
56 |
--------------------------------------------------------------------------------
/Circles/Views/Circles/CircleInvitationsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CircleInvitationsView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 7/19/23.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct CircleInvitationsView: View {
12 | //@Binding var invitations: [Matrix.InvitedRoom]
13 | @ObservedObject var session: Matrix.Session
14 | var container: ContainerRoom
15 |
16 | var body: some View {
17 | ScrollView {
18 | VStack(alignment: .leading, spacing: 10) {
19 | let invitations = session.invitations.values.filter { $0.type == ROOM_TYPE_CIRCLE }
20 | if invitations.isEmpty {
21 | Text("No pending invitations")
22 | } else {
23 | ForEach(invitations) { room in
24 | let user = room.session.getUser(userId: room.sender)
25 | InvitedCircleCard(room: room, user: user, container: container)
26 | .frame(maxWidth: 350)
27 |
28 | Divider()
29 | }
30 | }
31 | }
32 | .padding()
33 | }
34 | .navigationTitle(Text("Circle Invitations"))
35 | }
36 | }
37 |
38 | /*
39 | struct CircleInvitationsView_Previews: PreviewProvider {
40 | static var previews: some View {
41 | CircleInvitationsView()
42 | }
43 | }
44 | */
45 |
--------------------------------------------------------------------------------
/Circles/Views/Circles/CirclePicker.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2020, 2021 Kombucha Digital Privacy Systems LLC
2 | //
3 | // CirclePicker.swift
4 | // Circles for iOS
5 | //
6 | // Created by Charles Wright on 11/10/20.
7 | //
8 |
9 | import SwiftUI
10 | import Matrix
11 |
12 | struct CirclePicker: View {
13 | //@ObservedObject var container: ContainerRoom
14 | @EnvironmentObject var appSession: CirclesApplicationSession
15 | @Binding var selected: Set
16 |
17 | var body: some View {
18 | ScrollView {
19 | VStack(spacing: 5) {
20 | let container = appSession.timelines
21 | //List {
22 | let rooms = container.rooms.values
23 | .filter({$0.creator == container.session.creds.userId})
24 | .sorted { $0.timestamp < $1.timestamp }
25 |
26 | ForEach(rooms) { circle in
27 | //Text(circle.roomId.stringValue)
28 | Button(action: {
29 | if selected.contains(circle) {
30 | selected.remove(circle)
31 | }
32 | else {
33 | selected.insert(circle)
34 | }
35 | }) {
36 | HStack {
37 | Image(systemName: selected.contains(circle) ? SystemImages.checkmarkCircle.rawValue : SystemImages.circle.rawValue)
38 | .resizable()
39 | .frame(width: 25, height: 25)
40 | .foregroundColor(.gray)
41 |
42 | RoomAvatarView(room: circle, avatarText: .none)
43 | .clipShape(Circle())
44 | .frame(width: 50, height: 50)
45 | Text(circle.name ?? "unnamed")
46 | //.fontWeight(.bold)
47 | Spacer()
48 | }
49 | .padding()
50 | }
51 | .buttonStyle(.plain)
52 | }
53 | .padding(.horizontal)
54 | }
55 | }
56 | }
57 | }
58 |
59 | /*
60 | struct StreamPicker_Previews: PreviewProvider {
61 | static var previews: some View {
62 | StreamPicker()
63 | }
64 | }
65 | */
66 |
--------------------------------------------------------------------------------
/Circles/Views/Circles/CircleRenameView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CircleRenameView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 4/15/24.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct CircleRenameView: View {
12 | @ObservedObject var room: Matrix.Room
13 | @Environment(\.presentationMode) var presentation
14 |
15 | @State var newName: String
16 |
17 | enum FocusField {
18 | case circleName
19 | }
20 | @FocusState var focus: FocusField?
21 |
22 | init(room: Matrix.Room) {
23 | self.room = room
24 | self._newName = State(wrappedValue: room.name ?? "")
25 | }
26 |
27 | var body: some View {
28 | VStack(alignment: .center, spacing: 100) {
29 | Spacer()
30 |
31 | HStack {
32 | TextField(room.name ?? "", text: $newName, prompt: Text("New name"))
33 | .textFieldStyle(.roundedBorder)
34 | .textInputAutocapitalization(.words)
35 | .focused($focus, equals: .circleName)
36 | .frame(width: 300, height: 40)
37 | .onAppear {
38 | self.focus = .circleName
39 | }
40 | Button(action: {
41 | self.newName = ""
42 | }) {
43 | Image(systemName: SystemImages.xmark.rawValue)
44 | .foregroundColor(.gray)
45 | }
46 | }
47 |
48 | AsyncButton(action: {
49 | try await room.setName(newName: newName)
50 | self.presentation.wrappedValue.dismiss()
51 | }) {
52 | Text("Update")
53 | }
54 |
55 | Spacer()
56 | }
57 | .navigationTitle("Rename \(room.name ?? "circle")")
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Circles/Views/Circles/UnifiedTimelineComposerSheet.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UnifiedTimelineComposerSheet.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 7/12/24.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct UnifiedTimelineComposerSheet: View {
12 | @ObservedObject var timelines: TimelineSpace
13 | @State var selectedRoom: Matrix.Room?
14 | @Environment(\.presentationMode) var presentation
15 | @State var createMyFirstCircle = false
16 |
17 | var body: some View {
18 | let circles = timelines.circles
19 | let me = timelines.session.me
20 |
21 | if let room = selectedRoom {
22 | PostComposer(room: room)
23 | } else if circles.count == 1,
24 | let onlyCircle = circles.first
25 | {
26 | PostComposer(room: onlyCircle)
27 | } else if circles.count > 1 {
28 | VStack(alignment: .center) {
29 | Spacer()
30 |
31 | Text("You have \(circles.count) circles. Which one would you like to share with?")
32 | .padding()
33 |
34 | ScrollView {
35 | VStack(alignment: .leading) {
36 | ForEach(circles) { circle in
37 | Button(action: {
38 | self.selectedRoom = circle
39 | }) {
40 | TimelineOverviewCard(room: circle, user: me)
41 | }
42 | .buttonStyle(.plain)
43 | }
44 | }
45 | }
46 | .frame(maxWidth: 400)
47 |
48 | Spacer()
49 |
50 | Button(role: .destructive, action: {
51 | self.presentation.wrappedValue.dismiss()
52 | }) {
53 | Text("Cancel")
54 | }
55 | .padding()
56 | }
57 | } else if createMyFirstCircle {
58 | CircleCreationSheet(container: timelines)
59 | } else {
60 | VStack(alignment: .center) {
61 | Text("It looks like you don't have any circles yet. Would you like to create one now?")
62 |
63 | Button(action: {
64 | self.createMyFirstCircle = true
65 | }) {
66 | Text("Create my first circle")
67 | }
68 |
69 | Button(role: .destructive, action: {
70 | self.presentation.wrappedValue.dismiss()
71 | }) {
72 | Text("Cancel")
73 | }
74 | .padding()
75 | }
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/Circles/Views/Circles/UnifiedTimelineSettingsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UnifiedTimelineSettingsView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 7/11/24.
6 | //
7 |
8 | import SwiftUI
9 | import PhotosUI
10 | import Matrix
11 |
12 | struct UnifiedTimelineSettingsView: View {
13 | @ObservedObject var space: TimelineSpace
14 |
15 | @State var showConfirmUnfollow = false
16 | @State var roomToUnfollow: Matrix.Room?
17 |
18 | @State var showInviteSheet = false
19 | @State var showShareSheet = false
20 |
21 | @State var showConfirmResend = false
22 | @State var showConfirmCancelInvite = false
23 |
24 | @ViewBuilder
25 | var followingSection: some View {
26 | let timelines = space.following
27 | if timelines.count > 0 {
28 | Section("Timelines I'm Following (\(timelines.count))") {
29 | ForEach(timelines) { room in
30 | let user = space.session.getUser(userId: room.creator)
31 | CircleFollowingRow(container: space, room: room, user: user)
32 | }
33 | }
34 | }
35 | }
36 |
37 | @ViewBuilder
38 | var circlesSection: some View {
39 | let circles = space.circles
40 | if circles.count > 0 {
41 | Section("My Circles") {
42 | ForEach(circles) { circle in
43 | /*
44 | let name = circle.name ?? ""
45 | let followerCount = Int.max(circle.joinedMembers.count - 1, 0)
46 | Text("\(name) (\(followerCount) followers)")
47 | */
48 | if let name = circle.name {
49 | NavigationLink(destination: SingleTimelineSettingsView(room: circle)) {
50 | HStack {
51 | RoomAvatarView(room: circle, avatarText: .none)
52 | .frame(width: 40, height: 40)
53 | Text(name)
54 | }
55 | }
56 | }
57 | }
58 | }
59 | }
60 | }
61 |
62 | var body: some View {
63 | VStack {
64 | Form {
65 | circlesSection
66 | followingSection
67 | }
68 | }
69 | .navigationTitle("Circles Settings")
70 | }
71 | }
72 |
73 |
--------------------------------------------------------------------------------
/Circles/Views/Circles/UnifiedTimelineView.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2020, 2021 Kombucha Digital Privacy Systems LLC
2 | //
3 | // CircleTimelineScreen.swift
4 | // Circles for iOS
5 | //
6 | // Created by Charles Wright on 12/17/20.
7 | //
8 |
9 | import SwiftUI
10 | import PhotosUI
11 | import Matrix
12 |
13 | struct UnifiedTimelineView: View {
14 | @ObservedObject var space: TimelineSpace
15 | @Environment(\.presentationMode) var presentation
16 |
17 | //@State var showComposer = false
18 | @State var showPhotosPicker: Bool = false
19 | @State var selectedItem: PhotosPickerItem?
20 | @State var showNewPostInSheetStyle = false
21 | //@State var image: UIImage?
22 |
23 | @State var viewModel = TimelineViewModel()
24 |
25 | var toolbarMenu: some View {
26 | NavigationLink(destination: UnifiedTimelineSettingsView(space: space)){
27 | Label("Settings", systemImage: SystemImages.gearshapeFill.rawValue)
28 | }
29 | }
30 |
31 | var stupidSwiftUiTrick: Int {
32 | print("DEBUGUI\tStreamTimeline rendering for Circle \(space.roomId)")
33 | return 0
34 | }
35 |
36 | var body: some View {
37 | NavigationStack {
38 | ZStack {
39 | let _ = self.stupidSwiftUiTrick // foo
40 |
41 | UnifiedTimeline(space: space)
42 | .navigationBarTitle("All Posts", displayMode: .inline)
43 | .toolbar {
44 | ToolbarItemGroup(placement: .automatic) {
45 | toolbarMenu
46 | }
47 | }
48 | .onAppear {
49 | print("DEBUGUI\tStreamTimeline appeared for Circle \(space.roomId)")
50 | }
51 | .onDisappear {
52 | print("DEBUGUI\tStreamTimeline disappeared for Circle \(space.roomId)")
53 | }
54 | .sheet(isPresented: $showNewPostInSheetStyle) {
55 | UnifiedTimelineComposerSheet(timelines: space)
56 | }
57 |
58 | let circles = space.circles
59 | if circles.count > 0 {
60 | VStack {
61 | Spacer()
62 | HStack {
63 | Spacer()
64 | Button(action: {
65 | showNewPostInSheetStyle = true
66 | }) {
67 | Label("New post", systemImage: SystemImages.plusCircleFill.rawValue)
68 | }
69 | .buttonStyle(PillButtonStyle())
70 | .padding(10)
71 | }
72 | }
73 | }
74 | }
75 | }
76 | .environmentObject(viewModel)
77 | }
78 | }
79 |
80 |
--------------------------------------------------------------------------------
/Circles/Views/CirclesLogoView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CirclesLogoView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 11/10/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct CirclesLogoView: View {
11 | @Environment(\.colorScheme) var colorScheme
12 |
13 | var body: some View {
14 | let imageName = colorScheme == .dark ? "circles-logo-dark" : "circles-logo-light"
15 | BasicImage(name: imageName)
16 | }
17 | }
18 |
19 | #Preview {
20 | CirclesLogoView()
21 | }
22 |
--------------------------------------------------------------------------------
/Circles/Views/Composer/ImagePicker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImagePicker.swift
3 | // Circles for iOS
4 | //
5 | // Created by Charles Wright on 7/22/20.
6 | //
7 |
8 | import UIKit
9 | import SwiftUI
10 |
11 | // Apple has deprecated this approach, so we must switch to the new `PhotosPicker`
12 |
13 | // Inspired by https://www.appcoda.com/swiftui-camera-photo-library/
14 |
15 | struct ImagePicker: UIViewControllerRepresentable {
16 | func makeCoordinator() -> Coordinator {
17 | Coordinator(self)
18 | }
19 |
20 | //@Binding var selectedImage: UIImage?
21 | @Environment(\.presentationMode) private var presentationMode
22 |
23 | var sourceType: UIImagePickerController.SourceType = .photoLibrary
24 | var allowEditing = false
25 |
26 | var completion: (UIImage?) -> Void = { _ in }
27 |
28 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIImagePickerController {
29 |
30 | let imagePicker = UIImagePickerController()
31 | //imagePicker.allowsEditing = false
32 | imagePicker.allowsEditing = allowEditing
33 | imagePicker.sourceType = sourceType
34 |
35 | imagePicker.delegate = context.coordinator
36 |
37 | return imagePicker
38 | }
39 |
40 | func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext) {
41 |
42 | }
43 |
44 | final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
45 |
46 | var parent: ImagePicker
47 |
48 | init(_ parent: ImagePicker) {
49 | self.parent = parent
50 | }
51 |
52 | func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
53 |
54 | if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
55 | //parent.selectedImage = image
56 | parent.presentationMode.wrappedValue.dismiss()
57 |
58 | // cvw: This is where we should call our "completion" closure to do whatever action we're supposed to do with the new image
59 | parent.completion(image)
60 | }
61 | else {
62 | // Just dismiss the view
63 | parent.presentationMode.wrappedValue.dismiss()
64 | // Don't need to call the completion handler
65 | parent.completion(nil)
66 | }
67 | }
68 | }
69 |
70 | }
71 |
72 |
73 |
74 | /*
75 | struct ImagePicker_Previews: PreviewProvider {
76 | static var previews: some View {
77 | ImagePicker()
78 | }
79 | }
80 | */
81 |
--------------------------------------------------------------------------------
/Circles/Views/Composer/PostComposerScreen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostComposerSheet.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 7/6/21.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | /*
12 | struct PostComposerScreen: View {
13 | var room: Matrix.Room
14 | var parentMessage: Matrix.Message?
15 | var editingMessage: Matrix.Message?
16 | @State var isPresented = true
17 | @State var title: String
18 |
19 | init(room: Matrix.Room, parentMessage: Matrix.Message? = nil, editingMessage: Matrix.Message? = nil, isPresented: Bool = true) {
20 | self.room = room
21 | self.parentMessage = parentMessage
22 | self.editingMessage = editingMessage
23 | self.isPresented = isPresented
24 |
25 | switch (parentMessage,editingMessage) {
26 | case (.none, .none):
27 | self.title = "New Post"
28 | case (.none, .some):
29 | self.title = "Edit Post"
30 | case (.some, .none):
31 | self.title = "New Reply"
32 | case (.some, .some):
33 | self.title = "Edit Reply"
34 | }
35 | }
36 |
37 |
38 | var body: some View {
39 | VStack {
40 | ScrollView {
41 | if let parent = parentMessage {
42 | MessageCard(message: parent, isThreaded: true)
43 | .padding(3)
44 | .padding(.bottom, 5)
45 | }
46 | PostComposer(room: room, parent: parentMessage, editing: editingMessage)
47 | .padding(3)
48 | .padding(.leading, 10)
49 | }
50 | }
51 | .navigationTitle(self.title)
52 | }
53 | }
54 | */
55 |
56 | /*
57 | struct MessageComposerSheet_Previews: PreviewProvider {
58 | static var previews: some View {
59 | MessageComposerSheet()
60 | }
61 | }
62 | */
63 |
--------------------------------------------------------------------------------
/Circles/Views/Groups/GenericPersonDetailView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GenericPersonDetailView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 12/14/23.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct GenericPersonDetailView: View {
12 | @ObservedObject var user: Matrix.User
13 |
14 | var header: some View {
15 | HStack {
16 | UserAvatarView(user: user)
17 | .frame(width: 160, height: 160, alignment: .center)
18 | VStack(alignment: .leading) {
19 | Text(user.displayName ?? "")
20 | .font(.title)
21 | .fontWeight(.bold)
22 | Text(user.id)
23 | .font(.subheadline)
24 | }
25 | }
26 | }
27 |
28 | var body: some View {
29 | VStack {
30 | ScrollView {
31 | header
32 |
33 | //status
34 |
35 | Divider()
36 |
37 | Button(action: {}) {
38 | Label("Invite to connect", systemImage: SystemImages.link.rawValue)
39 | .padding()
40 | }
41 |
42 | Button(action: {}) {
43 | Label("Invite to follow me", systemImage: SystemImages.personLineDottedPersonFill.rawValue)
44 | .padding()
45 | }
46 |
47 | Button(role: .destructive, action: {}) {
48 | Label("Ignore this user", systemImage: SystemImages.personFillXmark.rawValue)
49 | .padding()
50 | }
51 | }
52 | }
53 | .padding()
54 | .onAppear {
55 | // Hit the Homeserver to make sure we have the latest
56 | //user.matrix.getDisplayName(userId: user.id) { _ in }
57 | user.refreshProfile()
58 | }
59 | .navigationTitle(user.displayName ?? user.userId.username)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Circles/Views/Groups/GroupHeader.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2020, 2021 Kombucha Digital Privacy Systems LLC
2 | //
3 | // GroupHeader.swift
4 | // Circles for iOS
5 | //
6 | // Created by Charles Wright on 11/18/20.
7 | //
8 |
9 | import SwiftUI
10 | import Matrix
11 |
12 | struct GroupHeader: View {
13 | @ObservedObject var room: Matrix.Room
14 | let content: Content
15 |
16 | init(room: Matrix.Room, @ViewBuilder content: () -> Content) {
17 | self.room = room
18 | self.content = content()
19 | }
20 |
21 | var title: some View {
22 | Text(room.name ?? room.id)
23 | .font(.title)
24 | .fontWeight(.bold)
25 | }
26 |
27 | var subtitle: some View {
28 | Text(room.topic ?? "")
29 | .font(.headline)
30 | .foregroundColor(Color.gray)
31 | }
32 |
33 | var avatar: some View {
34 | RoomAvatarView(room: room, avatarText: .none)
35 | .frame(maxWidth: 150, minHeight: 120, maxHeight: 120)
36 | .shadow(radius: 3)
37 | .padding(5)
38 | }
39 |
40 | var body: some View {
41 | HStack {
42 |
43 | avatar
44 |
45 | VStack(alignment: .center) {
46 | title
47 | .multilineTextAlignment(.center)
48 |
49 | subtitle
50 | .multilineTextAlignment(.center)
51 | .padding(.bottom)
52 |
53 | content
54 | }
55 |
56 | Spacer()
57 | }
58 | .padding(.top, 5)
59 | }
60 | }
61 |
62 | /*
63 | struct GroupHeader_Previews: PreviewProvider {
64 | static var previews: some View {
65 | GroupHeader()
66 | }
67 | }
68 | */
69 |
--------------------------------------------------------------------------------
/Circles/Views/Groups/GroupInvitationsIndicator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GroupInvitationsIndicator.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 8/9/23.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct GroupInvitationsIndicator: View {
12 | //@Binding var invitations: [Matrix.InvitedRoom]
13 | @ObservedObject var session: Matrix.Session
14 | @ObservedObject var container: ContainerRoom
15 |
16 | var body: some View {
17 | VStack {
18 | let invitations = session.invitations.values.filter { $0.type == ROOM_TYPE_GROUP }
19 | if invitations.count > 0 {
20 | HStack {
21 | Spacer()
22 | NavigationLink(destination: GroupInvitationsView(session: session, container: container)) {
23 | Label("You have \(invitations.count) pending invitation(s)", systemImage: "star")
24 | .fontWeight(.bold)
25 | .padding()
26 | }
27 | Spacer()
28 | Image(systemName: SystemImages.chevronRight.rawValue)
29 | .font(.system(size: 24))
30 | .padding()
31 | }
32 | .foregroundColor(.white)
33 | .background(Color.accentColor)
34 | .frame(maxHeight: 60)
35 | }
36 | if DebugModel.shared.debugMode {
37 | Text("Debug: \(invitations.count) invitations here; \(session.invitations.count) total in the session")
38 | .foregroundColor(.red)
39 | .padding()
40 | }
41 | }
42 | }
43 | }
44 |
45 | /*
46 | struct GroupInvitationsIndicator_Previews: PreviewProvider {
47 | static var previews: some View {
48 | GroupInvitationsIndicator()
49 | }
50 | }
51 | */
52 |
--------------------------------------------------------------------------------
/Circles/Views/Groups/GroupInvitationsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GroupInvitationsView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 8/9/23.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct GroupInvitationsView: View {
12 | //@Binding var invitations: [Matrix.InvitedRoom]
13 | @ObservedObject var session: Matrix.Session
14 | var container: ContainerRoom
15 |
16 | var body: some View {
17 | ScrollView {
18 | VStack(alignment: .leading, spacing: 10) {
19 | let invitations = session.invitations.values.filter { $0.type == ROOM_TYPE_GROUP }
20 | if invitations.isEmpty {
21 | Text("No pending invitations")
22 | } else {
23 | ForEach(invitations) { room in
24 | let user = room.session.getUser(userId: room.sender)
25 | InvitedGroupCard(room: room, user: user, container: container)
26 | .frame(maxWidth: 350)
27 |
28 | Divider()
29 | }
30 | }
31 | }
32 | .padding()
33 | }
34 | .navigationTitle(Text("Group Invitations"))
35 | }
36 | }
37 |
38 | /*
39 | struct GroupInvitationsView_Previews: PreviewProvider {
40 | static var previews: some View {
41 | GroupInvitationsView()
42 | }
43 | }
44 | */
45 |
--------------------------------------------------------------------------------
/Circles/Views/Help/CirclesHelpView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CirclesHelpView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 5/28/24.
6 | //
7 |
8 | import SwiftUI
9 | import MarkdownUI
10 |
11 | struct CirclesHelpView: View {
12 |
13 | let helpTextMarkdown = """
14 | # Circles
15 |
16 | Tip: A **circle** works like a secure, private version of Facebook or Twitter. Everyone posts to their own timeline, and you see posts from all the timelines that you're following.
17 |
18 | A circle is a good way to share things with lots of people who don't all know each other, but who all know you.
19 |
20 | For example, you may have lots of aunts and uncles and cousins from the different sides of your family.
21 | Or, you might have several friends from many different places where you've lived.
22 |
23 | If you want to connect a bunch of people who *do* all know each other, then it's better to create a **Group** instead.
24 | """
25 |
26 | var body: some View {
27 | ScrollView {
28 | HStack {
29 | BasicImage(name: "iStock-1356527683")
30 | BasicImage(name: "iStock-1304744459")
31 | BasicImage(name: "iStock-1225782571")
32 | BasicImage(name: "iStock-640313068")
33 | }
34 |
35 | Markdown(helpTextMarkdown)
36 | }
37 | .scrollIndicators(.hidden)
38 | .padding()
39 | }
40 | }
41 |
42 | #Preview {
43 | CirclesHelpView()
44 | }
45 |
--------------------------------------------------------------------------------
/Circles/Views/Home/SystemNoticesView.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2020, 2021 Kombucha Digital Privacy Systems LLC
2 | //
3 | // SystemNoticesView.swift
4 | // Circles for iOS
5 | //
6 | // Created by Charles Wright on 3/3/21.
7 | //
8 |
9 | import SwiftUI
10 | import Matrix
11 |
12 | /*
13 | struct SystemNoticesView: View {
14 | var session: CirclesApplicationSession
15 | @State var scrollEventId: EventId?
16 |
17 | var body: some View {
18 | VStack(alignment: .leading) {
19 | if let room = session.matrix.systemNoticesRoom {
20 | TimelineView(room: room)
21 | .padding(.leading)
22 | } else {
23 | Text("No current notices")
24 | Spacer()
25 | }
26 | }
27 | }
28 | }
29 |
30 | struct SystemNoticesScreen: View {
31 | var session: CirclesApplicationSession
32 |
33 | var body: some View {
34 | VStack {
35 | Label("System Notices", systemImage: "exclamationmark.triangle.fill")
36 | .font(.title2)
37 | .padding()
38 |
39 | SystemNoticesView(session: session)
40 | .navigationBarTitle(Text("System Notices"))
41 | .padding()
42 | }
43 | }
44 | }
45 | */
46 |
47 | /*
48 | struct SystemNoticesView_Previews: PreviewProvider {
49 | static var previews: some View {
50 | SystemNoticesView()
51 | }
52 | }
53 | */
54 |
--------------------------------------------------------------------------------
/Circles/Views/Login/LegacyLoginScreen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LegacyLoginScreen.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 10/31/23.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 | import KeychainAccess
11 |
12 | struct LegacyLoginScreen: View {
13 | @ObservedObject var session: LegacyLoginSession
14 | var store: CirclesStore
15 |
16 | @AppStorage("previousUserIds") var previousUserIds: [UserId] = []
17 |
18 | @State var password: String = ""
19 | @State var showPassword = false
20 |
21 | var body: some View {
22 | VStack {
23 | if DebugModel.shared.debugMode {
24 | Text("m.login.password")
25 | .foregroundColor(.red)
26 | }
27 | Spacer()
28 |
29 | Text("Enter password for \(session.userId.stringValue)")
30 | .font(.title2)
31 | .fontWeight(.bold)
32 | SecureFieldWithEye(password: $password,
33 | isNewPassword: false,
34 | placeholder: "Password")
35 | .textContentType(.password)
36 | .frame(width: 300, height: 40)
37 | .onAppear {
38 | // Attempt to load the saved password that Matrix.swift should have saved in our Keychain
39 | let keychain = Keychain(server: "https://\(session.userId.domain)", protocolType: .https)
40 | keychain.getSharedPassword(session.userId.stringValue) { (passwd, error) in
41 | if self.password.isEmpty,
42 | let savedPassword = passwd
43 | {
44 | self.password = savedPassword
45 | }
46 | }
47 | }
48 |
49 | AsyncButton(action: {
50 | try await session.login(password: password)
51 |
52 | // Add our user id to the list, for easy login in the future
53 | let allUserIds: Set = Set(previousUserIds).union([session.userId])
54 | previousUserIds = allUserIds.sorted { $0.stringValue < $1.stringValue }
55 |
56 | // Save our password in the Keychain
57 | let keychain = Keychain(server: "https://\(session.userId.domain)", protocolType: .https)
58 | keychain.setSharedPassword(password, account: session.userId.stringValue)
59 | }) {
60 | Text("Log In")
61 | }
62 | .buttonStyle(BigRoundedButtonStyle())
63 |
64 | Spacer()
65 |
66 | AsyncButton(role: .destructive, action: {
67 | try await store.disconnect()
68 | }) {
69 | Text("Cancel")
70 | }
71 | }
72 | .padding()
73 | }
74 | }
75 |
76 |
--------------------------------------------------------------------------------
/Circles/Views/Login/SecretStorageCreationScreen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SecretStorageCreationScreen.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 1/29/24.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 | import IDZSwiftCommonCrypto
11 |
12 | struct SecretStorageCreationScreen: View {
13 | var store: CirclesStore
14 | @ObservedObject var matrix: Matrix.Session
15 |
16 | var body: some View {
17 | VStack {
18 |
19 | Spacer()
20 |
21 | Text("This account does not appear to be set up for secure storage on the server.")
22 | .font(.title2)
23 | .padding()
24 |
25 | Spacer()
26 |
27 | Label("Tip: If this is a new account, tap \"Set up secure storage\" to get started.", systemImage: "lightbulb.fill")
28 |
29 | AsyncButton(action: {
30 | guard let _ = matrix.secretStore // let secretStore
31 | else {
32 | print("No secret storage!")
33 | return
34 | }
35 | let bytes = try Random.generateBytes(byteCount: 32)
36 | let data = Data(bytes)
37 | let keyId = try Random.generateBytes(byteCount: 16)
38 | .map {
39 | String(format: "%02hhx", $0)
40 | }
41 | .joined()
42 | let description = try Matrix.SecretStore.generateKeyDescription(key: data, keyId: keyId)
43 | let key = Matrix.SecretStorageKey(key: data, keyId: keyId, description: description)
44 | print("Initializing secret storage with key id \(keyId)")
45 | try await store.initSecretStorage(key: key)
46 | print("Done initializing secret storage")
47 | }) {
48 | Text("Set up secure storage")
49 | .padding()
50 | }
51 | .padding()
52 |
53 | Spacer()
54 |
55 | AsyncButton(role: .destructive, action: {
56 | try await store.disconnect()
57 | }) {
58 | Text("Cancel")
59 | }
60 | }
61 | .padding()
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Circles/Views/People/FollowersView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FollowersView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 4/18/24.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct FollowersView: View {
12 | @ObservedObject var profile: ProfileSpace
13 | @Binding var followers: [Matrix.User]
14 |
15 | var body: some View {
16 | ScrollView {
17 | LazyVStack(alignment: .leading, spacing: 10) {
18 | ForEach(followers) { user in
19 | NavigationLink(destination: PersonDetailView(user: user, myProfileRoom: profile)) {
20 | PersonHeaderRow(user: user, profile: profile)
21 | .contentShape(Rectangle())
22 | }
23 | .buttonStyle(.plain)
24 | Divider()
25 | }
26 | }
27 | }
28 | .padding()
29 | .navigationTitle("My Followers")
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Circles/Views/People/FollowingView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FollowingView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 4/18/24.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct FollowingView: View {
12 | @ObservedObject var profile: ProfileSpace
13 | @Binding var following: [Matrix.User]
14 |
15 | var body: some View {
16 | ScrollView {
17 | LazyVStack(alignment: .leading, spacing: 10) {
18 | ForEach(following) { user in
19 | NavigationLink(destination: PersonDetailView(user: user, myProfileRoom: profile)) {
20 | PersonHeaderRow(user: user, profile: profile)
21 | .contentShape(Circle())
22 | }
23 | .buttonStyle(.plain)
24 | Divider()
25 | }
26 | }
27 | }
28 | .padding()
29 | .navigationTitle("Following")
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Circles/Views/People/FriendsOfFriendsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FriendsOfFriendsView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 4/18/24.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct FriendsOfFriendsView: View {
12 | @ObservedObject var profile: ProfileSpace
13 | @ObservedObject var people: ContainerRoom
14 | @Binding var friendsOfFriends: [UserId]?
15 |
16 | var body: some View {
17 | ScrollView {
18 | if let userIds = friendsOfFriends {
19 | LazyVStack(alignment: .leading, spacing: 10) {
20 | ForEach(userIds, id: \.self) { userId in
21 | if userId != profile.session.creds.userId {
22 | let user = profile.session.getUser(userId: userId)
23 |
24 | NavigationLink(destination: PersonDetailView(user: user, myProfileRoom: profile)) {
25 | PersonHeaderRow(user: user, profile: profile)
26 | //.contentShape(Rectangle())
27 | }
28 | .buttonStyle(.plain)
29 |
30 | Divider()
31 | }
32 | }
33 | }
34 | } else {
35 | ProgressView("Loading...")
36 | }
37 | }
38 | .padding()
39 | .navigationTitle("Friends of Friends")
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Circles/Views/People/InviteToFollowMeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InviteToFollowMeView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 5/2/24.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct InviteToFollowMeView: View {
12 | var user: Matrix.User
13 | @Environment(\.presentationMode) var presentation
14 |
15 | @State var selected: Set = []
16 |
17 | var body: some View {
18 | ScrollView {
19 | VStack {
20 |
21 | Text("Which of your timelines do you want \(user.displayName ?? user.userId.username) to follow?")
22 | .font(.title2)
23 | .padding()
24 |
25 | Text("Select one or more")
26 |
27 | CirclePicker(selected: $selected)
28 | .padding()
29 |
30 | AsyncButton(action: {
31 | for room in selected {
32 | if room.invitedMembers.contains(user.userId) {
33 | print("User \(user.userId) is already following us in circle \(room.name ?? room.roomId.stringValue)")
34 | } else {
35 | try await room.invite(userId: user.userId)
36 | }
37 | }
38 | }) {
39 | Text("Send \(selected.count) invitation(s)")
40 | }
41 | .disabled(selected.isEmpty)
42 | .padding()
43 |
44 | Button(role: .destructive, action: {
45 | self.presentation.wrappedValue.dismiss()
46 | }) {
47 | Text("Cancel")
48 | }
49 | .padding()
50 | }
51 | }
52 | .navigationTitle("Invite to Follow Me")
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Circles/Views/People/MutualFriendsSection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MutualFriendsSection.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 4/17/24.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct MutualFriendsSection: View {
12 | @ObservedObject var user: Matrix.User
13 | @ObservedObject var profile: ProfileSpace
14 |
15 | @State var mutualFriends: [Matrix.User]? = nil
16 |
17 |
18 | var body: some View {
19 | LazyVStack(alignment: .leading) {
20 | Text("MUTUAL FRIENDS")
21 | .font(.subheadline)
22 | .foregroundColor(.gray)
23 | .padding(.top)
24 | if let friends = mutualFriends {
25 | if friends.isEmpty {
26 | Text("No mutual friends")
27 | .padding()
28 | } else {
29 | ForEach(friends) { friend in
30 | // FIXME: Should probably check to see if we're connected to this person's profile space
31 | // And if we are, use the ConnectedPersonDetailView
32 | NavigationLink(destination: PersonDetailView(user: friend, myProfileRoom: profile)) {
33 | PersonHeaderRow(user: friend, profile: profile)
34 | }
35 | .buttonStyle(.plain)
36 | }
37 | }
38 | } else {
39 | ProgressView()
40 | }
41 | }
42 | .onAppear {
43 | Task {
44 | let session = user.session
45 | // First find the set of circles that we're both in
46 | let rooms: [Matrix.Room] = session.rooms.values.filter { room in
47 | room.type == ROOM_TYPE_CIRCLE && room.joinedMembers.contains(user.userId)
48 | }
49 | // Find the users in those circles who are not (1) me or (2) them
50 | let users: Set = rooms.reduce([], { curr, room in
51 | let roomMembers: [Matrix.User] = room.joinedMembers.filter {
52 | $0 != user.userId && $0 != user.session.creds.userId
53 | }.compactMap {
54 | session.getUser(userId: $0)
55 | }
56 | return curr.union(roomMembers)
57 | })
58 | // Sort the set in order to get an Array
59 | let sortedUsers = users.sorted(by: {u0,u1 in
60 | u0.userId.stringValue < u1.userId.stringValue
61 | })
62 | // Update the UI state on the main thread
63 | await MainActor.run {
64 | self.mutualFriends = sortedUsers
65 | }
66 | }
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Circles/Views/People/PeopleInvitationsIndicator.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2023 FUTO Holdings Inc
2 | //
3 | // PeopleInvitationsIndicator.swift
4 | // Circles
5 | //
6 | // Created by Michael Hollister on 9/5/23.
7 | //
8 |
9 | import SwiftUI
10 | import Matrix
11 |
12 | struct PeopleInvitationsIndicator: View {
13 | //@Binding var invitations: [Matrix.InvitedRoom]
14 | @ObservedObject var session: Matrix.Session
15 | var container: ContainerRoom
16 |
17 | @State var invitations: [Matrix.InvitedRoom] = []
18 |
19 | var body: some View {
20 | HStack {
21 | Spacer()
22 |
23 | if !invitations.isEmpty {
24 | NavigationLink(destination: PeopleInvitationsView(session: session, people: container)) {
25 | Label("\(invitations.count) invitation(s) to connect", systemImage: "envelope.open.fill")
26 | }
27 | }
28 |
29 | Spacer()
30 | }
31 | .onAppear {
32 | invitations = session.invitations.values.filter { $0.type == M_SPACE }
33 | }
34 | }
35 | }
36 |
37 | /*
38 | struct PeopleInvitationsIndicator_Previews: PreviewProvider {
39 | static var previews: some View {
40 | PeopleInvitationsIndicator()
41 | }
42 | }
43 | */
44 |
--------------------------------------------------------------------------------
/Circles/Views/People/PersonDetailView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UnconnectedPersonDetailView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 7/31/23.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct PersonDetailView: View {
12 | @ObservedObject var user: Matrix.User
13 | @ObservedObject var myProfileRoom: ProfileSpace
14 |
15 | @State private var alertTitle: String = ""
16 | @State private var alertMessage: String = ""
17 | @State private var showAlert = false
18 |
19 | var status: some View {
20 | HStack {
21 | Text("Latest Status:")
22 | .fontWeight(.bold)
23 | Text(user.statusMessage ?? "(no status message)")
24 | }
25 | .font(.subheadline)
26 | }
27 |
28 | var header: some View {
29 | HStack {
30 | Spacer()
31 |
32 | VStack {
33 | UserAvatarView(user: user)
34 | .frame(width: 160, height: 160, alignment: .center)
35 | // .padding(.leading)
36 | Text(user.displayName ?? "")
37 | .font(.title)
38 | .fontWeight(.bold)
39 | Text(user.userId.stringValue)
40 | .font(.subheadline)
41 | .foregroundColor(.gray)
42 | NavigationLink(destination: InviteToFollowMeView(user: user)) {
43 | Label("Invite to follow me", systemImage: "circle.hexagonpath")
44 | }
45 | }
46 |
47 | Spacer()
48 | }
49 | }
50 |
51 | var body: some View {
52 | ScrollView {
53 | VStack(alignment: .leading) {
54 | header
55 |
56 | //status
57 |
58 | Divider()
59 |
60 | MutualFriendsSection(user: user, profile: myProfileRoom)
61 | }
62 | }
63 | .padding()
64 | .onAppear {
65 | // Hit the Homeserver to make sure we have the latest
66 | //user.matrix.getDisplayName(userId: user.id) { _ in }
67 | user.refreshProfile()
68 | }
69 | .navigationTitle(Text(user.displayName ?? user.userId.username))
70 | }
71 | }
72 |
73 | /*
74 | struct UnconnectedPersonDetailView_Previews: PreviewProvider {
75 | static var previews: some View {
76 | UnconnectedPersonDetailView()
77 | }
78 | }
79 | */
80 |
--------------------------------------------------------------------------------
/Circles/Views/People/PersonsCircleRow.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2020, 2021 Kombucha Digital Privacy Systems LLC
2 | //
3 | // PersonsChannelRow.swift
4 | // Circles for iOS
5 | //
6 | // Created by Charles Wright on 12/3/20.
7 | //
8 |
9 | import SwiftUI
10 | import Matrix
11 |
12 | struct PersonsCircleRow: View {
13 | @ObservedObject var room: Matrix.SpaceChildRoom
14 | @ObservedObject var user: Matrix.User
15 |
16 | init(room: Matrix.SpaceChildRoom) {
17 | self.room = room
18 | self.user = room.session.getUser(userId: room.creator)
19 | }
20 |
21 | var roomName: String {
22 | room.name ?? ""
23 | }
24 |
25 | var userName: String {
26 | return user.displayName ?? "\(user.userId)"
27 | }
28 |
29 | var body: some View {
30 | HStack(alignment: .center) {
31 | let frameSize: CGFloat = 40
32 |
33 | RoomAvatarView(room: room, avatarText: .none)
34 | .frame(width: frameSize, height: frameSize)
35 | .clipped()
36 |
37 | VStack(alignment: .leading) {
38 |
39 | Text(roomName)
40 | .font(.headline)
41 | .fontWeight(.semibold)
42 |
43 | /*
44 | Text(room.id)
45 | .font(.caption)
46 | .foregroundColor(Color.gray)
47 | */
48 |
49 | }
50 |
51 | //Image(systemName: SystemImages.chevronRight.rawValue)
52 |
53 | Spacer()
54 |
55 | }
56 | .padding(.leading, 5)
57 | }
58 | }
59 |
60 | /*
61 | struct PersonsChannelRow_Previews: PreviewProvider {
62 | static var previews: some View {
63 | PersonsChannelRow()
64 | }
65 | }
66 | */
67 |
--------------------------------------------------------------------------------
/Circles/Views/People/SelfDetailView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SelfDetailView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 7/31/23.
6 | //
7 |
8 | import SwiftUI
9 | import UIKit
10 | import CoreImage
11 | import CoreImage.CIFilterBuiltins
12 | import Matrix
13 |
14 | struct SelfDetailView: View {
15 | @ObservedObject var profile: ContainerRoom
16 |
17 | @State var showPicker = false
18 | @State var showConfirmRemove = false
19 |
20 | var body: some View {
21 | ScrollView {
22 | VStack(alignment: .leading) {
23 | HStack {
24 | Spacer()
25 | VStack(alignment: .center) {
26 | let me = profile.session.me
27 | UserAvatarView(user: me)
28 | .aspectRatio(contentMode: .fill)
29 | .frame(width: 240, height: 240)
30 |
31 | Text(me.displayName ?? me.userId.username)
32 | .font(.title)
33 | .fontWeight(.bold)
34 |
35 | Text(me.userId.stringValue)
36 | .font(.subheadline)
37 | .foregroundColor(.gray)
38 | }
39 | Spacer()
40 | }
41 | }
42 | .padding()
43 | }
44 | .navigationTitle(Text("Me"))
45 | }
46 | }
47 |
48 | /*
49 | struct SelfDetailView_Previews: PreviewProvider {
50 | static var previews: some View {
51 | SelfDetailView()
52 | }
53 | }
54 | */
55 |
--------------------------------------------------------------------------------
/Circles/Views/Photo Galleries/GalleryInvitationsIndicator.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2023 FUTO Holdings Inc
2 | //
3 | // GalleryInvitationsIndicator.swift
4 | // Circles
5 | //
6 | // Created by Michael Hollister on 9/5/23.
7 | //
8 |
9 | import SwiftUI
10 | import Matrix
11 |
12 | struct GalleryInvitationsIndicator: View {
13 | //@Binding var invitations: [Matrix.InvitedRoom]
14 | @ObservedObject var session: Matrix.Session
15 | var container: ContainerRoom
16 |
17 |
18 | var body: some View {
19 | VStack(alignment: .leading) {
20 | let invitations = session.invitations.values.filter { $0.type == ROOM_TYPE_PHOTOS }
21 | if !invitations.isEmpty {
22 |
23 | NavigationLink(destination: GalleryInvitationsView(session: session, container: container)) {
24 | HStack {
25 | Spacer()
26 | Label("You have \(invitations.count) pending invitation(s)", systemImage: "star")
27 | .fontWeight(.bold)
28 | .padding()
29 | Spacer()
30 | Image(systemName: SystemImages.chevronRight.rawValue)
31 | .font(.system(size: 24))
32 | .padding()
33 | }
34 | .foregroundColor(.white)
35 | .background(Color.accentColor)
36 | .frame(maxHeight: 80)
37 | }
38 | }
39 | }
40 | }
41 | }
42 |
43 | /*
44 | struct GalleryInvitationsIndicator_Previews: PreviewProvider {
45 | static var previews: some View {
46 | GalleryInvitationsIndicator()
47 | }
48 | }
49 | */
50 |
--------------------------------------------------------------------------------
/Circles/Views/Photo Galleries/GalleryInvitationsView.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2023 FUTO Holdings Inc
2 | //
3 | // GalleryInvitationsView.swift
4 | // Circles
5 | //
6 | // Created by Charles Wright on 8/1/23.
7 | //
8 |
9 | import SwiftUI
10 | import Matrix
11 |
12 | struct GalleryInvitationsView: View {
13 | @ObservedObject var session: Matrix.Session
14 | @ObservedObject var container: ContainerRoom
15 |
16 | var body: some View {
17 | ScrollView {
18 | VStack(spacing: 10) {
19 | let invitations = session.invitations.values.filter { $0.type == ROOM_TYPE_PHOTOS }
20 | if invitations.isEmpty {
21 | Text("No current invitations")
22 | } else {
23 | ForEach(invitations) { invitation in
24 | let user = session.getUser(userId: invitation.sender)
25 | GalleryInviteCard(room: invitation, user: user, container: container)
26 | Divider()
27 | }
28 | }
29 | }
30 | }
31 | .navigationTitle(Text("Invitations"))
32 | }
33 | }
34 |
35 | /*
36 | struct GalleryInvitationsView_Previews: PreviewProvider {
37 | static var previews: some View {
38 | GalleryInvitationsView()
39 | }
40 | }
41 | */
42 |
--------------------------------------------------------------------------------
/Circles/Views/Photo Galleries/PhotoContextMenu.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2023 FUTO Holdings Inc
2 | //
3 | // PhotoContextMenu.swift
4 | // Circles
5 | //
6 | // Created by Charles Wright on 4/18/23.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | import Matrix
13 |
14 | struct PhotoContextMenu: View {
15 | var message: Matrix.Message
16 | @Binding var showDetail: Bool
17 |
18 | var body: some View {
19 | let current = message.replacement ?? message
20 |
21 | if let content = current.content as? Matrix.MessageContent,
22 | content.msgtype == M_IMAGE,
23 | let imageContent = content as? Matrix.mImageContent
24 | {
25 | AsyncButton(action: {
26 | try await saveImage(content: imageContent, session: message.room.session)
27 | }) {
28 | Label("Save image", systemImage: "square.and.arrow.down")
29 | }
30 |
31 | if let thumbnail = current.thumbnail
32 | {
33 | let image = Image(uiImage: thumbnail)
34 | ShareLink(item: image, preview: SharePreview(imageContent.caption ?? "", image: image))
35 | }
36 | }
37 |
38 | Button(action: {
39 | message.objectWillChange.send()
40 | }) {
41 | Label("Refresh", systemImage: "arrow.clockwise")
42 | }
43 |
44 | Button(action: {
45 | self.showDetail = true
46 | }) {
47 | Label("Show detailed view", systemImage: "magnifyingglass")
48 | }
49 |
50 | if message.iCanRedact {
51 | AsyncButton(action: {
52 | try await deleteAndPurge(message: message)
53 | }) {
54 | Label("Delete", systemImage: SystemImages.trash.rawValue)
55 | }
56 | .foregroundColor(.red)
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Circles/Views/Photo Galleries/VideoDetailView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VideoDetailView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 4/17/24.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct VideoDetailView: View {
12 | @ObservedObject var message: Matrix.Message
13 |
14 | var body: some View {
15 | VideoContentView(message: message, autoplay: true, fullscreen: true)
16 | }
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/Circles/Views/Photo Galleries/VideoThumbnailCard.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VideoThumbnailCard.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 4/17/24.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct VideoThumbnailCard: View {
12 | @ObservedObject var message: Matrix.Message
13 | var height: CGFloat
14 | var width: CGFloat
15 | @State var playVideo: Bool = false
16 |
17 | @Environment(\.colorScheme) var colorScheme
18 |
19 | var body: some View {
20 | ZStack {
21 | if message.type == M_ROOM_MESSAGE {
22 | if let img = message.thumbnail {
23 | Image(uiImage: img)
24 | .renderingMode(.original)
25 | .resizable()
26 | .aspectRatio(contentMode: .fill)
27 | .frame(width: width, height: height, alignment: .center)
28 | .clipped()
29 |
30 | BasicImage(systemName: SystemImages.playCircle.rawValue)
31 | .foregroundColor(.white)
32 | .shadow(color: .black, radius: /*@START_MENU_TOKEN@*/10/*@END_MENU_TOKEN@*/)
33 | .frame(width: width/2, height: height/2)
34 | .onTapGesture {
35 | self.playVideo = true
36 | }
37 | .fullScreenCover(isPresented: $playVideo) {
38 | VideoDetailView(message: message)
39 | }
40 |
41 | } else {
42 | Color.gray
43 | .onAppear {
44 | let _ = Task {
45 | try await message.fetchThumbnail()
46 | }
47 | }
48 | ProgressView()
49 | }
50 | }
51 | else {
52 | VStack {
53 | let bgColor = colorScheme == .dark ? Color.black : Color.white
54 | BasicImage(systemName: SystemImages.lockRectangle.rawValue)
55 | .foregroundColor(Color.gray)
56 | .padding()
57 | VStack {
58 | Text("Decryption error")
59 | if DebugModel.shared.debugMode {
60 | Text("Message id: \(message.id)")
61 | .font(.footnote)
62 | }
63 | }
64 | .multilineTextAlignment(.center)
65 | .foregroundColor(.gray)
66 | .background(
67 | bgColor
68 | .opacity(0.5)
69 | )
70 | .padding(.bottom, 2)
71 | }
72 | }
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/Circles/Views/Rooms/BannedRoomMemberRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BannedRoomMemberRow.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 4/15/24.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct BannedRoomMemberRow: View {
12 | @ObservedObject var user: Matrix.User
13 | @ObservedObject var room: Matrix.Room
14 |
15 | @State var showConfirmUnban = false
16 |
17 | var body: some View {
18 | HStack {
19 | UserAvatarView(user: user)
20 | .frame(width: 60, height: 60)
21 | VStack(alignment: .leading) {
22 | UserNameView(user: user)
23 | Text(user.userId.stringValue)
24 | .font(.subheadline)
25 | .foregroundColor(.gray)
26 | }
27 | Spacer()
28 | Button(action: {
29 | self.showConfirmUnban = true
30 | }) {
31 | Label("Un-ban", systemImage: "trash.slash")
32 | }
33 | .disabled(!room.iCanUnban(userId: user.userId))
34 | .confirmationDialog(
35 | "Confirm un-banning",
36 | isPresented: $showConfirmUnban,
37 | actions: {
38 | AsyncButton(action: {}) {
39 | Text("Un-ban \(user.displayName ?? user.userId.stringValue)")
40 | }
41 | },
42 | message: {
43 | Label("This will allow the user to re-join in the future", systemImage: "exclamationmark.triangle.fill")
44 | .foregroundColor(.red)
45 | }
46 | )
47 | }
48 | }
49 | }
50 |
51 |
52 |
--------------------------------------------------------------------------------
/Circles/Views/Rooms/KnockOnRoomView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KnockOnRoomView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 4/9/24.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct KnockOnRoomView: View {
12 | var roomId: RoomId
13 | var session: Matrix.Session
14 |
15 | @Environment(\.presentationMode) var presentation
16 |
17 | @State var reason = ""
18 |
19 | var body: some View {
20 | VStack {
21 | Label("Request invitation", systemImage: "checkmark.circle.fill")
22 | .symbolRenderingMode(.multicolor)
23 | .font(.title2)
24 | .padding()
25 |
26 | VStack(alignment: .leading) {
27 | Text("Include an optional message:")
28 | .foregroundColor(.gray)
29 |
30 | TextEditor(text: $reason)
31 | .lineLimit(5)
32 | .border(Color.gray)
33 |
34 | Label("Warning: Your message will not be encrypted, and is accessible by all current members", systemImage: SystemImages.exclamationmarkShield.rawValue)
35 | .foregroundColor(.orange)
36 | }
37 |
38 | AsyncButton(action: {
39 | print("Sending knock to \(roomId.stringValue)")
40 | if reason.isEmpty {
41 | try await session.knock(roomId: roomId, reason: nil)
42 | } else {
43 | try await session.knock(roomId: roomId, reason: reason)
44 |
45 | }
46 | self.presentation.wrappedValue.dismiss()
47 | }) {
48 | Label("Send request for invite", systemImage: "paperplane.fill")
49 | }
50 | .padding()
51 |
52 | Spacer()
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Circles/Views/Rooms/RoomCryptoInfoView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RoomCryptoInfoView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 1/27/24.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 | import MatrixSDKCrypto
11 |
12 | struct RoomCryptoInfoView: View {
13 | @ObservedObject var room: Matrix.Room
14 |
15 | var body: some View {
16 | Form {
17 | ForEach(room.joinedMembers) { userId in
18 | let user = room.session.getUser(userId: userId)
19 | if !user.devices.isEmpty {
20 | Section(user.displayName ?? user.userId.stringValue) {
21 | ForEach(user.devices) { device in
22 | NavigationLink(destination: DeviceDetailsView(session: room.session, device: device)) {
23 | DeviceInfoView(session: room.session, device: device)
24 | }
25 | .buttonStyle(.plain)
26 | }
27 | }
28 | }
29 | }
30 | }
31 | .navigationTitle("Crypto Info")
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Circles/Views/Rooms/RoomDebugDetailsSection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RoomDebugDetailsSection.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 2/6/24.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct RoomDebugDetailsSection: View {
12 | @ObservedObject var room: Matrix.Room
13 |
14 | var body: some View {
15 | if DebugModel.shared.debugMode {
16 | Section("Matrix Debug Details") {
17 | Text("History visibiilty")
18 | .badge(room.historyVisibility?.rawValue ?? "unknown")
19 | Text("Join rule")
20 | .badge(room.joinRule?.rawValue ?? "unknown")
21 | Text("Encryption algorithm")
22 | .badge(room.encryptionParams?.algorithm.rawValue ?? "none")
23 | if let ms = room.encryptionParams?.rotationPeriodMs {
24 | let sec = ms / 1000
25 | Text("Rotation (sec)")
26 | .badge("\(sec)")
27 | }
28 | if let msgs = room.encryptionParams?.rotationPeriodMsgs {
29 | Text("Rotation (msgs)")
30 | .badge("\(msgs)")
31 | }
32 | }
33 | }
34 | }
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/Circles/Views/Rooms/RoomDefaultPowerLevelPicker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RoomDefaultPowerLevelPicker.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 4/9/24.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct RoomDefaultPowerLevelPicker: View {
12 | @ObservedObject var room: Matrix.Room
13 |
14 | @State var selected: PowerLevel
15 |
16 | init(room: Matrix.Room) {
17 | self.room = room
18 | let level = PowerLevel(power: room.powerLevels?.usersDefault ?? 0)
19 | self._selected = State(wrappedValue: level)
20 | }
21 |
22 | var body: some View {
23 | Picker("Default", selection: $selected) {
24 | ForEach(CIRCLES_POWER_LEVELS) { level in
25 | Text(level.description)
26 | .tag(level)
27 | }
28 | }
29 | .onChange(of: selected) { newLevel in
30 | Task {
31 | print("Setting new power level")
32 | try await room.setPowerLevel(usersDefault: newLevel.power)
33 | }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Circles/Views/Rooms/RoomInviteOneUserSheet.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RoomInviteOneUserSheet.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 12/15/23.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct RoomInviteOneUserSheet: View {
12 | @ObservedObject var room: Matrix.Room
13 | @ObservedObject var user: Matrix.User
14 |
15 | @Environment(\.presentationMode) var presentation
16 |
17 | @State private var message = ""
18 |
19 | var subtitle: String {
20 | switch room.type {
21 | case ROOM_TYPE_CIRCLE:
22 | return "To follow my \(room.name ?? "") timeline"
23 | case ROOM_TYPE_GROUP:
24 | return "To join \(room.name ?? "a group")"
25 | case ROOM_TYPE_SPACE:
26 | return "To connect with \(room.name ?? "me")"
27 | case ROOM_TYPE_PHOTOS:
28 | return "To see photos in \(room.name ?? "a gallery")"
29 | default:
30 | return "To join \(room.name ?? "")"
31 | }
32 | }
33 |
34 | var body: some View {
35 | VStack {
36 | VStack {
37 | Text("Inviting \(user.displayName ?? user.userId.stringValue)")
38 | .font(.title2)
39 | Text(subtitle)
40 | }
41 | .padding()
42 |
43 | VStack(alignment: .leading) {
44 | Text("Message:")
45 | TextEditor(text: $message)
46 | .overlay(RoundedRectangle(cornerRadius: 4).stroke(Color.gray))
47 | }
48 | .padding()
49 |
50 | HStack {
51 | Spacer()
52 |
53 | Button(role: .destructive, action: {
54 | self.presentation.wrappedValue.dismiss()
55 | }) {
56 | Text("Cancel")
57 | .padding(5)
58 | }
59 | .buttonStyle(.bordered)
60 |
61 | Spacer()
62 |
63 | AsyncButton(action: {
64 | try await room.invite(userId: user.userId, reason: message.isEmpty ? nil : message)
65 | self.presentation.wrappedValue.dismiss()
66 | }) {
67 | Text("Send invitation")
68 | .padding(5)
69 | }
70 | .buttonStyle(.bordered)
71 |
72 | Spacer()
73 | }
74 | }
75 | }
76 | }
77 |
78 |
--------------------------------------------------------------------------------
/Circles/Views/Rooms/RoomInvitedMemberRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CircleInvitedFollowerRow.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 11/7/23.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | // This renders a row in a list, showing a user who we have invited to join one of our rooms
12 | struct RoomInvitedMemberRow: View {
13 | var room: Matrix.Room
14 | @ObservedObject var user: Matrix.User
15 |
16 | @State var showConfirmCancel = false
17 |
18 | var body: some View {
19 | HStack {
20 | UserAvatarView(user: user)
21 | .frame(width: 60, height: 60)
22 | VStack(alignment: .leading) {
23 | UserNameView(user: user)
24 | Text(user.userId.stringValue)
25 | .font(.subheadline)
26 | .foregroundColor(.gray)
27 | }
28 |
29 | Spacer()
30 |
31 | Button(role: .destructive, action: {
32 | // Cancel invite
33 | self.showConfirmCancel = true
34 | }) {
35 | Image(systemName: SystemImages.trash.rawValue)
36 | }
37 | .disabled(!room.iCanKick)
38 | .confirmationDialog(
39 | "Cancel invitation?",
40 | isPresented: $showConfirmCancel,
41 | actions: {
42 | AsyncButton(role: .destructive, action: {
43 | try await room.kick(userId: user.userId, reason: "Canceling invitation")
44 | }) {
45 | Text("Cancel invitation")
46 | }
47 | }
48 | )
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Circles/Views/Rooms/RoomKnockDetailsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RoomKnockDetailsView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 10/10/23.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct RoomKnockDetailsView: View {
12 | @ObservedObject var room: Matrix.Room
13 |
14 |
15 |
16 | var body: some View {
17 | ScrollView {
18 | VStack {
19 | ForEach(room.knockingMembers) { userId in
20 | //ForEach([UserId("@cvwright:circu.li")!]) { userId in
21 | Divider()
22 | let user = room.session.getUser(userId: userId)
23 | KnockingUserCard(user: user, room: room)
24 | }
25 | }
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Circles/Views/Rooms/RoomKnockIndicator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RoomKnockIndicator.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 10/10/23.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct RoomKnockIndicator: View {
12 | @ObservedObject var room: Matrix.Room
13 |
14 | var body: some View {
15 | if room.iCanInvite {
16 | HStack {
17 | Spacer()
18 | NavigationLink(destination: RoomKnockDetailsView(room: room)) {
19 | Label("Review \(room.knockingMembers.count) request(s) for invitations", systemImage: "star.fill")
20 | .fontWeight(.bold)
21 | .foregroundColor(.white)
22 | .padding()
23 | }
24 | Spacer()
25 | }
26 | .background(Color.accentColor)
27 | .frame(maxHeight: 60)
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Circles/Views/Rooms/RoomMembersSection.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RoomMembersSection.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 11/7/23.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct RoomMembersSection: View {
12 | var title: String
13 | var users: [UserId]
14 | var room: Matrix.Room
15 |
16 | var body: some View {
17 | Section(title) {
18 | ForEach(users) { userId in
19 | let user = room.session.getUser(userId: userId)
20 | NavigationLink(destination: RoomMemberDetailView(user: user, room: room)) {
21 | RoomMemberRow(user: user, room: room)
22 | }
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Circles/Views/Rooms/RoomRenameView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RoomRenameView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 4/15/24.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct RoomRenameView: View {
12 | @ObservedObject var room: Matrix.Room
13 | @Environment(\.presentationMode) var presentation
14 |
15 | @State var newName: String
16 |
17 | enum FocusField {
18 | case roomName
19 | }
20 | @FocusState var focus: FocusField?
21 |
22 | init(room: Matrix.Room) {
23 | self.room = room
24 | self._newName = State(wrappedValue: room.name ?? "")
25 | }
26 |
27 | var body: some View {
28 | VStack(alignment: .center, spacing: 100) {
29 | Spacer()
30 |
31 | HStack {
32 | TextField(room.name ?? "", text: $newName, prompt: Text("New name"))
33 | .textFieldStyle(.roundedBorder)
34 | .textInputAutocapitalization(.words)
35 | .focused($focus, equals: .roomName)
36 | .frame(width: 300, height: 40)
37 | .onAppear {
38 | self.focus = .roomName
39 | }
40 | Button(action: {
41 | self.newName = ""
42 | }) {
43 | Image(systemName: SystemImages.xmark.rawValue)
44 | .foregroundColor(.gray)
45 | }
46 | }
47 |
48 | AsyncButton(action: {
49 | try await room.setName(newName: newName)
50 | self.presentation.wrappedValue.dismiss()
51 | }) {
52 | Text("Update")
53 | }
54 |
55 | Spacer()
56 | }
57 | .navigationTitle("Rename \(room.name ?? "")")
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Circles/Views/Rooms/RoomTopicEditorView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RoomTopicEditorView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 4/15/24.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct RoomTopicEditorView: View {
12 | @ObservedObject var room: Matrix.Room
13 | @Environment(\.presentationMode) var presentation
14 |
15 | @State var newTopic: String
16 |
17 | enum FocusField {
18 | case topic
19 | }
20 | @FocusState var focus: FocusField?
21 |
22 | init(room: Matrix.Room) {
23 | self.room = room
24 | self._newTopic = State(wrappedValue: room.topic ?? "")
25 | }
26 |
27 | var body: some View {
28 | VStack(alignment: .center, spacing: 100) {
29 | Spacer()
30 |
31 | HStack {
32 | TextField(room.name ?? "", text: $newTopic, prompt: Text("New topic"))
33 | .textFieldStyle(.roundedBorder)
34 | .textInputAutocapitalization(.sentences)
35 | .focused($focus, equals: .topic)
36 | .frame(width: 300, height: 40)
37 | .onAppear {
38 | self.focus = .topic
39 | }
40 | Button(action: {
41 | self.newTopic = ""
42 | }) {
43 | Image(systemName: SystemImages.xmark.rawValue)
44 | .foregroundColor(.gray)
45 | }
46 | }
47 |
48 | AsyncButton(action: {
49 | try await room.setTopic(newTopic: newTopic)
50 | self.presentation.wrappedValue.dismiss()
51 | }) {
52 | Text("Update")
53 | }
54 |
55 | Spacer()
56 | }
57 | .navigationTitle("Change Topic")
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Circles/Views/Rooms/ScanQrCodeAndKnockSheet.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScanQrCodeAndKnockView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 10/11/23.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct ScanQrCodeAndKnockSheet: View {
12 | var session: Matrix.Session
13 | @State var reason: String = ""
14 | @State var roomId: RoomId? = nil
15 | @Environment(\.presentationMode) var presentation
16 | @EnvironmentObject var app: CirclesApplicationSession
17 |
18 | var body: some View {
19 | VStack {
20 | if let roomId = roomId {
21 | if let room = session.invitations[roomId] {
22 |
23 | Spacer()
24 |
25 | Text("You already have an invitation!")
26 | .font(.title2)
27 |
28 | Spacer()
29 |
30 | let user = session.getUser(userId: room.sender)
31 | switch room.type {
32 | case ROOM_TYPE_CIRCLE:
33 | InvitedCircleCard(room: room, user: user, container: app.timelines)
34 | .frame(maxWidth: 400)
35 | case ROOM_TYPE_GROUP:
36 | InvitedGroupCard(room: room, user: user, container: app.groups)
37 | .frame(maxWidth: 400)
38 | case ROOM_TYPE_PHOTOS:
39 | GalleryInviteCard(room: room, user: user, container: app.galleries)
40 | .frame(maxWidth: 400)
41 | default:
42 | Label("This invitation does not look like it is for use with Circles", systemImage: "exclamationmark.triangle.fill")
43 | }
44 |
45 | Spacer()
46 |
47 | } else {
48 | KnockOnRoomView(roomId: roomId, session: session)
49 | }
50 | } else {
51 | ScanQrCodeView(roomId: $roomId)
52 | }
53 |
54 | Button(role: .destructive, action: {
55 | self.presentation.wrappedValue.dismiss()
56 | }) {
57 | Text("Cancel")
58 | .padding()
59 | }
60 | }
61 | .padding()
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Circles/Views/Rooms/ScanQrCodeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScanQrCodeView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 4/9/24.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 | import CodeScanner
11 |
12 | struct ScanQrCodeView: View {
13 | @Binding var roomId: RoomId?
14 |
15 | var body: some View {
16 | VStack {
17 | Text("Scan QR code to request invite")
18 | .font(.title)
19 | .padding()
20 |
21 | CodeScannerView(codeTypes: [.qr]) { response in
22 | if case let .success(result) = response {
23 | let scannedCode = result.string
24 | print("QR code scanning result = \(scannedCode)")
25 | if let scannedRoomId = RoomId(scannedCode) {
26 | print("QR code contains a valid roomId")
27 | self.roomId = scannedRoomId
28 | } else if let firstToken = scannedCode.split(separator: " ").first,
29 | let url = URL(string: firstToken.description)
30 | {
31 | print("QR code contains a valid URL \(url)")
32 | guard let host = url.host(),
33 | CIRCLES_DOMAINS.contains(host)
34 | else {
35 | print("QR code URL is not for one of our domains (found \(url.host() ?? "nil"))")
36 | return
37 | }
38 | for component in url.pathComponents {
39 | if let pathRoomId = RoomId(component) {
40 | print("Found roomId \(pathRoomId) in QR code URL path")
41 | self.roomId = pathRoomId
42 | return
43 | }
44 | }
45 | } else {
46 | print("QR does not contain a valid roomId: \(scannedCode)")
47 | }
48 | } else {
49 | print("QR code scanning failed")
50 | }
51 | }
52 | //.frame(width: 300, height: 300)
53 | //.padding()
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Circles/Views/Settings/DeactivateAccountCustomAlertView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DeactivateAccountCustomAlertView.swift
3 | // Circles
4 | //
5 | // Created by Dmytro Ryshchuk on 5/21/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct DeactivateAccountAlertModel {
11 | let userId: String
12 | let title: String
13 | let message: String
14 | }
15 |
16 | struct DeactivateAccountAlertView: View {
17 | @State private var text = ""
18 | var model: DeactivateAccountAlertModel
19 | var onConfirm: () -> Void
20 | var onCancel: () -> Void
21 |
22 | var body: some View {
23 | VStack {
24 | Text(model.title)
25 | .font(.headline)
26 | Text(model.message)
27 | .font(.subheadline)
28 | TextField(model.userId, text: $text)
29 | .textFieldStyle(RoundedBorderTextFieldStyle())
30 | .padding()
31 |
32 | AsyncButton(role: .destructive, action: {
33 | onConfirm()
34 | }) {
35 | Text("Permanently deactivate")
36 | }
37 | .disabled(text != model.userId)
38 |
39 | Button("Cancel") {
40 | onCancel()
41 | }
42 | .padding()
43 | }
44 | .padding()
45 | .background(Color.white)
46 | .cornerRadius(12)
47 | .shadow(radius: 10)
48 | .padding(.horizontal, 60)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Circles/Views/Settings/DevicesScreen.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2020, 2021 Kombucha Digital Privacy Systems LLC
2 | //
3 | // DevicesScreen.swift
4 | // Circles for iOS
5 | //
6 | // Created by Charles Wright on 2/26/21.
7 | //
8 |
9 | import SwiftUI
10 | import Matrix
11 |
12 | struct DevicesScreen: View {
13 | @ObservedObject var session: Matrix.Session
14 |
15 |
16 | var currentDeviceView: some View {
17 | VStack(alignment: .leading, spacing: 15) {
18 | if let dev = session.device {
19 | let myDeviceModel = UIDevice.current.model
20 | let iconName = myDeviceModel.components(separatedBy: .whitespaces).first?.lowercased() ?? SystemImages.desktopcomputer.rawValue
21 | Label("This \(myDeviceModel)", systemImage: iconName)
22 | .font(.headline)
23 |
24 | DeviceInfoView(session: session, device: dev)
25 | .padding(.leading)
26 | Divider()
27 | }
28 | }
29 | }
30 |
31 | var body: some View {
32 | Form {
33 | ForEach(session.devices, id: \.deviceId) { device in
34 | if !device.dehydrated {
35 | NavigationLink(destination: DeviceDetailsView(session: session, device: device)) {
36 | DeviceInfoView(session: session, device: device)
37 | }
38 | .buttonStyle(.plain)
39 | }
40 | }
41 | }
42 | .navigationTitle(Text("Active Login Sessions"))
43 | }
44 | }
45 |
46 | /*
47 | struct DevicesScreen_Previews: PreviewProvider {
48 | static var previews: some View {
49 | DevicesScreen()
50 | }
51 | }
52 | */
53 |
--------------------------------------------------------------------------------
/Circles/Views/Settings/IgnoredUsersView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IgnoredUsersView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 7/10/23.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct IgnoredUsersView: View {
12 | @ObservedObject var session: Matrix.Session
13 |
14 | var body: some View {
15 | Form {
16 | Section("Ignored Users") {
17 | let ignored = session.ignoredUserIds
18 | if ignored.isEmpty {
19 | Text("Not ignoring any users")
20 | .padding()
21 | } else {
22 | ForEach(ignored) { userId in
23 | let user = session.getUser(userId: userId)
24 |
25 | HStack {
26 | UserAvatarView(user: user)
27 | .frame(width: 60, height: 60)
28 | VStack(alignment: .leading) {
29 | UserNameView(user: user)
30 | Text(user.userId.stringValue)
31 | .font(.subheadline)
32 | .foregroundColor(.gray)
33 | }
34 | Spacer()
35 | AsyncButton(action: {
36 | try await user.session.unignoreUser(userId: user.userId)
37 | }) {
38 | Image(systemName: SystemImages.trash.rawValue)
39 | .foregroundColor(.red)
40 | }
41 | }
42 | }
43 | }
44 | }
45 | }
46 | .navigationTitle(Text("Ignored Users"))
47 | }
48 | }
49 |
50 | /*
51 | struct IgnoredUsersView_Previews: PreviewProvider {
52 | static var previews: some View {
53 | IgnoredUsersView()
54 | }
55 | }
56 | */
57 |
--------------------------------------------------------------------------------
/Circles/Views/Settings/ProfileSettingsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileSettingsView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 7/5/23.
6 | //
7 |
8 | import SwiftUI
9 | import PhotosUI
10 | import Matrix
11 |
12 | struct ProfileSettingsView: View {
13 | @ObservedObject var session: Matrix.Session
14 | //@ObservedObject var user: Matrix.User
15 |
16 | @State var showPicker = false
17 | @State var newAvatarImageItem: PhotosPickerItem?
18 |
19 | var body: some View {
20 | VStack {
21 | Form {
22 | HStack {
23 | Text("Profile picture")
24 | Spacer()
25 |
26 | PhotosPicker(selection: $newAvatarImageItem, matching: .images) {
27 | UserAvatarView(user: session.me)
28 | .frame(width: 80, height: 80)
29 | }
30 | .buttonStyle(.plain)
31 | .onChange(of: newAvatarImageItem) { _ in
32 | Task {
33 | if let data = try? await newAvatarImageItem?.loadTransferable(type: Data.self) {
34 | if let img = UIImage(data: data) {
35 | try await session.setMyAvatarImage(img)
36 | }
37 | }
38 | }
39 | }
40 | }
41 | NavigationLink(destination: UpdateDisplaynameView(session: session)) {
42 | Text("Your name")
43 | .badge(abbreviate(session.me.displayName))
44 | }
45 |
46 | Text("User ID")
47 | .badge(session.creds.userId.stringValue)
48 |
49 | /*
50 | NavigationLink(destination: UpdateStatusMessageView(session: session)) {
51 | Text("Status message")
52 | .badge(session.statusMessage ?? "(none)")
53 | }
54 | */
55 | }
56 | }
57 | .navigationTitle("Public Profile")
58 | }
59 | }
60 |
61 | /*
62 | struct ProfileSettingsView_Previews: PreviewProvider {
63 | static var previews: some View {
64 | ProfileSettingsView()
65 | }
66 | }
67 | */
68 |
--------------------------------------------------------------------------------
/Circles/Views/Settings/UpdateDisplaynameView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UpdateDisplaynameView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 7/6/23.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct UpdateDisplaynameView: View {
12 | @ObservedObject var session: Matrix.Session
13 | @Environment(\.presentationMode) var presentation
14 | //@ObservedObject var user: Matrix.User
15 | @State var newDisplayname: String
16 |
17 | enum FocusField {
18 | case displayname
19 | }
20 | @FocusState var focus: FocusField?
21 |
22 | init(session: Matrix.Session) {
23 | self.session = session
24 | self._newDisplayname = State(wrappedValue: session.me.displayName ?? "")
25 | }
26 |
27 | var body: some View {
28 | VStack(alignment: .center, spacing: 100) {
29 | //Text("Update displayname for user \(user.userId.stringValue)")
30 | Spacer()
31 |
32 | HStack {
33 | TextField(abbreviate(session.me.displayName, textIfEmpty: ""), text: $newDisplayname, prompt: Text("Your name"))
34 | .textContentType(.name)
35 | .textFieldStyle(.roundedBorder)
36 | .focused($focus, equals: .displayname)
37 | .frame(width: 300, height: 40)
38 | .onAppear {
39 | self.focus = .displayname
40 | }
41 |
42 | Button(action: {
43 | self.newDisplayname = ""
44 | }) {
45 | Image(systemName: SystemImages.xmark.rawValue)
46 | .foregroundColor(.gray)
47 | }
48 | }
49 |
50 | AsyncButton(action: {
51 | try await session.setMyDisplayName(newDisplayname)
52 | self.presentation.wrappedValue.dismiss()
53 | }) {
54 | Text("Update")
55 | }
56 |
57 | Spacer()
58 | }
59 | .navigationTitle("Change Name")
60 | }
61 | }
62 |
63 | /*
64 | struct UpdateDisplaynameView_Previews: PreviewProvider {
65 | static var previews: some View {
66 | UpdateDisplaynameView()
67 | }
68 | }
69 | */
70 |
--------------------------------------------------------------------------------
/Circles/Views/Settings/UpdateStatusMessageView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UpdateStatusMessageView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 7/6/23.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct UpdateStatusMessageView: View {
12 | @ObservedObject var session: Matrix.Session
13 | @Environment(\.presentationMode) var presentation
14 | //@ObservedObject var user: Matrix.User
15 | @State var newStatus = ""
16 |
17 | var body: some View {
18 | VStack(alignment: .center, spacing: 100) {
19 | //Text("Update displayname for user \(user.userId.stringValue)")
20 |
21 | Spacer()
22 |
23 | TextField(session.me.statusMessage ?? "", text: $newStatus, prompt: Text("New status message"))
24 | .frame(width: 300, height: 40)
25 |
26 | AsyncButton(action: {
27 | try await session.setMyStatus(message: newStatus)
28 | self.presentation.wrappedValue.dismiss()
29 | }) {
30 | Text("Update")
31 | }
32 |
33 | Spacer()
34 | }
35 | .padding()
36 | }
37 | }
38 |
39 | /*
40 | struct UpdateStatusMessageView_Previews: PreviewProvider {
41 | static var previews: some View {
42 | UpdateStatusMessageView()
43 | }
44 | }
45 | */
46 |
--------------------------------------------------------------------------------
/Circles/Views/Setup/CircleSetupInfo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CircleSetupInfo.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 5/28/24.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | class CircleSetupInfo: ObservableObject, Identifiable {
12 | var name: String
13 | @Published var avatar: UIImage?
14 |
15 | var id: String {
16 | name
17 | }
18 |
19 | init(name: String, avatar: UIImage? = nil) {
20 | self.name = name
21 | self.avatar = avatar
22 | }
23 |
24 | @MainActor
25 | func setAvatar(_ img: UIImage) {
26 | self.avatar = img
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Circles/Views/Setup/SetupCircleCard.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2021 Kombucha Digital Privacy Systems LLC
2 | //
3 | // SetupCircleCard.swift
4 | // Circles for iOS
5 | //
6 | // Created by Charles Wright on 5/24/21.
7 | //
8 |
9 | import SwiftUI
10 | import PhotosUI
11 | import Matrix
12 |
13 | struct SetupCircleCard: View {
14 | var matrix: Matrix.Session
15 | @ObservedObject var user: Matrix.User
16 | @ObservedObject var info: CircleSetupInfo
17 |
18 | //@State var showPicker = false
19 | @State var selectedItem: PhotosPickerItem?
20 |
21 | var body: some View {
22 | VStack(alignment: .leading) {
23 | HStack {
24 | ZStack {
25 | Color.gray
26 |
27 | if let img = info.avatar {
28 | Image(uiImage: img)
29 | .resizable()
30 | .scaledToFill()
31 | }
32 | }
33 | .clipShape(Circle())
34 | .frame(width: 100, height: 100, alignment: .center)
35 | .foregroundColor(.gray)
36 | .overlay(alignment: .bottomTrailing) {
37 | PhotosPicker(selection: $selectedItem) {
38 | Image(systemName: SystemImages.pencilCircleFill.rawValue)
39 | .symbolRenderingMode(.multicolor)
40 | .font(.system(size: 30))
41 | .foregroundColor(.accentColor)
42 | }
43 | .buttonStyle(.borderless)
44 | }
45 |
46 | VStack(alignment: .leading) {
47 | Text(info.name)
48 | .font(
49 | CustomFonts.nunito24
50 | .weight(.heavy)
51 | )
52 | Text(self.user.displayName ?? "")
53 | .font(CustomFonts.nunito16)
54 | }
55 | .padding(.leading)
56 | }
57 | .onChange(of: selectedItem) { newItem in
58 | Task {
59 | if let data = try? await newItem?.loadTransferable(type: Data.self),
60 | let img = UIImage(data: data)
61 | {
62 | info.setAvatar(img)
63 | }
64 | }
65 | }
66 | }
67 | }
68 | }
69 |
70 | /*
71 | struct SetupCircleCard_Previews: PreviewProvider {
72 | static var previews: some View {
73 | SetupCircleCard()
74 | }
75 | }
76 | */
77 |
--------------------------------------------------------------------------------
/Circles/Views/Setup/SetupIntroToCircles.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SetupIntroToCircles.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 5/30/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct SetupIntroToCircles: View {
11 | @Binding var stage: SetupScreen.Stage
12 |
13 | var body: some View {
14 | let elementWidth = UIScreen.main.bounds.width - 48
15 | let elementHeight: CGFloat = 48.0
16 | ZStack {
17 | Color.greyCool200
18 |
19 | VStack {
20 | CirclesHelpView()
21 |
22 | Button(action: {
23 | stage = .circlesSetup
24 | }) {
25 | Text("Next: Set up my circles")
26 | }
27 | .buttonStyle(BigRoundedButtonStyle(width: elementWidth, height: elementHeight))
28 | .font(
29 | CustomFonts.nunito16
30 | .weight(.bold)
31 | )
32 | .padding(.bottom, 38)
33 | }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Circles/Views/Setup/SetupScreen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SetupScreen.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 6/9/22.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct SetupScreen: View {
12 | var store: CirclesStore
13 | @ObservedObject var matrix: Matrix.Session
14 |
15 | enum Stage {
16 | case profileSetup
17 | case circlesIntro
18 | case circlesSetup
19 | }
20 | @State var stage: Stage = .profileSetup
21 | @State var displayName: String?
22 |
23 | var body: some View {
24 | switch stage {
25 | case .profileSetup:
26 | SetupAvatarView(matrix: matrix, displayName: $displayName, stage: $stage)
27 | .background(Color.greyCool200)
28 |
29 | case .circlesIntro:
30 | SetupIntroToCircles(stage: $stage)
31 | .background(Color.greyCool200)
32 |
33 | case .circlesSetup:
34 | SetupCirclesView(store: store, matrix: matrix, user: matrix.me)
35 | .background(Color.greyCool200)
36 | }
37 | }
38 | }
39 |
40 | /*
41 | struct SetupScreen_Previews: PreviewProvider {
42 | static var previews: some View {
43 | SetupScreen()
44 | }
45 | }
46 | */
47 |
--------------------------------------------------------------------------------
/Circles/Views/Signup/SignupStartForm.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SignupStartForm.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 9/8/21.
6 | //
7 |
8 | import SwiftUI
9 | import StoreKit
10 | import Matrix
11 |
12 | struct SignupStartForm: View {
13 | @ObservedObject var session: SignupSession
14 | var state: UIAA.SessionState
15 | @State private var canNotRegisterFlow: Bool = false
16 |
17 | var body: some View {
18 | VStack {
19 | Color.clear
20 | .onAppear {
21 | Task {
22 | if let freeFlow = state.flows.first(where: { flow in
23 | flow.stages.contains(AUTH_TYPE_FREE_SUBSCRIPTION) &&
24 | !flow.stages.contains(AUTH_TYPE_GOOGLE_SUBSCRIPTION) &&
25 | !flow.stages.contains(AUTH_TYPE_APPSTORE_SUBSCRIPTION)
26 | }) {
27 | await session.selectFlow(flow: freeFlow)
28 | } else {
29 | let appleFlow = state.flows.first(where: {
30 | $0.stages.contains(AUTH_TYPE_APPSTORE_SUBSCRIPTION)
31 | })
32 |
33 | if appleFlow != nil {
34 | await session.selectFlow(flow: appleFlow!)
35 | } else {
36 | canNotRegisterFlow = true
37 | }
38 | }
39 | }
40 | }
41 | VStack {
42 | Label("We apologize, but new accounts are not currently available on this server. Please check back later.", systemImage: SystemImages.exclamationmarkTriangle.rawValue)
43 | .multilineTextAlignment(.center)
44 | .padding(.horizontal, 20)
45 | }.opacity(canNotRegisterFlow ? 1 : 0)
46 | }
47 | }
48 | }
49 |
50 | /*
51 | struct SignupStartForm_Previews: PreviewProvider {
52 | static var previews: some View {
53 | SignupScreen()
54 | }
55 | }
56 | */
57 |
--------------------------------------------------------------------------------
/Circles/Views/Timeline/LikeButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LikeButton.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 7/19/24.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct LikeButton: View {
12 | @ObservedObject var message: Matrix.Message
13 |
14 | var body: some View {
15 | let likers = message.reactions["❤️"] ?? []
16 | let iLikedThisMessage = likers.contains(message.room.session.creds.userId)
17 | let iCanReact = message.room.iCanSendEvent(type: M_REACTION)
18 |
19 | AsyncButton(action: {
20 | // send ❤️ emoji reaction if we have not sent it yet
21 | // Otherwise retract it
22 | if iLikedThisMessage {
23 | // Redact the previous reaction message
24 | try await message.sendRemoveReaction("❤️")
25 | } else {
26 | // Send the reaction
27 | try await message.sendReaction("❤️")
28 | }
29 | }) {
30 | HStack(alignment: .center, spacing: 2) {
31 | let icon = iLikedThisMessage ? SystemImages.heartFill : SystemImages.heart
32 | let color = iLikedThisMessage ? Color.accentColor : Color.primary
33 | Image(systemName: icon.rawValue)
34 | .frame(width: 20, height: 20)
35 | .foregroundColor(color)
36 | Text("\(message.reactions["❤️"]?.count ?? 0)")
37 | }
38 | .font(Font.custom("Inter", size: 14).weight(.medium))
39 | .foregroundColor(Color.greyCool1000)
40 | }
41 | .disabled(!iCanReact)
42 | }
43 | }
44 |
45 |
--------------------------------------------------------------------------------
/Circles/Views/Timeline/MessageAuthorHeader.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2020, 2021 Kombucha Digital Privacy Systems LLC
2 | //
3 | // MessageAuthorHeader.swift
4 | // Circles for iOS
5 | //
6 | // Created by Charles Wright on 11/3/20.
7 | //
8 |
9 | import SwiftUI
10 | import Matrix
11 |
12 | struct MessageAuthorHeader: View {
13 | @ObservedObject var user: Matrix.User
14 |
15 | var body: some View {
16 | HStack(alignment: .center) {
17 | UserAvatarView(user: user)
18 | .frame(width: 45, height: 45)
19 |
20 | Text(user.displayName ?? user.userId.username)
21 | .font(.headline)
22 | .fontWeight(.semibold)
23 | .lineLimit(1)
24 | //.padding(1)
25 | }
26 | .onAppear {
27 | if user.avatarUrl == nil || user.displayName == nil {
28 | user.refreshProfile()
29 | }
30 | if user.avatar == nil && user.avatarUrl != nil {
31 | user.fetchAvatarImage()
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Circles/Views/Timeline/MessageSheetType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MessageSheetType.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 7/13/21.
6 | //
7 |
8 | import Foundation
9 |
10 | enum MessageSheetType: String {
11 | case edit
12 | case reporting
13 | case liked
14 | }
15 |
16 | extension MessageSheetType: Identifiable {
17 | var id: String { rawValue }
18 | }
19 |
--------------------------------------------------------------------------------
/Circles/Views/Timeline/MessageView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MessageView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 4/13/23.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import Matrix
11 |
12 | protocol MessageView: View {
13 | var message: Matrix.Message { get }
14 | var isLocalEcho: Bool { get }
15 | var isThreaded: Bool { get }
16 |
17 | init(message: Matrix.Message, isLocalEcho: Bool, isThreaded: Bool)
18 | }
19 |
--------------------------------------------------------------------------------
/Circles/Views/Timeline/StateEventView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StateEventView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 4/11/23.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import Matrix
11 |
12 | struct StateEventView: View {
13 | var message: Matrix.Message
14 | var roomType: String = "room"
15 |
16 | var body: some View {
17 | VStack {
18 | let sender = message.sender.displayName ?? "\(message.sender.userId)"
19 | switch message.type {
20 | case M_ROOM_CREATE:
21 | Text("*\(roomType.capitalized) created by \(sender)*")
22 |
23 | case M_ROOM_AVATAR:
24 | Text("*\(sender) set a new cover image*")
25 |
26 | case M_ROOM_NAME:
27 | Text("*\(sender) set the \(roomType) name*")
28 |
29 | case M_ROOM_TOPIC:
30 | Text("*\(sender) set the \(roomType) topic*")
31 |
32 | case M_ROOM_MEMBER:
33 | if let content = message.content as? RoomMemberContent
34 | {
35 | switch content.membership {
36 | case .invite:
37 | Text("*\(sender) invited \(message.stateKey ?? "ERROR Unknown user")*")
38 | case .ban:
39 | Text("*\(sender) banned \(message.stateKey ?? "ERROR Unknown user")*")
40 | case .join:
41 | if message.sender.userId.description == message.stateKey {
42 | Text("*\(sender) joined*")
43 | } else if let otherUser = message.stateKey {
44 | Text("*\(sender) added \(otherUser)*")
45 | } else {
46 | Text("*\(sender) updated their public profile*")
47 | }
48 | case .knock:
49 | Text("*\(sender) knocked*")
50 | case .leave:
51 | if message.sender.userId.description == message.stateKey {
52 | Text("*\(sender) left*")
53 | } else {
54 | Text("*\(sender) kicked \(message.stateKey ?? "ERROR Unknown user")*")
55 | }
56 | }
57 | } else {
58 | Text("*\(sender) updated the \(roomType) state*")
59 | }
60 | case M_ROOM_ENCRYPTION:
61 | Text("*\(sender) set the room encryption parameters*")
62 |
63 | default:
64 | Text("*\(sender) updated the \(roomType) state (\(message.type))*")
65 | }
66 | }
67 | .font(.caption)
68 | .padding(2)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Circles/Views/Timeline/ThreadView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThreadView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 7/27/23.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct ThreadView: View {
12 | @ObservedObject var room: Matrix.Room
13 | var root: Matrix.Message
14 | var messages: [Matrix.Message]
15 |
16 | init(room: Matrix.Room, root: Matrix.Message) {
17 | self.room = room
18 | self.root = root
19 | // We need an ordered collection to use a ForEach in the View, so sort the Set into an Array
20 | if let thread: Set = room.threads[root.eventId] {
21 | self.messages = [root] + thread.sorted(by: {$0.timestamp < $1.timestamp} )
22 | } else {
23 | self.messages = [root]
24 | }
25 | }
26 |
27 | var body: some View {
28 | VStack {
29 | ScrollView {
30 | ForEach(messages) { message in
31 | V(message: message, isLocalEcho: false, isThreaded: true)
32 | }
33 |
34 | if DebugModel.shared.debugMode {
35 | Text("Thread Id: \(root.eventId)")
36 | if let thread = room.threads[root.eventId] {
37 | Text("root + \(thread.count) messages in the thread")
38 | } else {
39 | Text("No thread found")
40 | }
41 | let check = room.messages.filter({ $0.eventId == root.eventId || $0.threadId == root.eventId })
42 | Text("\(check.count) messages in the room that should have matched")
43 | }
44 | }
45 | }
46 | .navigationTitle(Text("Thread"))
47 | }
48 | }
49 |
50 | /*
51 | struct ThreadView_Previews: PreviewProvider {
52 | static var previews: some View {
53 | ThreadView()
54 | }
55 | }
56 | */
57 |
--------------------------------------------------------------------------------
/Circles/Views/Timeline/TimelineViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimelineViewModel.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 7/30/24.
6 | //
7 |
8 | import Foundation
9 | import Matrix
10 |
11 | public class TimelineViewModel: ObservableObject {
12 | @Published var scrollPosition: EventId?
13 | }
14 |
--------------------------------------------------------------------------------
/Circles/Views/Timeline/UserAvatarView.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2020, 2021 Kombucha Digital Privacy Systems LLC
2 | //
3 | // ProfileImageView.swift
4 | // Circles for iOS
5 | //
6 | // Created by Charles Wright on 11/3/20.
7 | //
8 |
9 | import SwiftUI
10 | import Matrix
11 |
12 | struct UserAvatarView: View {
13 | @ObservedObject var user: Matrix.User
14 | @Environment(\.colorScheme) var colorScheme
15 |
16 | var defaultImageColor: Color {
17 | colorScheme == .dark
18 | ? Color.white
19 | : Color.black
20 | }
21 |
22 | var body: some View {
23 | if let avatar = user.avatar {
24 | BasicImage(uiImage: avatar)
25 | .aspectRatio(contentMode: .fill)
26 | .clipShape(Circle())
27 | } else {
28 | ZStack {
29 | let color = Color.background.randomColor(from: user.userId.stringValue)
30 |
31 | Image("")
32 | .resizable()
33 | .background(color)
34 | .aspectRatio(contentMode: .fit)
35 | .clipShape(Circle())
36 |
37 | let userIdCharacter = user.userId.stringValue.dropFirst().first?.uppercased()
38 |
39 | Text(String(user.displayName?.first?.uppercased() ?? userIdCharacter ?? ""))
40 | .fontWeight(.bold)
41 | .font(.title3)
42 | .foregroundColor(.white)
43 | .shadow(color: .gray, radius: 1)
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Circles/Views/Timeline/UserNameView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserNameView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 10/15/23.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct UserNameView: View {
12 | @ObservedObject var user: Matrix.User
13 |
14 | var body: some View {
15 | Text(user.displayName ?? user.userId.username)
16 | .onAppear {
17 | user.refreshProfile()
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Circles/Views/UIA/BsspekeEnrollSaveForm.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BsspekeEnrollSaveForm.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 3/31/23.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import Matrix
11 |
12 | struct BsspekeEnrollSaveForm: View {
13 | var session: any UIASession
14 |
15 | var body: some View {
16 | VStack {
17 | Spacer()
18 | ProgressView {
19 | Text("Completing passphrase enrollment")
20 | }
21 | Spacer()
22 | }
23 | .onAppear {
24 | Task {
25 | try await session.doBSSpekeEnrollSaveStage()
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Circles/Views/UIA/EmailEnrollSubmitTokenForm.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmailEnrollSubmitTokenForm.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 3/31/23.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import Matrix
11 |
12 | struct EmailEnrollSubmitTokenForm: View {
13 | var session: any UIASession
14 | var secret: String
15 |
16 | @State var token = ""
17 |
18 | enum FocusField {
19 | case token
20 | }
21 | @FocusState var focus: FocusField?
22 |
23 | @State var showAlert = false
24 | let alertTitle = "Invalid code"
25 | let alertMessage = "Please double-check the code and enter it again, or request a new code."
26 |
27 | var tokenIsValid: Bool {
28 | token.count == 6 && Int(token) != nil
29 | }
30 |
31 | var body: some View {
32 | VStack {
33 | Spacer()
34 | VStack {
35 | Text("Enter the 6-digit code that you received in your email")
36 | }
37 | Spacer()
38 | VStack {
39 | TextField("123456", text: $token, prompt: Text("6-Digit Code"))
40 | .customEmailTextFieldStyle(contentType: .oneTimeCode, keyboardType: .numberPad)
41 | .textFieldStyle(RoundedBorderTextFieldStyle())
42 | .focused($focus, equals: .token)
43 | .frame(width: 300.0, height: 40.0)
44 | .onAppear {
45 | self.focus = .token
46 | }
47 |
48 | AsyncButton(action: {
49 | do {
50 | try await session.doEmailEnrollSubmitTokenStage(token: token, secret: secret)
51 | } catch {
52 | print("Email submit token stage failed")
53 | self.showAlert = true
54 | }
55 | }) {
56 | Text("Submit")
57 | }
58 | .buttonStyle(BigRoundedButtonStyle())
59 | .disabled(!tokenIsValid)
60 | .alert(isPresented: $showAlert) {
61 | Alert(title: Text(self.alertTitle),
62 | message: Text(self.alertMessage),
63 | dismissButton: .default(Text("OK"), action: { self.token = "" })
64 | )
65 | }
66 |
67 | AsyncButton(action: {
68 | try await session.redoEmailEnrollRequestTokenStage()
69 | }) {
70 | Text("Send a new code")
71 | .padding()
72 | }
73 | }
74 | Spacer()
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Circles/Views/UIA/EmailLoginSubmitTokenForm.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmailLoginSubmitTokenForm.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 3/25/24.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import Matrix
11 |
12 | struct EmailLoginSubmitTokenForm: View {
13 | var session: any UIASession
14 | var secret: String
15 |
16 | @State var token = ""
17 |
18 | enum FocusField {
19 | case token
20 | }
21 | @FocusState var focus: FocusField?
22 |
23 | @State var showAlert = false
24 | let alertTitle = "Invalid code"
25 | let alertMessage = "Please double-check the code and enter it again, or request a new code."
26 |
27 | var tokenIsValid: Bool {
28 | token.count == 6 && Int(token) != nil
29 | }
30 |
31 | var body: some View {
32 | VStack {
33 | Spacer()
34 | VStack {
35 | Text("Enter the 6-digit code that you received in your email")
36 | }
37 | Spacer()
38 | VStack {
39 | TextField("123456", text: $token, prompt: Text("6-Digit Code"))
40 | .customEmailTextFieldStyle(contentType: .oneTimeCode, keyboardType: .numberPad)
41 | .textFieldStyle(RoundedBorderTextFieldStyle())
42 | .focused($focus, equals: .token)
43 | .frame(width: 300.0, height: 40.0)
44 | .onAppear {
45 | self.focus = .token
46 | }
47 |
48 | AsyncButton(action: {
49 | do {
50 | try await session.doEmailLoginSubmitTokenStage(token: token, secret: secret)
51 | } catch {
52 | print("Email login submit token stage failed")
53 | self.showAlert = true
54 | }
55 | }) {
56 | Text("Submit")
57 | }
58 | .buttonStyle(BigRoundedButtonStyle())
59 | .disabled(!tokenIsValid)
60 | .alert(isPresented: $showAlert) {
61 | Alert(title: Text(self.alertTitle),
62 | message: Text(self.alertMessage),
63 | dismissButton: .default(Text("OK"), action: { self.token = "" })
64 | )
65 | }
66 |
67 | AsyncButton(action: {
68 | try await session.redoEmailLoginRequestTokenStage()
69 | }) {
70 | Text("Send a new code")
71 | .padding()
72 | }
73 | }
74 | Spacer()
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Circles/Views/UIA/FreeSubscriptionForm.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FreeSubscriptionForm.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 1/3/24.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct FreeSubscriptionForm: View {
12 | var session: any UIASession
13 |
14 | var body: some View {
15 | VStack {
16 | Spacer()
17 |
18 | if let signup = session as? SignupSession {
19 | ProgressView("Registering free subscription with the server...")
20 | .task {
21 | try? await signup.doFreeSubscriptionStage()
22 | }
23 | } else {
24 | Label("Error: Free subscriptions are only available at registration time", systemImage: SystemImages.exclamationmarkTriangle.rawValue)
25 | }
26 |
27 | Spacer()
28 | }
29 | }
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/Circles/Views/UIA/UiaOverlayView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UiaOverlayView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 7/10/23.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | // This type is necessary because we need a View that observes the Matrix Session,
12 | // in order to catch the change in its UIA session.
13 | // This view is used in the main ContentView in the app, to overlay on top of the
14 | // main tabbed interface for the app's normal operation.
15 | struct UiaOverlayView: View {
16 | @ObservedObject var circles: CirclesApplicationSession
17 | @ObservedObject var matrix: Matrix.Session
18 |
19 | var body: some View {
20 | if let uia = matrix.uiaSession,
21 | !uia.isFinished
22 | {
23 | //Color.gray.opacity(0.5)
24 | UiaView(session: circles, uia: uia)
25 | .frame(minWidth: 325, maxWidth: 500, maxHeight: 700, alignment: .center)
26 | .background(in: RoundedRectangle(cornerRadius: 10))
27 | }
28 | }
29 | }
30 |
31 | /*
32 | struct UiaOverlayView_Previews: PreviewProvider {
33 | static var previews: some View {
34 | UiaOverlayView()
35 | }
36 | }
37 | */
38 |
--------------------------------------------------------------------------------
/Circles/Views/UIA/UiaView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UiaView.swift
3 | // Circles
4 | //
5 | // Created by Charles Wright on 6/22/23.
6 | //
7 |
8 | import SwiftUI
9 | import Matrix
10 |
11 | struct UiaView: View {
12 | var session: CirclesApplicationSession
13 | //var matrix: Matrix.Session
14 | @ObservedObject var uia: UIAuthSession
15 |
16 | var body: some View {
17 | VStack {
18 | Text("Authentication Required")
19 | .font(
20 | CustomFonts.nunito24
21 | .bold()
22 | )
23 | .padding()
24 |
25 | Spacer()
26 |
27 | // cvw: Based on the SignupScreen
28 | switch uia.state {
29 | case .notConnected:
30 | AsyncButton(action: {
31 | try await uia.connect()
32 | }) {
33 | Text("Tap to Authenticate")
34 | }
35 |
36 | case .failed(_): // let error
37 | Text("Authentication failed")
38 |
39 | case .canceled:
40 | Text("Authentication canceled")
41 |
42 | case .connected(let uiaaState):
43 | ProgressView()
44 | .onAppear {
45 | // Choose a flow
46 | // FIXME: Just go with the first one for now
47 | if let flow = uiaaState.flows.first {
48 | _ = Task { await uia.selectFlow(flow: flow) }
49 | }
50 | }
51 |
52 | case .inProgress(let uiaaState, let stages):
53 | UiaInProgressView(session: uia, state: uiaaState, stages: stages)
54 |
55 | case .finished(_): // let data
56 | Text("Success!")
57 | .onAppear {
58 | _ = Task {
59 | try await session.cancelUIA()
60 | }
61 | }
62 | }
63 | }
64 | .safeAreaInset(edge: .bottom) {
65 | AsyncButton(role: .destructive, action: {
66 | try await session.cancelUIA()
67 | }) {
68 | Text("Cancel")
69 | .padding()
70 | }
71 | }
72 | .padding(.horizontal)
73 | }
74 | }
75 |
76 | /*
77 | struct UiaView_Previews: PreviewProvider {
78 | static var previews: some View {
79 | UiaView()
80 | }
81 | }
82 | */
83 |
--------------------------------------------------------------------------------
/Circles/Views/View+Extensions/ButtonStyle+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ButtonStyle+Extensions.swift
3 | // Circles
4 | //
5 | // Created by Dmytro Ryshchuk on 6/7/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct BigRoundedButtonStyle: ButtonStyle {
11 | var width: CGFloat = 300.0
12 | var height: CGFloat = 40.0
13 | var color: Color = .accentColor
14 | var borderWidth: CGFloat = 0
15 | var textColor: Color = .white
16 |
17 | func makeBody(configuration: Configuration) -> some View {
18 | configuration.label
19 | .padding()
20 | .frame(width: width, height: height)
21 | .overlay(
22 | RoundedRectangle(cornerRadius: 10)
23 | .stroke(Color.white, lineWidth: borderWidth)
24 | )
25 | .foregroundColor(textColor)
26 | .background(configuration.isPressed ? color.opacity(0.8) : color)
27 | .cornerRadius(10)
28 | .scaleEffect(configuration.isPressed ? 0.95 : 1.0)
29 | }
30 | }
31 |
32 | struct PillButtonStyle: ButtonStyle {
33 | func makeBody(configuration: Configuration) -> some View {
34 | configuration.label
35 | .padding(.vertical, 5)
36 | .padding(.horizontal, 10)
37 | .foregroundColor(.white)
38 | .background(configuration.isPressed ? Color.accentColor.opacity(0.8) : Color.accentColor)
39 | .cornerRadius(16)
40 | .scaleEffect(configuration.isPressed ? 0.95 : 1.0)
41 | }
42 | }
43 |
44 | struct ReactionsButtonStyle: ButtonStyle {
45 | var buttonColor: Color
46 |
47 | func makeBody(configuration: Configuration) -> some View {
48 | configuration.label
49 | .padding([.top, .bottom], 7)
50 | .padding([.leading, .trailing], 12)
51 | .overlay(
52 | RoundedRectangle(cornerRadius: 20)
53 | .stroke(buttonColor, lineWidth: 2)
54 | )
55 | .foregroundColor(.gray)
56 | .clipShape(RoundedRectangle(cornerRadius: 20))
57 | .scaleEffect(configuration.isPressed ? 0.65 : 1.0)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Circles/Views/View+Extensions/View+Extentions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // View+Extentions.swift
3 | // Circles
4 | //
5 | // Created by Dmytro Ryshchuk on 6/10/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct BasicImage: View {
11 | var uiImage: UIImage? = nil
12 | var systemName: String? = nil
13 | var name: String? = nil
14 | var aspectRatio: ContentMode = .fit
15 |
16 | var body: some View {
17 | if let uiImage {
18 | Image(uiImage: uiImage)
19 | .resizable()
20 | .aspectRatio(contentMode: aspectRatio)
21 | } else if let systemName {
22 | Image(systemName: systemName)
23 | .resizable()
24 | .aspectRatio(contentMode: aspectRatio)
25 | } else if let name {
26 | Image(name)
27 | .resizable()
28 | .aspectRatio(contentMode: aspectRatio)
29 | } else {
30 | Image(systemName: SystemImages.xmarkIcloudFill.rawValue)
31 | .resizable()
32 | .aspectRatio(contentMode: aspectRatio)
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Circles/fonts/Inter-VariableFont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/circles-project/circles-ios/5aec0b5cdb379fe13113ec55f4475a020b9c1834/Circles/fonts/Inter-VariableFont.ttf
--------------------------------------------------------------------------------
/Circles/fonts/Nunito-VariableFont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/circles-project/circles-ios/5aec0b5cdb379fe13113ec55f4475a020b9c1834/Circles/fonts/Nunito-VariableFont.ttf
--------------------------------------------------------------------------------
/FUTO Circles.storekit:
--------------------------------------------------------------------------------
1 | {
2 | "identifier" : "D07CD12F",
3 | "nonRenewingSubscriptions" : [
4 |
5 | ],
6 | "products" : [
7 |
8 | ],
9 | "settings" : {
10 | "_applicationInternalID" : "6451446720",
11 | "_developerTeamID" : "2W7AC6T8T5",
12 | "_failTransactionsEnabled" : false,
13 | "_lastSynchronizedDate" : 723742617.266168,
14 | "_locale" : "en_US",
15 | "_storefront" : "USA",
16 | "_storeKitErrors" : [
17 | {
18 | "current" : null,
19 | "enabled" : false,
20 | "name" : "Load Products"
21 | },
22 | {
23 | "current" : null,
24 | "enabled" : false,
25 | "name" : "Purchase"
26 | },
27 | {
28 | "current" : null,
29 | "enabled" : false,
30 | "name" : "Verification"
31 | },
32 | {
33 | "current" : null,
34 | "enabled" : false,
35 | "name" : "App Store Sync"
36 | },
37 | {
38 | "current" : null,
39 | "enabled" : false,
40 | "name" : "Subscription Status"
41 | },
42 | {
43 | "current" : null,
44 | "enabled" : false,
45 | "name" : "App Transaction"
46 | },
47 | {
48 | "current" : null,
49 | "enabled" : false,
50 | "name" : "Manage Subscriptions Sheet"
51 | },
52 | {
53 | "current" : null,
54 | "enabled" : false,
55 | "name" : "Refund Request Sheet"
56 | },
57 | {
58 | "current" : null,
59 | "enabled" : false,
60 | "name" : "Offer Code Redeem Sheet"
61 | }
62 | ]
63 | },
64 | "subscriptionGroups" : [
65 | {
66 | "id" : "21418018",
67 | "localizations" : [
68 |
69 | ],
70 | "name" : "Online Accounts",
71 | "subscriptions" : [
72 | {
73 | "adHocOffers" : [
74 |
75 | ],
76 | "codeOffers" : [
77 |
78 | ],
79 | "displayPrice" : "1.99",
80 | "familyShareable" : false,
81 | "groupNumber" : 1,
82 | "internalID" : "6473546488",
83 | "introductoryOffer" : null,
84 | "localizations" : [
85 | {
86 | "description" : "A Circles account with 10GB secure storage",
87 | "displayName" : "Individual - Monthly",
88 | "locale" : "en_US"
89 | }
90 | ],
91 | "productID" : "org.futo.circles.individual_monthly",
92 | "recurringSubscriptionPeriod" : "P1M",
93 | "referenceName" : "Individual Monthly",
94 | "subscriptionGroupID" : "21418018",
95 | "type" : "RecurringSubscription"
96 | }
97 | ]
98 | }
99 | ],
100 | "version" : {
101 | "major" : 3,
102 | "minor" : 0
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/NSE/Constants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Constants.swift
3 | // NSE
4 | //
5 | // Created by Charles Wright on 3/8/24.
6 | //
7 |
8 | import Foundation
9 |
10 | let CIRCLES_APP_GROUP_NAME = "group.2W7AC6T8T5.org.futo.circles"
11 |
12 | let APP_PREFIX = "org.futo"
13 | let ROOM_TYPE_CIRCLE = APP_PREFIX+".social.timeline"
14 | let ROOM_TYPE_GROUP = APP_PREFIX+".social.group"
15 | let ROOM_TYPE_PHOTOS = APP_PREFIX+".social.gallery"
16 | let ROOM_TYPE_PROFILE = "m.space"
17 | let ROOM_TYPE_SPACE = "m.space"
18 |
--------------------------------------------------------------------------------
/NSE/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSExtension
6 |
7 | NSExtensionPointIdentifier
8 | com.apple.usernotifications.service
9 | NSExtensionPrincipalClass
10 | $(PRODUCT_MODULE_NAME).NotificationService
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/NSE/NSE.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.application-groups
6 |
7 | group.RX3RM99NR6.org.futo.circles
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Circles
2 | Circles was an end-to-end encrypted social network app with the goal of enabling
3 | friends and families to securely share stories and photos while
4 | safeguarding security and privacy.
5 |
6 | Prototype mobile apps for [Android](https://gitlab.futo.org/circles/circles-android) and [iOS](https://github.com/cvwright/circles-ios) were developed at FUTO between 2022 and 2024.
7 | Unfortunately it became apparent that making this project into a successful commercial product would require a level of effort far beyond what was feasible for our small team.
8 | Active development wrapped up in late 2024, and the code is now available under a very liberal Open Source license to allow for new forks or derivative works.
9 |
10 | Circles is built on [Matrix](https://matrix.org/), and as such, it inherits many nice
11 | properties from Matrix, including:
12 | * Federation - Anyone can run their own server, and users on different servers can communicate with each other seamlessly.
13 | * Open APIs and data formats - Circles uses standard Matrix message types, and it works
14 | with any spec-compliant Matrix server.
15 | * Security - Circles offers the same security guarantees as Matrix, using the same
16 | E2E encryption code as in Element and other popular Matrix clients.
17 |
18 | At the same time, anyone hoping to revive or fork this project should be
19 | aware of some important limitations from Matrix:
20 | * The standard Matrix `/login` API exposes the user's password to their homeserver;
21 | this almost certainly gives the homeserver a huge head start on cracking the user's *other*
22 | password, which Matrix uses to protect all of the user's encrypted secrets on the server.
23 | This is insecure in pratice because most human users will not bother to remember two
24 | distinct passwords for the same account.
25 | (We developed a custom authorization framework and implemented the BS-SPEKE PAKE protocol
26 | to work around this for Circles, but it requires running a custom authorization service
27 | on the homeserver.)
28 | * Matrix does not cryptographically verify room membership.
29 | Instead, clients like Circles must trust the server to tell them which user accounts are
30 | in each room.
31 | Then when the client sends a message, it provides each of those accounts with the decryption key.
32 | A malicious server can lie to the client about who is in the room, causing it to send the
33 | key to an adversary.
34 | (We did some preliminary work to help with this one, via MSC3917, but the changes
35 | required are extensive, our time was limited, and unfortunately this does not seem to be
36 | a top priority for anyone else in the community.)
37 | * Getting good performance out of Matrix servers is something of a dark art, and scalability
38 | would have been a challenge.
39 | We expected that we would need to write our own server if we wanted to grow the product.
40 |
--------------------------------------------------------------------------------
/assets/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/circles-project/circles-ios/5aec0b5cdb379fe13113ec55f4475a020b9c1834/assets/.DS_Store
--------------------------------------------------------------------------------
/assets/images/circles-and-groups.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/circles-project/circles-ios/5aec0b5cdb379fe13113ec55f4475a020b9c1834/assets/images/circles-and-groups.jpeg
--------------------------------------------------------------------------------
/assets/images/circles-screenshots.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/circles-project/circles-ios/5aec0b5cdb379fe13113ec55f4475a020b9c1834/assets/images/circles-screenshots.jpeg
--------------------------------------------------------------------------------
/assets/images/groups-screenshots.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/circles-project/circles-ios/5aec0b5cdb379fe13113ec55f4475a020b9c1834/assets/images/groups-screenshots.jpeg
--------------------------------------------------------------------------------
/assets/images/kickstarter-logo-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/circles-project/circles-ios/5aec0b5cdb379fe13113ec55f4475a020b9c1834/assets/images/kickstarter-logo-green.png
--------------------------------------------------------------------------------
/assets/images/photogallery-screenshots.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/circles-project/circles-ios/5aec0b5cdb379fe13113ec55f4475a020b9c1834/assets/images/photogallery-screenshots.jpeg
--------------------------------------------------------------------------------