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
pull/672/head
Morgan Pretty 2 years ago
parent fe14bb1b31
commit 7715c5ea09

@ -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 = "<group>"; };
7B7037422834B81F000DCF35 /* ReactionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionContainerView.swift; sourceTree = "<group>"; };
7B7037442834BCC0000DCF35 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = "<group>"; };
7B7CB188270430D20079FF93 /* CallMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMessageView.swift; sourceTree = "<group>"; };
7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallBanner.swift; sourceTree = "<group>"; };
7B7CB18F270FB2150079FF93 /* MiniCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiniCallView.swift; sourceTree = "<group>"; };
7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallRingTonePlayer.swift; sourceTree = "<group>"; };
@ -1293,7 +1289,6 @@
B83F2B87240CB75A000A54AB /* UIImage+Scaling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Scaling.swift"; sourceTree = "<group>"; };
B84664F4235022F30083A1CD /* MentionUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionUtilities.swift; sourceTree = "<group>"; };
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 = "<group>"; };
B849789525D4A2F500D0D0B3 /* LinkPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewView.swift; sourceTree = "<group>"; };
B84A89BB25DE328A0040017D /* ProfilePictureVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePictureVC.swift; sourceTree = "<group>"; };
B85357BE23A1AE0800AAF6CD /* SeedReminderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedReminderView.swift; sourceTree = "<group>"; };
@ -1304,7 +1299,6 @@
B86BD08323399ACF000F5AE3 /* Modal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modal.swift; sourceTree = "<group>"; };
B86BD08523399CEF000F5AE3 /* SeedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedModal.swift; sourceTree = "<group>"; };
B87588582644CA9D000E60D0 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
B8758859264503A6000E60D0 /* JoinOpenGroupModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinOpenGroupModal.swift; sourceTree = "<group>"; };
B877E24126CA12910007970A /* CallVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVC.swift; sourceTree = "<group>"; };
B877E24526CA13BA0007970A /* CallVC+Camera.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CallVC+Camera.swift"; sourceTree = "<group>"; };
B8783E9D23EB948D00404FB8 /* UILabel+Interaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+Interaction.swift"; sourceTree = "<group>"; };
@ -1322,7 +1316,6 @@
B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanQRCodeWrapperVC.swift; sourceTree = "<group>"; };
B894D0742339EDCF00B4D94D /* NukeDataModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeDataModal.swift; sourceTree = "<group>"; };
B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = "<group>"; };
B8AF4BB326A5204600583500 /* SendSeedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendSeedModal.swift; sourceTree = "<group>"; };
B8B320B6258C30D70020074B /* HTMLMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLMetadata.swift; sourceTree = "<group>"; };
B8B558F026C4BB0600693325 /* CameraManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraManager.swift; sourceTree = "<group>"; };
B8B558FE26C4E05E00693325 /* WebRTCSession+MessageHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebRTCSession+MessageHandling.swift"; sourceTree = "<group>"; };
@ -1818,6 +1811,7 @@
FD71161428D00D6700B47552 /* ThreadDisappearingMessagesViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadDisappearingMessagesViewModelSpec.swift; sourceTree = "<group>"; };
FD71161628D00DA400B47552 /* ThreadSettingsViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSettingsViewModelSpec.swift; sourceTree = "<group>"; };
FD71161928D00E1100B47552 /* NotificationContentViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentViewModelSpec.swift; sourceTree = "<group>"; };
FD71161B28D194FB00B47552 /* MentionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionInfo.swift; sourceTree = "<group>"; };
FD7162DA281B6C440060647B /* TypedTableAlias.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedTableAlias.swift; sourceTree = "<group>"; };
FD716E6328502DDD00C96BF4 /* CallManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManagerProtocol.swift; sourceTree = "<group>"; };
FD716E6528502EE200C96BF4 /* CurrentCallProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentCallProtocol.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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 */,

@ -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<UITouch>, 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<UITouch>, 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<UITouch>, with event: UIEvent?) {
if didTouchDownInside {
themeBackgroundColor = .clear
iconImageView.themeTintColor = .textPrimary
titleLabel.themeTextColor = .textPrimary
}
didTouchDownInside = false
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
if didTouchDownInside {
themeBackgroundColor = .clear
iconImageView.themeTintColor = .textPrimary
titleLabel.themeTextColor = .textPrimary
}
didTouchDownInside = false
}
}
}

@ -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))

@ -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

@ -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)

@ -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

@ -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

@ -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<Capability.Variant> = (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<Profile> = 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<Profile> = 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<String.Index> = maybeLhsRange, let rhsRange: Range<String.Index> = maybeRhsRange else {
return true
}
return (lhsRange.lowerBound < rhsRange.lowerBound)
}
}
// MARK: - Functions

@ -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
}
}

@ -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)
}

@ -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
}

@ -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)
}

@ -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)

@ -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?) {

@ -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)
}
}

@ -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 = []
}

@ -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)

@ -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)

@ -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)

@ -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()
}
}

@ -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)
}
}
}

@ -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)

@ -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)
}
}

@ -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)
}
}

@ -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,

@ -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)

@ -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<SQLRequest<MentionInfo>>? {
guard threadVariant != .contact || userPublicKey != threadId else { return nil }
let profile: TypedTableAlias<Profile> = TypedTableAlias()
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let openGroup: TypedTableAlias<OpenGroup> = 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<MentionInfo> = {
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]
])
}
}
}

@ -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)

@ -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<T>(_ 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))
)

@ -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
]
}

@ -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
]
}

@ -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
]
}

@ -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
]
}

@ -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
}

Loading…
Cancel
Save