Merge branch 'dev' into standardised-strings

pull/1023/head
Ryan ZHAO 1 year ago
commit 63658f70d7

@ -30,7 +30,15 @@ You can then add the Session repo to sync with upstream changes:
git remote add upstream https://github.com/oxen-io/session-ios
```
## 2. Pods
## 2. Submodules
Session requires a number of submodules to build, these can be retrieved by navigating to the project directory and running:
```
git submodule update --init --recursive
```
## 3. Pods
To build and configure the libraries Session uses, just run:
@ -38,7 +46,7 @@ To build and configure the libraries Session uses, just run:
pod install
```
## 3. Xcode
## 4. Xcode
Open the `Session.xcworkspace` in Xcode.

@ -258,4 +258,4 @@ fi
echo "}" >>"$modmap"
# Output to XCode just so the output is good
echo_message "info: libSessionUtil Ready"
echo_message "info: libSessionUtil Ready"

@ -98,10 +98,7 @@
7B1B52E028580D51006069F2 /* EmojiSkinTonePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1B52DC28580D50006069F2 /* EmojiSkinTonePicker.swift */; };
7B1D74AA27BCC16E0030B423 /* NSENotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */; };
7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AF27C365960030B423 /* Timer+MainThread.swift */; };
7B2561C22978B307005C086C /* MediaInfoVC+MediaInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2561C12978B307005C086C /* MediaInfoVC+MediaInfoView.swift */; };
7B2561C429874851005C086C /* SessionCarouselView+Info.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2561C329874851005C086C /* SessionCarouselView+Info.swift */; };
7B3A392E2977791E002FE4AC /* MediaInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3A392D2977791E002FE4AC /* MediaInfoVC.swift */; };
7B3A3930297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3A392F297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift */; };
7B3A39322980D02B002FE4AC /* SessionCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3A39312980D02B002FE4AC /* SessionCarouselView.swift */; };
7B3A3934298882D6002FE4AC /* SessionCarouselViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3A3933298882D6002FE4AC /* SessionCarouselViewDelegate.swift */; };
7B46AAAF28766DF4001AF2DC /* AllMediaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */; };
@ -112,6 +109,7 @@
7B521E0A29BFF84400C3C36A /* GroupLeavingJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B521E0929BFF84400C3C36A /* GroupLeavingJob.swift */; };
7B5233C42900E90F00F8F375 /* SessionLabelCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5233C32900E90F00F8F375 /* SessionLabelCarouselView.swift */; };
7B5233C6290636D700F8F375 /* _018_DisappearingMessagesConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5233C5290636D700F8F375 /* _018_DisappearingMessagesConfiguration.swift */; };
7B5802992AAEF1B50050EEB1 /* OpenGroupInvitationView_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5802982AAEF1B50050EEB1 /* OpenGroupInvitationView_SwiftUI.swift */; };
7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */; };
7B7037452834BCC0000DCF35 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037442834BCC0000DCF35 /* ReactionView.swift */; };
7B71A98F2925E2A600E54854 /* SessionFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B71A98E2925E2A600E54854 /* SessionFooterView.swift */; };
@ -124,10 +122,13 @@
7B81682828B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */; };
7B81682A28B6F1420069F315 /* ReactionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682928B6F1420069F315 /* ReactionResponse.swift */; };
7B81682C28B72F480069F315 /* PendingChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682B28B72F480069F315 /* PendingChange.swift */; };
7B81FB5A2AB01B17002FB267 /* LoadingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81FB582AB01AA8002FB267 /* LoadingIndicatorView.swift */; };
7B8914772A7CAAE200A4C627 /* SessionHostingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8914762A7CAAE200A4C627 /* SessionHostingViewController.swift */; };
7B8C44C528B49DDA00FBE25F /* NewConversationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8C44C428B49DDA00FBE25F /* NewConversationVC.swift */; };
7B8D5FC428332600008324D9 /* VisibleMessage+Reaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */; };
7B93D07127CF194000811CB6 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06F27CF194000811CB6 /* MessageRequestResponse.swift */; };
7B93D07727CF1A8A00811CB6 /* MockDataGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D07527CF1A8900811CB6 /* MockDataGenerator.swift */; };
7B9C1C7D2AF206CD003FEAE9 /* AttributedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9C1C7C2AF206CD003FEAE9 /* AttributedText.swift */; };
7B9F71C928470667006DFE7B /* ReactionListSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9F71C828470667006DFE7B /* ReactionListSheet.swift */; };
7B9F71D02852EEE2006DFE7B /* Emoji+Category.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9F71CB2852EEE2006DFE7B /* Emoji+Category.swift */; };
7B9F71D12852EEE2006DFE7B /* EmojiWithSkinTones+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9F71CC2852EEE2006DFE7B /* EmojiWithSkinTones+String.swift */; };
@ -136,7 +137,10 @@
7B9F71D42852EEE2006DFE7B /* Emoji+Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9F71CF2852EEE2006DFE7B /* Emoji+Name.swift */; };
7B9F71D72853100A006DFE7B /* Emoji+Available.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9F71D528531009006DFE7B /* Emoji+Available.swift */; };
7B9F71D82853100A006DFE7B /* EmojiWithSkinTones.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9F71D628531009006DFE7B /* EmojiWithSkinTones.swift */; };
7BA1E0E82A8087DB00123D0D /* SwiftUI+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA1E0E72A8087DB00123D0D /* SwiftUI+Utilities.swift */; };
7BA37AF92AEB365C002438F8 /* DocumentView_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA37AF82AEB365C002438F8 /* DocumentView_SwiftUI.swift */; };
7BA37AFB2AEB64CA002438F8 /* DisappearingMessageTimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA37AFA2AEB64CA002438F8 /* DisappearingMessageTimerView.swift */; };
7BA37AFD2AEF7C3D002438F8 /* VoiceMessageView_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA37AFC2AEF7C3D002438F8 /* VoiceMessageView_SwiftUI.swift */; };
7BA68909272A27BE00EFC32F /* SessionCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA68908272A27BE00EFC32F /* SessionCall.swift */; };
7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA6890C27325CCC00EFC32F /* SessionCallManager+CXCallController.swift */; };
7BA6890F27325CE300EFC32F /* SessionCallManager+CXProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA6890E27325CE300EFC32F /* SessionCallManager+CXProvider.swift */; };
@ -148,6 +152,7 @@
7BAF54D327ACCF01003D12F8 /* ShareAppExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54D127ACCF01003D12F8 /* ShareAppExtensionContext.swift */; };
7BAF54D427ACCF01003D12F8 /* SAEScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54D227ACCF01003D12F8 /* SAEScreenLockViewController.swift */; };
7BAFA1192A39669400B76CB9 /* BezierPathView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAFA1182A39669400B76CB9 /* BezierPathView.swift */; };
7BAFA75A2AAEA281001DA43E /* LinkPreviewView_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAFA7592AAEA281001DA43E /* LinkPreviewView_SwiftUI.swift */; };
7BB92B3F28C825FD0082762F /* NewConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BB92B3E28C825FD0082762F /* NewConversationViewModel.swift */; };
7BBBDC44286EAD2D00747E59 /* TappableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBBDC43286EAD2D00747E59 /* TappableLabel.swift */; };
7BBBDC462875600700747E59 /* DocumentTitleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBBDC452875600700747E59 /* DocumentTitleViewController.swift */; };
@ -156,7 +161,10 @@
7BC707F227290ACB002817AD /* SessionCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC707F127290ACB002817AD /* SessionCallManager.swift */; };
7BCD116C27016062006330F1 /* WebRTCSession+DataChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BCD116B27016062006330F1 /* WebRTCSession+DataChannel.swift */; };
7BD477A827EC39F5004E2822 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD477A727EC39F5004E2822 /* Atomic.swift */; };
7BD687D12A5D0D1200D8E455 /* MessageInfoScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD687D02A5D0D1200D8E455 /* MessageInfoScreen.swift */; };
7BD976972A776C76001B466F /* SessionCarouselView+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BE2701D2A64C11500CEB71A /* SessionCarouselView+SwiftUI.swift */; };
7BDCFC08242186E700641C39 /* NotificationServiceExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */; };
7BF8D1FB2A70AF57005F1D6E /* SwiftUI+Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BF8D1FA2A70AF57005F1D6E /* SwiftUI+Theme.swift */; };
7BFA8AE32831D0D4001876F3 /* ContextMenuVC+EmojiReactsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFA8AE22831D0D4001876F3 /* ContextMenuVC+EmojiReactsView.swift */; };
7BFD1A8A2745C4F000FB91B9 /* Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFD1A892745C4F000FB91B9 /* Permissions.swift */; };
7BFD1A8C2747150E00FB91B9 /* TurnServerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFD1A8B2747150E00FB91B9 /* TurnServerInfo.swift */; };
@ -164,6 +172,7 @@
9422EE2B2B8C3A97004C740D /* String+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9422EE2A2B8C3A97004C740D /* String+Utilities.swift */; };
943C6D822B75E061004ACE64 /* Message+DisappearingMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943C6D812B75E061004ACE64 /* Message+DisappearingMessages.swift */; };
943C6D842B86B5F1004ACE64 /* Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943C6D832B86B5F1004ACE64 /* Localization.swift */; };
94B3DC172AF8592200C88531 /* QuoteView_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */; };
9593A1E796C9E6BE2352EA6F /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F8B0BA5257C58DC6FF797278 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionSnodeKit.framework */; };
99978E3F7A80275823CA9014 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29E827FDF6C1032BB985740C /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework */; };
A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */; };
@ -821,7 +830,7 @@
FDC498BB2AC1606C00EDD897 /* AppNotificationUserInfoKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC498BA2AC1606C00EDD897 /* AppNotificationUserInfoKey.swift */; };
FDC498BE2AC1732E00EDD897 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = FD9401A32ABD04AC003A4834 /* Localizable.strings */; };
FDC498C22AC17BFC00EDD897 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = FD9401A32ABD04AC003A4834 /* Localizable.strings */; };
FDC6D6F32860607300B04575 /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7542807C4BB004C14C5 /* Environment.swift */; };
FDC6D6F32860607300B04575 /* SessionEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7542807C4BB004C14C5 /* SessionEnvironment.swift */; };
FDC6D7602862B3F600B04575 /* Dependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC6D75F2862B3F600B04575 /* Dependencies.swift */; };
FDCD2E032A41294E00964D6A /* LegacyGroupOnlyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCD2E022A41294E00964D6A /* LegacyGroupOnlyRequest.swift */; };
FDCDB8DE2810F73B00352A0C /* Differentiable+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCDB8DD2810F73B00352A0C /* Differentiable+Utilities.swift */; };
@ -1219,7 +1228,6 @@
70377AAA1918450100CAF501 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; };
76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = System/Library/Frameworks/MediaPlayer.framework; sourceTree = SDKROOT; };
7B02DF432A16F47B00ADCFD2 /* EmojiGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiGenerator.swift; sourceTree = "<group>"; };
7B02DF442A16F47B00ADCFD2 /* build_libSession_util.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = build_libSession_util.sh; sourceTree = "<group>"; };
7B0EFDED274F598600FFAAE7 /* TimestampUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimestampUtils.swift; sourceTree = "<group>"; };
7B0EFDEF275084AA00FFAAE7 /* CallMessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMessageCell.swift; sourceTree = "<group>"; };
7B0EFDF3275490EA00FFAAE7 /* ringing.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = ringing.mp3; sourceTree = "<group>"; };
@ -1232,10 +1240,7 @@
7B1B52DC28580D50006069F2 /* EmojiSkinTonePicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiSkinTonePicker.swift; sourceTree = "<group>"; };
7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSENotificationPresenter.swift; sourceTree = "<group>"; };
7B1D74AF27C365960030B423 /* Timer+MainThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timer+MainThread.swift"; sourceTree = "<group>"; };
7B2561C12978B307005C086C /* MediaInfoVC+MediaInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MediaInfoVC+MediaInfoView.swift"; sourceTree = "<group>"; };
7B2561C329874851005C086C /* SessionCarouselView+Info.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCarouselView+Info.swift"; sourceTree = "<group>"; };
7B3A392D2977791E002FE4AC /* MediaInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaInfoVC.swift; sourceTree = "<group>"; };
7B3A392F297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MediaInfoVC+MediaPreviewView.swift"; sourceTree = "<group>"; };
7B3A39312980D02B002FE4AC /* SessionCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCarouselView.swift; sourceTree = "<group>"; };
7B3A3933298882D6002FE4AC /* SessionCarouselViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCarouselViewDelegate.swift; sourceTree = "<group>"; };
7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllMediaViewController.swift; sourceTree = "<group>"; };
@ -1246,6 +1251,7 @@
7B521E0929BFF84400C3C36A /* GroupLeavingJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupLeavingJob.swift; sourceTree = "<group>"; };
7B5233C32900E90F00F8F375 /* SessionLabelCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionLabelCarouselView.swift; sourceTree = "<group>"; };
7B5233C5290636D700F8F375 /* _018_DisappearingMessagesConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _018_DisappearingMessagesConfiguration.swift; sourceTree = "<group>"; };
7B5802982AAEF1B50050EEB1 /* OpenGroupInvitationView_SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupInvitationView_SwiftUI.swift; sourceTree = "<group>"; };
7B7037422834B81F000DCF35 /* ReactionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionContainerView.swift; sourceTree = "<group>"; };
7B7037442834BCC0000DCF35 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = "<group>"; };
7B71A98E2925E2A600E54854 /* SessionFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionFooterView.swift; sourceTree = "<group>"; };
@ -1258,10 +1264,13 @@
7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _007_HomeQueryOptimisationIndexes.swift; sourceTree = "<group>"; };
7B81682928B6F1420069F315 /* ReactionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionResponse.swift; sourceTree = "<group>"; };
7B81682B28B72F480069F315 /* PendingChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingChange.swift; sourceTree = "<group>"; };
7B81FB582AB01AA8002FB267 /* LoadingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingIndicatorView.swift; sourceTree = "<group>"; };
7B8914762A7CAAE200A4C627 /* SessionHostingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionHostingViewController.swift; sourceTree = "<group>"; };
7B8C44C428B49DDA00FBE25F /* NewConversationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewConversationVC.swift; sourceTree = "<group>"; };
7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleMessage+Reaction.swift"; sourceTree = "<group>"; };
7B93D06F27CF194000811CB6 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = "<group>"; };
7B93D07527CF1A8900811CB6 /* MockDataGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockDataGenerator.swift; sourceTree = "<group>"; };
7B9C1C7C2AF206CD003FEAE9 /* AttributedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedText.swift; sourceTree = "<group>"; };
7B9F71C828470667006DFE7B /* ReactionListSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionListSheet.swift; sourceTree = "<group>"; };
7B9F71CB2852EEE2006DFE7B /* Emoji+Category.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Emoji+Category.swift"; sourceTree = "<group>"; };
7B9F71CC2852EEE2006DFE7B /* EmojiWithSkinTones+String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EmojiWithSkinTones+String.swift"; sourceTree = "<group>"; };
@ -1270,7 +1279,10 @@
7B9F71CF2852EEE2006DFE7B /* Emoji+Name.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Emoji+Name.swift"; sourceTree = "<group>"; };
7B9F71D528531009006DFE7B /* Emoji+Available.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Emoji+Available.swift"; sourceTree = "<group>"; };
7B9F71D628531009006DFE7B /* EmojiWithSkinTones.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiWithSkinTones.swift; sourceTree = "<group>"; };
7BA1E0E72A8087DB00123D0D /* SwiftUI+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftUI+Utilities.swift"; sourceTree = "<group>"; };
7BA37AF82AEB365C002438F8 /* DocumentView_SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentView_SwiftUI.swift; sourceTree = "<group>"; };
7BA37AFA2AEB64CA002438F8 /* DisappearingMessageTimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisappearingMessageTimerView.swift; sourceTree = "<group>"; };
7BA37AFC2AEF7C3D002438F8 /* VoiceMessageView_SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageView_SwiftUI.swift; sourceTree = "<group>"; };
7BA68908272A27BE00EFC32F /* SessionCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCall.swift; sourceTree = "<group>"; };
7BA6890C27325CCC00EFC32F /* SessionCallManager+CXCallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCallManager+CXCallController.swift"; sourceTree = "<group>"; };
7BA6890E27325CE300EFC32F /* SessionCallManager+CXProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCallManager+CXProvider.swift"; sourceTree = "<group>"; };
@ -1283,6 +1295,7 @@
7BAF54D227ACCF01003D12F8 /* SAEScreenLockViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SAEScreenLockViewController.swift; sourceTree = "<group>"; };
7BAF54D527ACD0E2003D12F8 /* ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = "<group>"; };
7BAFA1182A39669400B76CB9 /* BezierPathView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BezierPathView.swift; sourceTree = "<group>"; };
7BAFA7592AAEA281001DA43E /* LinkPreviewView_SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewView_SwiftUI.swift; sourceTree = "<group>"; };
7BB92B3E28C825FD0082762F /* NewConversationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewConversationViewModel.swift; sourceTree = "<group>"; };
7BBBDC43286EAD2D00747E59 /* TappableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TappableLabel.swift; sourceTree = "<group>"; };
7BBBDC452875600700747E59 /* DocumentTitleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentTitleViewController.swift; sourceTree = "<group>"; };
@ -1292,8 +1305,12 @@
7BC707F127290ACB002817AD /* SessionCallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCallManager.swift; sourceTree = "<group>"; };
7BCD116B27016062006330F1 /* WebRTCSession+DataChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebRTCSession+DataChannel.swift"; sourceTree = "<group>"; };
7BD477A727EC39F5004E2822 /* Atomic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = "<group>"; };
7BD687D02A5D0D1200D8E455 /* MessageInfoScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageInfoScreen.swift; sourceTree = "<group>"; };
7BD687D22A5D283200D8E455 /* build_libSession_util.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = build_libSession_util.sh; sourceTree = "<group>"; };
7BDCFC0424206E7300641C39 /* SessionNotificationServiceExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SessionNotificationServiceExtension.entitlements; sourceTree = "<group>"; };
7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtensionContext.swift; sourceTree = "<group>"; };
7BE2701D2A64C11500CEB71A /* SessionCarouselView+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCarouselView+SwiftUI.swift"; sourceTree = "<group>"; };
7BF8D1FA2A70AF57005F1D6E /* SwiftUI+Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftUI+Theme.swift"; sourceTree = "<group>"; };
7BFA8AE22831D0D4001876F3 /* ContextMenuVC+EmojiReactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContextMenuVC+EmojiReactsView.swift"; sourceTree = "<group>"; };
7BFD1A892745C4F000FB91B9 /* Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Permissions.swift; sourceTree = "<group>"; };
7BFD1A8B2747150E00FB91B9 /* TurnServerInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TurnServerInfo.swift; sourceTree = "<group>"; };
@ -1308,6 +1325,7 @@
9422EE2A2B8C3A97004C740D /* String+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Utilities.swift"; sourceTree = "<group>"; };
943C6D812B75E061004ACE64 /* Message+DisappearingMessages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+DisappearingMessages.swift"; sourceTree = "<group>"; };
943C6D832B86B5F1004ACE64 /* Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Localization.swift; sourceTree = "<group>"; };
94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteView_SwiftUI.swift; sourceTree = "<group>"; };
A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
A163E8AA16F3F6A90094D68B /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
A1C32D4D17A0652C000A904E /* AddressBook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AddressBook.framework; path = System/Library/Frameworks/AddressBook.framework; sourceTree = SDKROOT; };
@ -2018,7 +2036,7 @@
FDF0B74A28061F7A004C14C5 /* InteractionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionAttachment.swift; sourceTree = "<group>"; };
FDF0B74E28079E5E004C14C5 /* SendReadReceiptsJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendReadReceiptsJob.swift; sourceTree = "<group>"; };
FDF0B7502807BA56004C14C5 /* NotificationsProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsProtocol.swift; sourceTree = "<group>"; };
FDF0B7542807C4BB004C14C5 /* Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = "<group>"; };
FDF0B7542807C4BB004C14C5 /* SessionEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionEnvironment.swift; sourceTree = "<group>"; };
FDF0B7572807F368004C14C5 /* MessageReceiverError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReceiverError.swift; sourceTree = "<group>"; };
FDF0B7592807F3A3004C14C5 /* MessageSenderError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSenderError.swift; sourceTree = "<group>"; };
FDF0B75B2807F41D004C14C5 /* MessageSender+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MessageSender+Convenience.swift"; sourceTree = "<group>"; };
@ -2128,6 +2146,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
AA3B45F13C99CAEE72D01DAA /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
C331FF182558F9D300070591 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -2178,13 +2203,6 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
C8BB5A4618641C387640AE22 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
D221A086169C9E5E00537ABF /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -2437,6 +2455,18 @@
path = "Message Requests";
sourceTree = "<group>";
};
7B9C1C7B2AF1F236003FEAE9 /* SwiftUI */ = {
isa = PBXGroup;
children = (
7BA37AFC2AEF7C3D002438F8 /* VoiceMessageView_SwiftUI.swift */,
7BA37AF82AEB365C002438F8 /* DocumentView_SwiftUI.swift */,
7BAFA7592AAEA281001DA43E /* LinkPreviewView_SwiftUI.swift */,
7B5802982AAEF1B50050EEB1 /* OpenGroupInvitationView_SwiftUI.swift */,
94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */,
);
path = SwiftUI;
sourceTree = "<group>";
};
7B9F71CA2852EEE2006DFE7B /* Emoji */ = {
isa = PBXGroup;
children = (
@ -2495,6 +2525,7 @@
B8041A7325C8F758003C2166 /* Content Views */ = {
isa = PBXGroup;
children = (
7B9C1C7B2AF1F236003FEAE9 /* SwiftUI */,
34A8B3502190A40E00218A25 /* MediaAlbumView.swift */,
3488F9352191CC4000E524CC /* MediaView.swift */,
B8041A9425C8FA1D003C2166 /* MediaLoaderView.swift */,
@ -2735,10 +2766,13 @@
FD52090828B59411006098F6 /* ScreenLockUI.swift */,
FD37EA0828AA2D27003AE748 /* SessionTableViewModel.swift */,
FD37EA0628AA2CCA003AE748 /* SessionTableViewController.swift */,
7BE2701D2A64C11500CEB71A /* SessionCarouselView+SwiftUI.swift */,
7B4EF2592934743000CB351D /* SessionTableViewTitleView.swift */,
7B3A39312980D02B002FE4AC /* SessionCarouselView.swift */,
7B2561C329874851005C086C /* SessionCarouselView+Info.swift */,
7B3A3933298882D6002FE4AC /* SessionCarouselViewDelegate.swift */,
7B8914762A7CAAE200A4C627 /* SessionHostingViewController.swift */,
7B81FB582AB01AA8002FB267 /* LoadingIndicatorView.swift */,
);
path = Shared;
sourceTree = "<group>";
@ -2973,6 +3007,7 @@
C33100272559000A00070591 /* UIView+Utilities.swift */,
FD71161F28D97ABC00B47552 /* UIImage+Tinting.swift */,
FDBB25E62988BBBD00F1508E /* UIContextualAction+Theming.swift */,
7BA1E0E72A8087DB00123D0D /* SwiftUI+Utilities.swift */,
9422EE2A2B8C3A97004C740D /* String+Utilities.swift */,
);
path = Utilities;
@ -2997,6 +3032,7 @@
C38EF2A4255B6D93007E1867 /* ProfilePictureView.swift */,
FD71165A28E6DDBC00B47552 /* StyledNavigationController.swift */,
FD0B77AF29B69A65009169BA /* TopBannerController.swift */,
7B9C1C7C2AF206CD003FEAE9 /* AttributedText.swift */,
);
path = Components;
sourceTree = "<group>";
@ -3143,9 +3179,7 @@
4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */,
4C4AE69F224AF21900D4AF6F /* SendMediaNavigationController.swift */,
7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */,
7B3A392D2977791E002FE4AC /* MediaInfoVC.swift */,
7B3A392F297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift */,
7B2561C12978B307005C086C /* MediaInfoVC+MediaInfoView.swift */,
7BD687D02A5D0D1200D8E455 /* MessageInfoScreen.swift */,
);
path = "Media Viewing & Editing";
sourceTree = "<group>";
@ -3282,7 +3316,7 @@
FD23CE232A675C440000B97C /* Crypto+SessionMessagingKit.swift */,
FD859EF127BF6BA200510D0C /* Data+Utilities.swift */,
C38EF309255B6DBE007E1867 /* DeviceSleepManager.swift */,
FDF0B7542807C4BB004C14C5 /* Environment.swift */,
FDF0B7542807C4BB004C14C5 /* SessionEnvironment.swift */,
C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */,
C3A71D0A2558989C0043A11F /* MessageWrapper.swift */,
C38EF2F5255B6DBC007E1867 /* OWSAudioPlayer.h */,
@ -3828,6 +3862,7 @@
FD37E9D228A1FCDB003AE748 /* Theme+OceanDark.swift */,
FD37E9D428A1FCE8003AE748 /* Theme+OceanLight.swift */,
FD37EA0028A60473003AE748 /* UIKit+Theme.swift */,
7BF8D1FA2A70AF57005F1D6E /* SwiftUI+Theme.swift */,
);
path = Themes;
sourceTree = "<group>";
@ -4378,7 +4413,7 @@
FDE7214E287E50D50093DF33 /* Scripts */ = {
isa = PBXGroup;
children = (
7B02DF442A16F47B00ADCFD2 /* build_libSession_util.sh */,
7BD687D22A5D283200D8E455 /* build_libSession_util.sh */,
7B02DF432A16F47B00ADCFD2 /* EmojiGenerator.swift */,
FDE7214F287E50D50093DF33 /* ProtoWrappers.py */,
FDCCC6E82ABA7402002BBEF5 /* EmojiGenerator.swift */,
@ -4798,7 +4833,7 @@
buildConfigurationList = FD9BDDFC2A5D2294005F1EBC /* Build configuration list for PBXNativeTarget "SessionUtil" */;
buildPhases = (
FD9BDDFF2A5D229B005F1EBC /* Build libSessionUtil if Needed */,
C8BB5A4618641C387640AE22 /* Frameworks */,
AA3B45F13C99CAEE72D01DAA /* Frameworks */,
);
buildRules = (
);
@ -5640,13 +5675,16 @@
C331FFE32558FB0000070591 /* TabBar.swift in Sources */,
FD37E9D528A1FCE8003AE748 /* Theme+OceanLight.swift in Sources */,
FDF848F129406A30007DCAE5 /* Format.swift in Sources */,
7BA1E0E82A8087DB00123D0D /* SwiftUI+Utilities.swift in Sources */,
FD37E9C828A1D73F003AE748 /* Theme+Colors.swift in Sources */,
9422EE2B2B8C3A97004C740D /* String+Utilities.swift in Sources */,
FD37EA0128A60473003AE748 /* UIKit+Theme.swift in Sources */,
FD37E9CF28A1EB1B003AE748 /* Theme.swift in Sources */,
C331FFB92558FA8D00070591 /* UIView+Constraints.swift in Sources */,
7B9C1C7D2AF206CD003FEAE9 /* AttributedText.swift in Sources */,
FD0B77B029B69A65009169BA /* TopBannerController.swift in Sources */,
FD37E9F628A5F106003AE748 /* Configuration.swift in Sources */,
7BF8D1FB2A70AF57005F1D6E /* SwiftUI+Theme.swift in Sources */,
FDBB25E72988BBBE00F1508E /* UIContextualAction+Theming.swift in Sources */,
C331FFE02558FB0000070591 /* SearchBar.swift in Sources */,
FD16AB5B2A1DD7CA0083D849 /* PlaceholderIcon.swift in Sources */,
@ -6019,8 +6057,9 @@
FD17D79927F40AB800122BE0 /* _003_YDBToGRDBMigration.swift in Sources */,
FDF0B7512807BA56004C14C5 /* NotificationsProtocol.swift in Sources */,
B8DE1FB426C22F2F0079C9CE /* WebRTCSession.swift in Sources */,
FDC6D6F32860607300B04575 /* SessionEnvironment.swift in Sources */,
FDC13D5A2A1721C5007267C7 /* LegacyNotifyRequest.swift in Sources */,
FDC6D6F32860607300B04575 /* Environment.swift in Sources */,
FDC6D6F32860607300B04575 /* SessionEnvironment.swift in Sources */,
C3A3A171256E1D25004D228D /* SSKReachabilityManager.swift in Sources */,
FD245C59285065FC00B966DD /* ControlMessage.swift in Sources */,
FD6E4C8A2A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift in Sources */,
@ -6167,7 +6206,6 @@
7BAF54D027ACCEEC003D12F8 /* EmptySearchResultCell.swift in Sources */,
B8783E9E23EB948D00404FB8 /* UILabel+Interaction.swift in Sources */,
FD37E9D928A230F2003AE748 /* TraitObservingWindow.swift in Sources */,
7B3A3930297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift in Sources */,
B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */,
FD848B8F283EF2A8000E298B /* UIScrollView+Utilities.swift in Sources */,
B879D449247E1BE300DB3608 /* PathVC.swift in Sources */,
@ -6204,6 +6242,7 @@
34A8B3512190A40E00218A25 /* MediaAlbumView.swift in Sources */,
FD09C5E828264937000CE219 /* MediaDetailViewController.swift in Sources */,
3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */,
7BA37AFD2AEF7C3D002438F8 /* VoiceMessageView_SwiftUI.swift in Sources */,
7B8C44C528B49DDA00FBE25F /* NewConversationVC.swift in Sources */,
7B1B52E028580D51006069F2 /* EmojiSkinTonePicker.swift in Sources */,
B849789625D4A2F500D0D0B3 /* LinkPreviewView.swift in Sources */,
@ -6214,10 +6253,10 @@
C3548F0624456447009433A8 /* PNModeVC.swift in Sources */,
FD71164828E2CE8700B47552 /* SessionCell+AccessoryView.swift in Sources */,
B80A579F23DFF1F300876683 /* NewClosedGroupVC.swift in Sources */,
7B3A392E2977791E002FE4AC /* MediaInfoVC.swift in Sources */,
7BA68909272A27BE00EFC32F /* SessionCall.swift in Sources */,
B835247925C38D880089A44F /* MessageCell.swift in Sources */,
B86BD08623399CEF000F5AE3 /* SeedModal.swift in Sources */,
7BA37AF92AEB365C002438F8 /* DocumentView_SwiftUI.swift in Sources */,
FDB7400D28EBEC240094D718 /* DateHeaderCell.swift in Sources */,
B8D0A26925E4A2C200C1835E /* Onboarding.swift in Sources */,
34D1F0521F7E8EA30066283D /* GiphyDownloader.swift in Sources */,
@ -6242,6 +6281,7 @@
7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */,
7BBBDC462875600700747E59 /* DocumentTitleViewController.swift in Sources */,
FD71163F28E2C82C00B47552 /* SessionHeaderView.swift in Sources */,
7B8914772A7CAAE200A4C627 /* SessionHostingViewController.swift in Sources */,
B877E24226CA12910007970A /* CallVC.swift in Sources */,
FDC498B92AC15FE300EDD897 /* AppNotificationAction.swift in Sources */,
7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */,
@ -6252,6 +6292,7 @@
B88FA7F2260C3EB10049422F /* OpenGroupSuggestionGrid.swift in Sources */,
FD71160428C95B5600B47552 /* PhotoCollectionPickerViewModel.swift in Sources */,
FD37EA1928AC5CCA003AE748 /* NotificationSoundViewModel.swift in Sources */,
7BAFA75A2AAEA281001DA43E /* LinkPreviewView_SwiftUI.swift in Sources */,
FD71163E28E2C82900B47552 /* SessionCell.swift in Sources */,
4CA485BB2232339F004B9E7D /* PhotoCaptureViewController.swift in Sources */,
C328254925CA60E60062D0A7 /* ContextMenuVC+Action.swift in Sources */,
@ -6266,6 +6307,7 @@
FD37EA1728AC5605003AE748 /* NotificationContentViewModel.swift in Sources */,
B886B4A72398B23E00211ABE /* QRCodeVC.swift in Sources */,
C331FFF42558FF0300070591 /* PNOptionView.swift in Sources */,
94B3DC172AF8592200C88531 /* QuoteView_SwiftUI.swift in Sources */,
7BB92B3F28C825FD0082762F /* NewConversationViewModel.swift in Sources */,
4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */,
B82149C125D605C6009C0F2A /* InfoBanner.swift in Sources */,
@ -6295,8 +6337,10 @@
45C0DC1B1E68FE9000E04C47 /* UIApplication+OWS.swift in Sources */,
FDF222072818CECF000A4995 /* ConversationViewModel.swift in Sources */,
4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */,
7BD976972A776C76001B466F /* SessionCarouselView+SwiftUI.swift in Sources */,
B8041A9525C8FA1D003C2166 /* MediaLoaderView.swift in Sources */,
45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */,
7B81FB5A2AB01B17002FB267 /* LoadingIndicatorView.swift in Sources */,
7B9F71D42852EEE2006DFE7B /* Emoji+Name.swift in Sources */,
4CA46F4C219CCC630038ABDE /* CaptionView.swift in Sources */,
C328253025CA55370062D0A7 /* ContextMenuWindow.swift in Sources */,
@ -6327,11 +6371,11 @@
FD12A83F2AD63BDF00EEBA0D /* Navigatable.swift in Sources */,
7B9F71D22852EEE2006DFE7B /* Emoji+SkinTones.swift in Sources */,
7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */,
7B2561C22978B307005C086C /* MediaInfoVC+MediaInfoView.swift in Sources */,
B8569AE325CBB19A00DBA3DB /* DocumentView.swift in Sources */,
FDF848F529413EEC007DCAE5 /* SessionCell+Styling.swift in Sources */,
7BFD1A8A2745C4F000FB91B9 /* Permissions.swift in Sources */,
B85357BF23A1AE0800AAF6CD /* SeedReminderView.swift in Sources */,
7B5802992AAEF1B50050EEB1 /* OpenGroupInvitationView_SwiftUI.swift in Sources */,
FD716E6C28505E1C00C96BF4 /* MessageRequestsViewModel.swift in Sources */,
C35E8AAE2485E51D00ACB629 /* IP2Country.swift in Sources */,
B835249B25C3AB650089A44F /* VisibleMessageCell.swift in Sources */,
@ -6356,6 +6400,7 @@
FD37E9CC28A1E578003AE748 /* AppearanceViewController.swift in Sources */,
B8EB20F02640F7F000773E52 /* OpenGroupInvitationView.swift in Sources */,
C328254025CA55880062D0A7 /* ContextMenuVC.swift in Sources */,
7BD687D12A5D0D1200D8E455 /* MessageInfoScreen.swift in Sources */,
B8269D2925C7A4B400488AB4 /* InputView.swift in Sources */,
FD71162E28E168C700B47552 /* SettingsViewModel.swift in Sources */,
);
@ -7825,7 +7870,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 436;
CURRENT_PROJECT_VERSION = 437;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -7863,7 +7908,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 2.5.0;
MARKETING_VERSION = 2.5.1;
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
@ -7896,7 +7941,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 436;
CURRENT_PROJECT_VERSION = 437;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -7934,7 +7979,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 2.5.0;
MARKETING_VERSION = 2.5.1;
OTHER_LDFLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
PRODUCT_NAME = Session;

@ -41,7 +41,6 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "NO"
codeCoverageEnabled = "YES"
onlyGenerateCoverageForSpecifiedTargets = "YES">
<MacroExpansion>
<BuildableReference

@ -425,7 +425,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
private func tryToReconnect() {
reconnectTimer?.invalidate()
guard Environment.shared?.reachabilityManager.isReachable == true else {
guard SessionEnvironment.shared?.reachabilityManager.isReachable == true else {
reconnectTimer = Timer.scheduledTimerOnMainThread(withTimeInterval: 5, repeats: false) { _ in
self.tryToReconnect()
}

@ -3,6 +3,7 @@
import UIKit
import CallKit
import GRDB
import SessionUIKit
import SessionMessagingKit
import SignalCoreKit
import SignalUtilitiesKit
@ -219,7 +220,10 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
preconditionFailure() // FIXME: Handle more gracefully
}
if let conversationVC: ConversationVC = presentingVC as? ConversationVC, conversationVC.viewModel.threadData.threadId == call.sessionId {
if
let conversationVC: ConversationVC = (presentingVC as? TopBannerController)?.wrappedViewController() as? ConversationVC,
conversationVC.viewModel.threadData.threadId == call.sessionId
{
let callVC = CallVC(for: call)
callVC.conversationVC = conversationVC
conversationVC.inputAccessoryView?.isHidden = true

@ -172,6 +172,8 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
private lazy var answerButton: UIButton = {
let result = UIButton(type: .custom)
result.accessibilityIdentifier = "Answer call"
result.accessibilityLabel = "Answer call"
result.setImage(
UIImage(named: "AnswerCall")?
.withRenderingMode(.alwaysTemplate),

@ -202,7 +202,7 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate {
else { preconditionFailure() } // FIXME: Handle more gracefully
let callVC = CallVC(for: self.call)
if let conversationVC = presentingVC as? ConversationVC {
if let conversationVC = (presentingVC as? TopBannerController)?.wrappedViewController() as? ConversationVC {
callVC.conversationVC = conversationVC
conversationVC.inputAccessoryView?.isHidden = true
conversationVC.inputAccessoryView?.alpha = 0

@ -170,6 +170,7 @@ extension ContextMenuVC {
currentUserBlinded25PublicKey: String?,
currentUserIsOpenGroupModerator: Bool,
currentThreadIsMessageRequest: Bool,
forMessageInfoScreen: Bool,
delegate: ContextMenuActionDelegate?,
using dependencies: Dependencies = Dependencies()
) -> [Action]? {
@ -242,11 +243,9 @@ extension ContextMenuVC {
on: cellViewModel.threadOpenGroupServer
)
}
return !currentThreadIsMessageRequest
return !currentThreadIsMessageRequest && !forMessageInfoScreen
}()
let shouldShowInfo: Bool = (cellViewModel.attachments?.isEmpty == false)
let generatedActions: [Action] = [
(canRetry ? Action.retry(cellViewModel, delegate, using: dependencies) : nil),
(viewModelCanReply(cellViewModel) ? Action.reply(cellViewModel, delegate, using: dependencies) : nil),
@ -256,18 +255,18 @@ extension ContextMenuVC {
(canDelete ? Action.delete(cellViewModel, delegate, using: dependencies) : nil),
(canBan ? Action.ban(cellViewModel, delegate, using: dependencies) : nil),
(canBan ? Action.banAndDeleteAllMessages(cellViewModel, delegate, using: dependencies) : nil),
(shouldShowInfo ? Action.info(cellViewModel, delegate, using: dependencies) : nil),
(forMessageInfoScreen ? nil : Action.info(cellViewModel, delegate, using: dependencies)),
]
.appending(
contentsOf: (shouldShowEmojiActions ? recentEmojis : [])
.map { Action.react(cellViewModel, $0, delegate, using: dependencies) }
)
.appending(Action.emojiPlusButton(cellViewModel, delegate, using: dependencies))
.appending(forMessageInfoScreen ? nil : Action.emojiPlusButton(cellViewModel, delegate, using: dependencies))
.compactMap { $0 }
guard !generatedActions.isEmpty else { return [] }
return generatedActions.appending(Action.dismiss(delegate))
return generatedActions.appending(forMessageInfoScreen ? nil : Action.dismiss(delegate))
}
}

@ -13,6 +13,7 @@ import SessionUIKit
import SessionMessagingKit
import SessionUtilitiesKit
import SignalUtilitiesKit
import SwiftUI
import SessionSnodeKit
extension ConversationVC:
@ -814,6 +815,7 @@ extension ConversationVC:
on: self.viewModel.threadData.openGroupServer
),
currentThreadIsMessageRequest: (self.viewModel.threadData.threadIsMessageRequest == true),
forMessageInfoScreen: false,
delegate: self
)
else { return }
@ -1785,14 +1787,30 @@ extension ConversationVC:
// MARK: - ContextMenuActionDelegate
func info(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) {
let mediaInfoVC = MediaInfoVC(
attachments: (cellViewModel.attachments ?? []),
isOutgoing: (cellViewModel.variant == .standardOutgoing),
threadId: self.viewModel.threadData.threadId,
threadVariant: self.viewModel.threadData.threadVariant,
interactionId: cellViewModel.id
let actions: [ContextMenuVC.Action] = ContextMenuVC.actions(
for: cellViewModel,
recentEmojis: [],
currentUserPublicKey: self.viewModel.threadData.currentUserPublicKey,
currentUserBlinded15PublicKey: self.viewModel.threadData.currentUserBlinded15PublicKey,
currentUserBlinded25PublicKey: self.viewModel.threadData.currentUserBlinded25PublicKey,
currentUserIsOpenGroupModerator: OpenGroupManager.isUserModeratorOrAdmin(
self.viewModel.threadData.currentUserPublicKey,
for: self.viewModel.threadData.openGroupRoomToken,
on: self.viewModel.threadData.openGroupServer
),
currentThreadIsMessageRequest: (self.viewModel.threadData.threadIsMessageRequest == true),
forMessageInfoScreen: true,
delegate: self,
using: dependencies
) ?? []
let messageInfoViewController = MessageInfoViewController(
actions: actions,
messageViewModel: cellViewModel
)
navigationController?.pushViewController(mediaInfoVC, animated: true)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
self?.navigationController?.pushViewController(messageInfoViewController, animated: true)
}
}
func retry(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) {
@ -2395,7 +2413,8 @@ extension ConversationVC:
let url: URL = URL(fileURLWithPath: directory).appendingPathComponent(fileName)
// Set up audio session
guard Environment.shared?.audioSession.startAudioActivity(recordVoiceMessageActivity) == true else {
let isConfigured = (SessionEnvironment.shared?.audioSession.startAudioActivity(recordVoiceMessageActivity) == true)
guard isConfigured else {
return cancelVoiceMessageRecording()
}
@ -2515,7 +2534,7 @@ extension ConversationVC:
func stopVoiceMessageRecording() {
audioRecorder?.stop()
Environment.shared?.audioSession.endAudioActivity(recordVoiceMessageActivity)
SessionEnvironment.shared?.audioSession.endAudioActivity(recordVoiceMessageActivity)
}
// MARK: - Data Extraction Notifications

@ -257,8 +257,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M
guard let quoteDraftInfo = quoteDraftInfo else { return }
let hInset: CGFloat = 6 // Slight visual adjustment
let maxWidth = additionalContentContainer.bounds.width
let quoteView: QuoteView = QuoteView(
for: .draft,
authorId: quoteDraftInfo.model.authorId,
@ -268,9 +267,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M
currentUserBlinded15PublicKey: quoteDraftInfo.model.currentUserBlinded15PublicKey,
currentUserBlinded25PublicKey: quoteDraftInfo.model.currentUserBlinded25PublicKey,
direction: (quoteDraftInfo.isOutgoing ? .outgoing : .incoming),
attachment: quoteDraftInfo.model.attachment,
hInset: hInset,
maxWidth: maxWidth
attachment: quoteDraftInfo.model.attachment
) { [weak self] in
self?.quoteDraftInfo = nil
}

@ -138,6 +138,8 @@ final class CallMessageCell: MessageCell {
)
else { return }
self.accessibilityIdentifier = "Control message"
self.isAccessibilityElement = true
self.viewModel = cellViewModel
self.topConstraint.constant = (cellViewModel.shouldShowDateHeader ? 0 : CallMessageCell.inset)

@ -496,3 +496,44 @@ public class MediaView: UIView {
unloadBlock?()
}
}
// MARK: - SwiftUI
import SwiftUI
struct MediaView_SwiftUI: UIViewRepresentable {
public typealias UIViewType = MediaView
private let mediaCache: NSCache<NSString, AnyObject>?
public let attachment: Attachment
private let isOutgoing: Bool
private let cornerRadius: CGFloat
public init(
mediaCache: NSCache<NSString, AnyObject>? = nil,
attachment: Attachment,
isOutgoing: Bool,
cornerRadius: CGFloat
) {
self.mediaCache = mediaCache
self.attachment = attachment
self.isOutgoing = isOutgoing
self.cornerRadius = cornerRadius
}
func makeUIView(context: Context) -> MediaView {
let mediaView = MediaView(
mediaCache: mediaCache,
attachment: attachment,
isOutgoing: isOutgoing,
shouldSupressControls: true,
cornerRadius: cornerRadius
)
return mediaView
}
func updateUIView(_ mediaView: MediaView, context: Context) {
mediaView.loadMedia()
}
}

@ -34,8 +34,6 @@ final class QuoteView: UIView {
currentUserBlinded25PublicKey: String?,
direction: Direction,
attachment: Attachment?,
hInset: CGFloat,
maxWidth: CGFloat,
onCancel: (() -> ())? = nil
) {
self.onCancel = onCancel
@ -51,9 +49,7 @@ final class QuoteView: UIView {
currentUserBlinded15PublicKey: currentUserBlinded15PublicKey,
currentUserBlinded25PublicKey: currentUserBlinded25PublicKey,
direction: direction,
attachment: attachment,
hInset: hInset,
maxWidth: maxWidth
attachment: attachment
)
}
@ -74,9 +70,7 @@ final class QuoteView: UIView {
currentUserBlinded15PublicKey: String?,
currentUserBlinded25PublicKey: String?,
direction: Direction,
attachment: Attachment?,
hInset: CGFloat,
maxWidth: CGFloat
attachment: Attachment?
) {
// There's quite a bit of calculation going on here. It's a bit complex so don't make changes
// if you don't need to. If you do then test:
@ -90,21 +84,6 @@ final class QuoteView: UIView {
let labelStackViewVMargin = QuoteView.labelStackViewVMargin
let smallSpacing = Values.smallSpacing
let cancelButtonSize = QuoteView.cancelButtonSize
var availableWidth: CGFloat
// Subtract smallSpacing twice; once for the spacing in between the stack view elements and
// once for the trailing margin.
if attachment == nil {
availableWidth = maxWidth - 2 * hInset - Values.accentLineThickness - 2 * smallSpacing
}
else {
availableWidth = maxWidth - 2 * hInset - thumbnailSize - 2 * smallSpacing
}
if case .draft = mode {
availableWidth -= cancelButtonSize
}
var body: String? = quotedText
// Main stack view

@ -0,0 +1,106 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import SwiftUI
import SessionUIKit
import SessionMessagingKit
struct DocumentView_SwiftUI: View {
@Binding private var maxWidth: CGFloat?
static private let inset: CGFloat = 12
private let attachment: Attachment
private let textColor: ThemeValue
public init(maxWidth: Binding<CGFloat?>, attachment: Attachment, textColor: ThemeValue) {
self._maxWidth = maxWidth
self.attachment = attachment
self.textColor = textColor
}
var body: some View {
HStack(
alignment: .center,
spacing: 0
) {
ZStack {
Image(systemName: "doc")
.font(.system(size: Values.largeFontSize))
.foregroundColor(themeColor: textColor)
if attachment.isAudio {
Image(systemName: "music.note")
.resizable()
.renderingMode(.template)
.foregroundColor(themeColor: textColor)
.scaledToFit()
.frame(
width: 7,
height: 20,
alignment: .bottom
)
}
}
.frame(
width: 24 + Values.mediumSpacing * 2,
height: 32 + Values.smallSpacing * 2
)
.background(themeColor: .messageBubble_overlay)
VStack(
alignment: .leading
) {
Text(attachment.documentFileName)
.lineLimit(1)
.font(.system(size: Values.mediumFontSize))
.foregroundColor(themeColor: textColor)
.frame(
maxWidth: maxWidth,
alignment: .leading
)
Text(attachment.documentFileInfo)
.font(.system(size: Values.verySmallFontSize))
.foregroundColor(themeColor: textColor)
}
.fixedSize(horizontal: false, vertical: true)
.padding(.horizontal, Values.mediumSpacing)
Image(systemName: (attachment.isAudio ? "play.fill" : "arrow.down"))
.font(.system(size: Values.mediumFontSize))
.foregroundColor(themeColor: textColor)
.padding(.trailing, Self.inset)
}
.frame(width: maxWidth)
}
}
struct DocumentView_SwiftUI_Previews: PreviewProvider {
@State static private var maxWidth: CGFloat? = 200
static var previews: some View {
VStack {
DocumentView_SwiftUI(
maxWidth: $maxWidth,
attachment: Attachment(
variant: .standard,
contentType: "audio/mp4",
byteCount: 100
),
textColor: .messageBubble_outgoingText
)
.frame(height: 58)
DocumentView_SwiftUI(
maxWidth: $maxWidth,
attachment: Attachment(
variant: .standard,
contentType: "txt",
byteCount: 1000
),
textColor: .messageBubble_outgoingText
)
.frame(height: 58)
}
}
}

@ -0,0 +1,156 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import SwiftUI
import SessionUIKit
import SessionMessagingKit
public struct LinkPreviewView_SwiftUI: View {
private var state: LinkPreviewState
private var isOutgoing: Bool
private let maxWidth: CGFloat
private var messageViewModel: MessageViewModel?
private var bodyLabelTextColor: ThemeValue?
private var lastSearchText: String?
private let onCancel: (() -> ())?
private static let loaderSize: CGFloat = 24
private static let cancelButtonSize: CGFloat = 45
init(
state: LinkPreviewState,
isOutgoing: Bool,
maxWidth: CGFloat = .infinity,
messageViewModel: MessageViewModel? = nil,
bodyLabelTextColor: ThemeValue? = nil,
lastSearchText: String? = nil,
onCancel: (() -> ())? = nil
) {
self.state = state
self.isOutgoing = isOutgoing
self.maxWidth = maxWidth
self.messageViewModel = messageViewModel
self.bodyLabelTextColor = bodyLabelTextColor
self.lastSearchText = lastSearchText
self.onCancel = onCancel
}
public var body: some View {
ZStack(
alignment: .leading
) {
if state is LinkPreview.SentState {
if #available(iOS 14.0, *) {
ThemeManager.currentTheme.colorSwiftUI(for: .messageBubble_overlay).ignoresSafeArea()
} else {
ThemeManager.currentTheme.colorSwiftUI(for: .messageBubble_overlay)
}
}
HStack(
alignment: .center,
spacing: Values.mediumSpacing
) {
// Link preview image
let imageSize: CGFloat = state is LinkPreview.SentState ? 100 : 80
if let linkPreviewImage: UIImage = state.image {
Image(uiImage: linkPreviewImage)
.resizable()
.scaledToFill()
.foregroundColor(
themeColor: isOutgoing ?
.messageBubble_outgoingText :
.messageBubble_incomingText
)
.frame(
width: imageSize,
height: imageSize
)
.cornerRadius(state is LinkPreview.SentState ? 0 : 8)
} else if
state is LinkPreview.DraftState || state is LinkPreview.SentState,
let defaultImage: UIImage = UIImage(named: "Link")?.withRenderingMode(.alwaysTemplate)
{
Image(uiImage: defaultImage)
.foregroundColor(
themeColor: isOutgoing ?
.messageBubble_outgoingText :
.messageBubble_incomingText
)
.frame(
width: imageSize,
height: imageSize
)
.background(themeColor: .messageBubble_overlay)
.cornerRadius(state is LinkPreview.SentState ? 0 : 8)
} else {
ActivityIndicator(themeColor: .borderSeparator, width: 2)
.frame(
width: Self.loaderSize,
height: Self.loaderSize
)
}
// Link preview title
if let title: String = state.title {
Text(title)
.bold()
.font(.system(size: Values.smallFontSize))
.multilineTextAlignment(.leading)
.foregroundColor(
themeColor: isOutgoing ?
.messageBubble_outgoingText :
.messageBubble_incomingText
)
.fixedSize(horizontal: false, vertical: true)
.padding(.trailing, Values.mediumSpacing)
}
// Cancel button
if state is LinkPreview.DraftState {
Spacer(minLength: 0)
Button(action: {
onCancel?()
}, label: {
if let image: UIImage = UIImage(named: "X")?.withRenderingMode(.alwaysTemplate) {
Image(uiImage: image)
.foregroundColor(themeColor: .textPrimary)
}
})
.frame(
width: Self.cancelButtonSize,
height: Self.cancelButtonSize
)
}
}
}
}
}
struct LinkPreview_SwiftUI_Previews: PreviewProvider {
static var previews: some View {
VStack {
LinkPreviewView_SwiftUI(
state: LinkPreview.DraftState(
linkPreviewDraft: .init(
urlString: "https://github.com/oxen-io",
title: "Github - oxen-io/session-ios: A private messenger for iOS.",
jpegImageData: UIImage(named: "AppIcon")?.jpegData(compressionQuality: 1)
)
),
isOutgoing: true
)
.padding(.horizontal, Values.mediumSpacing)
LinkPreviewView_SwiftUI(
state: LinkPreview.LoadingState(),
isOutgoing: true
)
.frame(
width: .infinity,
height: 80
)
.padding(.horizontal, Values.mediumSpacing)
}
}
}

@ -0,0 +1,95 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import SwiftUI
import SessionUIKit
import SessionMessagingKit
struct OpenGroupInvitationView_SwiftUI: View {
private let name: String
private let url: String
private let textColor: ThemeValue
private let isOutgoing: Bool
private static let iconSize: CGFloat = 24
private static let iconImageViewSize: CGFloat = 48
init(
name: String,
url: String,
textColor: ThemeValue,
isOutgoing: Bool
) {
self.name = name
self.url = {
if let range = url.range(of: "?public_key=") {
return String(url[..<range.lowerBound])
}
return url
}()
self.textColor = textColor
self.isOutgoing = isOutgoing
}
var body: some View {
HStack(
alignment: .center,
spacing: Values.mediumSpacing
) {
// Icon
let iconName = (isOutgoing ? "Globe" : "Plus")
if let iconImage = UIImage(named: iconName)?
.resizedImage(to: CGSize(width: Self.iconSize, height: Self.iconSize))?
.withRenderingMode(.alwaysTemplate)
{
Image(uiImage: iconImage)
.foregroundColor(themeColor: (isOutgoing ? .messageBubble_outgoingText : .textPrimary))
.background(
Circle()
.fill(themeColor: (isOutgoing ? .messageBubble_overlay : .primary))
.frame(
width: Self.iconImageViewSize,
height: Self.iconImageViewSize
)
)
.frame(
width: Self.iconImageViewSize,
height: Self.iconImageViewSize
)
}
// Text
VStack(
alignment: .leading,
spacing: 2
) {
Text(name)
.bold()
.font(.system(size: Values.largeFontSize))
.foregroundColor(themeColor: textColor)
Text("view_open_group_invitation_description".localized())
.font(.system(size: Values.smallFontSize))
.foregroundColor(themeColor: textColor)
.padding(.bottom, 2)
Text(url)
.font(.system(size: Values.verySmallFontSize))
.foregroundColor(themeColor: textColor)
.multilineTextAlignment(.leading)
}
}
.padding(.all, Values.mediumSpacing)
}
}
struct OpenGroupInvitationView_SwiftUI_Previews: PreviewProvider {
static var previews: some View {
OpenGroupInvitationView_SwiftUI(
name: "Session",
url: "http://open.getsession.org/session?public_key=a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238",
textColor: .messageBubble_outgoingText,
isOutgoing: true
)
}
}

@ -0,0 +1,228 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import SwiftUI
import SessionUIKit
import SessionMessagingKit
import SessionUtilitiesKit
struct QuoteView_SwiftUI: View {
public enum Mode { case regular, draft }
public enum Direction { case incoming, outgoing }
public struct Info {
var mode: Mode
var authorId: String
var quotedText: String?
var threadVariant: SessionThread.Variant
var currentUserPublicKey: String?
var currentUserBlinded15PublicKey: String?
var currentUserBlinded25PublicKey: String?
var direction: Direction
var attachment: Attachment?
}
@State private var thumbnail: UIImage? = nil
private static let thumbnailSize: CGFloat = 48
private static let iconSize: CGFloat = 24
private static let labelStackViewSpacing: CGFloat = 2
private static let labelStackViewVMargin: CGFloat = 4
private static let cancelButtonSize: CGFloat = 33
private static let cornerRadius: CGFloat = 4
private var info: Info
private var onCancel: (() -> ())?
private var isCurrentUser: Bool {
return [
info.currentUserPublicKey,
info.currentUserBlinded15PublicKey,
info.currentUserBlinded25PublicKey
]
.compactMap { $0 }
.asSet()
.contains(info.authorId)
}
private var quotedText: String? {
if let quotedText = info.quotedText, !quotedText.isEmpty {
return quotedText
}
if let attachment = info.attachment {
return attachment.shortDescription
}
return nil
}
private var author: String? {
guard !isCurrentUser else { return "MEDIA_GALLERY_SENDER_NAME_YOU".localized() }
guard quotedText != nil else {
// When we can't find the quoted message we want to hide the author label
return Profile.displayNameNoFallback(
id: info.authorId,
threadVariant: info.threadVariant
)
}
return Profile.displayName(
id: info.authorId,
threadVariant: info.threadVariant
)
}
public init(info: Info, onCancel: (() -> ())? = nil) {
self.info = info
self.onCancel = onCancel
if let attachment = info.attachment, attachment.isVisualMedia {
attachment.thumbnail(
size: .small,
success: { [self] image, _ in
self.thumbnail = image
},
failure: {}
)
}
}
var body: some View {
HStack(
alignment: .center,
spacing: Values.smallSpacing
) {
if let attachment: Attachment = info.attachment {
// Attachment thumbnail
if let image: UIImage = {
if let thumbnail = self.thumbnail {
return thumbnail
}
let fallbackImageName: String = (MIMETypeUtil.isAudio(attachment.contentType) ? "attachment_audio" : "actionsheet_document_black")
return UIImage(named: fallbackImageName)?
.resizedImage(to: CGSize(width: Self.iconSize, height: Self.iconSize))?
.withRenderingMode(.alwaysTemplate)
}() {
Image(uiImage: image)
.foregroundColor(themeColor: {
switch info.mode {
case .regular: return (info.direction == .outgoing ?
.messageBubble_outgoingText :
.messageBubble_incomingText
)
case .draft: return .textPrimary
}
}())
.frame(
width: Self.thumbnailSize,
height: Self.thumbnailSize,
alignment: .center
)
.background(themeColor: .messageBubble_overlay)
.cornerRadius(Self.cornerRadius)
}
} else {
// Line view
let lineColor: ThemeValue = {
switch info.mode {
case .regular: return (info.direction == .outgoing ? .messageBubble_outgoingText : .primary)
case .draft: return .primary
}
}()
Rectangle()
.foregroundColor(themeColor: lineColor)
.frame(width: Values.accentLineThickness)
}
// Quoted text and author
VStack(
alignment: .leading,
spacing: Self.labelStackViewSpacing
) {
let targetThemeColor: ThemeValue = {
switch info.mode {
case .regular: return (info.direction == .outgoing ?
.messageBubble_outgoingText :
.messageBubble_incomingText
)
case .draft: return .textPrimary
}
}()
if let author = self.author {
Text(author)
.bold()
.font(.system(size: Values.smallFontSize))
.foregroundColor(themeColor: targetThemeColor)
}
if let quotedText = self.quotedText, let textColor = ThemeManager.currentTheme.color(for: targetThemeColor) {
AttributedText(
MentionUtilities.highlightMentions(
in: quotedText,
threadVariant: info.threadVariant,
currentUserPublicKey: info.currentUserPublicKey,
currentUserBlinded15PublicKey: info.currentUserBlinded15PublicKey,
currentUserBlinded25PublicKey: info.currentUserBlinded25PublicKey,
isOutgoingMessage: (info.direction == .outgoing),
textColor: textColor,
theme: ThemeManager.currentTheme,
primaryColor: ThemeManager.primaryColor,
attributes: [
.foregroundColor: textColor,
.font: UIFont.systemFont(ofSize: Values.smallFontSize)
]
)
)
.lineLimit(2)
} else {
Text("QUOTED_MESSAGE_NOT_FOUND".localized())
.font(.system(size: Values.smallFontSize))
.foregroundColor(themeColor: targetThemeColor)
}
}
.padding(.vertical, Self.labelStackViewVMargin)
if info.mode == .draft {
// Cancel button
Button(
action: {
onCancel?()
},
label: {
if let image = UIImage(named: "X")?.withRenderingMode(.alwaysTemplate) {
Image(uiImage: image)
.foregroundColor(themeColor: .textPrimary)
.frame(
width: Self.cancelButtonSize,
height: Self.cancelButtonSize,
alignment: .center
)
}
}
)
}
}
.padding(.trailing, Values.smallSpacing)
}
}
struct QuoteView_SwiftUI_Previews: PreviewProvider {
static var previews: some View {
ZStack {
if #available(iOS 14.0, *) {
ThemeManager.currentTheme.colorSwiftUI(for: .backgroundPrimary).ignoresSafeArea()
} else {
ThemeManager.currentTheme.colorSwiftUI(for: .backgroundPrimary)
}
QuoteView_SwiftUI(
info: QuoteView_SwiftUI.Info(
mode: .draft,
authorId: "",
threadVariant: .contact,
direction: .outgoing
)
)
.frame(height: 40)
}
}
}

@ -0,0 +1,95 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import SwiftUI
import SessionUIKit
import SessionMessagingKit
struct VoiceMessageView_SwiftUI: View {
@State var isPlaying: Bool = false
@State var time: String = "0:00"
@State var speed: String = "1.5×"
@State var progress: Double = 0.0
private static let width: CGFloat = 160
private static let toggleContainerSize: CGFloat = 20
private var attachment: Attachment
public init(attachment: Attachment) {
self.attachment = attachment
}
var body: some View {
ZStack(alignment: .leading) {
Rectangle()
.foregroundColor(themeColor: .messageBubble_overlay)
.frame(width: Self.width * progress)
HStack(
alignment: .center,
spacing: 0
) {
ZStack {
Circle()
.foregroundColor(themeColor: .backgroundSecondary)
.frame(
width: Self.toggleContainerSize,
height: Self.toggleContainerSize
)
if let toggleImage: UIImage = UIImage(named: isPlaying ? "Pause" : "Play")?.withRenderingMode(.alwaysTemplate) {
Image(uiImage: toggleImage)
.resizable()
.foregroundColor(themeColor: .textPrimary)
.scaledToFit()
.frame(
width: 8,
height: 8
)
}
if attachment.state == .downloading {
ActivityIndicator(themeColor: .textPrimary, width: 2)
.frame(
width: Self.toggleContainerSize,
height: Self.toggleContainerSize
)
}
}
Rectangle()
.foregroundColor(themeColor: .backgroundSecondary)
.frame(height: 1)
ZStack {
Capsule()
.foregroundColor(themeColor: .backgroundSecondary)
.frame(
width: 44,
height: Self.toggleContainerSize
)
Text(attachment.duration.defaulting(to: 0).formatted(format: .hoursMinutesSeconds))
.foregroundColor(themeColor: .textPrimary)
.font(.system(size: Values.smallFontSize))
}
}
.padding(.all, Values.smallSpacing)
}
.frame(
width: Self.width
)
}
}
struct VoiceMessageView_SwiftUI_Previews: PreviewProvider {
static var previews: some View {
VoiceMessageView_SwiftUI(
attachment: Attachment(
variant: .voiceMessage,
contentType: "mp4",
byteCount: 100
)
)
.frame(height: 58)
}
}

@ -566,9 +566,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
.outgoing :
.incoming
),
attachment: cellViewModel.quoteAttachment,
hInset: hInset,
maxWidth: maxWidth
attachment: cellViewModel.quoteAttachment
)
self.quoteView = quoteView
let quoteViewContainer = UIView(wrapping: quoteView, withInsets: UIEdgeInsets(top: 0, leading: hInset, bottom: 0, trailing: hInset))
@ -1098,6 +1096,143 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
}
}
static func getBodyAttributedText(
for cellViewModel: MessageViewModel,
theme: Theme,
primaryColor: Theme.PrimaryColor,
textColor: ThemeValue,
searchText: String?
) -> NSMutableAttributedString?
{
guard
let body: String = cellViewModel.body,
!body.isEmpty,
let actualTextColor: UIColor = theme.color(for: textColor),
let backgroundPrimaryColor: UIColor = theme.color(for: .backgroundPrimary),
let textPrimaryColor: UIColor = theme.color(for: .textPrimary)
else { return nil }
let isOutgoing: Bool = (cellViewModel.variant == .standardOutgoing)
let attributedText: NSMutableAttributedString = NSMutableAttributedString(
attributedString: MentionUtilities.highlightMentions(
in: body,
threadVariant: cellViewModel.threadVariant,
currentUserPublicKey: cellViewModel.currentUserPublicKey,
currentUserBlinded15PublicKey: cellViewModel.currentUserBlinded15PublicKey,
currentUserBlinded25PublicKey: cellViewModel.currentUserBlinded25PublicKey,
isOutgoingMessage: isOutgoing,
textColor: actualTextColor,
theme: theme,
primaryColor: primaryColor,
attributes: [
.foregroundColor: actualTextColor,
.font: UIFont.systemFont(ofSize: getFontSize(for: cellViewModel))
]
)
)
// Custom handle links
let links: [URL: NSRange] = {
guard let detector: NSDataDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else {
return [:]
}
// Note: The 'String.count' value is based on actual character counts whereas
// NSAttributedString and NSRange are both based on UTF-16 encoded lengths, so
// in order to avoid strings which contain emojis breaking strings which end
// with URLs we need to use the 'String.utf16.count' value when creating the range
return detector
.matches(
in: attributedText.string,
options: [],
range: NSRange(location: 0, length: attributedText.string.utf16.count)
)
.reduce(into: [:]) { result, match in
guard
let matchUrl: URL = match.url,
let originalRange: Range = Range(match.range, in: attributedText.string)
else { return }
/// If the URL entered didn't have a scheme it will default to 'http', we want to catch this and
/// set the scheme to 'https' instead as we don't load previews for 'http' so this will result
/// in more previews actually getting loaded without forcing the user to enter 'https://' before
/// every URL they enter
let originalString: String = String(attributedText.string[originalRange])
guard matchUrl.absoluteString != "http://\(originalString)" else {
guard let httpsUrl: URL = URL(string: "https://\(originalString)") else {
return
}
result[httpsUrl] = match.range
return
}
result[matchUrl] = match.range
}
}()
for (linkUrl, urlRange) in links {
attributedText.addAttributes(
[
.font: UIFont.systemFont(ofSize: getFontSize(for: cellViewModel)),
.foregroundColor: actualTextColor,
.underlineColor: actualTextColor,
.underlineStyle: NSUnderlineStyle.single.rawValue,
.attachment: linkUrl
],
range: urlRange
)
}
// If there is a valid search term then highlight each part that matched
if let searchText = searchText, searchText.count >= ConversationSearchController.minimumSearchTextLength {
let normalizedBody: String = attributedText.string.lowercased()
SessionThreadViewModel.searchTermParts(searchText)
.map { part -> String in
guard part.hasPrefix("\"") && part.hasSuffix("\"") else { return part }
let partRange = (part.index(after: part.startIndex)..<part.index(before: part.endIndex))
return String(part[partRange])
}
.forEach { part in
// Highlight all ranges of the text (Note: The search logic only finds
// results that start with the term so we use the regex below to ensure
// we only highlight those cases)
normalizedBody
.ranges(
of: (Singleton.appContext.isRTL ?
"(\(part.lowercased()))(^|[^a-zA-Z0-9])" :
"(^|[^a-zA-Z0-9])(\(part.lowercased()))"
),
options: [.regularExpression]
)
.forEach { range in
let targetRange: Range<String.Index> = {
let term: String = String(normalizedBody[range])
// If the matched term doesn't actually match the "part" value then it means
// we've matched a term after a non-alphanumeric character so need to shift
// the range over by 1
guard term.starts(with: part.lowercased()) else {
return (normalizedBody.index(after: range.lowerBound)..<range.upperBound)
}
return range
}()
let legacyRange: NSRange = NSRange(targetRange, in: normalizedBody)
attributedText.addThemeAttribute(.background(backgroundPrimaryColor), range: legacyRange)
attributedText.addThemeAttribute(.foreground(textPrimaryColor), range: legacyRange)
}
}
}
return attributedText
}
static func getBodyTappableLabel(
for cellViewModel: MessageViewModel,
with availableWidth: CGFloat,
@ -1105,7 +1240,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
searchText: String?,
delegate: TappableLabelDelegate?
) -> TappableLabel {
let isOutgoing: Bool = (cellViewModel.variant == .standardOutgoing)
let result: TappableLabel = TappableLabel()
result.setContentCompressionResistancePriority(.required, for: .vertical)
result.themeBackgroundColor = .clear
@ -1114,131 +1248,16 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
result.delegate = delegate
ThemeManager.onThemeChange(observer: result) { [weak result] theme, primaryColor in
guard
let actualTextColor: UIColor = theme.color(for: textColor),
let backgroundPrimaryColor: UIColor = theme.color(for: .backgroundPrimary),
let textPrimaryColor: UIColor = theme.color(for: .textPrimary)
else { return }
let hasPreviousSetText: Bool = ((result?.attributedText?.length ?? 0) > 0)
let attributedText: NSMutableAttributedString = NSMutableAttributedString(
attributedString: MentionUtilities.highlightMentions(
in: (cellViewModel.body ?? ""),
threadVariant: cellViewModel.threadVariant,
currentUserPublicKey: cellViewModel.currentUserPublicKey,
currentUserBlinded15PublicKey: cellViewModel.currentUserBlinded15PublicKey,
currentUserBlinded25PublicKey: cellViewModel.currentUserBlinded25PublicKey,
isOutgoingMessage: isOutgoing,
textColor: actualTextColor,
theme: theme,
primaryColor: primaryColor,
attributes: [
.foregroundColor: actualTextColor,
.font: UIFont.systemFont(ofSize: getFontSize(for: cellViewModel))
]
)
result?.attributedText = Self.getBodyAttributedText(
for: cellViewModel,
theme: theme,
primaryColor: primaryColor,
textColor: textColor,
searchText: searchText
)
// Custom handle links
let links: [URL: NSRange] = {
guard let detector: NSDataDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else {
return [:]
}
// Note: The 'String.count' value is based on actual character counts whereas
// NSAttributedString and NSRange are both based on UTF-16 encoded lengths, so
// in order to avoid strings which contain emojis breaking strings which end
// with URLs we need to use the 'String.utf16.count' value when creating the range
return detector
.matches(
in: attributedText.string,
options: [],
range: NSRange(location: 0, length: attributedText.string.utf16.count)
)
.reduce(into: [:]) { result, match in
guard
let matchUrl: URL = match.url,
let originalRange: Range = Range(match.range, in: attributedText.string)
else { return }
/// If the URL entered didn't have a scheme it will default to 'http', we want to catch this and
/// set the scheme to 'https' instead as we don't load previews for 'http' so this will result
/// in more previews actually getting loaded without forcing the user to enter 'https://' before
/// every URL they enter
let originalString: String = String(attributedText.string[originalRange])
guard matchUrl.absoluteString != "http://\(originalString)" else {
guard let httpsUrl: URL = URL(string: "https://\(originalString)") else {
return
}
result[httpsUrl] = match.range
return
}
result[matchUrl] = match.range
}
}()
for (linkUrl, urlRange) in links {
attributedText.addAttributes(
[
.font: UIFont.systemFont(ofSize: getFontSize(for: cellViewModel)),
.foregroundColor: actualTextColor,
.underlineColor: actualTextColor,
.underlineStyle: NSUnderlineStyle.single.rawValue,
.attachment: linkUrl
],
range: urlRange
)
}
// If there is a valid search term then highlight each part that matched
if let searchText = searchText, searchText.count >= ConversationSearchController.minimumSearchTextLength {
let normalizedBody: String = attributedText.string.lowercased()
SessionThreadViewModel.searchTermParts(searchText)
.map { part -> String in
guard part.hasPrefix("\"") && part.hasSuffix("\"") else { return part } // stringlint:disable
let partRange = (part.index(after: part.startIndex)..<part.index(before: part.endIndex))
return String(part[partRange])
}
.forEach { part in
// Highlight all ranges of the text (Note: The search logic only finds
// results that start with the term so we use the regex below to ensure
// we only highlight those cases)
normalizedBody
.ranges(
of: (Singleton.hasAppContext && Singleton.appContext.isRTL ?
"(\(part.lowercased()))(^|[^a-zA-Z0-9])" : // stringlint:disable
"(^|[^a-zA-Z0-9])(\(part.lowercased()))" // stringlint:disable
),
options: [.regularExpression]
)
.forEach { range in
let targetRange: Range<String.Index> = {
let term: String = String(normalizedBody[range])
// If the matched term doesn't actually match the "part" value then it means
// we've matched a term after a non-alphanumeric character so need to shift
// the range over by 1
guard term.starts(with: part.lowercased()) else {
return (normalizedBody.index(after: range.lowerBound)..<range.upperBound)
}
return range
}()
let legacyRange: NSRange = NSRange(targetRange, in: normalizedBody)
attributedText.addThemeAttribute(.background(backgroundPrimaryColor), range: legacyRange)
attributedText.addThemeAttribute(.foreground(textPrimaryColor), range: legacyRange)
}
}
}
result?.attributedText = attributedText
if let result: TappableLabel = result, !hasPreviousSetText {
let availableSpace = CGSize(width: availableWidth, height: .greatestFiniteMagnitude)
let size = result.sizeThatFits(availableSpace)

@ -74,3 +74,33 @@ class MediaGalleryNavigationController: UINavigationController {
relayoutBackgroundView()
}
}
// MARK: - SwiftUI
import SwiftUI
struct MediaGalleryNavigationController_SwiftUI: UIViewControllerRepresentable {
typealias UIViewControllerType = MediaGalleryNavigationController
public var viewControllers: [UIViewController]
private let transitioningDelegate: UIViewControllerTransitioningDelegate?
public init(
viewControllers: [UIViewController],
transitioningDelegate: UIViewControllerTransitioningDelegate? = nil
) {
self.viewControllers = viewControllers
self.transitioningDelegate = transitioningDelegate
}
func makeUIViewController(context: Context) -> MediaGalleryNavigationController {
let mediaGalleryNavigationController = MediaGalleryNavigationController()
mediaGalleryNavigationController.modalPresentationStyle = .fullScreen
mediaGalleryNavigationController.transitioningDelegate = transitioningDelegate
return mediaGalleryNavigationController
}
func updateUIViewController(_ mediaGalleryNavigationController: MediaGalleryNavigationController, context: Context) {
mediaGalleryNavigationController.viewControllers = viewControllers
}
}

@ -5,6 +5,7 @@ import GRDB
import DifferenceKit
import SignalUtilitiesKit
import SessionUtilitiesKit
import SwiftUI
public class MediaGalleryViewModel {
public typealias SectionModel = ArraySection<Section, Item>
@ -534,7 +535,8 @@ public class MediaGalleryViewModel {
threadVariant: SessionThread.Variant,
interactionId: Int64,
selectedAttachmentId: String,
options: [MediaGalleryOption]
options: [MediaGalleryOption],
useTransitioningDelegate: Bool = true
) -> UIViewController? {
// Load the data for the album immediately (needed before pushing to the screen so
// transitions work nicely)
@ -562,11 +564,14 @@ public class MediaGalleryViewModel {
let navController: MediaGalleryNavigationController = MediaGalleryNavigationController()
navController.viewControllers = [pageViewController]
navController.modalPresentationStyle = .fullScreen
navController.transitioningDelegate = pageViewController
if useTransitioningDelegate {
navController.transitioningDelegate = pageViewController
}
return navController
}
public static func createMediaTileViewController(
threadId: String,
threadVariant: SessionThread.Variant,

@ -0,0 +1,600 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import SwiftUI
import SessionUIKit
import SessionSnodeKit
import SessionUtilitiesKit
import SessionMessagingKit
struct MessageInfoScreen: View {
@EnvironmentObject var host: HostWrapper
@State var index = 1
static private let cornerRadius: CGFloat = 17
var actions: [ContextMenuVC.Action]
var messageViewModel: MessageViewModel
var isMessageFailed: Bool {
return [.failed, .failedToSync].contains(messageViewModel.state)
}
var body: some View {
ZStack (alignment: .topLeading) {
ScrollView(.vertical, showsIndicators: false) {
VStack(
alignment: .leading,
spacing: 10
) {
// Message bubble snapshot
MessageBubble(
messageViewModel: messageViewModel
)
.background(
RoundedRectangle(cornerRadius: Self.cornerRadius)
.fill(
themeColor: (messageViewModel.variant == .standardIncoming || messageViewModel.variant == .standardIncomingDeleted ?
.messageBubble_incomingBackground :
.messageBubble_outgoingBackground)
)
)
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading
)
.fixedSize(horizontal: false, vertical: true)
.padding(.top, Values.smallSpacing)
.padding(.bottom, Values.verySmallSpacing)
.padding(.horizontal, Values.largeSpacing)
if isMessageFailed {
let (image, statusText, tintColor) = messageViewModel.state.statusIconInfo(
variant: messageViewModel.variant,
hasAtLeastOneReadReceipt: messageViewModel.hasAtLeastOneReadReceipt
)
HStack(spacing: 6) {
if let image: UIImage = image?.withRenderingMode(.alwaysTemplate) {
Image(uiImage: image)
.resizable()
.scaledToFit()
.foregroundColor(themeColor: tintColor)
.frame(width: 13, height: 12)
}
if let statusText: String = statusText {
Text(statusText)
.font(.system(size: Values.verySmallFontSize))
.foregroundColor(themeColor: tintColor)
}
}
.padding(.top, -Values.smallSpacing)
.padding(.bottom, Values.verySmallSpacing)
.padding(.horizontal, Values.largeSpacing)
}
if let attachments = messageViewModel.attachments,
messageViewModel.cellType == .mediaMessage
{
let attachment: Attachment = attachments[(index - 1 + attachments.count) % attachments.count]
ZStack(alignment: .bottomTrailing) {
if attachments.count > 1 {
// Attachment carousel view
SessionCarouselView_SwiftUI(
index: $index,
isOutgoing: (messageViewModel.variant == .standardOutgoing),
contentInfos: attachments
)
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading
)
} else {
MediaView_SwiftUI(
attachment: attachments[0],
isOutgoing: (messageViewModel.variant == .standardOutgoing),
cornerRadius: 0
)
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading
)
.aspectRatio(1, contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 15))
.padding(.horizontal, Values.largeSpacing)
}
if [ .downloaded, .uploaded ].contains(attachment.state) {
Button {
self.showMediaFullScreen(attachment: attachment)
} label: {
ZStack {
Circle()
.foregroundColor(.init(white: 0, opacity: 0.4))
Image(systemName: "arrow.up.left.and.arrow.down.right")
.font(.system(size: 13))
.foregroundColor(.white)
}
.frame(width: 26, height: 26)
}
.padding(.bottom, Values.smallSpacing)
.padding(.trailing, 38)
}
}
.padding(.vertical, Values.verySmallSpacing)
// Attachment Info
ZStack {
VStack(
alignment: .leading,
spacing: Values.mediumSpacing
) {
InfoBlock(title: "ATTACHMENT_INFO_FILE_ID".localized() + ":") {
Text(attachment.serverId ?? "")
.font(.system(size: Values.mediumFontSize))
.foregroundColor(themeColor: .textPrimary)
}
HStack(
alignment: .center
) {
InfoBlock(title: "ATTACHMENT_INFO_FILE_TYPE".localized() + ":") {
Text(attachment.contentType)
.font(.system(size: Values.mediumFontSize))
.foregroundColor(themeColor: .textPrimary)
}
Spacer()
InfoBlock(title: "ATTACHMENT_INFO_FILE_SIZE".localized() + ":") {
Text(Format.fileSize(attachment.byteCount))
.font(.system(size: Values.mediumFontSize))
.foregroundColor(themeColor: .textPrimary)
}
Spacer()
}
HStack(
alignment: .center
) {
let resolution: String = {
guard let width = attachment.width, let height = attachment.height else { return "N/A" }
return "\(width)×\(height)"
}()
InfoBlock(title: "ATTACHMENT_INFO_RESOLUTION".localized() + ":") {
Text(resolution)
.font(.system(size: Values.mediumFontSize))
.foregroundColor(themeColor: .textPrimary)
}
Spacer()
let duration: String = {
guard let duration = attachment.duration else { return "N/A" }
return floor(duration).formatted(format: .videoDuration)
}()
InfoBlock(title: "ATTACHMENT_INFO_DURATION".localized() + ":") {
Text(duration)
.font(.system(size: Values.mediumFontSize))
.foregroundColor(themeColor: .textPrimary)
}
Spacer()
}
}
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading
)
.padding(.all, Values.largeSpacing)
}
.frame(maxHeight: .infinity)
.background(themeColor: .backgroundSecondary)
.cornerRadius(Self.cornerRadius)
.fixedSize(horizontal: false, vertical: true)
.padding(.vertical, Values.verySmallSpacing)
.padding(.horizontal, Values.largeSpacing)
}
// Message Info
ZStack {
VStack(
alignment: .leading,
spacing: Values.mediumSpacing
) {
InfoBlock(title: "MESSAGE_INFO_SENT".localized() + ":") {
Text(messageViewModel.dateForUI.fromattedForMessageInfo)
.font(.system(size: Values.mediumFontSize))
.foregroundColor(themeColor: .textPrimary)
}
InfoBlock(title: "MESSAGE_INFO_RECEIVED".localized() + ":") {
Text(messageViewModel.receivedDateForUI.fromattedForMessageInfo)
.font(.system(size: Values.mediumFontSize))
.foregroundColor(themeColor: .textPrimary)
}
if isMessageFailed {
let failureText: String = messageViewModel.mostRecentFailureText ?? "SEND_FAILED_NOTIFICATION_BODY".localized()
InfoBlock(title: "ALERT_ERROR_TITLE".localized() + ":") {
Text(failureText)
.font(.system(size: Values.mediumFontSize))
.foregroundColor(themeColor: .danger)
}
}
InfoBlock(title: "MESSAGE_INFO_FROM".localized() + ":") {
HStack(
spacing: 10
) {
let (info, additionalInfo) = ProfilePictureView.getProfilePictureInfo(
size: .message,
publicKey: messageViewModel.authorId,
threadVariant: .contact, // Always show the display picture in 'contact' mode
customImageData: nil,
profile: messageViewModel.profile,
profileIcon: (messageViewModel.isSenderOpenGroupModerator ? .crown : .none)
)
let size: ProfilePictureView.Size = .list
if let info: ProfilePictureView.Info = info {
ProfilePictureSwiftUI(
size: size,
info: info,
additionalInfo: additionalInfo
)
.frame(
width: size.viewSize,
height: size.viewSize,
alignment: .topLeading
)
}
VStack(
alignment: .leading,
spacing: Values.verySmallSpacing
) {
if !messageViewModel.authorName.isEmpty {
Text(messageViewModel.authorName)
.bold()
.font(.system(size: Values.mediumLargeFontSize))
.foregroundColor(themeColor: .textPrimary)
}
Text(messageViewModel.authorId)
.font(.spaceMono(size: Values.smallFontSize))
.foregroundColor(themeColor: .textPrimary)
}
}
}
}
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading
)
.padding(.all, Values.largeSpacing)
}
.frame(maxHeight: .infinity)
.background(themeColor: .backgroundSecondary)
.cornerRadius(Self.cornerRadius)
.fixedSize(horizontal: false, vertical: true)
.padding(.vertical, Values.verySmallSpacing)
.padding(.horizontal, Values.largeSpacing)
// Actions
if !actions.isEmpty {
ZStack {
VStack(
alignment: .leading,
spacing: 0
) {
ForEach(
0...(actions.count - 1),
id: \.self
) { index in
let tintColor: ThemeValue = actions[index].themeColor
Button(
action: {
actions[index].work()
dismiss()
},
label: {
HStack(spacing: Values.largeSpacing) {
Image(uiImage: actions[index].icon!.withRenderingMode(.alwaysTemplate))
.resizable()
.scaledToFit()
.foregroundColor(themeColor: tintColor)
.frame(width: 26, height: 26)
Text(actions[index].title)
.bold()
.font(.system(size: Values.mediumLargeFontSize))
.foregroundColor(themeColor: tintColor)
}
.frame(maxWidth: .infinity, alignment: .topLeading)
}
)
.frame(height: 60)
if index < (actions.count - 1) {
Divider()
.foregroundColor(themeColor: .borderSeparator)
}
}
}
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading
)
.padding(.horizontal, Values.largeSpacing)
}
.frame(maxHeight: .infinity)
.background(themeColor: .backgroundSecondary)
.cornerRadius(Self.cornerRadius)
.fixedSize(horizontal: false, vertical: true)
.padding(.vertical, Values.verySmallSpacing)
.padding(.horizontal, Values.largeSpacing)
}
}
}
}
.background(themeColor: .backgroundPrimary)
}
private func showMediaFullScreen(attachment: Attachment) {
if let mediaGalleryView = MediaGalleryViewModel.createDetailViewController(
for: messageViewModel.threadId,
threadVariant: messageViewModel.threadVariant,
interactionId: messageViewModel.id,
selectedAttachmentId: attachment.id,
options: [ .sliderEnabled ],
useTransitioningDelegate: false
) {
self.host.controller?.present(mediaGalleryView, animated: true)
}
}
func dismiss() {
self.host.controller?.navigationController?.popViewController(animated: true)
}
}
struct MessageBubble: View {
@State private var maxWidth: CGFloat?
static private let cornerRadius: CGFloat = 18
static private let inset: CGFloat = 12
let messageViewModel: MessageViewModel
var bodyLabelTextColor: ThemeValue {
messageViewModel.variant == .standardOutgoing ?
.messageBubble_outgoingText :
.messageBubble_incomingText
}
var body: some View {
ZStack {
switch messageViewModel.cellType {
case .textOnlyMessage:
let maxWidth: CGFloat = (VisibleMessageCell.getMaxWidth(for: messageViewModel) - 2 * Self.inset)
VStack(
alignment: .leading,
spacing: 0
) {
if let linkPreview: LinkPreview = messageViewModel.linkPreview {
switch linkPreview.variant {
case .standard:
LinkPreviewView_SwiftUI(
state: LinkPreview.SentState(
linkPreview: linkPreview,
imageAttachment: messageViewModel.linkPreviewAttachment
),
isOutgoing: (messageViewModel.variant == .standardOutgoing),
maxWidth: maxWidth,
messageViewModel: messageViewModel,
bodyLabelTextColor: bodyLabelTextColor,
lastSearchText: nil
)
case .openGroupInvitation:
OpenGroupInvitationView_SwiftUI(
name: (linkPreview.title ?? ""),
url: linkPreview.url,
textColor: bodyLabelTextColor,
isOutgoing: (messageViewModel.variant == .standardOutgoing))
}
}
else {
if let quote = messageViewModel.quote {
QuoteView_SwiftUI(
info: .init(
mode: .regular,
authorId: quote.authorId,
quotedText: quote.body,
threadVariant: messageViewModel.threadVariant,
currentUserPublicKey: messageViewModel.currentUserPublicKey,
currentUserBlinded15PublicKey: messageViewModel.currentUserBlinded15PublicKey,
currentUserBlinded25PublicKey: messageViewModel.currentUserBlinded25PublicKey,
direction: (messageViewModel.variant == .standardOutgoing ? .outgoing : .incoming),
attachment: messageViewModel.quoteAttachment
)
)
.fixedSize(horizontal: false, vertical: true)
.padding(.top, Self.inset)
.padding(.horizontal, Self.inset)
.padding(.bottom, -Values.smallSpacing)
}
}
if let bodyText: NSAttributedString = VisibleMessageCell.getBodyAttributedText(
for: messageViewModel,
theme: ThemeManager.currentTheme,
primaryColor: ThemeManager.primaryColor,
textColor: bodyLabelTextColor,
searchText: nil
) {
AttributedText(bodyText)
.padding(.all, Self.inset)
}
}
case .mediaMessage:
if let bodyText: NSAttributedString = VisibleMessageCell.getBodyAttributedText(
for: messageViewModel,
theme: ThemeManager.currentTheme,
primaryColor: ThemeManager.primaryColor,
textColor: bodyLabelTextColor,
searchText: nil
) {
AttributedText(bodyText)
.padding(.all, Self.inset)
}
case .voiceMessage:
if let attachment: Attachment = messageViewModel.attachments?.first(where: { $0.isAudio }){
// TODO: Playback Info and check if playing function is needed
VoiceMessageView_SwiftUI(attachment: attachment)
}
case .audio, .genericAttachment:
if let attachment: Attachment = messageViewModel.attachments?.first {
VStack(
alignment: .leading,
spacing: Values.smallSpacing
) {
DocumentView_SwiftUI(
maxWidth: $maxWidth,
attachment: attachment,
textColor: bodyLabelTextColor
)
.modifier(MaxWidthEqualizer.notify)
.frame(
width: maxWidth,
alignment: .leading
)
if let bodyText: NSAttributedString = VisibleMessageCell.getBodyAttributedText(
for: messageViewModel,
theme: ThemeManager.currentTheme,
primaryColor: ThemeManager.primaryColor,
textColor: bodyLabelTextColor,
searchText: nil
) {
ZStack{
AttributedText(bodyText)
.padding(.horizontal, Self.inset)
.padding(.bottom, Self.inset)
}
.modifier(MaxWidthEqualizer.notify)
.frame(
width: maxWidth,
alignment: .leading
)
}
}
.modifier(MaxWidthEqualizer(width: $maxWidth))
}
default: EmptyView()
}
}
}
}
struct InfoBlock<Content>: View where Content: View {
let title: String
let content: () -> Content
private let minWidth: CGFloat = 100
var body: some View {
VStack(
alignment: .leading,
spacing: Values.verySmallSpacing
) {
Text(self.title)
.bold()
.font(.system(size: Values.mediumLargeFontSize))
.foregroundColor(themeColor: .textPrimary)
self.content()
}
.frame(
minWidth: minWidth,
alignment: .leading
)
}
}
final class MessageInfoViewController: SessionHostingViewController<MessageInfoScreen> {
init(actions: [ContextMenuVC.Action], messageViewModel: MessageViewModel) {
let messageInfoView = MessageInfoScreen(
actions: actions,
messageViewModel: messageViewModel
)
super.init(rootView: messageInfoView)
}
@MainActor required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
let customTitleFontSize = Values.largeFontSize
setNavBarTitle("message_info_title".localized(), customFontSize: customTitleFontSize)
}
}
struct MessageInfoView_Previews: PreviewProvider {
static var messageViewModel: MessageViewModel {
let result = MessageViewModel(
optimisticMessageId: UUID(),
threadId: "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54gdfsg",
threadVariant: .contact,
threadExpirationType: nil,
threadExpirationTimer: nil,
threadOpenGroupServer: nil,
threadOpenGroupPublicKey: nil,
threadContactNameInternal: "Test",
timestampMs: SnodeAPI.currentOffsetTimestampMs(),
receivedAtTimestampMs: SnodeAPI.currentOffsetTimestampMs(),
authorId: "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54gdfsg",
authorNameInternal: "Test",
body: "Mauris sapien dui, sagittis et fringilla eget, tincidunt vel mauris. Mauris bibendum quis ipsum ac pulvinar. Integer semper elit vitae placerat efficitur. Quisque blandit scelerisque orci, a fringilla dui. In a sollicitudin tortor. Vivamus consequat sollicitudin felis, nec pretium dolor bibendum sit amet. Integer non congue risus, id imperdiet diam. Proin elementum enim at felis commodo semper. Pellentesque magna magna, laoreet nec hendrerit in, suscipit sit amet risus. Nulla et imperdiet massa. Donec commodo felis quis arcu dignissim lobortis. Praesent nec fringilla felis, ut pharetra sapien. Donec ac dignissim nisi, non lobortis justo. Nulla congue velit nec sodales bibendum. Nullam feugiat, mauris ac consequat posuere, eros sem dignissim nulla, ac convallis dolor sem rhoncus dolor. Cras ut luctus risus, quis viverra mauris.",
expiresStartedAtMs: nil,
expiresInSeconds: nil,
state: .failed,
isSenderOpenGroupModerator: false,
currentUserProfile: Profile.fetchOrCreateCurrentUser(),
quote: nil,
quoteAttachment: nil,
linkPreview: nil,
linkPreviewAttachment: nil,
attachments: nil
)
return result
}
static var actions: [ContextMenuVC.Action] {
return [
.reply(messageViewModel, nil, using: Dependencies()),
.retry(messageViewModel, nil, using: Dependencies()),
.delete(messageViewModel, nil, using: Dependencies())
]
}
static var previews: some View {
MessageInfoScreen(
actions: actions,
messageViewModel: messageViewModel
)
}
}

@ -51,7 +51,7 @@ class PhotoCapture: NSObject {
func startAudioCapture() throws {
assertIsOnSessionQueue()
guard Environment.shared?.audioSession.startAudioActivity(recordingAudioActivity) == true else {
guard SessionEnvironment.shared?.audioSession.startAudioActivity(recordingAudioActivity) == true else {
throw PhotoCaptureError.assertionError(description: "unable to capture audio activity")
}
@ -82,7 +82,7 @@ class PhotoCapture: NSObject {
}
session.removeInput(audioDeviceInput)
self.audioDeviceInput = nil
Environment.shared?.audioSession.endAudioActivity(recordingAudioActivity)
SessionEnvironment.shared?.audioSession.endAudioActivity(recordingAudioActivity)
}
func startCapture() -> AnyPublisher<Void, Error> {

@ -88,7 +88,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
)
if Environment.shared?.callManager.wrappedValue?.currentCall == nil {
if SessionEnvironment.shared?.callManager.wrappedValue?.currentCall == nil {
UserDefaults.sharedLokiProject?[.isCallOngoing] = false
UserDefaults.sharedLokiProject?[.lastCallPreOffer] = nil
}
@ -391,8 +391,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
// May as well run these on the background thread
Environment.shared?.audioSession.setup()
Environment.shared?.reachabilityManager.setup()
SessionEnvironment.shared?.audioSession.setup()
SessionEnvironment.shared?.reachabilityManager.setup()
}
private func showFailedStartupAlert(
@ -833,7 +833,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
let callVC: CallVC = CallVC(for: call)
if let conversationVC: ConversationVC = presentingVC as? ConversationVC, conversationVC.viewModel.threadData.threadId == call.sessionId {
if
let conversationVC: ConversationVC = (presentingVC as? TopBannerController)?.wrappedViewController() as? ConversationVC,
conversationVC.viewModel.threadData.threadId == call.sessionId
{
callVC.conversationVC = conversationVC
conversationVC.inputAccessoryView?.isHidden = true
conversationVC.inputAccessoryView?.alpha = 0

@ -47,10 +47,10 @@ public class AppEnvironment {
public func setup() {
// Hang certain singletons on Environment too.
Environment.shared?.callManager.mutate {
SessionEnvironment.shared?.callManager.mutate {
$0 = callManager
}
Environment.shared?.notificationsManager.mutate {
SessionEnvironment.shared?.notificationsManager.mutate {
$0 = notificationPresenter
}
setupLogFiles()

@ -57,6 +57,8 @@
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>seed1.getsession.org</key>

@ -45,7 +45,7 @@ final class PathStatusView: UIView {
// MARK: - Initialization
private let size: Size
private let reachability: Reachability? = Environment.shared?.reachabilityManager.reachability
private let reachability: Reachability? = SessionEnvironment.shared?.reachabilityManager.reachability
init(size: Size = .small) {
self.size = size

@ -253,7 +253,7 @@ private final class LineView: UIView {
private var dotViewWidthConstraint: NSLayoutConstraint!
private var dotViewHeightConstraint: NSLayoutConstraint!
private var dotViewAnimationTimer: Timer!
private let reachability: Reachability? = Environment.shared?.reachabilityManager.reachability
private let reachability: Reachability? = SessionEnvironment.shared?.reachabilityManager.reachability
enum Location {
case top, middle, bottom

@ -0,0 +1,83 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import SwiftUI
import SessionUIKit
public struct ActivityIndicator: View {
@State private var strokeStart: Double = 0.95
@State private var strokeEnd: Double = 1.0
@State private var shorten: Bool = false
@State private var isRotating: Bool = false
private var themeColor: ThemeValue
private var width: CGFloat
public init(themeColor: ThemeValue, width: CGFloat) {
self.themeColor = themeColor
self.width = width
}
public var body: some View {
GeometryReader { (geometry: GeometryProxy) in
Circle()
.trim(from: strokeStart, to: strokeEnd)
.stroke(
themeColor: themeColor,
style: StrokeStyle(
lineWidth: width,
lineCap: .round
)
)
.frame(
width: geometry.size.width,
height: geometry.size.height
)
.rotationEffect(!self.isRotating ? .degrees(0) : .degrees(360))
}
.aspectRatio(1, contentMode: .fit)
.onAppear {
withAnimation(
Animation
.timingCurve(0.4, 0.0, 0.2, 1.0, duration: 1.5)
.repeatForever(autoreverses: false)
) {
self.isRotating = true
}
self.trimStroke()
Timer.scheduledTimer(withTimeInterval: 1.5, repeats: true) { _ in
self.trimStroke()
}
}
}
private func trimStroke() {
self.shorten = !self.shorten
if self.shorten {
self.strokeStart = 0.0
self.strokeEnd = 1.0
} else {
self.strokeStart = 0.0
self.strokeEnd = 0.0
}
withAnimation(.linear(duration: 1.5)) {
if self.shorten {
self.strokeStart = 1.0
} else {
self.strokeEnd = 1.0
}
}
}
}
struct ActivityIndicator_Previews: PreviewProvider {
static var previews: some View {
ActivityIndicator(themeColor: .textPrimary, width: 2)
.frame(
width: 40,
height: 40
)
}
}

@ -102,7 +102,7 @@ class ScreenLockUI {
return .none;
}
if Environment.shared?.isRequestingPermission == true {
if SessionEnvironment.shared?.isRequestingPermission == true {
return .none;
}

@ -0,0 +1,228 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import SwiftUI
public struct SessionCarouselView_SwiftUI: View {
@Binding var index: Int
let isOutgoing: Bool
var contentInfos: [Attachment]
let numberOfPages: Int
public init(index: Binding<Int>, isOutgoing: Bool, contentInfos: [Attachment]) {
self._index = index
self.isOutgoing = isOutgoing
self.contentInfos = contentInfos
self.numberOfPages = contentInfos.count
let first = self.contentInfos.first!
let last = self.contentInfos.last!
self.contentInfos.append(first)
self.contentInfos.insert(last, at: 0)
}
public var body: some View {
HStack(spacing: 0) {
ArrowView(index: $index, numberOfPages: numberOfPages, type: .decrement)
.zIndex(1)
PageView(index: $index, numberOfPages: self.numberOfPages) {
ForEach(self.contentInfos) { attachment in
MediaView_SwiftUI(
attachment: attachment,
isOutgoing: self.isOutgoing,
cornerRadius: 0
)
}
}
.aspectRatio(1, contentMode: .fit)
ArrowView(index: $index, numberOfPages: numberOfPages, type: .increment)
.zIndex(1)
}
}
}
struct ArrowView: View {
@Binding var index: Int
let numberOfPages: Int
let maxIndex: Int
let type: ArrowType
enum ArrowType {
case increment
case decrement
}
init(index: Binding<Int>, numberOfPages: Int, type: ArrowType) {
self._index = index
self.numberOfPages = numberOfPages
self.maxIndex = numberOfPages + 1
self.type = type
}
var body: some View {
let imageName = self.type == .decrement ? "chevron.left" : "chevron.right"
Button {
if self.type == .decrement {
decrement()
} else {
increment()
}
} label: {
Image(systemName: imageName)
.font(.system(size: 20))
.foregroundColor(themeColor: .textPrimary)
.frame(width: 30, height: 30)
}
}
func decrement() {
withAnimation(.easeOut) {
self.index -= 1
}
if self.index == 0 {
self.index = self.maxIndex - 1
}
}
func increment() {
withAnimation(.easeOut) {
self.index += 1
}
if self.index == self.maxIndex {
self.index = 1
}
}
}
struct PageView<Content>: View where Content: View {
@Binding var index: Int
let numberOfPages: Int
let maxIndex: Int
let content: () -> Content
@State private var offset = CGFloat.zero
@State private var dragging = false
init(index: Binding<Int>, numberOfPages: Int, @ViewBuilder content: @escaping () -> Content) {
self._index = index
self.numberOfPages = numberOfPages
self.content = content
self.maxIndex = numberOfPages + 1
}
var body: some View {
ZStack(alignment: .bottom) {
GeometryReader { geometry in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
self.content()
.frame(width: geometry.size.width, height: geometry.size.height)
}
}
.content.offset(x: self.offset(in: geometry), y: 0)
.frame(width: geometry.size.width, alignment: .leading)
.clipShape(RoundedRectangle(cornerRadius: 15))
.gesture(
DragGesture(coordinateSpace: .local)
.onChanged { value in
self.dragging = true
self.offset = -CGFloat(self.index) * geometry.size.width + value.translation.width
}
.onEnded { value in
let predictedEndOffset = -CGFloat(self.index) * geometry.size.width + value.predictedEndTranslation.width
let predictedIndex = Int(round(predictedEndOffset / -geometry.size.width))
self.index = self.clampedIndex(from: predictedIndex)
withAnimation(.easeOut(duration: 0.2)) {
self.dragging = false
}
// FIXME: This is a workaround for withAnimation() not having completion callback
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
switch self.index {
case 0: self.index = self.maxIndex - 1
case self.maxIndex: self.index = 1
default: break
}
}
}
)
}
.clipped()
PageControl(index: $index, maxIndex: numberOfPages - 1)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 8, trailing: 0))
}
}
func offset(in geometry: GeometryProxy) -> CGFloat {
if self.dragging {
return max(min(self.offset, 0), -CGFloat(self.maxIndex) * geometry.size.width)
} else {
return -CGFloat(self.index) * geometry.size.width
}
}
func clampedIndex(from predictedIndex: Int) -> Int {
let newIndex = min(max(predictedIndex, self.index - 1), self.index + 1)
guard newIndex >= 0 else { return 0 }
guard newIndex <= maxIndex else { return maxIndex }
return newIndex
}
}
struct PageControl: View {
@Binding var index: Int
let maxIndex: Int
var body: some View {
ZStack {
Capsule()
.foregroundColor(.init(white: 0, opacity: 0.4))
HStack(spacing: 4) {
ForEach(0...maxIndex, id: \.self) { index in
Circle()
.fill(index == ((self.index - 1) % (self.maxIndex + 1)) ? Color.white : Color.gray)
.frame(width: 7, height: 7)
}
}
.padding(6)
}
.fixedSize(horizontal: true, vertical: true)
.frame(
maxWidth: .infinity,
maxHeight: 19
)
}
}
struct SessionCarouselView_SwiftUI_Previews: PreviewProvider {
@State static var index = 1
static var previews: some View {
ZStack {
if #available(iOS 14.0, *) {
Color.black.ignoresSafeArea()
} else {
Color.black
}
SessionCarouselView_SwiftUI(
index: $index,
isOutgoing: true,
contentInfos: [
Attachment(
variant: .standard,
contentType: "jpeg",
byteCount: 100
),
Attachment(
variant: .standard,
contentType: "jpeg",
byteCount: 100
)
]
)
}
}
}

@ -0,0 +1,97 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import SwiftUI
import SessionUIKit
public class HostWrapper: ObservableObject {
public weak var controller: UIViewController?
}
public class SessionHostingViewController<Content>: UIHostingController<ModifiedContent<Content,SwiftUI._EnvironmentKeyWritingModifier<HostWrapper?>>> where Content : View {
public override var preferredStatusBarStyle: UIStatusBarStyle {
return ThemeManager.currentTheme.statusBarStyle
}
lazy var navBarTitleLabel: UILabel = {
let result = UILabel()
result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
result.themeTextColor = .textPrimary
result.textAlignment = .center
result.alpha = 1
return result
}()
lazy var crossfadeLabel: UILabel = {
let result = UILabel()
result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
result.themeTextColor = .textPrimary
result.textAlignment = .center
result.alpha = 0
return result
}()
public init(rootView:Content) {
let container = HostWrapper()
let modified = rootView.environmentObject(container) as! ModifiedContent<Content, _EnvironmentKeyWritingModifier<HostWrapper?>>
super.init(rootView: modified)
container.controller = self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func viewDidLoad() {
super.viewDidLoad()
navigationItem.backButtonTitle = ""
view.themeBackgroundColor = .backgroundPrimary
ThemeManager.applyNavigationStylingIfNeeded(to: self)
setNeedsStatusBarAppearanceUpdate()
}
internal func setNavBarTitle(_ title: String, customFontSize: CGFloat? = nil) {
let container = UIView()
navBarTitleLabel.text = title
crossfadeLabel.text = title
if let customFontSize = customFontSize {
navBarTitleLabel.font = .boldSystemFont(ofSize: customFontSize)
crossfadeLabel.font = .boldSystemFont(ofSize: customFontSize)
}
container.addSubview(navBarTitleLabel)
container.addSubview(crossfadeLabel)
navBarTitleLabel.pin(to: container)
crossfadeLabel.pin(to: container)
navigationItem.titleView = container
}
internal func setUpNavBarSessionHeading() {
let headingImageView = UIImageView(
image: UIImage(named: "SessionHeading")?
.withRenderingMode(.alwaysTemplate)
)
headingImageView.themeTintColor = .textPrimary
headingImageView.contentMode = .scaleAspectFit
headingImageView.set(.width, to: 150)
headingImageView.set(.height, to: Values.mediumFontSize)
navigationItem.titleView = headingImageView
}
internal func setUpNavBarSessionIcon() {
let logoImageView = UIImageView()
logoImageView.image = #imageLiteral(resourceName: "SessionGreen32")
logoImageView.contentMode = .scaleAspectFit
logoImageView.set(.width, to: 32)
logoImageView.set(.height, to: 32)
navigationItem.titleView = logoImageView
}
}

@ -38,7 +38,7 @@ public extension Date {
var fromattedForMessageInfo: String {
let formatter: DateFormatter = DateFormatter()
formatter.locale = Locale.current
formatter.dateFormat = "h:mm a EEE, DD/MM/YYYY"
formatter.dateFormat = "h:mm a EEE, dd/MM/YYYY"
return formatter.string(from: self)
}

@ -111,10 +111,10 @@ public enum Permissions {
// the picker view then will dismiss, too. The selection process cannot be finished
// this way. So we add a flag (isRequestingPermission) to prevent the ScreenLockUI
// from showing when we request the photo library permission.
Environment.shared?.isRequestingPermission = true
SessionEnvironment.shared?.isRequestingPermission = true
PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
Environment.shared?.isRequestingPermission = false
SessionEnvironment.shared?.isRequestingPermission = false
if [ PHAuthorizationStatus.authorized, PHAuthorizationStatus.limited ].contains(status) {
onAuthorized()
}

@ -753,7 +753,7 @@ public extension Interaction {
}
// Clear out any notifications for the interactions we mark as read
Environment.shared?.notificationsManager.wrappedValue?.cancelNotifications(
SessionEnvironment.shared?.notificationsManager.wrappedValue?.cancelNotifications(
identifiers: interactionInfo
.map { interactionInfo in
Interaction.notificationIdentifier(

@ -70,7 +70,7 @@ extension MessageReceiver {
.fetchOrCreate(db, id: sender, variant: .contact, shouldBeVisible: nil)
if !interaction.wasRead {
Environment.shared?.notificationsManager.wrappedValue?
SessionEnvironment.shared?.notificationsManager.wrappedValue?
.notifyUser(
db,
forIncomingCall: interaction,
@ -88,7 +88,7 @@ extension MessageReceiver {
.fetchOrCreate(db, id: sender, variant: .contact, shouldBeVisible: nil)
if !interaction.wasRead {
Environment.shared?.notificationsManager.wrappedValue?
SessionEnvironment.shared?.notificationsManager.wrappedValue?
.notifyUser(
db,
forIncomingCall: interaction,
@ -108,7 +108,7 @@ extension MessageReceiver {
}
// Ensure we have a call manager before continuing
guard let callManager: CallManagerProtocol = Environment.shared?.callManager.wrappedValue else { return }
guard let callManager: CallManagerProtocol = SessionEnvironment.shared?.callManager.wrappedValue else { return }
// Ignore pre offer message after the same call instance has been generated
if let currentCall: CurrentCallProtocol = callManager.currentCall, currentCall.uuid == message.uuid {
@ -136,7 +136,7 @@ extension MessageReceiver {
// Ensure we have a call manager before continuing
guard
let callManager: CallManagerProtocol = Environment.shared?.callManager.wrappedValue,
let callManager: CallManagerProtocol = SessionEnvironment.shared?.callManager.wrappedValue,
let currentCall: CurrentCallProtocol = callManager.currentCall,
currentCall.uuid == message.uuid,
let sdp: String = message.sdps.first
@ -152,7 +152,7 @@ extension MessageReceiver {
guard
let currentWebRTCSession: WebRTCSession = WebRTCSession.current,
currentWebRTCSession.uuid == message.uuid,
let callManager: CallManagerProtocol = Environment.shared?.callManager.wrappedValue,
let callManager: CallManagerProtocol = SessionEnvironment.shared?.callManager.wrappedValue,
var currentCall: CurrentCallProtocol = callManager.currentCall,
currentCall.uuid == message.uuid,
let sender: String = message.sender
@ -178,7 +178,7 @@ extension MessageReceiver {
guard
WebRTCSession.current?.uuid == message.uuid,
let callManager: CallManagerProtocol = Environment.shared?.callManager.wrappedValue,
let callManager: CallManagerProtocol = SessionEnvironment.shared?.callManager.wrappedValue,
let currentCall: CurrentCallProtocol = callManager.currentCall,
currentCall.uuid == message.uuid,
let sender: String = message.sender

@ -362,7 +362,7 @@ extension MessageReceiver {
guard variant == .standardIncoming && !interaction.wasRead else { return interactionId }
// Use the same identifier for notifications when in backgroud polling to prevent spam
Environment.shared?.notificationsManager.wrappedValue?
SessionEnvironment.shared?.notificationsManager.wrappedValue?
.notifyUser(
db,
for: interaction,
@ -433,7 +433,7 @@ extension MessageReceiver {
// Don't notify if the reaction was added before the lastest read timestamp for
// the conversation
if sender != currentUserPublicKey && !timestampAlreadyRead {
Environment.shared?.notificationsManager.wrappedValue?
SessionEnvironment.shared?.notificationsManager.wrappedValue?
.notifyUser(
db,
forReaction: reaction,

@ -700,6 +700,7 @@ public extension MessageViewModel {
body: String?,
expiresStartedAtMs: Double?,
expiresInSeconds: TimeInterval?,
state: RecipientState.State = .sending,
isSenderOpenGroupModerator: Bool,
currentUserProfile: Profile,
quote: Quote?,
@ -732,7 +733,7 @@ public extension MessageViewModel {
self.expiresStartedAtMs = expiresStartedAtMs
self.expiresInSeconds = expiresInSeconds
self.state = .sending
self.state = state
self.hasAtLeastOneReadReceipt = false
self.mostRecentFailureText = nil
self.isSenderOpenGroupModerator = isSenderOpenGroupModerator

@ -18,7 +18,7 @@ public class AudioActivity: NSObject {
}
deinit {
Environment.shared?.audioSession.ensureAudioSessionActivationStateAfterDelay()
SessionEnvironment.shared?.audioSession.ensureAudioSessionActivationStateAfterDelay()
}
// MARK:
@ -84,9 +84,9 @@ public class OWSAudioSession: NSObject {
func ensureAudioCategory() throws {
if aggregateBehaviors.contains(.audioMessagePlayback) {
Environment.shared?.proximityMonitoringManager.add(lifetime: self)
SessionEnvironment.shared?.proximityMonitoringManager.add(lifetime: self)
} else {
Environment.shared?.proximityMonitoringManager.remove(lifetime: self)
SessionEnvironment.shared?.proximityMonitoringManager.remove(lifetime: self)
}
if aggregateBehaviors.contains(.call) {

@ -13,21 +13,46 @@ public extension ProfilePictureView {
additionalProfile: Profile? = nil,
additionalProfileIcon: ProfileIcon = .none
) {
let (info, additionalInfo): (Info?, Info?) = Self.getProfilePictureInfo(
size: self.size,
publicKey: publicKey,
threadVariant: threadVariant,
customImageData: customImageData,
profile: profile,
profileIcon: profileIcon,
additionalProfile: additionalProfile,
additionalProfileIcon: additionalProfileIcon
)
guard let info: Info = info else { return }
update(info, additionalInfo: additionalInfo)
}
static func getProfilePictureInfo(
size: Size,
publicKey: String,
threadVariant: SessionThread.Variant,
customImageData: Data?,
profile: Profile?,
profileIcon: ProfileIcon = .none,
additionalProfile: Profile? = nil,
additionalProfileIcon: ProfileIcon = .none
) -> (Info?, Info?) {
// If we are given 'customImageData' then only use that
guard customImageData == nil else { return update(Info(imageData: customImageData)) }
guard customImageData == nil else { return (Info(imageData: customImageData), nil) }
// Otherwise there are conversation-type-specific behaviours
switch threadVariant {
case .community:
let placeholderImage: UIImage = {
switch self.size {
switch size {
case .navigation, .message: return #imageLiteral(resourceName: "SessionWhite16")
case .list: return #imageLiteral(resourceName: "SessionWhite24")
case .hero: return #imageLiteral(resourceName: "SessionWhite40")
}
}()
update(
return (
Info(
imageData: placeholderImage.pngData(),
inset: UIEdgeInsets(
@ -38,13 +63,14 @@ public extension ProfilePictureView {
),
icon: profileIcon,
forcedBackgroundColor: .theme(.classicDark, color: .borderSeparator)
)
),
nil
)
case .legacyGroup, .group:
guard !publicKey.isEmpty else { return }
guard !publicKey.isEmpty else { return (nil, nil) }
update(
return (
Info(
imageData: (
profile.map { ProfileManager.profileAvatar(profile: $0) } ??
@ -53,14 +79,14 @@ public extension ProfilePictureView {
text: (profile?.displayName(for: threadVariant))
.defaulting(to: publicKey),
size: (additionalProfile != nil ?
self.size.multiImageSize :
self.size.viewSize
size.multiImageSize :
size.viewSize
)
).pngData()
),
icon: profileIcon
),
additionalInfo: additionalProfile
additionalProfile
.map { otherProfile in
Info(
imageData: (
@ -68,7 +94,7 @@ public extension ProfilePictureView {
PlaceholderIcon.generate(
seed: otherProfile.id,
text: otherProfile.displayName(for: threadVariant),
size: self.size.multiImageSize
size: size.multiImageSize
).pngData()
),
icon: additionalProfileIcon
@ -91,9 +117,9 @@ public extension ProfilePictureView {
)
case .contact:
guard !publicKey.isEmpty else { return }
guard !publicKey.isEmpty else { return (nil, nil) }
update(
return (
Info(
imageData: (
profile.map { ProfileManager.profileAvatar(profile: $0) } ??
@ -102,13 +128,14 @@ public extension ProfilePictureView {
text: (profile?.displayName(for: threadVariant))
.defaulting(to: publicKey),
size: (additionalProfile != nil ?
self.size.multiImageSize :
self.size.viewSize
size.multiImageSize :
size.viewSize
)
).pngData()
),
icon: profileIcon
)
),
nil
)
}
}

@ -3,8 +3,8 @@
import Foundation
import SessionUtilitiesKit
public class Environment {
public static var shared: Environment?
public class SessionEnvironment {
public static var shared: SessionEnvironment?
public let reachabilityManager: SSKReachabilityManager
@ -37,8 +37,8 @@ public class Environment {
self.windowManager = windowManager
self.isRequestingPermission = false
if Environment.shared == nil {
Environment.shared = self
if SessionEnvironment.shared == nil {
SessionEnvironment.shared = self
}
}
@ -55,8 +55,8 @@ public class Environment {
public class SMKEnvironment: NSObject {
@objc public static let shared: SMKEnvironment = SMKEnvironment()
@objc public var audioSession: OWSAudioSession? { Environment.shared?.audioSession }
@objc public var windowManager: OWSWindowManager? { Environment.shared?.windowManager }
@objc public var audioSession: OWSAudioSession? { SessionEnvironment.shared?.audioSession }
@objc public var windowManager: OWSWindowManager? { SessionEnvironment.shared?.windowManager }
@objc public var isRequestingPermission: Bool { (Environment.shared?.isRequestingPermission == true) }
@objc public var isRequestingPermission: Bool { (SessionEnvironment.shared?.isRequestingPermission == true) }
}

@ -159,7 +159,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
// Notify the user if the call message wasn't already read
if !interaction.wasRead {
Environment.shared?.notificationsManager.wrappedValue?
SessionEnvironment.shared?.notificationsManager.wrappedValue?
.notifyUser(
db,
forIncomingCall: interaction,
@ -228,7 +228,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
AppSetup.setupEnvironment(
retrySetupIfDatabaseInvalid: true,
appSpecificBlock: {
Environment.shared?.notificationsManager.mutate {
SessionEnvironment.shared?.notificationsManager.mutate {
$0 = NSENotificationPresenter()
}
},

@ -53,7 +53,7 @@ final class ShareNavController: UINavigationController, ShareViewDelegate {
AppSetup.setupEnvironment(
appSpecificBlock: {
Environment.shared?.notificationsManager.mutate {
SessionEnvironment.shared?.notificationsManager.mutate {
$0 = NoopNotificationsManager()
}
},

@ -0,0 +1,52 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import SwiftUI
struct AttributedTextBlock {
let content: String
let font: Font?
let color: Color?
let underlineColor: Color?
}
public struct AttributedText: View {
var attributedText: NSAttributedString?
private var descriptions: [AttributedTextBlock] = []
public init(_ attributedText: NSAttributedString?) {
self.attributedText = attributedText
self.extractDescriptions()
}
private mutating func extractDescriptions() {
if let text = attributedText {
text.enumerateAttributes(in: NSMakeRange(0, text.length), options: [], using: { (attribute, range, stop) in
let substring = (text.string as NSString).substring(with: range)
let font = (attribute[.font] as? UIFont).map { Font($0) }
let color = (attribute[.foregroundColor] as? UIColor).map { Color($0) }
let underlineColor = (attribute[.underlineColor] as? UIColor).map { Color($0) }
descriptions.append(
AttributedTextBlock(
content: substring,
font: font,
color: color,
underlineColor: underlineColor
)
)
})
}
}
public var body: some View {
descriptions.map { description in
var text: Text = Text(description.content)
if let font: Font = description.font { text = text.font(font) }
if let color: Color = description.color { text = text.foregroundColor(color) }
if let underlineColor = description.underlineColor { text = text.underline(color: underlineColor) }
return text
}.reduce(Text("")) { (result, text) in
result + text
}
}
}

@ -547,3 +547,34 @@ public final class ProfilePictureView: UIView {
additionalProfileIconBackgroundView.layer.cornerRadius = (size.iconSize / 2)
}
}
import SwiftUI
public struct ProfilePictureSwiftUI: UIViewRepresentable {
public typealias UIViewType = ProfilePictureView
var size: ProfilePictureView.Size
var info: ProfilePictureView.Info
var additionalInfo: ProfilePictureView.Info?
public init(
size: ProfilePictureView.Size,
info: ProfilePictureView.Info,
additionalInfo: ProfilePictureView.Info? = nil
) {
self.size = size
self.info = info
self.additionalInfo = additionalInfo
}
public func makeUIView(context: Context) -> ProfilePictureView {
ProfilePictureView(size: size)
}
public func updateUIView(_ profilePictureView: ProfilePictureView, context: Context) {
profilePictureView.update(
info,
additionalInfo: additionalInfo
)
}
}

@ -150,6 +150,12 @@ public class TopBannerController: UIViewController {
}
// MARK: - Functions
public func wrappedViewController() -> UIViewController? {
if let navVC = child as? UINavigationController {
return navVC.topViewController
}
return child
}
public func attachChild() {
child.willMove(toParent: self)

@ -1,4 +1,5 @@
import UIKit
import SwiftUI
@objc(LKFonts)
public final class Fonts : NSObject {
@ -11,3 +12,13 @@ public final class Fonts : NSObject {
return UIFont(name: "SpaceMono-Bold", size: size)!
}
}
public extension Font {
static func spaceMono(size: CGFloat) -> Font {
return Font.custom("SpaceMono-Regular", size: size)
}
static func boldSpaceMono(size: CGFloat) -> Font {
return Font.custom("SpaceMono-Bold", size: size)
}
}

@ -0,0 +1,38 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import SwiftUI
public extension View {
func foregroundColor(themeColor: ThemeValue) -> some View {
return self.foregroundColor(
ThemeManager.currentTheme.colorSwiftUI(for: themeColor)
)
}
func background(themeColor: ThemeValue) -> some View {
if #available(iOSApplicationExtension 14.0, *) {
return self.background(
ThemeManager.currentTheme.colorSwiftUI(for: themeColor)?.ignoresSafeArea()
)
} else {
return self.background(
ThemeManager.currentTheme.colorSwiftUI(for: themeColor)
)
}
}
}
public extension Shape {
func fill(themeColor: ThemeValue) -> some View {
return self.fill(
ThemeManager.currentTheme.colorSwiftUI(for: themeColor) ?? Color.primary
)
}
func stroke(themeColor: ThemeValue, style: StrokeStyle) -> some View {
return self.stroke(
ThemeManager.currentTheme.colorSwiftUI(for: themeColor) ?? Color.primary,
style: style
)
}
}

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit.UIColor
import SwiftUI
internal enum Theme_ClassicDark: ThemeColors {
static let theme: [ThemeValue: UIColor] = [
@ -120,4 +121,122 @@ internal enum Theme_ClassicDark: ThemeColors {
// Unread Marker
.unreadMarker: .primary
]
static let themeSwiftUI: [ThemeValue: Color] = [
// General
.white: .white,
.black: .black,
.clear: .clear,
.primary: .primary,
.defaultPrimary: Theme.PrimaryColor.green.colorSwiftUI,
.warning: .warning,
.danger: .dangerDark,
.disabled: .disabledDark,
.backgroundPrimary: .classicDark0,
.backgroundSecondary: .classicDark1,
.textPrimary: .classicDark6,
.textSecondary: .classicDark5,
.borderSeparator: .classicDark3,
// Path
.path_connected: .pathConnected,
.path_connecting: .pathConnecting,
.path_error: .pathError,
.path_unknown: .classicDark4,
// TextBox
.textBox_background: .classicDark1,
.textBox_border: .classicDark3,
// MessageBubble
.messageBubble_outgoingBackground: .primary,
.messageBubble_incomingBackground: .classicDark2,
.messageBubble_outgoingText: .classicDark0,
.messageBubble_incomingText: .classicDark6,
.messageBubble_overlay: .black_06,
.messageBubble_deliveryStatus: .classicDark5,
// MenuButton
.menuButton_background: .primary,
.menuButton_icon: .classicDark6,
.menuButton_outerShadow: .primary,
.menuButton_innerShadow: .classicDark6,
// RadioButton
.radioButton_selectedBackground: .primary,
.radioButton_unselectedBackground: .clear,
.radioButton_selectedBorder: .classicDark6,
.radioButton_unselectedBorder: .classicDark6,
.radioButton_disabledSelectedBackground: .disabledDark,
.radioButton_disabledUnselectedBackground: .clear,
.radioButton_disabledBorder: .disabledDark,
// SessionButton
.sessionButton_text: .primary,
.sessionButton_background: .clear,
.sessionButton_highlight: .classicDark6.opacity(0.3),
.sessionButton_border: .primary,
.sessionButton_filledText: .classicDark6,
.sessionButton_filledBackground: .classicDark1,
.sessionButton_filledHighlight: .classicDark3,
.sessionButton_destructiveText: .dangerDark,
.sessionButton_destructiveBackground: .clear,
.sessionButton_destructiveHighlight: .dangerDark.opacity(0.3),
.sessionButton_destructiveBorder: .dangerDark,
// SolidButton
.solidButton_background: .classicDark3,
// Settings
.settings_tertiaryAction: .primary,
.settings_tabBackground: .classicDark1,
// Appearance
.appearance_sectionBackground: .classicDark1,
.appearance_buttonBackground: .classicDark1,
// Alert
.alert_text: .classicDark6,
.alert_background: .classicDark1,
.alert_buttonBackground: .classicDark1,
// ConversationButton
.conversationButton_background: .classicDark1,
.conversationButton_unreadBackground: .classicDark2,
.conversationButton_unreadStripBackground: .primary,
.conversationButton_unreadBubbleBackground: .classicDark3,
.conversationButton_unreadBubbleText: .classicDark6,
.conversationButton_swipeDestructive: .dangerDark,
.conversationButton_swipeSecondary: .classicDark2,
.conversationButton_swipeTertiary: Theme.PrimaryColor.orange.colorSwiftUI,
.conversationButton_swipeRead: .classicDark3,
// InputButton
.inputButton_background: .classicDark2,
// ContextMenu
.contextMenu_background: .classicDark1,
.contextMenu_highlight: .primary,
.contextMenu_text: .classicDark6,
.contextMenu_textHighlight: .classicDark0,
// Call
.callAccept_background: Theme.PrimaryColor.green.colorSwiftUI,
.callDecline_background: .dangerDark,
// Reactions
.reactions_contextBackground: .classicDark2,
.reactions_contextMoreBackground: .classicDark1,
// NewConversation
.newConversation_background: .classicDark1,
// Profile
.profileIcon: .primary,
.profileIcon_greenPrimaryColor: .black,
.profileIcon_background: .white,
// Unread Marker
.unreadMarker: .primary
]
}

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit.UIColor
import SwiftUI
internal enum Theme_ClassicLight: ThemeColors {
static let theme: [ThemeValue: UIColor] = [
@ -120,4 +121,122 @@ internal enum Theme_ClassicLight: ThemeColors {
// Unread Marker
.unreadMarker: .black
]
static let themeSwiftUI: [ThemeValue: Color] = [
// General
.white: .white,
.black: .black,
.clear: .clear,
.primary: .primary,
.defaultPrimary: Theme.PrimaryColor.green.colorSwiftUI,
.warning: .warning,
.danger: .dangerLight,
.disabled: .disabledLight,
.backgroundPrimary: .classicLight6,
.backgroundSecondary: .classicLight5,
.textPrimary: .classicLight0,
.textSecondary: .classicLight1,
.borderSeparator: .classicLight2,
// Path
.path_connected: .pathConnected,
.path_connecting: .pathConnecting,
.path_error: .pathError,
.path_unknown: .classicLight4,
// TextBox
.textBox_background: .classicLight6,
.textBox_border: .classicLight2,
// MessageBubble
.messageBubble_outgoingBackground: .primary,
.messageBubble_incomingBackground: .classicLight4,
.messageBubble_outgoingText: .classicLight0,
.messageBubble_incomingText: .classicLight0,
.messageBubble_overlay: .black_06,
.messageBubble_deliveryStatus: .classicLight1,
// MenuButton
.menuButton_background: .primary,
.menuButton_icon: .classicLight6,
.menuButton_outerShadow: .classicLight0,
.menuButton_innerShadow: .classicLight6,
// RadioButton
.radioButton_selectedBackground: .primary,
.radioButton_unselectedBackground: .clear,
.radioButton_selectedBorder: .classicLight0,
.radioButton_unselectedBorder: .classicLight0,
.radioButton_disabledSelectedBackground: .disabledLight,
.radioButton_disabledUnselectedBackground: .clear,
.radioButton_disabledBorder: .disabledLight,
// OutlineButton
.sessionButton_text: .classicLight0,
.sessionButton_background: .clear,
.sessionButton_highlight: .classicLight0.opacity(0.1),
.sessionButton_border: .classicLight0,
.sessionButton_filledText: .classicLight6,
.sessionButton_filledBackground: .classicLight0,
.sessionButton_filledHighlight: .classicLight1,
.sessionButton_destructiveText: .dangerLight,
.sessionButton_destructiveBackground: .clear,
.sessionButton_destructiveHighlight: .dangerLight.opacity(0.3),
.sessionButton_destructiveBorder: .dangerLight,
// SolidButton
.solidButton_background: .classicLight3,
// Settings
.settings_tertiaryAction: .classicLight0,
.settings_tabBackground: .classicLight5,
// AppearanceButton
.appearance_sectionBackground: .classicLight6,
.appearance_buttonBackground: .classicLight6,
// Alert
.alert_text: .classicLight0,
.alert_background: .classicLight6,
.alert_buttonBackground: .classicLight6,
// ConversationButton
.conversationButton_background: .classicLight6,
.conversationButton_unreadBackground: .classicLight6,
.conversationButton_unreadStripBackground: .primary,
.conversationButton_unreadBubbleBackground: .classicLight3,
.conversationButton_unreadBubbleText: .classicLight0,
.conversationButton_swipeDestructive: .dangerLight,
.conversationButton_swipeSecondary: .classicLight1,
.conversationButton_swipeTertiary: Theme.PrimaryColor.orange.colorSwiftUI,
.conversationButton_swipeRead: .classicLight3,
// InputButton
.inputButton_background: .classicLight4,
// ContextMenu
.contextMenu_background: .classicLight6,
.contextMenu_highlight: .primary,
.contextMenu_text: .classicLight0,
.contextMenu_textHighlight: .classicLight0,
// Call
.callAccept_background: Theme.PrimaryColor.green.colorSwiftUI,
.callDecline_background: .dangerLight,
// Reactions
.reactions_contextBackground: .classicLight4,
.reactions_contextMoreBackground: .classicLight6,
// NewConversation
.newConversation_background: .classicLight6,
// Profile
.profileIcon: .primary,
.profileIcon_greenPrimaryColor: .primary,
.profileIcon_background: .black,
// Unread Marker
.unreadMarker: .black
]
}

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit.UIColor
import SwiftUI
import SessionUtilitiesKit
// MARK: - Primary Colors
@ -24,6 +25,15 @@ public extension Theme {
self = primaryColor
}
internal init?(color: Color?) {
guard
let color: Color = color,
let primaryColor: PrimaryColor = PrimaryColor.allCases.first(where: { $0.colorSwiftUI == color })
else { return nil }
self = primaryColor
}
public var color: UIColor {
switch self {
case .green: return #colorLiteral(red: 0.1921568627, green: 0.9450980392, blue: 0.5882352941, alpha: 1) // #31F196
@ -35,6 +45,33 @@ public extension Theme {
case .yellow: return #colorLiteral(red: 0.9803921569, green: 0.8392156863, blue: 0.3411764706, alpha: 1) // #FAD657
}
}
// FIXME: Clean it when Xcode can show the color panel in "return Color(#colorLiteral())"
public var colorSwiftUI: Color {
switch self {
case .green:
let color: Color = Color(#colorLiteral(red: 0.1921568627, green: 0.9450980392, blue: 0.5882352941, alpha: 1)) // #31F196
return color
case .blue:
let color: Color = Color(#colorLiteral(red: 0.3411764706, green: 0.7882352941, blue: 0.9803921569, alpha: 1)) // #57C9FA
return color
case .purple:
let color: Color = Color(#colorLiteral(red: 0.7882352941, green: 0.5764705882, blue: 1, alpha: 1)) // #C993FF
return color
case .pink:
let color: Color = Color(#colorLiteral(red: 1, green: 0.5843137255, blue: 0.937254902, alpha: 1)) // #FF95EF
return color
case .red:
let color: Color = Color(#colorLiteral(red: 1, green: 0.6117647059, blue: 0.5568627451, alpha: 1)) // #FF9C8E
return color
case .orange:
let color: Color = Color(#colorLiteral(red: 0.9882352941, green: 0.6941176471, blue: 0.3490196078, alpha: 1)) // #FCB159
return color
case .yellow:
let color: Color = Color(#colorLiteral(red: 0.9803921569, green: 0.8392156863, blue: 0.3411764706, alpha: 1)) // #FAD657
return color
}
}
}
}
@ -92,3 +129,56 @@ public extension UIColor {
return ThemeManager.primaryColor.color
})
}
internal extension Color {
static let warning: Color = Color(#colorLiteral(red: 0.9882352941, green: 0.6941176471, blue: 0.3490196078, alpha: 1)) // #FCB159
static let dangerDark: Color = Color(#colorLiteral(red: 1, green: 0.2274509804, blue: 0.2274509804, alpha: 1)) // #FF3A3A
static let dangerLight: Color = Color(#colorLiteral(red: 0.8823529412, green: 0.1764705882, blue: 0.09803921569, alpha: 1)) // #E12D19
static let disabledDark: Color = Color(#colorLiteral(red: 0.4274509804, green: 0.4274509804, blue: 0.4274509804, alpha: 1 )) // #6D6D6D
static let disabledLight: Color = Color(#colorLiteral(red: 0.631372549, green: 0.6352941176, blue: 0.631372549, alpha: 1)) // #A1A2A1
static let black_06: Color = Color(#colorLiteral(red: 0, green: 0, blue: 0, alpha: 0.06)) // #000000
static let pathConnected: Color = Color(#colorLiteral(red: 0.1921568627, green: 0.9450980392, blue: 0.5882352941, alpha: 1)) // #31F196
static let pathConnecting: Color = Color(#colorLiteral(red: 0.9882352941, green: 0.6941176471, blue: 0.3490196078, alpha: 1)) // #FCB159
static let pathError: Color = Color(#colorLiteral(red: 0.9176470588, green: 0.3333333333, blue: 0.2705882353, alpha: 1)) // #EA5545
static let classicDark0: Color = Color(#colorLiteral(red: 0, green: 0, blue: 0, alpha: 1)) // #000000
static let classicDark1: Color = Color(#colorLiteral(red: 0.1058823529, green: 0.1058823529, blue: 0.1058823529, alpha: 1)) // #1B1B1B
static let classicDark2: Color = Color(#colorLiteral(red: 0.1764705882, green: 0.1764705882, blue: 0.1764705882, alpha: 1)) // #2D2D2D
static let classicDark3: Color = Color(#colorLiteral(red: 0.2549019608, green: 0.2549019608, blue: 0.2549019608, alpha: 1)) // #414141
static let classicDark4: Color = Color(#colorLiteral(red: 0.462745098, green: 0.462745098, blue: 0.462745098, alpha: 1)) // #767676
static let classicDark5: Color = Color(#colorLiteral(red: 0.631372549, green: 0.6352941176, blue: 0.631372549, alpha: 1)) // #A1A2A1
static let classicDark6: Color = Color(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)) // #FFFFFF
static let classicLight0: Color = Color(#colorLiteral(red: 0, green: 0, blue: 0, alpha: 1)) // #000000
static let classicLight1: Color = Color(#colorLiteral(red: 0.4274509804, green: 0.4274509804, blue: 0.4274509804, alpha: 1)) // #6D6D6D
static let classicLight2: Color = Color(#colorLiteral(red: 0.631372549, green: 0.6352941176, blue: 0.631372549, alpha: 1)) // #A1A2A1
static let classicLight3: Color = Color(#colorLiteral(red: 0.8745098039, green: 0.8745098039, blue: 0.8745098039, alpha: 1)) // #DFDFDF
static let classicLight4: Color = Color(#colorLiteral(red: 0.9411764706, green: 0.9411764706, blue: 0.9411764706, alpha: 1)) // #F0F0F0
static let classicLight5: Color = Color(#colorLiteral(red: 0.9764705882, green: 0.9764705882, blue: 0.9764705882, alpha: 1)) // #F9F9F9
static let classicLight6: Color = Color(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)) // #FFFFFF
static let oceanDark0: Color = Color(#colorLiteral(red: 0, green: 0, blue: 0, alpha: 1)) // #000000
static let oceanDark1: Color = Color(#colorLiteral(red: 0.1019607843, green: 0.1098039216, blue: 0.1568627451, alpha: 1)) // #1A1C28
static let oceanDark2: Color = Color(#colorLiteral(red: 0.1450980392, green: 0.1529411765, blue: 0.2078431373, alpha: 1)) // #252735
static let oceanDark3: Color = Color(#colorLiteral(red: 0.168627451, green: 0.1764705882, blue: 0.2509803922, alpha: 1)) // #2B2D40
static let oceanDark4: Color = Color(#colorLiteral(red: 0.2392156863, green: 0.2901960784, blue: 0.3647058824, alpha: 1)) // #3D4A5D
static let oceanDark5: Color = Color(#colorLiteral(red: 0.6509803922, green: 0.662745098, blue: 0.8078431373, alpha: 1)) // #A6A9CE
static let oceanDark6: Color = Color(#colorLiteral(red: 0.3607843137, green: 0.6666666667, blue: 0.8, alpha: 1)) // #5CAACC
static let oceanDark7: Color = Color(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)) // #FFFFFF
static let oceanLight0: Color = Color(#colorLiteral(red: 0, green: 0, blue: 0, alpha: 1)) // #000000
static let oceanLight1: Color = Color(#colorLiteral(red: 0.09803921569, green: 0.2039215686, blue: 0.3647058824, alpha: 1)) // #19345D
static let oceanLight2: Color = Color(#colorLiteral(red: 0.4156862745, green: 0.431372549, blue: 0.5647058824, alpha: 1)) // #6A6E90
static let oceanLight3: Color = Color(#colorLiteral(red: 0.3607843137, green: 0.6666666667, blue: 0.8, alpha: 1)) // #5CAACC
static let oceanLight4: Color = Color(#colorLiteral(red: 0.7019607843, green: 0.9294117647, blue: 0.9490196078, alpha: 1)) // #B3EDF2
static let oceanLight5: Color = Color(#colorLiteral(red: 0.9058823529, green: 0.9529411765, blue: 0.9568627451, alpha: 1)) // #E7F3F4
static let oceanLight6: Color = Color(#colorLiteral(red: 0.9254901961, green: 0.9803921569, blue: 0.9843137255, alpha: 1)) // #ECFAFB
static let oceanLight7: Color = Color(#colorLiteral(red: 0.9882352941, green: 1, blue: 1, alpha: 1)) // #FCFFFF
}
public extension Color {
static var primary: Color {
return Color(UIColor.primary)
}
}

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit.UIColor
import SwiftUI
internal enum Theme_OceanDark: ThemeColors {
static let theme: [ThemeValue: UIColor] = [
@ -120,4 +121,122 @@ internal enum Theme_OceanDark: ThemeColors {
// Unread Marker
.unreadMarker: .primary
]
static let themeSwiftUI: [ThemeValue: Color] = [
// General
.white: .white,
.black: .black,
.clear: .clear,
.primary: .primary,
.defaultPrimary: Theme.PrimaryColor.blue.colorSwiftUI,
.warning: .warning,
.danger: .dangerDark,
.disabled: .disabledDark,
.backgroundPrimary: .oceanDark2,
.backgroundSecondary: .oceanDark1,
.textPrimary: .oceanDark7,
.textSecondary: .oceanDark5,
.borderSeparator: .oceanDark4,
// Path
.path_connected: .pathConnected,
.path_connecting: .pathConnecting,
.path_error: .pathError,
.path_unknown: .oceanDark4,
// TextBox
.textBox_background: .oceanDark1,
.textBox_border: .oceanDark4,
// MessageBubble
.messageBubble_outgoingBackground: .primary,
.messageBubble_incomingBackground: .oceanDark4,
.messageBubble_outgoingText: .oceanDark0,
.messageBubble_incomingText: .oceanDark7,
.messageBubble_overlay: .black_06,
.messageBubble_deliveryStatus: .oceanDark5,
// MenuButton
.menuButton_background: .primary,
.menuButton_icon: .oceanDark7,
.menuButton_outerShadow: .primary,
.menuButton_innerShadow: .oceanDark7,
// RadioButton
.radioButton_selectedBackground: .primary,
.radioButton_unselectedBackground: .clear,
.radioButton_selectedBorder: .oceanDark7,
.radioButton_unselectedBorder: .oceanDark7,
.radioButton_disabledSelectedBackground: .disabledDark,
.radioButton_disabledUnselectedBackground: .clear,
.radioButton_disabledBorder: .disabledDark,
// SessionButton
.sessionButton_text: .primary,
.sessionButton_background: .clear,
.sessionButton_highlight: .oceanDark7.opacity(0.3),
.sessionButton_border: .primary,
.sessionButton_filledText: .oceanDark7,
.sessionButton_filledBackground: .oceanDark1,
.sessionButton_filledHighlight: .oceanDark3,
.sessionButton_destructiveText: .dangerDark,
.sessionButton_destructiveBackground: .clear,
.sessionButton_destructiveHighlight: .dangerDark.opacity(0.3),
.sessionButton_destructiveBorder: .dangerDark,
// SolidButton
.solidButton_background: .oceanDark2,
// Settings
.settings_tertiaryAction: .primary,
.settings_tabBackground: .oceanDark1,
// Appearance
.appearance_sectionBackground: .oceanDark3,
.appearance_buttonBackground: .oceanDark3,
// Alert
.alert_text: .oceanDark7,
.alert_background: .oceanDark3,
.alert_buttonBackground: .oceanDark3,
// ConversationButton
.conversationButton_background: .oceanDark3,
.conversationButton_unreadBackground: .oceanDark4,
.conversationButton_unreadStripBackground: .primary,
.conversationButton_unreadBubbleBackground: .primary,
.conversationButton_unreadBubbleText: .oceanDark0,
.conversationButton_swipeDestructive: .dangerDark,
.conversationButton_swipeSecondary: .oceanDark2,
.conversationButton_swipeTertiary: Theme.PrimaryColor.orange.colorSwiftUI,
.conversationButton_swipeRead: .primary,
// InputButton
.inputButton_background: .oceanDark4,
// ContextMenu
.contextMenu_background: .oceanDark2,
.contextMenu_highlight: .primary,
.contextMenu_text: .oceanDark7,
.contextMenu_textHighlight: .oceanDark0,
// Call
.callAccept_background: Theme.PrimaryColor.green.colorSwiftUI,
.callDecline_background: .dangerDark,
// Reactions
.reactions_contextBackground: .oceanDark1,
.reactions_contextMoreBackground: .oceanDark2,
// NewConversation
.newConversation_background: .oceanDark3,
// Profile
.profileIcon: .primary,
.profileIcon_greenPrimaryColor: .black,
.profileIcon_background: .white,
// Unread Marker
.unreadMarker: .primary
]
}

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit.UIColor
import SwiftUI
internal enum Theme_OceanLight: ThemeColors {
static let theme: [ThemeValue: UIColor] = [
@ -120,4 +121,122 @@ internal enum Theme_OceanLight: ThemeColors {
// Unread Marker
.unreadMarker: .black
]
static let themeSwiftUI: [ThemeValue: Color] = [
// General
.white: .white,
.black: .black,
.clear: .clear,
.primary: .primary,
.defaultPrimary: Theme.PrimaryColor.blue.colorSwiftUI,
.warning: .warning,
.danger: .dangerLight,
.disabled: .disabledLight,
.backgroundPrimary: .oceanLight7,
.backgroundSecondary: .oceanLight6,
.textPrimary: .oceanLight1,
.textSecondary: .oceanLight2,
.borderSeparator: .oceanLight3,
// Path
.path_connected: .pathConnected,
.path_connecting: .pathConnecting,
.path_error: .pathError,
.path_unknown: .oceanLight5,
// TextBox
.textBox_background: .oceanLight7,
.textBox_border: .oceanLight3,
// MessageBubble
.messageBubble_outgoingBackground: .primary,
.messageBubble_incomingBackground: .oceanLight4,
.messageBubble_outgoingText: .oceanLight1,
.messageBubble_incomingText: .oceanLight1,
.messageBubble_overlay: .black_06,
.messageBubble_deliveryStatus: .oceanLight2,
// MenuButton
.menuButton_background: .primary,
.menuButton_icon: .white,
.menuButton_outerShadow: .black,
.menuButton_innerShadow: .white,
// RadioButton
.radioButton_selectedBackground: .primary,
.radioButton_unselectedBackground: .clear,
.radioButton_selectedBorder: .oceanLight1,
.radioButton_unselectedBorder: .oceanLight3,
.radioButton_disabledSelectedBackground: .disabledLight,
.radioButton_disabledUnselectedBackground: .clear,
.radioButton_disabledBorder: .disabledLight,
// SessionButton
.sessionButton_text: .oceanLight1,
.sessionButton_background: .clear,
.sessionButton_highlight: .oceanLight1.opacity(0.1),
.sessionButton_border: .oceanLight1,
.sessionButton_filledText: .oceanLight7,
.sessionButton_filledBackground: .oceanLight1,
.sessionButton_filledHighlight: .oceanLight2,
.sessionButton_destructiveText: .dangerLight,
.sessionButton_destructiveBackground: .clear,
.sessionButton_destructiveHighlight: .dangerLight.opacity(0.3),
.sessionButton_destructiveBorder: .dangerLight,
// SolidButton
.solidButton_background: .oceanLight5,
// Settings
.settings_tertiaryAction: .oceanLight1,
.settings_tabBackground: .oceanLight6,
// Appearance
.appearance_sectionBackground: .oceanLight7,
.appearance_buttonBackground: .oceanLight7,
// Alert
.alert_text: .oceanLight0,
.alert_background: .oceanLight7,
.alert_buttonBackground: .oceanLight7,
// ConversationButton
.conversationButton_background: .oceanLight7,
.conversationButton_unreadBackground: .oceanLight6,
.conversationButton_unreadStripBackground: .primary,
.conversationButton_unreadBubbleBackground: .primary,
.conversationButton_unreadBubbleText: .oceanLight1,
.conversationButton_swipeDestructive: .dangerLight,
.conversationButton_swipeSecondary: .oceanLight2,
.conversationButton_swipeTertiary: Theme.PrimaryColor.orange.colorSwiftUI,
.conversationButton_swipeRead: .primary,
// InputButton
.inputButton_background: .oceanLight5,
// ContextMenu
.contextMenu_background: .oceanLight7,
.contextMenu_highlight: .primary,
.contextMenu_text: .oceanLight0,
.contextMenu_textHighlight: .oceanLight0,
// Call
.callAccept_background: Theme.PrimaryColor.green.colorSwiftUI,
.callDecline_background: .dangerLight,
// Reactions
.reactions_contextBackground: .oceanLight7,
.reactions_contextMoreBackground: .oceanLight6,
// NewConversation
.newConversation_background: .oceanLight7,
// Profile
.profileIcon: .primary,
.profileIcon_greenPrimaryColor: .primary,
.profileIcon_background: .oceanLight1,
// Unread Marker
.unreadMarker: .black
]
}

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit.UIColor
import SwiftUI
import SessionUtilitiesKit
// MARK: - Theme
@ -65,12 +66,36 @@ public enum Theme: String, CaseIterable, Codable, EnumStringSetting {
default: return colors[value]
}
}
private var colorsSwiftUI: [ThemeValue: Color] {
switch self {
case .classicDark: return Theme_ClassicDark.themeSwiftUI
case .classicLight: return Theme_ClassicLight.themeSwiftUI
case .oceanDark: return Theme_OceanDark.themeSwiftUI
case .oceanLight: return Theme_OceanLight.themeSwiftUI
}
}
public func colorSwiftUI(for themeValue: ThemeValue) -> Color? {
switch themeValue {
case .value(let value, let alpha): return colorSwiftUI(for: value)?.opacity(alpha)
case .highlighted(let value, let alwaysDarken):
switch (self.interfaceStyle, alwaysDarken) {
case (.light, _), (_, true): return (colorSwiftUI(for: value)?.grayscale(0.06) as? Color)
default: return (colorSwiftUI(for: value)?.brightness(0.08) as? Color)
}
default: return colorsSwiftUI[themeValue]
}
}
}
// MARK: - ThemeColors
public protocol ThemeColors {
static var theme: [ThemeValue: UIColor] { get }
static var themeSwiftUI: [ThemeValue: Color] { get }
}
// MARK: - ThemedNavigation

@ -14,8 +14,10 @@ public final class Values : NSObject {
@objc public static let verySmallFontSize = isIPhone5OrSmaller ? CGFloat(10) : CGFloat(12)
@objc public static let smallFontSize = isIPhone5OrSmaller ? CGFloat(13) : CGFloat(15)
@objc public static let mediumFontSize = isIPhone5OrSmaller ? CGFloat(15) : CGFloat(17)
@objc public static let largeFontSize = isIPhone5OrSmaller ? CGFloat(17) : CGFloat(19)
@objc public static let veryLargeFontSize = isIPhone5OrSmaller ? CGFloat(22) : CGFloat(24)
@objc public static let mediumLargeFontSize = isIPhone5OrSmaller ? CGFloat(17) : CGFloat(19)
@objc public static let largeFontSize = isIPhone5OrSmaller ? CGFloat(20) : CGFloat(22)
@objc public static let veryLargeFontSize = isIPhone5OrSmaller ? CGFloat(24) : CGFloat(26)
@objc public static let superLargeFontSize = isIPhone5OrSmaller ? CGFloat(31) : CGFloat(33)
@objc public static let massiveFontSize = CGFloat(50)
// MARK: - Element Sizes

@ -0,0 +1,91 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import SwiftUI
import UIKit
import SessionUtilitiesKit
struct ViewControllerHolder {
weak var value: UIViewController?
}
struct ViewControllerKey: EnvironmentKey {
static var defaultValue: ViewControllerHolder {
return ViewControllerHolder(value: Singleton.appContext.mainWindow?.rootViewController)
}
}
extension EnvironmentValues {
public var viewController: UIViewController? {
get { return self[ViewControllerKey.self].value }
set { self[ViewControllerKey.self].value = newValue }
}
}
public struct UIView_SwiftUI: UIViewRepresentable {
public typealias UIViewType = UIView
private let view: UIView
public init(view: UIView) {
self.view = view
}
public func makeUIView(context: Context) -> UIView {
return self.view
}
public func updateUIView(_ uiView: UIView, context: Context) {
uiView.layoutIfNeeded()
}
}
// MARK: MaxWidthEqualizer
/// PreferenceKey to report the max width of the view.
struct MaxWidthPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0.0
// We `reduce` to just take the max value from all values reported.
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
/// Convenience view modifier that observe its size, and notify the value back to parent view via `MaxWidthPreferenceKey`.
public struct MaxWidthNotify: ViewModifier {
/// We embed a transparent background view, to the current view to get the size via `GeometryReader`.
/// The `MaxWidthPreferenceKey` will be reported, when the frame of this view is updated.
private var sizeView: some View {
GeometryReader { geometry in
Color.clear.preference(key: MaxWidthPreferenceKey.self, value: geometry.frame(in: .global).size.width)
}
}
public func body(content: Content) -> some View {
content.background(sizeView)
}
}
/// Convenience modifier to use in the parent view to observe `MaxWidthPreferenceKey` from children, and bind the value to `$width`.
public struct MaxWidthEqualizer: ViewModifier {
@Binding var width: CGFloat?
public static var notify: MaxWidthNotify {
MaxWidthNotify()
}
public init(width: Binding<CGFloat?>) {
self._width = width
}
public func body(content: Content) -> some View {
content.onPreferenceChange(MaxWidthPreferenceKey.self) { value in
let oldWidth: CGFloat = width ?? 0
if value > oldWidth {
width = value
}
}
}
}

@ -0,0 +1,74 @@
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
import Foundation
import AVFoundation
import SessionMessagingKit
import SignalCoreKit
public protocol OWSVideoPlayerDelegate: AnyObject {
func videoPlayerDidPlayToCompletion(_ videoPlayer: OWSVideoPlayer)
}
public class OWSVideoPlayer {
public let avPlayer: AVPlayer
let audioActivity: AudioActivity
public weak var delegate: OWSVideoPlayerDelegate?
@objc public init(url: URL) {
self.avPlayer = AVPlayer(url: url)
self.audioActivity = AudioActivity(audioDescription: "[OWSVideoPlayer] url:\(url)", behavior: .playback)
NotificationCenter.default.addObserver(self,
selector: #selector(playerItemDidPlayToCompletion(_:)),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: avPlayer.currentItem)
}
// MARK: Playback Controls
@objc
public func pause() {
avPlayer.pause()
SessionEnvironment.shared?.audioSession.endAudioActivity(self.audioActivity)
}
@objc
public func play() {
let success = (SessionEnvironment.shared?.audioSession.startAudioActivity(self.audioActivity) == true)
assert(success)
guard let item = avPlayer.currentItem else {
owsFailDebug("video player item was unexpectedly nil")
return
}
if item.currentTime() == item.duration {
// Rewind for repeated plays, but only if it previously played to end.
avPlayer.seek(to: CMTime.zero, toleranceBefore: .zero, toleranceAfter: .zero)
}
avPlayer.play()
}
@objc
public func stop() {
avPlayer.pause()
avPlayer.seek(to: CMTime.zero, toleranceBefore: .zero, toleranceAfter: .zero)
SessionEnvironment.shared?.audioSession.endAudioActivity(self.audioActivity)
}
@objc(seekToTime:)
public func seek(to time: CMTime) {
avPlayer.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero)
}
// MARK: private
@objc
private func playerItemDidPlayToCompletion(_ notification: Notification) {
self.delegate?.videoPlayerDidPlayToCompletion(self)
SessionEnvironment.shared?.audioSession.endAudioActivity(self.audioActivity)
}
}

@ -60,7 +60,7 @@ public enum AppSetup {
)
assert(success)
Environment.shared = Environment(
SessionEnvironment.shared = SessionEnvironment(
reachabilityManager: SSKReachabilityManagerImpl(),
audioSession: OWSAudioSession(),
proximityMonitoringManager: OWSProximityMonitoringManagerImpl(),

Loading…
Cancel
Save