From 7715c5ea09c2b9597c1392b2b857fc426bfb7fa5 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 14 Sep 2022 18:32:23 +1000 Subject: [PATCH] Did some more styling and fixed a few UI bugs Fixed a bug where the time label would no longer appear in the context menu Fixed a bug where the tile label in the context menu could be clipped Tweaked the context menu appearance animation to look less jumpy when at the edges of the screen --- Session.xcodeproj/project.pbxproj | 20 +- .../ContextMenuVC+ActionView.swift | 93 +++++-- .../ContextMenuVC+EmojiReactsView.swift | 8 +- .../Context Menu/ContextMenuVC.swift | 139 ++++++---- .../Conversations/ConversationSearch.swift | 10 +- .../ConversationVC+Interaction.swift | 107 ++++++-- Session/Conversations/ConversationVC.swift | 2 +- .../Conversations/ConversationViewModel.swift | 134 ++-------- .../EmojiPickerCollectionView.swift | 8 +- .../Conversations/Input View/InputView.swift | 47 +++- .../Input View/InputViewButton.swift | 9 +- .../Input View/MentionSelectionView.swift | 18 +- .../VoiceMessageRecordingView.swift | 249 +++++++++++------- .../Message Cells/CallMessageCell.swift | 5 +- .../Content Views/CallMessageView.swift | 57 ---- .../Content Views/ReactionContainerView.swift | 57 +++- .../Content Views/ReactionView.swift | 36 +-- .../Message Cells/MessageCell.swift | 1 - .../Message Cells/VisibleMessageCell.swift | 4 +- .../Views & Modals/JoinOpenGroupModal.swift | 117 -------- .../Views & Modals/ReactionListSheet.swift | 33 +-- .../Views & Modals/ScrollToBottomButton.swift | 9 +- .../Views & Modals/SendSeedModal.swift | 75 ------ .../Views & Modals/UserDetailsSheet.swift | 84 ------ Session/Shared/UserCell.swift | 8 +- .../Sheets & Modals/ConfirmationModal.swift | 8 +- .../Shared Models/MentionInfo.swift | 110 ++++++++ .../Shared Models/MessageViewModel.swift | 18 +- .../SessionThreadViewModel.swift | 8 +- .../Themes/Theme+ClassicDark.swift | 11 +- .../Themes/Theme+ClassicLight.swift | 11 +- .../Style Guide/Themes/Theme+OceanDark.swift | 11 +- .../Style Guide/Themes/Theme+OceanLight.swift | 11 +- SessionUIKit/Style Guide/Themes/Theme.swift | 9 + 34 files changed, 788 insertions(+), 739 deletions(-) delete mode 100644 Session/Conversations/Message Cells/Content Views/CallMessageView.swift delete mode 100644 Session/Conversations/Views & Modals/JoinOpenGroupModal.swift delete mode 100644 Session/Conversations/Views & Modals/SendSeedModal.swift delete mode 100644 Session/Conversations/Views & Modals/UserDetailsSheet.swift create mode 100644 SessionMessagingKit/Shared Models/MentionInfo.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 1167cdaef..39731d7e0 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -121,7 +121,6 @@ 7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */; }; 7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */; }; 7B7037452834BCC0000DCF35 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037442834BCC0000DCF35 /* ReactionView.swift */; }; - 7B7CB189270430D20079FF93 /* CallMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB188270430D20079FF93 /* CallMessageView.swift */; }; 7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */; }; 7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */; }; 7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */; }; @@ -201,7 +200,6 @@ B83524A525C3BA4B0089A44F /* InfoMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B83524A425C3BA4B0089A44F /* InfoMessageCell.swift */; }; B83F2B88240CB75A000A54AB /* UIImage+Scaling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B83F2B87240CB75A000A54AB /* UIImage+Scaling.swift */; }; B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B84664F4235022F30083A1CD /* MentionUtilities.swift */; }; - B848A4C5269EAAA200617031 /* UserDetailsSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B848A4C4269EAAA200617031 /* UserDetailsSheet.swift */; }; B849789625D4A2F500D0D0B3 /* LinkPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B849789525D4A2F500D0D0B3 /* LinkPreviewView.swift */; }; B84A89BC25DE328A0040017D /* ProfilePictureVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B84A89BB25DE328A0040017D /* ProfilePictureVC.swift */; }; B85357BF23A1AE0800AAF6CD /* SeedReminderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85357BE23A1AE0800AAF6CD /* SeedReminderView.swift */; }; @@ -210,7 +208,6 @@ B8569AE325CBB19A00DBA3DB /* DocumentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8569AE225CBB19A00DBA3DB /* DocumentView.swift */; }; B86BD08423399ACF000F5AE3 /* Modal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08323399ACF000F5AE3 /* Modal.swift */; }; B86BD08623399CEF000F5AE3 /* SeedModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08523399CEF000F5AE3 /* SeedModal.swift */; }; - B875885A264503A6000E60D0 /* JoinOpenGroupModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8758859264503A6000E60D0 /* JoinOpenGroupModal.swift */; }; B877E24226CA12910007970A /* CallVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B877E24126CA12910007970A /* CallVC.swift */; }; B877E24626CA13BA0007970A /* CallVC+Camera.swift in Sources */ = {isa = PBXBuildFile; fileRef = B877E24526CA13BA0007970A /* CallVC+Camera.swift */; }; B8783E9E23EB948D00404FB8 /* UILabel+Interaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8783E9D23EB948D00404FB8 /* UILabel+Interaction.swift */; }; @@ -236,7 +233,6 @@ B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */; }; B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0742339EDCF00B4D94D /* NukeDataModal.swift */; }; B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */; }; - B8AF4BB426A5204600583500 /* SendSeedModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AF4BB326A5204600583500 /* SendSeedModal.swift */; }; B8B320B7258C30D70020074B /* HTMLMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B320B6258C30D70020074B /* HTMLMetadata.swift */; }; B8B558F126C4BB0600693325 /* CameraManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B558F026C4BB0600693325 /* CameraManager.swift */; }; B8B558FF26C4E05E00693325 /* WebRTCSession+MessageHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B558FE26C4E05E00693325 /* WebRTCSession+MessageHandling.swift */; }; @@ -719,6 +715,7 @@ FD71161528D00D6700B47552 /* ThreadDisappearingMessagesViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71161428D00D6700B47552 /* ThreadDisappearingMessagesViewModelSpec.swift */; }; FD71161728D00DA400B47552 /* ThreadSettingsViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71161628D00DA400B47552 /* ThreadSettingsViewModelSpec.swift */; }; FD71161A28D00E1100B47552 /* NotificationContentViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71161928D00E1100B47552 /* NotificationContentViewModelSpec.swift */; }; + FD71161C28D194FB00B47552 /* MentionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71161B28D194FB00B47552 /* MentionInfo.swift */; }; FD7162DB281B6C440060647B /* TypedTableAlias.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7162DA281B6C440060647B /* TypedTableAlias.swift */; }; FD716E6428502DDD00C96BF4 /* CallManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD716E6328502DDD00C96BF4 /* CallManagerProtocol.swift */; }; FD716E6628502EE200C96BF4 /* CurrentCallProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD716E6528502EE200C96BF4 /* CurrentCallProtocol.swift */; }; @@ -1198,7 +1195,6 @@ 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = ""; }; 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionContainerView.swift; sourceTree = ""; }; 7B7037442834BCC0000DCF35 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = ""; }; - 7B7CB188270430D20079FF93 /* CallMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMessageView.swift; sourceTree = ""; }; 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallBanner.swift; sourceTree = ""; }; 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiniCallView.swift; sourceTree = ""; }; 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallRingTonePlayer.swift; sourceTree = ""; }; @@ -1293,7 +1289,6 @@ B83F2B87240CB75A000A54AB /* UIImage+Scaling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Scaling.swift"; sourceTree = ""; }; B84664F4235022F30083A1CD /* MentionUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionUtilities.swift; sourceTree = ""; }; B847570023D568EB00759540 /* SignalServiceKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SignalServiceKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - B848A4C4269EAAA200617031 /* UserDetailsSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailsSheet.swift; sourceTree = ""; }; B849789525D4A2F500D0D0B3 /* LinkPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewView.swift; sourceTree = ""; }; B84A89BB25DE328A0040017D /* ProfilePictureVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePictureVC.swift; sourceTree = ""; }; B85357BE23A1AE0800AAF6CD /* SeedReminderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedReminderView.swift; sourceTree = ""; }; @@ -1304,7 +1299,6 @@ B86BD08323399ACF000F5AE3 /* Modal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modal.swift; sourceTree = ""; }; B86BD08523399CEF000F5AE3 /* SeedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedModal.swift; sourceTree = ""; }; B87588582644CA9D000E60D0 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; - B8758859264503A6000E60D0 /* JoinOpenGroupModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinOpenGroupModal.swift; sourceTree = ""; }; B877E24126CA12910007970A /* CallVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVC.swift; sourceTree = ""; }; B877E24526CA13BA0007970A /* CallVC+Camera.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CallVC+Camera.swift"; sourceTree = ""; }; B8783E9D23EB948D00404FB8 /* UILabel+Interaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+Interaction.swift"; sourceTree = ""; }; @@ -1322,7 +1316,6 @@ B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanQRCodeWrapperVC.swift; sourceTree = ""; }; B894D0742339EDCF00B4D94D /* NukeDataModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeDataModal.swift; sourceTree = ""; }; B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = ""; }; - B8AF4BB326A5204600583500 /* SendSeedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendSeedModal.swift; sourceTree = ""; }; B8B320B6258C30D70020074B /* HTMLMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLMetadata.swift; sourceTree = ""; }; B8B558F026C4BB0600693325 /* CameraManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraManager.swift; sourceTree = ""; }; B8B558FE26C4E05E00693325 /* WebRTCSession+MessageHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebRTCSession+MessageHandling.swift"; sourceTree = ""; }; @@ -1818,6 +1811,7 @@ FD71161428D00D6700B47552 /* ThreadDisappearingMessagesViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadDisappearingMessagesViewModelSpec.swift; sourceTree = ""; }; FD71161628D00DA400B47552 /* ThreadSettingsViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSettingsViewModelSpec.swift; sourceTree = ""; }; FD71161928D00E1100B47552 /* NotificationContentViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentViewModelSpec.swift; sourceTree = ""; }; + FD71161B28D194FB00B47552 /* MentionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionInfo.swift; sourceTree = ""; }; FD7162DA281B6C440060647B /* TypedTableAlias.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedTableAlias.swift; sourceTree = ""; }; FD716E6328502DDD00C96BF4 /* CallManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManagerProtocol.swift; sourceTree = ""; }; FD716E6528502EE200C96BF4 /* CurrentCallProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentCallProtocol.swift; sourceTree = ""; }; @@ -2368,7 +2362,6 @@ B8D84EA225DF745A005A043E /* LinkPreviewState.swift */, B8EB20EF2640F7F000773E52 /* OpenGroupInvitationView.swift */, 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */, - 7B7CB188270430D20079FF93 /* CallMessageView.swift */, 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */, 7B7037442834BCC0000DCF35 /* ReactionView.swift */, ); @@ -2392,11 +2385,8 @@ isa = PBXGroup; children = ( B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */, - B8AF4BB326A5204600583500 /* SendSeedModal.swift */, - B8758859264503A6000E60D0 /* JoinOpenGroupModal.swift */, B82149C025D605C6009C0F2A /* InfoBanner.swift */, C374EEEA25DA3CA70073A857 /* ConversationTitleView.swift */, - B848A4C4269EAAA200617031 /* UserDetailsSheet.swift */, FD4B200D283492210034334B /* InsetLockableTableView.swift */, 7B9F71C828470667006DFE7B /* ReactionListSheet.swift */, ); @@ -3843,6 +3833,7 @@ FD848B8C283E0B26000E298B /* MessageInputTypes.swift */, FD848B86283B844B000E298B /* MessageViewModel.swift */, FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */, + FD71161B28D194FB00B47552 /* MentionInfo.swift */, ); path = "Shared Models"; sourceTree = ""; @@ -5462,6 +5453,7 @@ FDF0B74928060D13004C14C5 /* QuotedReplyModel.swift in Sources */, FD77289A284AF1BD0018502F /* Sodium+Utilities.swift in Sources */, FD5C7309285007920029977D /* BlindedIdLookup.swift in Sources */, + FD71161C28D194FB00B47552 /* MentionInfo.swift in Sources */, 7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */, C300A5F22554B09800555489 /* MessageSender.swift in Sources */, B8B558FF26C4E05E00693325 /* WebRTCSession+MessageHandling.swift in Sources */, @@ -5707,7 +5699,6 @@ B82B408E239DC00D00A248E7 /* DisplayNameVC.swift in Sources */, B835246E25C38ABF0089A44F /* ConversationVC.swift in Sources */, 7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */, - B8AF4BB426A5204600583500 /* SendSeedModal.swift in Sources */, B877E24226CA12910007970A /* CallVC.swift in Sources */, 7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */, C374EEEB25DA3CA70073A857 /* ConversationTitleView.swift in Sources */, @@ -5725,7 +5716,6 @@ B8F5F71A25F1B35C003BF8D4 /* MediaPlaceholderView.swift in Sources */, 4C21D5D8223AC60F00EF8A77 /* PhotoCapture.swift in Sources */, C331FFF32558FF0300070591 /* PathStatusView.swift in Sources */, - B848A4C5269EAAA200617031 /* UserDetailsSheet.swift in Sources */, 34B6A903218B3F63007C4606 /* TypingIndicatorView.swift in Sources */, 7BAF54CF27ACCEEC003D12F8 /* GlobalSearchViewController.swift in Sources */, FD37EA1728AC5605003AE748 /* NotificationContentViewModel.swift in Sources */, @@ -5749,7 +5739,6 @@ FD7115F428C71EB200B47552 /* ThreadDisappearingMessagesViewModel.swift in Sources */, B8D84ECF25E3108A005A043E /* ExpandingAttachmentsButton.swift in Sources */, 7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */, - B875885A264503A6000E60D0 /* JoinOpenGroupModal.swift in Sources */, FD87DCFC28B755B800AF0F98 /* BlockedContactsViewController.swift in Sources */, B8CCF6432397711F0091D419 /* SettingsVC.swift in Sources */, 7B13E1E92810F01300BD4F64 /* SessionCallManager+Action.swift in Sources */, @@ -5770,7 +5759,6 @@ B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */, 7B9F71D72853100A006DFE7B /* Emoji+Available.swift in Sources */, FD09C5E628260FF9000CE219 /* MediaGalleryViewModel.swift in Sources */, - 7B7CB189270430D20079FF93 /* CallMessageView.swift in Sources */, 7B9F71D32852EEE2006DFE7B /* Emoji.swift in Sources */, C328250F25CA06020062D0A7 /* VoiceMessageView.swift in Sources */, FD7115F028C5D7DE00B47552 /* SettingHeaderView.swift in Sources */, diff --git a/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift b/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift index 21648bd31..2e8e37b2d 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift @@ -11,6 +11,27 @@ extension ContextMenuVC { private let action: Action private let dismiss: () -> Void + private var didTouchDownInside: Bool = false + + // MARK: - UI + + private let iconImageView: UIImageView = { + let result: UIImageView = UIImageView() + result.contentMode = .center + result.themeTintColor = .textPrimary + result.set(.width, to: ActionView.iconImageViewSize) + result.set(.height, to: ActionView.iconImageViewSize) + + return result + }() + + private let titleLabel: UILabel = { + let result: UILabel = UILabel() + result.font = .systemFont(ofSize: Values.mediumFontSize) + result.themeTextColor = .textPrimary + + return result + }() // MARK: - Lifecycle @@ -32,23 +53,12 @@ extension ContextMenuVC { } private func setUpViewHierarchy() { - // Icon - let iconSize = ActionView.iconSize - let iconImageView: UIImageView = UIImageView( - image: action.icon? - .resizedImage(to: CGSize(width: iconSize, height: iconSize))? - .withRenderingMode(.alwaysTemplate) - ) - iconImageView.set(.width, to: ActionView.iconImageViewSize) - iconImageView.set(.height, to: ActionView.iconImageViewSize) - iconImageView.contentMode = .center - iconImageView.tintColor = Colors.text + themeBackgroundColor = .clear - // Title - let titleLabel = UILabel() + iconImageView.image = action.icon? + .resizedImage(to: CGSize(width: ActionView.iconSize, height: ActionView.iconSize))? + .withRenderingMode(.alwaysTemplate) titleLabel.text = action.title - titleLabel.textColor = Colors.text - titleLabel.font = .systemFont(ofSize: Values.mediumFontSize) // Stack view let stackView: UIStackView = UIStackView(arrangedSubviews: [ iconImageView, titleLabel ]) @@ -78,5 +88,58 @@ extension ContextMenuVC { action.work() dismiss() } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + guard + isUserInteractionEnabled, + let location: CGPoint = touches.first?.location(in: self), + bounds.contains(location) + else { return } + + didTouchDownInside = true + themeBackgroundColor = .contextMenu_highlight + iconImageView.themeTintColor = .contextMenu_textHighlight + titleLabel.themeTextColor = .contextMenu_textHighlight + } + + override func touchesMoved(_ touches: Set, with event: UIEvent?) { + guard + isUserInteractionEnabled, + let location: CGPoint = touches.first?.location(in: self), + bounds.contains(location), + didTouchDownInside + else { + if didTouchDownInside { + themeBackgroundColor = .clear + iconImageView.themeTintColor = .textPrimary + titleLabel.themeTextColor = .textPrimary + } + return + } + + themeBackgroundColor = .contextMenu_highlight + iconImageView.themeTintColor = .contextMenu_textHighlight + titleLabel.themeTextColor = .contextMenu_textHighlight + } + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + if didTouchDownInside { + themeBackgroundColor = .clear + iconImageView.themeTintColor = .textPrimary + titleLabel.themeTextColor = .textPrimary + } + + didTouchDownInside = false + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent?) { + if didTouchDownInside { + themeBackgroundColor = .clear + iconImageView.themeTintColor = .textPrimary + titleLabel.themeTextColor = .textPrimary + } + + didTouchDownInside = false + } } } diff --git a/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift b/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift index 6ba658182..5cedd297f 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift @@ -33,9 +33,9 @@ extension ContextMenuVC { } private func setUpViewHierarchy() { - let emojiLabel = UILabel() - emojiLabel.text = self.action.title + let emojiLabel: UILabel = UILabel() emojiLabel.font = .systemFont(ofSize: Values.veryLargeFontSize) + emojiLabel.text = self.action.title emojiLabel.set(.height, to: ContextMenuVC.EmojiReactsView.size) addSubview(emojiLabel) emojiLabel.pin(to: self) @@ -84,7 +84,7 @@ extension ContextMenuVC { private func setUpViewHierarchy() { // Icon image let iconImageView = UIImageView(image: #imageLiteral(resourceName: "ic_plus_24").withRenderingMode(.alwaysTemplate)) - iconImageView.tintColor = Colors.text + iconImageView.themeTintColor = .textPrimary iconImageView.set(.width, to: iconSize) iconImageView.set(.height, to: iconSize) iconImageView.contentMode = .scaleAspectFit @@ -93,7 +93,7 @@ extension ContextMenuVC { // Background isUserInteractionEnabled = true - backgroundColor = Colors.sessionEmojiPlusButtonBackground + themeBackgroundColor = .reactions_contextMoreBackground // Tap gesture recognizer let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) diff --git a/Session/Conversations/Context Menu/ContextMenuVC.swift b/Session/Conversations/Context Menu/ContextMenuVC.swift index 976e8febf..51c216874 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC.swift @@ -10,6 +10,7 @@ final class ContextMenuVC: UIViewController { private let snapshot: UIView private let frame: CGRect + private var targetFrame: CGRect = .zero private let cellViewModel: MessageViewModel private let actions: [Action] private let dismiss: () -> Void @@ -19,8 +20,8 @@ final class ContextMenuVC: UIViewController { private lazy var blurView: UIVisualEffectView = UIVisualEffectView(effect: nil) private lazy var emojiBar: UIView = { - let result = UIView() - result.layer.shadowColor = UIColor.black.cgColor + let result: UIView = UIView() + result.themeShadowColor = .black result.layer.shadowOffset = CGSize.zero result.layer.shadowOpacity = 0.4 result.layer.shadowRadius = 4 @@ -30,21 +31,21 @@ final class ContextMenuVC: UIViewController { }() private lazy var emojiPlusButton: EmojiPlusButton = { - let result = EmojiPlusButton( + let result: EmojiPlusButton = EmojiPlusButton( action: self.actions.first(where: { $0.isEmojiPlus }), dismiss: snDismiss ) + result.clipsToBounds = true result.set(.width, to: EmojiPlusButton.size) result.set(.height, to: EmojiPlusButton.size) - result.layer.cornerRadius = EmojiPlusButton.size / 2 - result.layer.masksToBounds = true + result.layer.cornerRadius = (EmojiPlusButton.size / 2) return result }() private lazy var menuView: UIView = { let result: UIView = UIView() - result.layer.shadowColor = UIColor.black.cgColor + result.themeShadowColor = .black result.layer.shadowOffset = CGSize.zero result.layer.shadowOpacity = 0.4 result.layer.shadowRadius = 4 @@ -55,11 +56,8 @@ final class ContextMenuVC: UIViewController { private lazy var timestampLabel: UILabel = { let result: UILabel = UILabel() result.font = .systemFont(ofSize: Values.verySmallFontSize) - result.textColor = (isLightMode ? .black : .white) - - if let dateForUI: Date = cellViewModel.dateForUI { - result.text = dateForUI.formattedForDisplay - } + result.text = cellViewModel.dateForUI.formattedForDisplay + result.themeTextColor = .textPrimary return result }() @@ -96,35 +94,24 @@ final class ContextMenuVC: UIViewController { super.viewDidLoad() // Background color - view.backgroundColor = .clear + view.themeBackgroundColor = .clear // Blur view.addSubview(blurView) blurView.pin(to: view) // Snapshot - snapshot.layer.shadowColor = UIColor.black.cgColor + snapshot.themeShadowColor = .black snapshot.layer.shadowOffset = CGSize.zero snapshot.layer.shadowOpacity = 0.4 snapshot.layer.shadowRadius = 4 view.addSubview(snapshot) - // Timestamp - view.addSubview(timestampLabel) - timestampLabel.center(.vertical, in: snapshot) - - if cellViewModel.variant == .standardOutgoing { - timestampLabel.pin(.right, to: .left, of: snapshot, withInset: -Values.smallSpacing) - } - else { - timestampLabel.pin(.left, to: .right, of: snapshot, withInset: Values.smallSpacing) - } - // Emoji reacts - let emojiBarBackgroundView = UIView() - emojiBarBackgroundView.backgroundColor = Colors.receivedMessageBackground - emojiBarBackgroundView.layer.cornerRadius = ContextMenuVC.actionViewHeight / 2 - emojiBarBackgroundView.layer.masksToBounds = true + let emojiBarBackgroundView: UIView = UIView() + emojiBarBackgroundView.clipsToBounds = true + emojiBarBackgroundView.themeBackgroundColor = .reactions_contextBackground + emojiBarBackgroundView.layer.cornerRadius = (ContextMenuVC.actionViewHeight / 2) emojiBar.addSubview(emojiBarBackgroundView) emojiBarBackgroundView.pin(to: emojiBar) @@ -150,10 +137,10 @@ final class ContextMenuVC: UIViewController { view.addSubview(emojiBar) // Menu - let menuBackgroundView = UIView() - menuBackgroundView.backgroundColor = Colors.receivedMessageBackground + let menuBackgroundView: UIView = UIView() + menuBackgroundView.clipsToBounds = true + menuBackgroundView.themeBackgroundColor = .contextMenu_background menuBackgroundView.layer.cornerRadius = ContextMenuVC.menuCornerRadius - menuBackgroundView.layer.masksToBounds = true menuView.addSubview(menuBackgroundView) menuBackgroundView.pin(to: menuView) @@ -163,30 +150,40 @@ final class ContextMenuVC: UIViewController { .map { action -> ActionView in ActionView(for: action, dismiss: snDismiss) } ) menuStackView.axis = .vertical - menuView.addSubview(menuStackView) - menuStackView.pin(to: menuView) + menuBackgroundView.addSubview(menuStackView) + menuStackView.pin(to: menuBackgroundView) view.addSubview(menuView) + // Timestamp + view.addSubview(timestampLabel) + timestampLabel.pin(.top, to: .top, of: menuView) + timestampLabel.set(.height, to: ContextMenuVC.actionViewHeight) + + if cellViewModel.variant == .standardOutgoing { + timestampLabel.pin(.right, to: .left, of: menuView, withInset: -Values.mediumSpacing) + } + else { + timestampLabel.pin(.left, to: .right, of: menuView, withInset: Values.mediumSpacing) + } + // Constrains let menuHeight: CGFloat = CGFloat(menuStackView.arrangedSubviews.count) * ContextMenuVC.actionViewHeight let spacing: CGFloat = Values.smallSpacing - let targetFrame: CGRect = calculateFrame(menuHeight: menuHeight, spacing: spacing) + self.targetFrame = calculateFrame(menuHeight: menuHeight, spacing: spacing) - snapshot.pin(.left, to: .left, of: view, withInset: targetFrame.origin.x) - snapshot.pin(.top, to: .top, of: view, withInset: targetFrame.origin.y) - snapshot.set(.width, to: targetFrame.width) - snapshot.set(.height, to: targetFrame.height) - emojiBar.pin(.bottom, to: .top, of: snapshot, withInset: -spacing) - menuView.pin(.top, to: .bottom, of: snapshot, withInset: spacing) + // Position the snapshot view in it's original message position + snapshot.frame = self.frame + emojiBar.pin(.bottom, to: .top, of: view, withInset: targetFrame.minY - spacing) + menuView.pin(.top, to: .top, of: view, withInset: targetFrame.maxY + spacing) switch cellViewModel.variant { case .standardOutgoing: - menuView.pin(.right, to: .right, of: snapshot) - emojiBar.pin(.right, to: .right, of: snapshot) - + menuView.pin(.right, to: .right, of: view, withInset: -(UIScreen.main.bounds.width - targetFrame.maxX)) + emojiBar.pin(.right, to: .right, of: view, withInset: -(UIScreen.main.bounds.width - targetFrame.maxX)) + case .standardIncoming: - menuView.pin(.left, to: .left, of: snapshot) - emojiBar.pin(.left, to: .left, of: snapshot) + menuView.pin(.left, to: .left, of: view, withInset: targetFrame.minX) + emojiBar.pin(.left, to: .left, of: view, withInset: targetFrame.minX) default: break // Should never occur } @@ -199,10 +196,32 @@ final class ContextMenuVC: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - UIView.animate(withDuration: 0.25) { - self.blurView.effect = UIBlurEffect(style: .regular) - self.menuView.alpha = 1 + // Fade the menus in and animate the snapshot from it's starting position to where it + // needs to be on screen in order to fit the menu + let view: UIView = self.view + let targetFrame: CGRect = self.targetFrame + + UIView.animate(withDuration: 0.3) { [weak self] in + self?.blurView.effect = UIBlurEffect(style: .regular) + self?.menuView.alpha = 1 } + + UIView.animate( + withDuration: 0.3, + delay: 0, + usingSpringWithDamping: 0.8, + initialSpringVelocity: 0.6, + options: .curveEaseInOut, + animations: { [weak self] in + self?.snapshot.pin(.left, to: .left, of: view, withInset: targetFrame.origin.x) + self?.snapshot.pin(.top, to: .top, of: view, withInset: targetFrame.origin.y) + self?.snapshot.set(.width, to: targetFrame.width) + self?.snapshot.set(.height, to: targetFrame.height) + self?.snapshot.superview?.setNeedsLayout() + self?.snapshot.superview?.layoutIfNeeded() + }, + completion: nil + ) } func calculateFrame(menuHeight: CGFloat, spacing: CGFloat) -> CGRect { @@ -261,13 +280,35 @@ final class ContextMenuVC: UIViewController { } func snDismiss() { + let currentFrame: CGRect = self.snapshot.frame + let originalFrame: CGRect = self.frame + + // Remove the snapshot view from the view hierarchy to remove its constaints (and prevent + // them from causing animation bugs - also need to turn 'translatesAutoresizingMaskIntoConstraints' + // back on so autod layout doesn't mess with the frame manipulation) + let oldSuperview: UIView? = self.snapshot.superview + self.snapshot.removeFromSuperview() + oldSuperview?.insertSubview(self.snapshot, aboveSubview: self.blurView) + + self.snapshot.translatesAutoresizingMaskIntoConstraints = true + self.snapshot.frame = currentFrame + + UIView.animate( + withDuration: 0.15, + delay: 0, + options: .curveEaseOut, + animations: { [weak self] in + self?.snapshot.frame = originalFrame + }, + completion: nil + ) + UIView.animate( withDuration: 0.25, animations: { [weak self] in self?.blurView.effect = nil self?.menuView.alpha = 0 self?.emojiBar.alpha = 0 - self?.snapshot.alpha = 0 self?.timestampLabel.alpha = 0 }, completion: { [weak self] _ in diff --git a/Session/Conversations/ConversationSearch.swift b/Session/Conversations/ConversationSearch.swift index 9789b85db..d957543d0 100644 --- a/Session/Conversations/ConversationSearch.swift +++ b/Session/Conversations/ConversationSearch.swift @@ -175,10 +175,18 @@ public final class SearchResultsBar: UIView { backgroundView.alpha = Values.lowOpacity addSubview(backgroundView) backgroundView.pin(to: self) - let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) + + let blurView = UIVisualEffectView() addSubview(blurView) blurView.pin(to: self) + ThemeManager.onThemeChange(observer: blurView) { [weak blurView] theme, _ in + switch theme.interfaceStyle { + case .light: blurView?.effect = UIBlurEffect(style: .light) + default: blurView?.effect = UIBlurEffect(style: .dark) + } + } + // Separator let separator = UIView() separator.backgroundColor = Colors.text.withAlphaComponent(0.2) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 34e695aaf..ccb929807 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -358,10 +358,17 @@ extension ConversationVC: if text.contains(mnemonic) && !viewModel.threadData.threadIsNoteToSelf && !hasPermissionToSendSeed { // Warn the user if they're about to send their seed to someone - let modal = SendSeedModal() - modal.modalPresentationStyle = .overFullScreen - modal.modalTransitionStyle = .crossDissolve - modal.proceed = { [weak self] in self?.sendMessage(hasPermissionToSendSeed: true) } + let modal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: "modal_send_seed_title".localized(), + explanation: "modal_send_seed_explanation".localized(), + confirmTitle: "modal_send_seed_send_button_title".localized(), + confirmStyle: .danger, + cancelStyle: .textPrimary, + onConfirm: { [weak self] _ in self?.sendMessage(hasPermissionToSendSeed: true) } + ) + ) + return present(modal, animated: true, completion: nil) } @@ -474,12 +481,19 @@ extension ConversationVC: if text.contains(mnemonic) && !viewModel.threadData.threadIsNoteToSelf && !hasPermissionToSendSeed { // Warn the user if they're about to send their seed to someone - let modal = SendSeedModal() - modal.modalPresentationStyle = .overFullScreen - modal.modalTransitionStyle = .crossDissolve - modal.proceed = { [weak self] in - self?.sendAttachments(attachments, with: text, hasPermissionToSendSeed: true, onComplete: onComplete) - } + let modal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: "modal_send_seed_title".localized(), + explanation: "modal_send_seed_explanation".localized(), + confirmTitle: "modal_send_seed_send_button_title".localized(), + confirmStyle: .danger, + cancelStyle: .textPrimary, + onConfirm: { [weak self] _ in + self?.sendAttachments(attachments, with: text, hasPermissionToSendSeed: true, onComplete: onComplete) + } + ) + ) + return present(modal, animated: true, completion: nil) } @@ -635,7 +649,7 @@ extension ConversationVC: // MARK: --Mentions - func handleMentionSelected(_ mentionInfo: ConversationViewModel.MentionInfo, from view: MentionSelectionView) { + func handleMentionSelected(_ mentionInfo: MentionInfo, from view: MentionSelectionView) { guard let currentMentionStartIndex = currentMentionStartIndex else { return } mentions.append(mentionInfo) @@ -1010,14 +1024,6 @@ extension ConversationVC: reply(cellViewModel) } - func showUserDetails(for profile: Profile) { - let userDetailsSheet = UserDetailsSheet(for: profile) - userDetailsSheet.modalPresentationStyle = .overFullScreen - userDetailsSheet.modalTransitionStyle = .crossDissolve - - present(userDetailsSheet, animated: true, completion: nil) - } - func startThread(with sessionId: String, openGroupServer: String?, openGroupPublicKey: String?) { guard SessionId.Prefix(from: sessionId) == .blinded else { Storage.shared.write { db in @@ -1354,11 +1360,66 @@ extension ConversationVC: func joinOpenGroup(name: String?, url: String) { // Open groups can be unsafe, so always ask the user whether they want to join one - let joinOpenGroupModal: JoinOpenGroupModal = JoinOpenGroupModal(name: name, url: url) - joinOpenGroupModal.modalPresentationStyle = .overFullScreen - joinOpenGroupModal.modalTransitionStyle = .crossDissolve + let finalName: String = (name ?? "Open Group") + let message: String = "Are you sure you want to join the \(finalName) open group?"; + let modal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: "Join \(finalName)?", + attributedExplanation: NSMutableAttributedString(string: message) + .adding( + attributes: [ .font: UIFont.boldSystemFont(ofSize: Values.smallFontSize) ], + range: (message as NSString).range(of: finalName) + ), + confirmTitle: "Join", + onConfirm: { modal in + guard let presentingViewController: UIViewController = modal.presentingViewController else { + return + } + guard let (room, server, publicKey) = OpenGroupManager.parseOpenGroup(from: url) else { + let errorModal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: "Couldn't Join", + cancelTitle: "BUTTON_OK".localized(), + cancelStyle: .textPrimary + ) + ) + + return presentingViewController.present(errorModal, animated: true, completion: nil) + } + + Storage.shared + .writeAsync { db in + OpenGroupManager.shared.add( + db, + roomToken: room, + server: server, + publicKey: publicKey, + isConfigMessage: false + ) + } + .done(on: DispatchQueue.main) { _ in + Storage.shared.writeAsync { db in + try MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete() // FIXME: It's probably cleaner to do this inside addOpenGroup(...) + } + } + .catch(on: DispatchQueue.main) { error in + let errorModal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: "Couldn't Join", + explanation: error.localizedDescription, + cancelTitle: "BUTTON_OK".localized(), + cancelStyle: .textPrimary + ) + ) + + presentingViewController.present(errorModal, animated: true, completion: nil) + } + .retainUntilComplete() + } + ) + ) - present(joinOpenGroupModal, animated: true, completion: nil) + present(modal, animated: true, completion: nil) } // MARK: - ContextMenuActionDelegate diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index e39dcffce..dff584305 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -44,7 +44,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl // Mentions var currentMentionStartIndex: String.Index? - var mentions: [ConversationViewModel.MentionInfo] = [] + var mentions: [MentionInfo] = [] // Scrolling & paging var isUserScrolling = false diff --git a/Session/Conversations/ConversationViewModel.swift b/Session/Conversations/ConversationViewModel.swift index 567b50c29..2be7fd079 100644 --- a/Session/Conversations/ConversationViewModel.swift +++ b/Session/Conversations/ConversationViewModel.swift @@ -321,123 +321,39 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { // MARK: - Mentions - public struct MentionInfo: FetchableRecord, Decodable { - fileprivate static let threadVariantKey = CodingKeys.threadVariant.stringValue - fileprivate static let openGroupServerKey = CodingKeys.openGroupServer.stringValue - fileprivate static let openGroupRoomTokenKey = CodingKeys.openGroupRoomToken.stringValue - - let profile: Profile - let threadVariant: SessionThread.Variant - let openGroupServer: String? - let openGroupRoomToken: String? - } - public func mentions(for query: String = "") -> [MentionInfo] { let threadData: SessionThreadViewModel = self.threadData - let results: [MentionInfo] = Storage.shared + return Storage.shared .read { db -> [MentionInfo] in let userPublicKey: String = getUserHexEncodedPublicKey(db) + let pattern: FTS5Pattern? = try? SessionThreadViewModel.pattern(db, searchTerm: query, forTable: Profile.self) + let capabilities: Set = (threadData.threadVariant != .openGroup ? + nil : + try? Capability + .select(.variant) + .filter(Capability.Columns.openGroupServer == threadData.openGroupServer) + .asRequest(of: Capability.Variant.self) + .fetchSet(db) + ) + .defaulting(to: []) + let targetPrefix: SessionId.Prefix = (capabilities.contains(.blind) ? + .blinded : + .standard + ) - switch threadData.threadVariant { - case .contact: - guard userPublicKey != threadData.threadId else { return [] } - - return [Profile.fetchOrCreate(db, id: threadData.threadId)] - .map { profile in - MentionInfo( - profile: profile, - threadVariant: threadData.threadVariant, - openGroupServer: nil, - openGroupRoomToken: nil - ) - } - .filter { - query.count < 2 || - $0.profile.displayName(for: $0.threadVariant).contains(query) - } - - case .closedGroup: - let profile: TypedTableAlias = TypedTableAlias() - - return try GroupMember - .select( - profile.allColumns(), - SQL("\(threadData.threadVariant)").forKey(MentionInfo.threadVariantKey) - ) - .filter(GroupMember.Columns.groupId == threadData.threadId) - .filter(GroupMember.Columns.profileId != userPublicKey) - .filter(GroupMember.Columns.role == GroupMember.Role.standard) - .joining( - required: GroupMember.profile - .aliased(profile) - // Note: LIKE is case-insensitive in SQLite - .filter( - query.count < 2 || ( - profile[.nickname] != nil && - profile[.nickname].like("%\(query)%") - ) || ( - profile[.nickname] == nil && - profile[.name].like("%\(query)%") - ) - ) - ) - .asRequest(of: MentionInfo.self) - .fetchAll(db) - - case .openGroup: - let profile: TypedTableAlias = TypedTableAlias() - - return try Interaction - .select( - profile.allColumns(), - SQL("\(threadData.threadVariant)").forKey(MentionInfo.threadVariantKey), - SQL("\(threadData.openGroupServer)").forKey(MentionInfo.openGroupServerKey), - SQL("\(threadData.openGroupRoomToken)").forKey(MentionInfo.openGroupRoomTokenKey) - ) - .distinct() - .group(Interaction.Columns.authorId) - .filter(Interaction.Columns.threadId == threadData.threadId) - .filter(Interaction.Columns.authorId != userPublicKey) - .joining( - required: Interaction.profile - .aliased(profile) - // Note: LIKE is case-insensitive in SQLite - .filter( - query.count < 2 || ( - profile[.nickname] != nil && - profile[.nickname].like("%\(query)%") - ) || ( - profile[.nickname] == nil && - profile[.name].like("%\(query)%") - ) - ) - ) - .order(Interaction.Columns.timestampMs.desc) - .limit(20) - .asRequest(of: MentionInfo.self) - .fetchAll(db) - } + return (try MentionInfo + .query( + userPublicKey: userPublicKey, + threadId: threadData.threadId, + threadVariant: threadData.threadVariant, + targetPrefix: targetPrefix, + pattern: pattern + )? + .fetchAll(db)) + .defaulting(to: []) } .defaulting(to: []) - - guard query.count >= 2 else { - return results.sorted { lhs, rhs -> Bool in - lhs.profile.displayName(for: lhs.threadVariant) < rhs.profile.displayName(for: rhs.threadVariant) - } - } - - return results - .sorted { lhs, rhs -> Bool in - let maybeLhsRange = lhs.profile.displayName(for: lhs.threadVariant).lowercased().range(of: query.lowercased()) - let maybeRhsRange = rhs.profile.displayName(for: rhs.threadVariant).lowercased().range(of: query.lowercased()) - - guard let lhsRange: Range = maybeLhsRange, let rhsRange: Range = maybeRhsRange else { - return true - } - - return (lhsRange.lowerBound < rhsRange.lowerBound) - } } // MARK: - Functions diff --git a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift index a9bfcd1ae..a8dceb7da 100644 --- a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift +++ b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit +import SessionUIKit import SessionUtilitiesKit protocol EmojiPickerCollectionViewDelegate: AnyObject { @@ -63,7 +64,7 @@ class EmojiPickerCollectionView: UICollectionView { withReuseIdentifier: EmojiSectionHeader.reuseIdentifier ) - backgroundColor = .clear + themeBackgroundColor = .clear let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) panGestureRecognizer.require(toFail: longPressGesture) @@ -303,7 +304,7 @@ private class EmojiCell: UICollectionViewCell { override init(frame: CGRect) { super.init(frame: frame) - backgroundColor = .clear + themeBackgroundColor = .clear emojiLabel.font = .boldSystemFont(ofSize: 32) contentView.addSubview(emojiLabel) @@ -341,7 +342,7 @@ private class EmojiSectionHeader: UICollectionReusableView { ) label.font = .systemFont(ofSize: Values.smallFontSize) - label.textColor = Colors.text + label.themeTextColor = .textPrimary addSubview(label) label.autoPinEdgesToSuperviewMargins() label.setCompressionResistanceHigh() @@ -355,6 +356,7 @@ private class EmojiSectionHeader: UICollectionReusableView { var labelSize = label.sizeThatFits(size) labelSize.width += layoutMargins.left + layoutMargins.right labelSize.height += layoutMargins.top + layoutMargins.bottom + return labelSize } } diff --git a/Session/Conversations/Input View/InputView.swift b/Session/Conversations/Input View/InputView.swift index bd84e410a..131830205 100644 --- a/Session/Conversations/Input View/InputView.swift +++ b/Session/Conversations/Input View/InputView.swift @@ -78,16 +78,24 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M private lazy var mentionsViewContainer: UIView = { let result: UIView = UIView() + result.alpha = 0 + let backgroundView = UIView() backgroundView.themeBackgroundColor = .backgroundSecondary backgroundView.alpha = Values.lowOpacity result.addSubview(backgroundView) backgroundView.pin(to: result) - let blurView: UIVisualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) + let blurView: UIVisualEffectView = UIVisualEffectView() result.addSubview(blurView) blurView.pin(to: result) - result.alpha = 0 + + ThemeManager.onThemeChange(observer: blurView) { [weak blurView] theme, _ in + switch theme.interfaceStyle { + case .light: blurView?.effect = UIBlurEffect(style: .light) + default: blurView?.effect = UIBlurEffect(style: .dark) + } + } return result }() @@ -144,10 +152,17 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M addSubview(backgroundView) backgroundView.pin(to: self) - let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) + let blurView = UIVisualEffectView() addSubview(blurView) blurView.pin(to: self) + ThemeManager.onThemeChange(observer: blurView) { [weak blurView] theme, _ in + switch theme.interfaceStyle { + case .light: blurView?.effect = UIBlurEffect(style: .light) + default: blurView?.effect = UIBlurEffect(style: .dark) + } + } + // Separator let separator = UIView() separator.themeBackgroundColor = .borderSeparator @@ -380,15 +395,23 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M showVoiceMessageUI() } - func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch) { - guard let voiceMessageRecordingView = voiceMessageRecordingView, inputViewButton == voiceMessageButton else { return } - let location = touch.location(in: voiceMessageRecordingView) + func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) { + guard + let voiceMessageRecordingView: VoiceMessageRecordingView = voiceMessageRecordingView, + inputViewButton == voiceMessageButton, + let location = touch?.location(in: voiceMessageRecordingView) + else { return } + voiceMessageRecordingView.handleLongPressMoved(to: location) } - func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch) { - guard let voiceMessageRecordingView = voiceMessageRecordingView, inputViewButton == voiceMessageButton else { return } - let location = touch.location(in: voiceMessageRecordingView) + func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch?) { + guard + let voiceMessageRecordingView: VoiceMessageRecordingView = voiceMessageRecordingView, + inputViewButton == voiceMessageButton, + let location = touch?.location(in: voiceMessageRecordingView) + else { return } + voiceMessageRecordingView.handleLongPressEnded(at: location) } @@ -443,7 +466,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M ) } - func showMentionsUI(for candidates: [ConversationViewModel.MentionInfo]) { + func showMentionsUI(for candidates: [MentionInfo]) { mentionsView.candidates = candidates let mentionCellHeight = (Values.smallProfilePictureSize + 2 * Values.smallSpacing) @@ -455,7 +478,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M } } - func handleMentionSelected(_ mentionInfo: ConversationViewModel.MentionInfo, from view: MentionSelectionView) { + func handleMentionSelected(_ mentionInfo: MentionInfo, from view: MentionSelectionView) { delegate?.handleMentionSelected(mentionInfo, from: view) } @@ -482,6 +505,6 @@ protocol InputViewDelegate: ExpandingAttachmentsButtonDelegate, VoiceMessageReco func showLinkPreviewSuggestionModal() func handleSendButtonTapped() func inputTextViewDidChangeContent(_ inputTextView: InputTextView) - func handleMentionSelected(_ mentionInfo: ConversationViewModel.MentionInfo, from view: MentionSelectionView) + func handleMentionSelected(_ mentionInfo: MentionInfo, from view: MentionSelectionView) func didPasteImageFromPasteboard(_ image: UIImage) } diff --git a/Session/Conversations/Input View/InputViewButton.swift b/Session/Conversations/Input View/InputViewButton.swift index ca42e1943..741c4d963 100644 --- a/Session/Conversations/Input View/InputViewButton.swift +++ b/Session/Conversations/Input View/InputViewButton.swift @@ -56,10 +56,17 @@ final class InputViewButton: UIView { addSubview(backgroundView) backgroundView.pin(to: self) - let blurView: UIVisualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) + let blurView: UIVisualEffectView = UIVisualEffectView() addSubview(blurView) blurView.pin(to: self) + ThemeManager.onThemeChange(observer: blurView) { [weak blurView] theme, _ in + switch theme.interfaceStyle { + case .light: blurView?.effect = UIBlurEffect(style: .light) + default: blurView?.effect = UIBlurEffect(style: .dark) + } + } + themeBorderColor = .borderSeparator layer.borderWidth = Values.separatorThickness } diff --git a/Session/Conversations/Input View/MentionSelectionView.swift b/Session/Conversations/Input View/MentionSelectionView.swift index 0eceafcea..7385d77fb 100644 --- a/Session/Conversations/Input View/MentionSelectionView.swift +++ b/Session/Conversations/Input View/MentionSelectionView.swift @@ -6,7 +6,7 @@ import SessionUtilitiesKit import SignalUtilitiesKit final class MentionSelectionView: UIView, UITableViewDataSource, UITableViewDelegate { - var candidates: [ConversationViewModel.MentionInfo] = [] { + var candidates: [MentionInfo] = [] { didSet { tableView.isScrollEnabled = (candidates.count > 4) tableView.reloadData() @@ -27,7 +27,7 @@ final class MentionSelectionView: UIView, UITableViewDataSource, UITableViewDele result.dataSource = self result.delegate = self result.separatorStyle = .none - result.backgroundColor = .clear + result.themeBackgroundColor = .clear result.showsVerticalScrollIndicator = false result.register(view: Cell.self) @@ -55,7 +55,7 @@ final class MentionSelectionView: UIView, UITableViewDataSource, UITableViewDele // Top separator let topSeparator: UIView = UIView() - topSeparator.backgroundColor = Colors.separator + topSeparator.themeBackgroundColor = .borderSeparator topSeparator.set(.height, to: Values.separatorThickness) addSubview(topSeparator) topSeparator.pin(.leading, to: .leading, of: self) @@ -64,7 +64,7 @@ final class MentionSelectionView: UIView, UITableViewDataSource, UITableViewDele // Bottom separator let bottomSeparator: UIView = UIView() - bottomSeparator.backgroundColor = Colors.separator + bottomSeparator.themeBackgroundColor = .borderSeparator bottomSeparator.set(.height, to: Values.separatorThickness) addSubview(bottomSeparator) @@ -116,8 +116,8 @@ private extension MentionSelectionView { private lazy var displayNameLabel: UILabel = { let result: UILabel = UILabel() - result.textColor = Colors.text result.font = .systemFont(ofSize: Values.smallFontSize) + result.themeTextColor = .textPrimary result.lineBreakMode = .byTruncatingTail return result @@ -125,7 +125,7 @@ private extension MentionSelectionView { lazy var separator: UIView = { let result: UIView = UIView() - result.backgroundColor = Colors.separator + result.themeBackgroundColor = .borderSeparator result.set(.height, to: Values.separatorThickness) return result @@ -147,11 +147,11 @@ private extension MentionSelectionView { private func setUpViewHierarchy() { // Cell background color - backgroundColor = .clear + themeBackgroundColor = .settings_tabBackground // Highlight color let selectedBackgroundView = UIView() - selectedBackgroundView.backgroundColor = .clear + selectedBackgroundView.themeBackgroundColor = .settings_tabHighlight self.selectedBackgroundView = selectedBackgroundView // Profile picture image view @@ -210,5 +210,5 @@ private extension MentionSelectionView { // MARK: - Delegate protocol MentionSelectionViewDelegate: AnyObject { - func handleMentionSelected(_ mention: ConversationViewModel.MentionInfo, from view: MentionSelectionView) + func handleMentionSelected(_ mention: MentionInfo, from view: MentionSelectionView) } diff --git a/Session/Conversations/Input View/VoiceMessageRecordingView.swift b/Session/Conversations/Input View/VoiceMessageRecordingView.swift index 5a3f2a71f..5c19a3d41 100644 --- a/Session/Conversations/Input View/VoiceMessageRecordingView.swift +++ b/Session/Conversations/Input View/VoiceMessageRecordingView.swift @@ -1,5 +1,10 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. -final class VoiceMessageRecordingView : UIView { +import UIKit +import SessionUIKit +import SignalUtilitiesKit + +final class VoiceMessageRecordingView: UIView { private let voiceMessageButtonFrame: CGRect private weak var delegate: VoiceMessageRecordingViewDelegate? private lazy var slideToCancelStackViewRightConstraint = slideToCancelStackView.pin(.right, to: .right, of: self) @@ -10,103 +15,118 @@ final class VoiceMessageRecordingView : UIView { private let recordingStartDate = Date() private var recordingTimer: Timer? - // MARK: UI Components + // MARK: - UI Components + private lazy var iconImageView: UIImageView = { - let result = UIImageView() - result.image = UIImage(named: "Microphone")!.withTint(.white) + let result: UIImageView = UIImageView() + result.image = UIImage(named: "Microphone")? + .withRenderingMode(.alwaysTemplate) + result.themeTintColor = .white result.contentMode = .scaleAspectFit - let size = VoiceMessageRecordingView.iconSize - result.set(.width, to: size) - result.set(.height, to: size) + result.set(.width, to: VoiceMessageRecordingView.iconSize) + result.set(.height, to: VoiceMessageRecordingView.iconSize) + return result }() private lazy var circleView: UIView = { - let result = UIView() - result.backgroundColor = Colors.destructive - let size = VoiceMessageRecordingView.circleSize - result.set(.width, to: size) - result.set(.height, to: size) - result.layer.cornerRadius = size / 2 - result.layer.masksToBounds = true + let result: UIView = UIView() + result.clipsToBounds = true + result.themeBackgroundColor = .danger + result.set(.width, to: VoiceMessageRecordingView.circleSize) + result.set(.height, to: VoiceMessageRecordingView.circleSize) + result.layer.cornerRadius = (VoiceMessageRecordingView.circleSize / 2) + return result }() private lazy var pulseView: UIView = { - let result = UIView() - result.backgroundColor = Colors.destructive - result.layer.cornerRadius = VoiceMessageRecordingView.circleSize / 2 + let result: UIView = UIView() + result.themeBackgroundColor = .danger + result.layer.cornerRadius = (VoiceMessageRecordingView.circleSize / 2) result.layer.masksToBounds = true result.alpha = 0.5 + return result }() private lazy var slideToCancelStackView: UIStackView = { - let result = UIStackView() + let result: UIStackView = UIStackView() result.axis = .horizontal result.spacing = Values.smallSpacing result.alignment = .center + return result }() private lazy var chevronImageView: UIImageView = { - let chevronSize = VoiceMessageRecordingView.chevronSize - let chevronColor = (isLightMode ? UIColor.black : UIColor.white).withAlphaComponent(Values.mediumOpacity) - let result = UIImageView(image: UIImage(named: "small_chevron_left")!.withTint(chevronColor)) + let result: UIImageView = UIImageView( + image: UIImage(named: "small_chevron_left")? + .withRenderingMode(.alwaysTemplate) + ) + result.themeTintColor = .textPrimary result.contentMode = .scaleAspectFit - result.set(.width, to: chevronSize) - result.set(.height, to: chevronSize) + result.alpha = Values.mediumOpacity + result.set(.width, to: VoiceMessageRecordingView.chevronSize) + result.set(.height, to: VoiceMessageRecordingView.chevronSize) + return result }() private lazy var slideToCancelLabel: UILabel = { - let result = UILabel() - result.text = NSLocalizedString("vc_conversation_voice_message_cancel_message", comment: "") + let result: UILabel = UILabel() result.font = .systemFont(ofSize: Values.smallFontSize) - result.textColor = Colors.text.withAlphaComponent(Values.mediumOpacity) + result.text = "vc_conversation_voice_message_cancel_message".localized() + result.themeTextColor = .textPrimary + result.alpha = Values.mediumOpacity + return result }() private lazy var cancelButton: UIButton = { - let result = UIButton() - result.setTitle("Cancel", for: UIControl.State.normal) - result.titleLabel!.font = .boldSystemFont(ofSize: Values.smallFontSize) - result.setTitleColor(Colors.text, for: UIControl.State.normal) + let result: UIButton = UIButton() + result.setTitle("cancel".localized(), for: .normal) + result.titleLabel?.font = .boldSystemFont(ofSize: Values.smallFontSize) + result.setThemeTitleColor(.textPrimary, for: .normal) result.addTarget(self, action: #selector(handleCancelButtonTapped), for: UIControl.Event.touchUpInside) result.alpha = 0 + return result }() private lazy var durationStackView: UIStackView = { - let result = UIStackView() + let result: UIStackView = UIStackView() result.axis = .horizontal result.spacing = Values.smallSpacing result.alignment = .center + return result }() private lazy var dotView: UIView = { - let result = UIView() - result.backgroundColor = Colors.destructive - let dotSize = VoiceMessageRecordingView.dotSize - result.set(.width, to: dotSize) - result.set(.height, to: dotSize) - result.layer.cornerRadius = dotSize / 2 - result.layer.masksToBounds = true + let result: UIView = UIView() + result.clipsToBounds = true + result.themeBackgroundColor = .danger + result.set(.width, to: VoiceMessageRecordingView.dotSize) + result.set(.height, to: VoiceMessageRecordingView.dotSize) + result.layer.cornerRadius = (VoiceMessageRecordingView.dotSize / 2) + return result }() private lazy var durationLabel: UILabel = { - let result = UILabel() - result.textColor = Colors.text + let result: UILabel = UILabel() result.font = .systemFont(ofSize: Values.smallFontSize) + result.themeTextColor = .textPrimary result.text = "0:00" + return result }() private lazy var lockView = LockView() - // MARK: Settings + // MARK: - Settings + private static let circleSize: CGFloat = 96 private static let pulseSize: CGFloat = 24 private static let iconSize: CGFloat = 28 @@ -114,11 +134,14 @@ final class VoiceMessageRecordingView : UIView { private static let dotSize: CGFloat = 16 private static let lockViewHitMargin: CGFloat = 40 - // MARK: Lifecycle + // MARK: - Lifecycle + init(voiceMessageButtonFrame: CGRect, delegate: VoiceMessageRecordingViewDelegate?) { self.voiceMessageButtonFrame = voiceMessageButtonFrame self.delegate = delegate + super.init(frame: CGRect.zero) + setUpViewHierarchy() recordingTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in self?.updateDurationLabel() @@ -141,57 +164,67 @@ final class VoiceMessageRecordingView : UIView { // Icon let iconSize = VoiceMessageRecordingView.iconSize addSubview(iconImageView) + let voiceMessageButtonCenter = voiceMessageButtonFrame.center - iconImageView.pin(.left, to: .left, of: self, withInset: voiceMessageButtonCenter.x - iconSize / 2) - iconImageView.pin(.top, to: .top, of: self, withInset: voiceMessageButtonCenter.y - iconSize / 2) + iconImageView.pin(.left, to: .left, of: self, withInset: (voiceMessageButtonCenter.x - (iconSize / 2))) + iconImageView.pin(.top, to: .top, of: self, withInset: (voiceMessageButtonCenter.y - (iconSize / 2))) + // Circle insertSubview(circleView, at: 0) circleView.center(in: iconImageView) + // Pulse insertSubview(pulseView, at: 0) pulseView.center(in: circleView) + // Slide to cancel stack view slideToCancelStackView.addArrangedSubview(chevronImageView) slideToCancelStackView.addArrangedSubview(slideToCancelLabel) addSubview(slideToCancelStackView) slideToCancelStackViewRightConstraint.isActive = true slideToCancelStackView.center(.vertical, in: iconImageView) + // Cancel button addSubview(cancelButton) cancelButton.center(.horizontal, in: self) cancelButton.center(.vertical, in: iconImageView) + // Duration stack view durationStackView.addArrangedSubview(dotView) durationStackView.addArrangedSubview(durationLabel) addSubview(durationStackView) + durationStackView.pin(.left, to: .left, of: self, withInset: Values.largeSpacing) durationStackView.center(.vertical, in: iconImageView) + // Lock view addSubview(lockView) lockView.centerXAnchor.constraint(equalTo: iconImageView.centerXAnchor, constant: 2).isActive = true lockViewBottomConstraint.isActive = true } - // MARK: Updating + // MARK: - Updating + @objc private func updateDurationLabel() { let interval = Date().timeIntervalSince(recordingStartDate) durationLabel.text = OWSFormat.formatDurationSeconds(Int(interval)) } - // MARK: Animation + // MARK: - Animation + func animate() { layoutIfNeeded() + slideToCancelStackViewRightConstraint.isActive = false slideToCancelLabelCenterHorizontalConstraint.isActive = true lockViewBottomConstraint.constant = -Values.mediumSpacing + UIView.animate(withDuration: 0.25, animations: { [weak self] in - guard let self = self else { return } - self.alpha = 1 - self.layoutIfNeeded() + self?.alpha = 1 + self?.layoutIfNeeded() }, completion: { [weak self] _ in - guard let self = self else { return } - self.fadeOutDotView() - self.pulse() + self?.fadeOutDotView() + self?.pulse() }) } @@ -218,24 +251,24 @@ final class VoiceMessageRecordingView : UIView { let expandedFrame = CGRect(center: pulseView.center, size: CGSize(width: expandedSize, height: expandedSize)) pulseViewWidthConstraint.constant = expandedSize pulseViewHeightConstraint.constant = expandedSize + UIView.animate(withDuration: 1, animations: { [weak self] in - guard let self = self else { return } - self.layoutIfNeeded() - self.pulseView.frame = expandedFrame - self.pulseView.layer.cornerRadius = expandedSize / 2 - self.pulseView.alpha = 0 + self?.layoutIfNeeded() + self?.pulseView.frame = expandedFrame + self?.pulseView.layer.cornerRadius = (expandedSize / 2) + self?.pulseView.alpha = 0 }, completion: { [weak self] _ in - guard let self = self else { return } - self.pulseViewWidthConstraint.constant = collapsedSize - self.pulseViewHeightConstraint.constant = collapsedSize - self.pulseView.frame = collapsedFrame - self.pulseView.layer.cornerRadius = collapsedSize / 2 - self.pulseView.alpha = 0.5 - self.pulse() + self?.pulseViewWidthConstraint.constant = collapsedSize + self?.pulseViewHeightConstraint.constant = collapsedSize + self?.pulseView.frame = collapsedFrame + self?.pulseView.layer.cornerRadius = (collapsedSize / 2) + self?.pulseView.alpha = 0.5 + self?.pulse() }) } - // MARK: Interaction + // MARK: - Interaction + func handleLongPressMoved(to location: CGPoint) { if location.x < bounds.center.x { let translationX = location.x - bounds.center.x @@ -244,12 +277,15 @@ final class VoiceMessageRecordingView : UIView { let labelDamping: CGFloat = 3 let chevronX = (chevronDamping * (sqrt(abs(translationX)) / sqrt(chevronDamping))) * sign let labelX = (labelDamping * (sqrt(abs(translationX)) / sqrt(labelDamping))) * sign + chevronImageView.transform = CGAffineTransform(translationX: chevronX, y: 0) slideToCancelLabel.transform = CGAffineTransform(translationX: labelX, y: 0) - } else { + } + else { chevronImageView.transform = .identity slideToCancelLabel.transform = .identity } + if isValidLockViewLocation(location) { if !lockView.isExpanded { UIView.animate(withDuration: 0.25) { @@ -257,7 +293,8 @@ final class VoiceMessageRecordingView : UIView { } } lockView.expandIfNeeded() - } else { + } + else { if lockView.isExpanded { UIView.animate(withDuration: 0.25) { self.lockViewBottomConstraint.constant = -Values.mediumSpacing @@ -270,18 +307,21 @@ final class VoiceMessageRecordingView : UIView { func handleLongPressEnded(at location: CGPoint) { if pulseView.frame.contains(location) { delegate?.endVoiceMessageRecording() - } else if isValidLockViewLocation(location) { + } + else if isValidLockViewLocation(location) { let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleCircleViewTap)) circleView.addGestureRecognizer(tapGestureRecognizer) + UIView.animate(withDuration: 0.25, delay: 0, options: .transitionCrossDissolve, animations: { self.lockView.alpha = 0 - self.iconImageView.image = UIImage(named: "ArrowUp")!.withTint(.white) + self.iconImageView.image = UIImage(named: "ArrowUp")?.withRenderingMode(.alwaysTemplate) self.slideToCancelStackView.alpha = 0 self.cancelButton.alpha = 1 }, completion: { _ in // Do nothing }) - } else { + } + else { delegate?.cancelVoiceMessageRecording() } } @@ -294,27 +334,30 @@ final class VoiceMessageRecordingView : UIView { delegate?.cancelVoiceMessageRecording() } - // MARK: Convenience + // MARK: - Convenience + private func isValidLockViewLocation(_ location: CGPoint) -> Bool { let lockViewHitMargin = VoiceMessageRecordingView.lockViewHitMargin + return location.y < 0 && location.x > (lockView.frame.minX - lockViewHitMargin) && location.x < (lockView.frame.maxX + lockViewHitMargin) } } -// MARK: Lock View -extension VoiceMessageRecordingView { +// MARK: - Lock View - fileprivate final class LockView : UIView { +extension VoiceMessageRecordingView { + fileprivate final class LockView: UIView { private lazy var widthConstraint = set(.width, to: LockView.width) private(set) var isExpanded = false private lazy var stackView: UIStackView = { - let result = UIStackView() + let result: UIStackView = UIStackView() result.axis = .vertical result.spacing = Values.smallSpacing result.alignment = .center result.isLayoutMarginsRelativeArrangement = true result.layoutMargins = UIEdgeInsets(top: 12, leading: 0, bottom: 8, trailing: 0) + return result }() @@ -325,45 +368,64 @@ extension VoiceMessageRecordingView { override init(frame: CGRect) { super.init(frame: frame) + setUpViewHierarchy() } required init?(coder: NSCoder) { super.init(coder: coder) + setUpViewHierarchy() } private func setUpViewHierarchy() { - let iconTint: UIColor = isLightMode ? .black : .white // Background & blur - let backgroundView = UIView() - backgroundView.backgroundColor = isLightMode ? .white : .black + let backgroundView: UIView = UIView() + backgroundView.themeBackgroundColor = .backgroundSecondary// .backgroundColor = isLightMode ? .white : .black backgroundView.alpha = Values.lowOpacity addSubview(backgroundView) backgroundView.pin(to: self) - let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) + + let blurView = UIVisualEffectView() addSubview(blurView) blurView.pin(to: self) + + ThemeManager.onThemeChange(observer: blurView) { [weak blurView] theme, _ in + switch theme.interfaceStyle { + case .light: blurView?.effect = UIBlurEffect(style: .light) + default: blurView?.effect = UIBlurEffect(style: .dark) + } + } + // Size & shape widthConstraint.isActive = true - layer.cornerRadius = LockView.width / 2 + layer.cornerRadius = (LockView.width / 2) layer.masksToBounds = true + // Border + themeBorderColor = .borderSeparator layer.borderWidth = 1 - let borderColor = (isLightMode ? UIColor.black : UIColor.white).withAlphaComponent(Values.veryLowOpacity) - layer.borderColor = borderColor.cgColor + // Lock icon - let lockIconImageView = UIImageView(image: UIImage(named: "ic_lock_outline")!.withTint(iconTint)) - let lockIconSize = LockView.lockIconSize - lockIconImageView.set(.width, to: lockIconSize) - lockIconImageView.set(.height, to: lockIconSize) + let lockIconImageView: UIImageView = UIImageView( + image: UIImage(named: "ic_lock_outline")? + .withRenderingMode(.alwaysTemplate) + ) + lockIconImageView.themeTintColor = .textPrimary + lockIconImageView.set(.width, to: LockView.lockIconSize) + lockIconImageView.set(.height, to: LockView.lockIconSize) stackView.addArrangedSubview(lockIconImageView) + // Chevron icon - let chevronIconImageView = UIImageView(image: UIImage(named: "ic_chevron_up")!.withTint(iconTint)) - let chevronIconSize = LockView.chevronIconSize - chevronIconImageView.set(.width, to: chevronIconSize) - chevronIconImageView.set(.height, to: chevronIconSize) + let chevronIconImageView: UIImageView = UIImageView( + image: UIImage(named: "ic_chevron_up")? + .withRenderingMode(.alwaysTemplate) + ) + chevronIconImageView.themeTintColor = .textPrimary + chevronIconImageView.set(.width, to: LockView.chevronIconSize) + chevronIconImageView.set(.height, to: LockView.chevronIconSize) stackView.addArrangedSubview(chevronIconImageView) + // Stack view addSubview(stackView) stackView.pin(to: self) @@ -371,10 +433,14 @@ extension VoiceMessageRecordingView { func expandIfNeeded() { guard !isExpanded else { return } + isExpanded = true + let expansionMargin = LockView.expansionMargin let newWidth = LockView.width + 2 * expansionMargin + widthConstraint.constant = newWidth + UIView.animate(withDuration: 0.25) { self.layer.cornerRadius = newWidth / 2 self.stackView.layoutMargins = UIEdgeInsets(top: 12 + expansionMargin, leading: 0, bottom: 8 + expansionMargin, trailing: 0) @@ -384,9 +450,12 @@ extension VoiceMessageRecordingView { func collapseIfNeeded() { guard isExpanded else { return } + isExpanded = false + let newWidth = LockView.width widthConstraint.constant = newWidth + UIView.animate(withDuration: 0.25) { self.layer.cornerRadius = newWidth / 2 self.stackView.layoutMargins = UIEdgeInsets(top: 12, leading: 0, bottom: 8, trailing: 0) diff --git a/Session/Conversations/Message Cells/CallMessageCell.swift b/Session/Conversations/Message Cells/CallMessageCell.swift index 8793db91a..913778606 100644 --- a/Session/Conversations/Message Cells/CallMessageCell.swift +++ b/Session/Conversations/Message Cells/CallMessageCell.swift @@ -20,7 +20,8 @@ final class CallMessageCell: MessageCell { private lazy var iconImageView: UIImageView = UIImageView() private lazy var infoImageView: UIImageView = { let result: UIImageView = UIImageView( - image: UIImage(named: "ic_info")?.withRenderingMode(.alwaysTemplate) + image: UIImage(named: "ic_info")? + .withRenderingMode(.alwaysTemplate) ) result.themeTintColor = .textPrimary @@ -144,7 +145,7 @@ final class CallMessageCell: MessageCell { infoImageViewHeightConstraint.constant = (shouldShowInfoIcon ? CallMessageCell.iconSize : 0) label.text = cellViewModel.body - timestampLabel.text = cellViewModel.dateForUI?.formattedForDisplay + timestampLabel.text = cellViewModel.dateForUI.formattedForDisplay } override func dynamicUpdate(with cellViewModel: MessageViewModel, playbackInfo: ConversationViewModel.PlaybackInfo?) { diff --git a/Session/Conversations/Message Cells/Content Views/CallMessageView.swift b/Session/Conversations/Message Cells/Content Views/CallMessageView.swift deleted file mode 100644 index f27302f8f..000000000 --- a/Session/Conversations/Message Cells/Content Views/CallMessageView.swift +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import UIKit -import SessionUIKit -import SessionMessagingKit - -final class CallMessageView: UIView { - private static let iconSize: CGFloat = 24 - private static let iconImageViewSize: CGFloat = 40 - - // MARK: - Lifecycle - - init(cellViewModel: MessageViewModel, textColor: UIColor) { - super.init(frame: CGRect.zero) - - setUpViewHierarchy(cellViewModel: cellViewModel, textColor: textColor) - } - - override init(frame: CGRect) { - preconditionFailure("Use init(viewItem:textColor:) instead.") - } - - required init?(coder: NSCoder) { - preconditionFailure("Use init(viewItem:textColor:) instead.") - } - - private func setUpViewHierarchy(cellViewModel: MessageViewModel, textColor: UIColor) { - // Image view - let imageView: UIImageView = UIImageView( - image: UIImage(named: "Phone")? - .resizedImage(to: CGSize(width: CallMessageView.iconSize, height: CallMessageView.iconSize))? - .withRenderingMode(.alwaysTemplate) - ) - imageView.tintColor = textColor - imageView.contentMode = .center - - let iconImageViewSize = CallMessageView.iconImageViewSize - imageView.set(.width, to: iconImageViewSize) - imageView.set(.height, to: iconImageViewSize) - - // Body label - let titleLabel = UILabel() - titleLabel.lineBreakMode = .byTruncatingTail - titleLabel.text = cellViewModel.body - titleLabel.textColor = textColor - titleLabel.font = .systemFont(ofSize: Values.mediumFontSize) - - // Stack view - let stackView = UIStackView(arrangedSubviews: [ imageView, titleLabel ]) - stackView.axis = .horizontal - stackView.alignment = .center - stackView.isLayoutMarginsRelativeArrangement = true - stackView.layoutMargins = UIEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 12) - addSubview(stackView) - stackView.pin(to: self, withInset: Values.smallSpacing) - } -} diff --git a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift index 982195f50..42378cadc 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift @@ -2,11 +2,13 @@ import UIKit import SessionUIKit +import SignalUtilitiesKit final class ReactionContainerView: UIView { var showingAllReactions = false private var showNumbers = true private var maxEmojisPerLine = isIPhone6OrSmaller ? 5 : 6 + private var oldSize: CGSize = .zero var reactions: [ReactionViewModel] = [] var reactionViews: [ReactionButton] = [] @@ -14,35 +16,52 @@ final class ReactionContainerView: UIView { // MARK: - UI private lazy var mainStackView: UIStackView = { - let result = UIStackView(arrangedSubviews: [ reactionContainerView ]) + let result: UIStackView = UIStackView(arrangedSubviews: [ reactionContainerView, collapseButton ]) result.axis = .vertical result.spacing = Values.smallSpacing result.alignment = .center + return result }() private lazy var reactionContainerView: UIStackView = { - let result = UIStackView() + let result: UIStackView = UIStackView() result.axis = .vertical result.spacing = Values.smallSpacing result.alignment = .leading + return result }() var expandButton: ExpandingReactionButton? var collapseButton: UIStackView = { - let arrow = UIImageView(image: UIImage(named: "ic_chevron_up")?.resizedImage(to: CGSize(width: 15, height: 13))?.withRenderingMode(.alwaysTemplate)) - arrow.tintColor = Colors.text + let arrow = UIImageView( + image: UIImage(named: "ic_chevron_up")? + .resizedImage(to: CGSize(width: 15, height: 13))? + .withRenderingMode(.alwaysTemplate) + ) + arrow.themeTintColor = .textPrimary - let textLabel = UILabel() - textLabel.text = "Show less" + let textLabel: UILabel = UILabel() textLabel.font = .systemFont(ofSize: Values.verySmallFontSize) - textLabel.textColor = Colors.text + textLabel.text = "Show less" + textLabel.themeTextColor = .textPrimary - let result = UIStackView(arrangedSubviews: [ UIView.hStretchingSpacer(), arrow, textLabel, UIView.hStretchingSpacer() ]) + let leftSpacer: UIView = UIView.hStretchingSpacer() + let rightSpacer: UIView = UIView.hStretchingSpacer() + let result: UIStackView = UIStackView(arrangedSubviews: [ + leftSpacer, + arrow, + textLabel, + rightSpacer + ]) + result.isLayoutMarginsRelativeArrangement = true result.spacing = Values.verySmallSpacing result.alignment = .center + result.isHidden = true + rightSpacer.set(.width, to: .width, of: leftSpacer) + return result }() @@ -65,6 +84,23 @@ final class ReactionContainerView: UIView { addSubview(mainStackView) mainStackView.pin(to: self) + collapseButton.set(.width, to: .width, of: mainStackView) + } + + override func layoutSubviews() { + super.layoutSubviews() + + // Note: We update the 'collapseButton.layoutMargins' to try to make the "show less" + // button appear horizontally centered (if we don't do this it gets offset to one side) + guard frame != CGRect.zero, frame.size != oldSize else { return } + + collapseButton.layoutMargins = UIEdgeInsets( + top: 0, + leading: -frame.minX, + bottom: 0, + trailing: -((superview?.frame.width ?? 0) - frame.maxX) + ) + oldSize = frame.size } public func update(_ reactions: [ReactionViewModel], showNumbers: Bool) { @@ -135,7 +171,7 @@ final class ReactionContainerView: UIView { } if numberOfLines > 1 { - mainStackView.addArrangedSubview(collapseButton) + collapseButton.isHidden = false } else { showingAllReactions = false @@ -148,8 +184,7 @@ final class ReactionContainerView: UIView { subview.removeFromSuperview() } - mainStackView.removeArrangedSubview(collapseButton) - collapseButton.removeFromSuperview() + collapseButton.isHidden = true reactionViews = [] } diff --git a/Session/Conversations/Message Cells/Content Views/ReactionView.swift b/Session/Conversations/Message Cells/Content Views/ReactionView.swift index 01cf006f3..63d1aaf16 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionView.swift @@ -39,11 +39,11 @@ final class ReactionButton: UIView { } private func setUpViewHierarchy() { - let emojiLabel = UILabel() - emojiLabel.text = viewModel.emoji.rawValue + let emojiLabel: UILabel = UILabel() emojiLabel.font = .systemFont(ofSize: fontSize) + emojiLabel.text = viewModel.emoji.rawValue - let stackView = UIStackView(arrangedSubviews: [ emojiLabel ]) + let stackView: UIStackView = UIStackView(arrangedSubviews: [ emojiLabel ]) stackView.axis = .horizontal stackView.spacing = spacing stackView.alignment = .center @@ -52,19 +52,20 @@ final class ReactionButton: UIView { addSubview(stackView) stackView.pin(to: self) + themeBorderColor = (viewModel.showBorder ? .primary : .clear) + themeBackgroundColor = .messageBubble_incomingBackground + layer.cornerRadius = (self.height / 2) + layer.borderWidth = 1 // Intentionally 1pt (instead of 'Values.separatorThickness') set(.height, to: self.height) - backgroundColor = Colors.receivedMessageBackground - layer.cornerRadius = self.height / 2 - - if viewModel.showBorder { - self.addBorder(with: Colors.accent) - } if showNumber || viewModel.number > 1 { let numberLabel = UILabel() - numberLabel.text = viewModel.number < 1000 ? "\(viewModel.number)" : String(format: "%.1f", Float(viewModel.number) / 1000) + "k" numberLabel.font = .systemFont(ofSize: fontSize) - numberLabel.textColor = Colors.text + numberLabel.text = (viewModel.number < 1000 ? + "\(viewModel.number)" : + String(format: "%.1f", Float(viewModel.number) / 1000) + "k" + ) + numberLabel.themeTextColor = .textPrimary stackView.addArrangedSubview(numberLabel) } } @@ -100,18 +101,17 @@ final class ExpandingReactionButton: UIView { var rightMargin: CGFloat = 0 for emoji in self.emojis.reversed() { - let container = UIView() + let container: UIView = UIView() container.set(.width, to: size) container.set(.height, to: size) - container.backgroundColor = Colors.receivedMessageBackground + container.themeBorderColor = .backgroundPrimary + container.themeBackgroundColor = .messageBubble_incomingBackground container.layer.cornerRadius = size / 2 - container.layer.borderWidth = 1 - // FIXME: This is going to have issues when swapping between light/dark mode - container.layer.borderColor = (isDarkMode ? UIColor.black.cgColor : UIColor.white.cgColor) + container.layer.borderWidth = 1 // Intentionally 1pt (instead of 'Values.separatorThickness') - let emojiLabel = UILabel() - emojiLabel.text = emoji.rawValue + let emojiLabel: UILabel = UILabel() emojiLabel.font = .systemFont(ofSize: Values.verySmallFontSize) + emojiLabel.text = emoji.rawValue container.addSubview(emojiLabel) emojiLabel.center(in: container) diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index 785abe7d8..baf2d3340 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -88,7 +88,6 @@ protocol MessageCellDelegate: ReactionDelegate { func handleItemSwiped(_ cellViewModel: MessageViewModel, state: SwipeState) func openUrl(_ urlString: String) func handleReplyButtonTapped(for cellViewModel: MessageViewModel) - func showUserDetails(for profile: Profile) func startThread(with sessionId: String, openGroupServer: String?, openGroupPublicKey: String?) func showReactionList(_ cellViewModel: MessageViewModel, selectedReaction: EmojiWithSkinTones?) func needsLayout(for cellViewModel: MessageViewModel, expandingReactions: Bool) diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 2c666337d..9df3bc5df 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -375,11 +375,11 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { } private func populateHeader(for cellViewModel: MessageViewModel, shouldInsetHeader: Bool) { - guard let date: Date = cellViewModel.dateForUI else { return } + guard cellViewModel.shouldShowDateHeader else { return } let dateBreakLabel: UILabel = UILabel() dateBreakLabel.font = .boldSystemFont(ofSize: Values.verySmallFontSize) - dateBreakLabel.text = date.formattedForDisplay + dateBreakLabel.text = cellViewModel.dateForUI.formattedForDisplay dateBreakLabel.themeTextColor = .textPrimary dateBreakLabel.textAlignment = .center headerView.addSubview(dateBreakLabel) diff --git a/Session/Conversations/Views & Modals/JoinOpenGroupModal.swift b/Session/Conversations/Views & Modals/JoinOpenGroupModal.swift deleted file mode 100644 index f3ff7e39a..000000000 --- a/Session/Conversations/Views & Modals/JoinOpenGroupModal.swift +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import UIKit -import GRDB -import SessionMessagingKit -import SessionUtilitiesKit - -final class JoinOpenGroupModal: Modal { - private let name: String - private let url: String - - // MARK: - Lifecycle - - init(name: String?, url: String) { - self.name = (name ?? "Open Group") - self.url = url - - super.init() - } - - override init(afterClosed: (() -> ())? = nil) { - preconditionFailure("Use init(name:url:) instead.") - } - - required init?(coder: NSCoder) { - preconditionFailure("Use init(name:url:) instead.") - } - - override func populateContentView() { - // Title - let titleLabel = UILabel() - titleLabel.textColor = Colors.text - titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize) - titleLabel.text = "Join \(name)?" - titleLabel.textAlignment = .center - - // Message - let messageLabel = UILabel() - messageLabel.textColor = Colors.text - messageLabel.font = .systemFont(ofSize: Values.smallFontSize) - let message = "Are you sure you want to join the \(name) open group?"; - let attributedMessage = NSMutableAttributedString(string: message) - attributedMessage.addAttributes([ .font : UIFont.boldSystemFont(ofSize: Values.smallFontSize) ], range: (message as NSString).range(of: name)) - messageLabel.attributedText = attributedMessage - messageLabel.numberOfLines = 0 - messageLabel.lineBreakMode = .byWordWrapping - messageLabel.textAlignment = .center - - // Join button - let joinButton = UIButton() - joinButton.set(.height, to: Values.mediumButtonHeight) - joinButton.layer.cornerRadius = Modal.buttonCornerRadius - joinButton.backgroundColor = Colors.buttonBackground - joinButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize) - joinButton.setTitleColor(Colors.text, for: UIControl.State.normal) - joinButton.setTitle("Join", for: UIControl.State.normal) - joinButton.addTarget(self, action: #selector(joinOpenGroup), for: UIControl.Event.touchUpInside) - - // Button stack view - let buttonStackView = UIStackView(arrangedSubviews: [ cancelButton, joinButton ]) - buttonStackView.axis = .horizontal - buttonStackView.spacing = Values.mediumSpacing - buttonStackView.distribution = .fillEqually - - // Content stack view - let contentStackView = UIStackView(arrangedSubviews: [ titleLabel, messageLabel ]) - contentStackView.axis = .vertical - contentStackView.spacing = Values.largeSpacing - - // Main stack view - let spacing = Values.largeSpacing - Values.smallFontSize / 2 - let mainStackView = UIStackView(arrangedSubviews: [ contentStackView, buttonStackView ]) - mainStackView.axis = .vertical - mainStackView.spacing = spacing - contentView.addSubview(mainStackView) - mainStackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing) - mainStackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing) - contentView.pin(.trailing, to: .trailing, of: mainStackView, withInset: Values.largeSpacing) - contentView.pin(.bottom, to: .bottom, of: mainStackView, withInset: spacing) - } - - // MARK: - Interaction - - @objc private func joinOpenGroup() { - guard let presentingViewController: UIViewController = self.presentingViewController else { return } - guard let (room, server, publicKey) = OpenGroupManager.parseOpenGroup(from: url) else { - let alert = UIAlertController(title: "Couldn't Join", message: nil, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "BUTTON_OK".localized(), style: .default, handler: nil)) - - return presentingViewController.present(alert, animated: true, completion: nil) - } - - presentingViewController.dismiss(animated: true, completion: nil) - - Storage.shared - .writeAsync { db in - OpenGroupManager.shared.add( - db, - roomToken: room, - server: server, - publicKey: publicKey, - isConfigMessage: false - ) - } - .done(on: DispatchQueue.main) { _ in - Storage.shared.writeAsync { db in - try MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete() // FIXME: It's probably cleaner to do this inside addOpenGroup(...) - } - } - .catch(on: DispatchQueue.main) { error in - let alert = UIAlertController(title: "Couldn't Join", message: error.localizedDescription, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "BUTTON_OK".localized(), style: .default, handler: nil)) - presentingViewController.present(alert, animated: true, completion: nil) - } - .retainUntilComplete() - } -} diff --git a/Session/Conversations/Views & Modals/ReactionListSheet.swift b/Session/Conversations/Views & Modals/ReactionListSheet.swift index 3c9b8b889..b55bb9246 100644 --- a/Session/Conversations/Views & Modals/ReactionListSheet.swift +++ b/Session/Conversations/Views & Modals/ReactionListSheet.swift @@ -29,13 +29,13 @@ final class ReactionListSheet: BaseVC { private lazy var contentView: UIView = { let result: UIView = UIView() - result.backgroundColor = Colors.modalBackground + result.themeBackgroundColor = .backgroundSecondary let line: UIView = UIView() - line.backgroundColor = Colors.border.withAlphaComponent(0.5) + line.themeBackgroundColor = .borderSeparator result.addSubview(line) - line.set(.height, to: 0.5) + line.set(.height, to: Values.separatorThickness) line.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.top ], to: result) return result @@ -61,7 +61,7 @@ final class ReactionListSheet: BaseVC { let result: UICollectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout) result.register(view: Cell.self) result.set(.height, to: 48) - result.backgroundColor = .clear + result.themeBackgroundColor = .clear result.isScrollEnabled = true result.showsHorizontalScrollIndicator = false result.dataSource = self @@ -73,7 +73,7 @@ final class ReactionListSheet: BaseVC { private lazy var detailInfoLabel: UILabel = { let result: UILabel = UILabel() result.font = .systemFont(ofSize: Values.mediumFontSize) - result.textColor = Colors.grey.withAlphaComponent(0.8) + result.themeTextColor = .textSecondary result.set(.height, to: 32) return result @@ -95,7 +95,7 @@ final class ReactionListSheet: BaseVC { result.delegate = self result.register(view: UserCell.self) result.separatorStyle = .none - result.backgroundColor = .clear + result.themeBackgroundColor = .clear result.showsVerticalScrollIndicator = false return result @@ -397,7 +397,8 @@ extension ReactionListSheet: UITableViewDelegate, UITableViewDataSource { accessory: (cellViewModel.reaction.authorId == self.messageViewModel.currentUserPublicKey ? .x : .none - ) + ), + themeBackgroundColor: .backgroundSecondary ) return cell @@ -431,22 +432,25 @@ extension ReactionListSheet { private lazy var snContentView: UIView = { let result = UIView() - result.backgroundColor = Colors.receivedMessageBackground - result.set(.height, to: Cell.contentViewHeight) + result.themeBackgroundColor = .messageBubble_incomingBackground result.layer.cornerRadius = Cell.contentViewCornerRadius + result.layer.borderWidth = 1 // Intentionally 1pt (instead of 'Values.separatorThickness') + result.set(.height, to: Cell.contentViewHeight) + return result }() private lazy var emojiLabel: UILabel = { - let result = UILabel() + let result: UILabel = UILabel() result.font = .systemFont(ofSize: Values.mediumFontSize) + return result }() private lazy var numberLabel: UILabel = { - let result = UILabel() - result.textColor = Colors.text + let result: UILabel = UILabel() result.font = .systemFont(ofSize: Values.mediumFontSize) + result.themeTextColor = .textPrimary return result }() @@ -488,15 +492,12 @@ extension ReactionListSheet { count: Int, isCurrentSelection: Bool ) { - snContentView.addBorder( - with: (isCurrentSelection == true ? Colors.accent : .clear) - ) - emojiLabel.text = emoji numberLabel.text = (count < 1000 ? "\(count)" : String(format: "%.1fk", Float(count) / 1000) ) + snContentView.themeBorderColor = (isCurrentSelection ? .primary : .clear) } } } diff --git a/Session/Conversations/Views & Modals/ScrollToBottomButton.swift b/Session/Conversations/Views & Modals/ScrollToBottomButton.swift index 7a3bf9793..413dbdbb2 100644 --- a/Session/Conversations/Views & Modals/ScrollToBottomButton.swift +++ b/Session/Conversations/Views & Modals/ScrollToBottomButton.swift @@ -37,10 +37,17 @@ final class ScrollToBottomButton: UIView { addSubview(backgroundView) backgroundView.pin(to: self) - let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) + let blurView = UIVisualEffectView() addSubview(blurView) blurView.pin(to: self) + ThemeManager.onThemeChange(observer: blurView) { [weak blurView] theme, _ in + switch theme.interfaceStyle { + case .light: blurView?.effect = UIBlurEffect(style: .light) + default: blurView?.effect = UIBlurEffect(style: .dark) + } + } + // Size & shape set(.width, to: ScrollToBottomButton.size) set(.height, to: ScrollToBottomButton.size) diff --git a/Session/Conversations/Views & Modals/SendSeedModal.swift b/Session/Conversations/Views & Modals/SendSeedModal.swift deleted file mode 100644 index e7027a85e..000000000 --- a/Session/Conversations/Views & Modals/SendSeedModal.swift +++ /dev/null @@ -1,75 +0,0 @@ - -final class SendSeedModal : Modal { - var proceed: (() -> Void)? = nil - - private lazy var titleLabel: UILabel = { - let result = UILabel() - result.textColor = Colors.text - result.font = .boldSystemFont(ofSize: Values.mediumFontSize) - result.text = NSLocalizedString("modal_send_seed_title", comment: "") - result.textAlignment = .center - return result - }() - - private lazy var explanationLabel: UILabel = { - let result = UILabel() - result.textColor = Colors.text - result.font = .systemFont(ofSize: Values.smallFontSize) - result.text = NSLocalizedString("modal_send_seed_explanation", comment: "") - result.numberOfLines = 0 - result.lineBreakMode = .byWordWrapping - result.textAlignment = .center - return result - }() - - private lazy var sendSeedButton: UIButton = { - let result = UIButton() - result.set(.height, to: Values.mediumButtonHeight) - result.layer.cornerRadius = Modal.buttonCornerRadius - if isDarkMode { - result.backgroundColor = Colors.destructive - } - result.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize) - result.setTitleColor(isLightMode ? Colors.destructive : Colors.text, for: UIControl.State.normal) - result.setTitle(NSLocalizedString("modal_send_seed_send_button_title", comment: ""), for: UIControl.State.normal) - result.addTarget(self, action: #selector(sendSeed), for: UIControl.Event.touchUpInside) - return result - }() - - private lazy var buttonStackView: UIStackView = { - let result = UIStackView(arrangedSubviews: [ cancelButton, sendSeedButton ]) - result.axis = .horizontal - result.spacing = Values.mediumSpacing - result.distribution = .fillEqually - return result - }() - - private lazy var contentStackView: UIStackView = { - let result = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel ]) - result.axis = .vertical - result.spacing = Values.largeSpacing - return result - }() - - private lazy var mainStackView: UIStackView = { - let result = UIStackView(arrangedSubviews: [ contentStackView, buttonStackView ]) - result.axis = .vertical - result.spacing = Values.largeSpacing - Values.smallFontSize / 2 - return result - }() - - // MARK: Lifecycle - override func populateContentView() { - contentView.addSubview(mainStackView) - mainStackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing) - mainStackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing) - contentView.pin(.trailing, to: .trailing, of: mainStackView, withInset: Values.largeSpacing) - contentView.pin(.bottom, to: .bottom, of: mainStackView, withInset: mainStackView.spacing) - } - - // MARK: Interaction - @objc private func sendSeed() { - proceed?() - presentingViewController?.dismiss(animated: true, completion: nil) - } -} diff --git a/Session/Conversations/Views & Modals/UserDetailsSheet.swift b/Session/Conversations/Views & Modals/UserDetailsSheet.swift deleted file mode 100644 index f4cc9d16b..000000000 --- a/Session/Conversations/Views & Modals/UserDetailsSheet.swift +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import UIKit -import SessionUIKit -import SessionMessagingKit - -final class UserDetailsSheet: Sheet { - private let profile: Profile - - init(for profile: Profile) { - self.profile = profile - - super.init(nibName: nil, bundle: nil) - } - - override init(nibName: String?, bundle: Bundle?) { - preconditionFailure("Use init(for:) instead.") - } - - required init?(coder: NSCoder) { - preconditionFailure("Use init(for:) instead.") - } - - override func populateContentView() { - // Profile picture view - let profilePictureView = ProfilePictureView() - let size = Values.largeProfilePictureSize - profilePictureView.size = size - profilePictureView.set(.width, to: size) - profilePictureView.set(.height, to: size) - profilePictureView.update( - publicKey: profile.id, - profile: profile, - threadVariant: .contact - ) - - // Display name label - let displayNameLabel = UILabel() - let displayName = profile.displayName() - displayNameLabel.text = displayName - displayNameLabel.font = .boldSystemFont(ofSize: Values.largeFontSize) - displayNameLabel.textColor = Colors.text - displayNameLabel.numberOfLines = 1 - displayNameLabel.lineBreakMode = .byTruncatingTail - - // Session ID label - let sessionIDLabel = UILabel() - sessionIDLabel.textColor = Colors.text - sessionIDLabel.font = Fonts.spaceMono(ofSize: isIPhone5OrSmaller ? Values.mediumFontSize : 20) - sessionIDLabel.numberOfLines = 0 - sessionIDLabel.lineBreakMode = .byCharWrapping - sessionIDLabel.accessibilityLabel = "Session ID label" - sessionIDLabel.text = profile.id - - // Session ID label container - let sessionIDLabelContainer = UIView() - sessionIDLabelContainer.addSubview(sessionIDLabel) - sessionIDLabel.pin(to: sessionIDLabelContainer, withInset: Values.mediumSpacing) - sessionIDLabelContainer.layer.cornerRadius = TextField.cornerRadius - sessionIDLabelContainer.layer.borderWidth = 1 - sessionIDLabelContainer.layer.borderColor = isLightMode ? UIColor.black.cgColor : UIColor.white.cgColor - - // Copy button - let copyButton = OutlineButton(style: .regular, size: .medium) - copyButton.setTitle("copy".localized(), for: .normal) - copyButton.addTarget(self, action: #selector(copySessionID), for: UIControl.Event.touchUpInside) - copyButton.set(.width, to: 160) - - // Stack view - let stackView = UIStackView(arrangedSubviews: [ profilePictureView, displayNameLabel, sessionIDLabelContainer, copyButton, UIView.vSpacer(Values.largeSpacing) ]) - stackView.axis = .vertical - stackView.spacing = Values.largeSpacing - stackView.alignment = .center - - // Constraints - contentView.addSubview(stackView) - stackView.pin(to: contentView, withInset: Values.largeSpacing) - } - - @objc private func copySessionID() { - UIPasteboard.general.string = profile.id - presentingViewController?.dismiss(animated: true, completion: nil) - } -} diff --git a/Session/Shared/UserCell.swift b/Session/Shared/UserCell.swift index e5b8554f0..9ffff74d8 100644 --- a/Session/Shared/UserCell.swift +++ b/Session/Shared/UserCell.swift @@ -75,9 +75,6 @@ final class UserCell: UITableViewCell { } private func setUpViewHierarchy() { - // Background color - themeBackgroundColor = .conversationButton_background - // Highlight color let selectedBackgroundView = UIView() selectedBackgroundView.themeBackgroundColor = .clear // Disabled for now @@ -127,8 +124,11 @@ final class UserCell: UITableViewCell { profile: Profile?, isZombie: Bool, mediumFont: Bool = false, - accessory: Accessory + accessory: Accessory, + themeBackgroundColor: ThemeValue = .conversationButton_background ) { + self.themeBackgroundColor = themeBackgroundColor + profilePictureView.update( publicKey: publicKey, profile: profile, diff --git a/Session/Sheets & Modals/ConfirmationModal.swift b/Session/Sheets & Modals/ConfirmationModal.swift index ab7d0cafb..44261e518 100644 --- a/Session/Sheets & Modals/ConfirmationModal.swift +++ b/Session/Sheets & Modals/ConfirmationModal.swift @@ -179,11 +179,11 @@ public class ConfirmationModal: Modal { init(info: Info) { self.internalOnConfirm = { viewController in - info.onConfirm?(viewController) - - guard info.dismissOnConfirm else { return } + if info.dismissOnConfirm { + viewController.dismiss(animated: true) + } - viewController.dismiss(animated: true) + info.onConfirm?(viewController) } super.init(afterClosed: info.afterClosed) diff --git a/SessionMessagingKit/Shared Models/MentionInfo.swift b/SessionMessagingKit/Shared Models/MentionInfo.swift new file mode 100644 index 000000000..0c469e864 --- /dev/null +++ b/SessionMessagingKit/Shared Models/MentionInfo.swift @@ -0,0 +1,110 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import GRDB +import SessionUtilitiesKit + +public struct MentionInfo: FetchableRecord, Decodable { + fileprivate static let threadVariantKey: SQL = SQL(stringLiteral: CodingKeys.threadVariant.stringValue) + fileprivate static let openGroupServerKey: SQL = SQL(stringLiteral: CodingKeys.openGroupServer.stringValue) + fileprivate static let openGroupRoomTokenKey: SQL = SQL(stringLiteral: CodingKeys.openGroupRoomToken.stringValue) + + fileprivate static let profileString: String = CodingKeys.profile.stringValue + + public let profile: Profile + public let threadVariant: SessionThread.Variant + public let openGroupServer: String? + public let openGroupRoomToken: String? +} + +public extension MentionInfo { + static func query( + userPublicKey: String, + threadId: String, + threadVariant: SessionThread.Variant, + targetPrefix: SessionId.Prefix, + pattern: FTS5Pattern? + ) -> AdaptedFetchRequest>? { + guard threadVariant != .contact || userPublicKey != threadId else { return nil } + + let profile: TypedTableAlias = TypedTableAlias() + let interaction: TypedTableAlias = TypedTableAlias() + let openGroup: TypedTableAlias = TypedTableAlias() + + let prefixLiteral: SQL = SQL(stringLiteral: "\(targetPrefix.rawValue)%") + let profileFullTextSearch: SQL = SQL(stringLiteral: Profile.fullTextSearchTableName) + + /// **Note:** The `\(MentionInfo.profileKey).*` value **MUST** be first + let limitSQL: SQL? = (threadVariant == .openGroup ? SQL("LIMIT 20") : nil) + + let request: SQLRequest = { + guard let pattern: FTS5Pattern = pattern else { + return """ + SELECT + \(Profile.self).*, + MAX(\(interaction[.timestampMs])), -- Want the newest interaction (for sorting) + \(SQL("\(threadVariant) AS \(MentionInfo.threadVariantKey)")), + \(openGroup[.server]) AS \(MentionInfo.openGroupServerKey), + \(openGroup[.roomToken]) AS \(MentionInfo.openGroupRoomTokenKey) + + FROM \(Profile.self) + JOIN \(Interaction.self) ON ( + \(SQL("\(interaction[.threadId]) = \(threadId)")) AND + \(interaction[.authorId]) = \(profile[.id]) + ) + LEFT JOIN \(OpenGroup.self) ON \(SQL("\(openGroup[.threadId]) = \(threadId)")) + + WHERE ( + \(SQL("\(profile[.id]) != \(userPublicKey)")) AND ( + \(SQL("\(threadVariant) != \(SessionThread.Variant.openGroup)")) OR + \(SQL("\(profile[.id]) LIKE '\(prefixLiteral)'")) + ) + ) + GROUP BY \(profile[.id]) + ORDER BY \(interaction[.timestampMs].desc) + \(limitSQL ?? "") + """ + } + + // If we do have a search patern then use FTS + let matchLiteral: SQL = SQL(stringLiteral: "\(Profile.Columns.nickname.name):\(pattern.rawPattern) OR \(Profile.Columns.name.name):\(pattern.rawPattern)") + + return """ + SELECT + \(Profile.self).*, + MAX(\(interaction[.timestampMs])), -- Want the newest interaction (for sorting) + \(SQL("\(threadVariant) AS \(MentionInfo.threadVariantKey)")), + \(openGroup[.server]) AS \(MentionInfo.openGroupServerKey), + \(openGroup[.roomToken]) AS \(MentionInfo.openGroupRoomTokenKey) + + FROM \(profileFullTextSearch) + JOIN \(Profile.self) ON ( + \(Profile.self).rowid = \(profileFullTextSearch).rowid AND + \(SQL("\(profile[.id]) != \(userPublicKey)")) AND ( + \(SQL("\(threadVariant) != \(SessionThread.Variant.openGroup)")) OR + \(SQL("\(profile[.id]) LIKE '\(prefixLiteral)'")) + ) + ) + JOIN \(Interaction.self) ON ( + \(SQL("\(interaction[.threadId]) = \(threadId)")) AND + \(interaction[.authorId]) = \(profile[.id]) + ) + LEFT JOIN \(OpenGroup.self) ON \(SQL("\(openGroup[.threadId]) = \(threadId)")) + + WHERE \(profileFullTextSearch) MATCH '\(matchLiteral)' + GROUP BY \(profile[.id]) + ORDER BY \(interaction[.timestampMs].desc) + \(limitSQL ?? "") + """ + }() + + return request.adapted { db in + let adapters = try splittingRowAdapters(columnCounts: [ + Profile.numberOfSelectedColumns(db) + ]) + + return ScopeAdapter([ + MentionInfo.profileString: adapters[0] + ]) + } + } +} diff --git a/SessionMessagingKit/Shared Models/MessageViewModel.swift b/SessionMessagingKit/Shared Models/MessageViewModel.swift index e0f069a46..2ea629d67 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel.swift @@ -34,6 +34,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, public static let cellTypeKey: SQL = SQL(stringLiteral: CodingKeys.cellType.stringValue) public static let authorNameKey: SQL = SQL(stringLiteral: CodingKeys.authorName.stringValue) public static let shouldShowProfileKey: SQL = SQL(stringLiteral: CodingKeys.shouldShowProfile.stringValue) + public static let shouldShowDateHeaderKey: SQL = SQL(stringLiteral: CodingKeys.shouldShowDateHeader.stringValue) public static let positionInClusterKey: SQL = SQL(stringLiteral: CodingKeys.positionInCluster.stringValue) public static let isOnlyMessageInClusterKey: SQL = SQL(stringLiteral: CodingKeys.isOnlyMessageInCluster.stringValue) public static let isLastKey: SQL = SQL(stringLiteral: CodingKeys.isLast.stringValue) @@ -119,8 +120,11 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, /// A flag indicating whether the profile view should be displayed public let shouldShowProfile: Bool - /// This value will be used to populate the date header, if it's null then the header will be hidden - public let dateForUI: Date? + /// A flag which controls whether the date header should be displayed + public let shouldShowDateHeader: Bool + + /// This value will be used to populate the Context Menu and date header (if present) + public var dateForUI: Date { Date(timeIntervalSince1970: (TimeInterval(self.timestampMs) / 1000)) } /// This value specifies whether the body contains only emoji characters public let containsOnlyEmoji: Bool? @@ -184,7 +188,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, authorName: self.authorName, senderName: self.senderName, shouldShowProfile: self.shouldShowProfile, - dateForUI: self.dateForUI, + shouldShowDateHeader: self.shouldShowDateHeader, containsOnlyEmoji: self.containsOnlyEmoji, glyphCount: self.glyphCount, previousVariant: self.previousVariant, @@ -395,10 +399,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, // Need a profile to be able to show it self.profile != nil ), - dateForUI: (shouldShowDateOnThisModel ? - Date(timeIntervalSince1970: (TimeInterval(self.timestampMs) / 1000)) : - nil - ), + shouldShowDateHeader: shouldShowDateOnThisModel, containsOnlyEmoji: self.body?.containsOnlyEmoji, glyphCount: self.body?.glyphCount, previousVariant: prevModel?.variant, @@ -546,7 +547,7 @@ public extension MessageViewModel { self.authorName = "" self.senderName = nil self.shouldShowProfile = false - self.dateForUI = nil + self.shouldShowDateHeader = false self.containsOnlyEmoji = nil self.glyphCount = nil self.previousVariant = nil @@ -710,6 +711,7 @@ public extension MessageViewModel { \(CellType.textOnlyMessage) AS \(ViewModel.cellTypeKey), '' AS \(ViewModel.authorNameKey), false AS \(ViewModel.shouldShowProfileKey), + false AS \(ViewModel.shouldShowDateHeaderKey), \(Position.middle) AS \(ViewModel.positionInClusterKey), false AS \(ViewModel.isOnlyMessageInClusterKey), false AS \(ViewModel.isLastKey) diff --git a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index 6167128d4..33741c6ff 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -947,6 +947,10 @@ public extension SessionThreadViewModel { } static func pattern(_ db: Database, searchTerm: String) throws -> FTS5Pattern { + return try pattern(db, searchTerm: searchTerm, forTable: Interaction.self) + } + + static func pattern(_ db: Database, searchTerm: String, forTable table: T.Type) throws -> FTS5Pattern where T: TableRecord, T: ColumnExpressible { // Note: FTS doesn't support both prefix/suffix wild cards so don't bother trying to // add a prefix one let rawPattern: String = searchTermParts(searchTerm) @@ -955,9 +959,9 @@ public extension SessionThreadViewModel { /// There are cases where creating a pattern can fail, we want to try and recover from those cases /// by failling back to simpler patterns if needed - let maybePattern: FTS5Pattern? = (try? db.makeFTS5Pattern(rawPattern: rawPattern, forTable: Interaction.self)) + let maybePattern: FTS5Pattern? = (try? db.makeFTS5Pattern(rawPattern: rawPattern, forTable: table)) .defaulting( - to: (try? db.makeFTS5Pattern(rawPattern: searchTerm, forTable: Interaction.self)) + to: (try? db.makeFTS5Pattern(rawPattern: searchTerm, forTable: table)) .defaulting(to: FTS5Pattern(matchingAnyTokenIn: searchTerm)) ) diff --git a/SessionUIKit/Style Guide/Themes/Theme+ClassicDark.swift b/SessionUIKit/Style Guide/Themes/Theme+ClassicDark.swift index 30dc56d71..6c275661c 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+ClassicDark.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+ClassicDark.swift @@ -93,8 +93,17 @@ internal enum Theme_ClassicDark: ThemeColors { // InputButton .inputButton_background: .classicDark1, + // ContextMenu + .contextMenu_background: .classicDark1, + .contextMenu_highlight: .primary, + .contextMenu_textHighlight: .classicDark0, + // Call .callAccept_background: Theme.PrimaryColor.green.color, - .callDecline_background: .dangerDark + .callDecline_background: .dangerDark, + + // Reactions + .reactions_contextBackground: .classicDark2, + .reactions_contextMoreBackground: .classicDark3 ] } diff --git a/SessionUIKit/Style Guide/Themes/Theme+ClassicLight.swift b/SessionUIKit/Style Guide/Themes/Theme+ClassicLight.swift index e8cb6286b..729e38507 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+ClassicLight.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+ClassicLight.swift @@ -93,8 +93,17 @@ internal enum Theme_ClassicLight: ThemeColors { // InputButton .inputButton_background: .classicDark6, + // ContextMenu + .contextMenu_background: .classicLight6, + .contextMenu_highlight: .primary, + .contextMenu_textHighlight: .classicLight0, + // Call .callAccept_background: Theme.PrimaryColor.green.color, - .callDecline_background: .dangerLight + .callDecline_background: .dangerLight, + + // Reactions + .reactions_contextBackground: .classicLight4, + .reactions_contextMoreBackground: .classicLight6 ] } diff --git a/SessionUIKit/Style Guide/Themes/Theme+OceanDark.swift b/SessionUIKit/Style Guide/Themes/Theme+OceanDark.swift index 11c17219d..d7581a662 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+OceanDark.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+OceanDark.swift @@ -93,8 +93,17 @@ internal enum Theme_OceanDark: ThemeColors { // InputButton .inputButton_background: .oceanDark4, + // ContextMenu + .contextMenu_background: .oceanDark2, + .contextMenu_highlight: .primary, + .contextMenu_textHighlight: .oceanDark0, + // Call .callAccept_background: Theme.PrimaryColor.green.color, - .callDecline_background: .dangerDark + .callDecline_background: .dangerDark, + + // Reactions + .reactions_contextBackground: .oceanDark1, + .reactions_contextMoreBackground: .oceanDark2 ] } diff --git a/SessionUIKit/Style Guide/Themes/Theme+OceanLight.swift b/SessionUIKit/Style Guide/Themes/Theme+OceanLight.swift index 5ff01aecb..0d2b640a6 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+OceanLight.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+OceanLight.swift @@ -93,8 +93,17 @@ internal enum Theme_OceanLight: ThemeColors { // InputButton .inputButton_background: .oceanLight6, + // ContextMenu + .contextMenu_background: .oceanLight6, + .contextMenu_highlight: .primary, + .contextMenu_textHighlight: .oceanLight0, + // Call .callAccept_background: Theme.PrimaryColor.green.color, - .callDecline_background: .dangerLight + .callDecline_background: .dangerLight, + + // Reactions + .reactions_contextBackground: .oceanLight6, + .reactions_contextMoreBackground: .oceanLight5 ] } diff --git a/SessionUIKit/Style Guide/Themes/Theme.swift b/SessionUIKit/Style Guide/Themes/Theme.swift index 91c5cb0f9..71c17c416 100644 --- a/SessionUIKit/Style Guide/Themes/Theme.swift +++ b/SessionUIKit/Style Guide/Themes/Theme.swift @@ -144,7 +144,16 @@ public enum ThemeValue { // InputButton case inputButton_background + // ContextMenu + case contextMenu_background + case contextMenu_highlight + case contextMenu_textHighlight + // Call case callAccept_background case callDecline_background + + // Reactions + case reactions_contextBackground + case reactions_contextMoreBackground }