diff --git a/Session/Conversations V2/ConversationVC+Interaction.swift b/Session/Conversations V2/ConversationVC+Interaction.swift index 303003d8a..0df2678d8 100644 --- a/Session/Conversations V2/ConversationVC+Interaction.swift +++ b/Session/Conversations V2/ConversationVC+Interaction.swift @@ -253,6 +253,15 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc updateMentions(for: newText) } + func showLinkPreviewSuggestionModal() { + let linkPreviewModel = LinkPreviewModal() { [weak self] in + self?.snInputView.autoGenerateLinkPreview() + } + linkPreviewModel.modalPresentationStyle = .overFullScreen + linkPreviewModel.modalTransitionStyle = .crossDissolve + present(linkPreviewModel, animated: true, completion: nil) + } + // MARK: Mentions private func updateMentions(for newText: String) { if newText.count < oldText.count { @@ -378,6 +387,33 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc } } + func showFailedMessageSheet(for tsMessage: TSOutgoingMessage) { + let thread = self.thread + let sheet = UIAlertController(title: tsMessage.mostRecentFailureText, message: nil, preferredStyle: .actionSheet) + sheet.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + sheet.addAction(UIAlertAction(title: "Delete", style: .destructive, handler: { _ in + Storage.write { transaction in + tsMessage.remove(with: transaction) + Storage.shared.cancelPendingMessageSendJobIfNeeded(for: tsMessage.timestamp, using: transaction) + } + })) + sheet.addAction(UIAlertAction(title: "Resend", style: .default, handler: { _ in + let message = VisibleMessage.from(tsMessage) + Storage.write { transaction in + var attachments: [TSAttachmentStream] = [] + tsMessage.attachmentIds.forEach { attachmentID in + guard let attachmentID = attachmentID as? String else { return } + let attachment = TSAttachment.fetch(uniqueId: attachmentID, transaction: transaction) + guard let stream = attachment as? TSAttachmentStream else { return } + attachments.append(stream) + } + MessageSender.prep(attachments, for: message, using: transaction) + MessageSender.send(message, in: thread, using: transaction) + } + })) + present(sheet, animated: true, completion: nil) + } + func handleViewItemDoubleTapped(_ viewItem: ConversationViewItem) { switch viewItem.messageCellType { case .audio: speedUpAudio(for: viewItem) @@ -452,6 +488,14 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc } // MARK: Voice Message Playback + @objc func handleAudioDidFinishPlayingNotification(_ notification: Notification) { + guard let audioPlayer = audioPlayer, let viewItem = audioPlayer.owner as? ConversationViewItem, + let index = viewItems.firstIndex(where: { $0 === viewItem }), index < (viewItems.endIndex - 1) else { return } + let nextViewItem = viewItems[index + 1] + guard nextViewItem.messageCellType == .audio else { return } + playOrPauseAudio(for: nextViewItem) + } + func playOrPauseAudio(for viewItem: ConversationViewItem) { guard let attachment = viewItem.attachmentStream else { return } let fileManager = FileManager.default diff --git a/Session/Conversations V2/ConversationVC.swift b/Session/Conversations V2/ConversationVC.swift index 524355c69..cd37e2a56 100644 --- a/Session/Conversations V2/ConversationVC.swift +++ b/Session/Conversations V2/ConversationVC.swift @@ -124,6 +124,8 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, UITableViewD notificationCenter.addObserver(self, selector: #selector(handleKeyboardWillHideNotification(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) notificationCenter.addObserver(self, selector: #selector(handleAudioDidFinishPlayingNotification(_:)), name: .SNAudioDidFinishPlaying, object: nil) notificationCenter.addObserver(self, selector: #selector(addOrRemoveBlockedBanner), name: NSNotification.Name(rawValue: kNSNotificationName_BlockListDidChange), object: nil) + // Mentions + MentionsManager.populateUserPublicKeyCacheIfNeeded(for: thread.uniqueId!) } override func viewDidLayoutSubviews() { @@ -323,14 +325,6 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, UITableViewD func getMediaCache() -> NSCache { return mediaCache } - - @objc private func handleAudioDidFinishPlayingNotification(_ notification: Notification) { - guard let audioPlayer = audioPlayer, let viewItem = audioPlayer.owner as? ConversationViewItem, - let index = viewItems.firstIndex(where: { $0 === viewItem }), index < (viewItems.endIndex - 1) else { return } - let nextViewItem = viewItems[index + 1] - guard nextViewItem.messageCellType == .audio else { return } - playOrPauseAudio(for: nextViewItem) - } func scrollToBottom(isAnimated: Bool) { guard !isUserScrolling else { return } @@ -363,42 +357,6 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, UITableViewD isLoadingMore = true viewModel.loadAnotherPageOfMessages() } - - func showLinkPreviewSuggestionModal() { - let linkPreviewModel = LinkPreviewModal() { [weak self] in - self?.snInputView.autoGenerateLinkPreview() - } - linkPreviewModel.modalPresentationStyle = .overFullScreen - linkPreviewModel.modalTransitionStyle = .crossDissolve - present(linkPreviewModel, animated: true, completion: nil) - } - - func showFailedMessageSheet(for tsMessage: TSOutgoingMessage) { - let thread = self.thread - let sheet = UIAlertController(title: tsMessage.mostRecentFailureText, message: nil, preferredStyle: .actionSheet) - sheet.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) - sheet.addAction(UIAlertAction(title: "Delete", style: .destructive, handler: { _ in - Storage.write { transaction in - tsMessage.remove(with: transaction) - Storage.shared.cancelPendingMessageSendJobIfNeeded(for: tsMessage.timestamp, using: transaction) - } - })) - sheet.addAction(UIAlertAction(title: "Resend", style: .default, handler: { _ in - let message = VisibleMessage.from(tsMessage) - Storage.write { transaction in - var attachments: [TSAttachmentStream] = [] - tsMessage.attachmentIds.forEach { attachmentID in - guard let attachmentID = attachmentID as? String else { return } - let attachment = TSAttachment.fetch(uniqueId: attachmentID, transaction: transaction) - guard let stream = attachment as? TSAttachmentStream else { return } - attachments.append(stream) - } - MessageSender.prep(attachments, for: message, using: transaction) - MessageSender.send(message, in: thread, using: transaction) - } - })) - present(sheet, animated: true, completion: nil) - } // MARK: Convenience private func getScrollButtonOpacity() -> CGFloat { diff --git a/Session/Conversations V2/Input View/InputView.swift b/Session/Conversations V2/Input View/InputView.swift index 225c955cb..26b483eca 100644 --- a/Session/Conversations V2/Input View/InputView.swift +++ b/Session/Conversations V2/Input View/InputView.swift @@ -33,10 +33,22 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate, private lazy var mentionsView: MentionSelectionView = { let result = MentionSelectionView() - result.alpha = 0 result.delegate = self return result }() + + private lazy var mentionsViewContainer: UIView = { + let result = UIView() + let backgroundView = UIView() + backgroundView.backgroundColor = isLightMode ? .white : .black + backgroundView.alpha = Values.lowOpacity + result.addSubview(backgroundView) + backgroundView.pin(to: result) + let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) + result.addSubview(blurView) + blurView.pin(to: result) + return result + }() private lazy var inputTextView = InputTextView(delegate: self) @@ -102,10 +114,12 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate, mainStackView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing ], to: self) mainStackView.pin(.bottom, to: .bottom, of: self, withInset: -2) // Mentions - addSubview(mentionsView) + addSubview(mentionsViewContainer) + mentionsViewContainer.pin([ UIView.HorizontalEdge.left, UIView.HorizontalEdge.right ], to: self) + mentionsViewContainer.pin(.bottom, to: .top, of: self) + mentionsViewContainer.addSubview(mentionsView) + mentionsView.pin(to: mentionsViewContainer) mentionsViewHeightConstraint.isActive = true - mentionsView.pin([ UIView.HorizontalEdge.left, UIView.HorizontalEdge.right ], to: self) - mentionsView.pin(.bottom, to: .top, of: self) // Voice message button addSubview(voiceMessageButtonContainer) voiceMessageButtonContainer.center(in: sendButton) @@ -189,7 +203,7 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate, // MARK: Interaction override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { - if mentionsView.frame.contains(point) { + if mentionsViewContainer.frame.contains(point) { return true } else { return super.point(inside: point, with: event) @@ -267,7 +281,7 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate, func hideMentionsUI() { UIView.animate(withDuration: 0.25, animations: { - self.mentionsView.alpha = 0 + self.mentionsViewContainer.alpha = 0 }, completion: { _ in self.mentionsViewHeightConstraint.constant = 0 self.mentionsView.tableView.contentOffset = CGPoint.zero @@ -276,19 +290,20 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate, func showMentionsUI(for candidates: [Mention], in thread: TSThread) { if let openGroup = Storage.shared.getOpenGroup(for: thread.uniqueId!) { - mentionsView.publicChatServer = openGroup.server - mentionsView.publicChatChannel = openGroup.channel + mentionsView.openGroupServer = openGroup.server + mentionsView.openGroupChannel = openGroup.channel } - mentionsView.mentionCandidates = candidates - mentionsViewHeightConstraint.constant = CGFloat(candidates.count * 42) + mentionsView.candidates = candidates + let mentionCellHeight = Values.smallProfilePictureSize + 2 * Values.smallSpacing + mentionsViewHeightConstraint.constant = CGFloat(min(3, candidates.count)) * mentionCellHeight layoutIfNeeded() UIView.animate(withDuration: 0.25) { - self.mentionsView.alpha = 1 + self.mentionsViewContainer.alpha = 1 } } func handleMentionSelected(_ mention: Mention, from view: MentionSelectionView) { - print(mention.displayName) + delegate.handleMentionSelected(mention, from: view) } // MARK: Convenience @@ -313,4 +328,5 @@ protocol InputViewDelegate : VoiceMessageRecordingViewDelegate { func handleSendButtonTapped() func handleQuoteViewCancelButtonTapped() func inputTextViewDidChangeContent(_ inputTextView: InputTextView) + func handleMentionSelected(_ mention: Mention, from view: MentionSelectionView) } diff --git a/Session/Conversations V2/Input View/MentionSelectionView.swift b/Session/Conversations V2/Input View/MentionSelectionView.swift index a1ce1d4b2..3debdc275 100644 --- a/Session/Conversations V2/Input View/MentionSelectionView.swift +++ b/Session/Conversations V2/Input View/MentionSelectionView.swift @@ -1,18 +1,17 @@ final class MentionSelectionView : UIView, UITableViewDataSource, UITableViewDelegate { - @objc var mentionCandidates: [Mention] = [] { didSet { tableView.reloadData() } } - @objc var publicChatServer: String? - var publicChatChannel: UInt64? - @objc var delegate: MentionSelectionViewDelegate? - - // MARK: Convenience - @objc(setPublicChatChannel:) - func setPublicChatChannel(to publicChatChannel: UInt64) { - self.publicChatChannel = publicChatChannel != 0 ? publicChatChannel : nil + var candidates: [Mention] = [] { + didSet { + tableView.isScrollEnabled = (candidates.count > 4) + tableView.reloadData() + } } + var openGroupServer: String? + var openGroupChannel: UInt64? + var delegate: MentionSelectionViewDelegate? // MARK: Components - @objc lazy var tableView: UITableView = { // TODO: Make this private + lazy var tableView: UITableView = { // TODO: Make this private let result = UITableView() result.dataSource = self result.delegate = self @@ -35,8 +34,10 @@ final class MentionSelectionView : UIView, UITableViewDataSource, UITableViewDel } private func setUpViewHierarchy() { + // Table view addSubview(tableView) tableView.pin(to: self) + // Top separator let topSeparator = UIView() topSeparator.backgroundColor = Colors.separator topSeparator.set(.height, to: Values.separatorThickness) @@ -44,6 +45,7 @@ final class MentionSelectionView : UIView, UITableViewDataSource, UITableViewDel topSeparator.pin(.leading, to: .leading, of: self) topSeparator.pin(.top, to: .top, of: self) topSeparator.pin(.trailing, to: .trailing, of: self) + // Bottom separator let bottomSeparator = UIView() bottomSeparator.backgroundColor = Colors.separator bottomSeparator.set(.height, to: Values.separatorThickness) @@ -55,22 +57,22 @@ final class MentionSelectionView : UIView, UITableViewDataSource, UITableViewDel // MARK: Data func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return mentionCandidates.count + return candidates.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! Cell - let mentionCandidate = mentionCandidates[indexPath.row] + let mentionCandidate = candidates[indexPath.row] cell.mentionCandidate = mentionCandidate - cell.publicChatServer = publicChatServer - cell.publicChatChannel = publicChatChannel - cell.separator.isHidden = (indexPath.row == (mentionCandidates.count - 1)) + cell.openGroupServer = openGroupServer + cell.openGroupChannel = openGroupChannel + cell.separator.isHidden = (indexPath.row == (candidates.count - 1)) return cell } // MARK: Interaction func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let mentionCandidate = mentionCandidates[indexPath.row] + let mentionCandidate = candidates[indexPath.row] delegate?.handleMentionSelected(mentionCandidate, from: self) } } @@ -81,8 +83,8 @@ private extension MentionSelectionView { final class Cell : UITableViewCell { var mentionCandidate = Mention(publicKey: "", displayName: "") { didSet { update() } } - var publicChatServer: String? - var publicChatChannel: UInt64? + var openGroupServer: String? + var openGroupChannel: UInt64? // MARK: Components private lazy var profilePictureView = ProfilePictureView() @@ -119,36 +121,36 @@ private extension MentionSelectionView { } private func setUpViewHierarchy() { - // Set the cell background color - backgroundColor = Colors.cellBackground - // Set up the highlight color + // Cell background color + backgroundColor = .clear + // Highlight color let selectedBackgroundView = UIView() - selectedBackgroundView.backgroundColor = Colors.cellBackground // Intentionally not Colors.cellSelected + selectedBackgroundView.backgroundColor = .clear self.selectedBackgroundView = selectedBackgroundView - // Set up the profile picture image view - let profilePictureViewSize = Values.verySmallProfilePictureSize + // Profile picture image view + let profilePictureViewSize = Values.smallProfilePictureSize profilePictureView.set(.width, to: profilePictureViewSize) profilePictureView.set(.height, to: profilePictureViewSize) profilePictureView.size = profilePictureViewSize - // Set up the main stack view - let stackView = UIStackView(arrangedSubviews: [ profilePictureView, displayNameLabel ]) - stackView.axis = .horizontal - stackView.alignment = .center - stackView.spacing = Values.mediumSpacing - stackView.set(.height, to: profilePictureViewSize) - contentView.addSubview(stackView) - stackView.pin(.leading, to: .leading, of: contentView, withInset: Values.mediumSpacing) - stackView.pin(.top, to: .top, of: contentView, withInset: Values.smallSpacing) - contentView.pin(.trailing, to: .trailing, of: stackView, withInset: Values.mediumSpacing) - contentView.pin(.bottom, to: .bottom, of: stackView, withInset: Values.smallSpacing) - stackView.set(.width, to: UIScreen.main.bounds.width - 2 * Values.mediumSpacing) - // Set up the moderator icon image view + // Main stack view + let mainStackView = UIStackView(arrangedSubviews: [ profilePictureView, displayNameLabel ]) + mainStackView.axis = .horizontal + mainStackView.alignment = .center + mainStackView.spacing = Values.mediumSpacing + mainStackView.set(.height, to: profilePictureViewSize) + contentView.addSubview(mainStackView) + mainStackView.pin(.leading, to: .leading, of: contentView, withInset: Values.mediumSpacing) + mainStackView.pin(.top, to: .top, of: contentView, withInset: Values.smallSpacing) + contentView.pin(.trailing, to: .trailing, of: mainStackView, withInset: Values.mediumSpacing) + contentView.pin(.bottom, to: .bottom, of: mainStackView, withInset: Values.smallSpacing) + mainStackView.set(.width, to: UIScreen.main.bounds.width - 2 * Values.mediumSpacing) + // Moderator icon image view moderatorIconImageView.set(.width, to: 20) moderatorIconImageView.set(.height, to: 20) contentView.addSubview(moderatorIconImageView) - moderatorIconImageView.pin(.trailing, to: .trailing, of: profilePictureView) - moderatorIconImageView.pin(.bottom, to: .bottom, of: profilePictureView, withInset: 3.5) - // Set up the separator + moderatorIconImageView.pin(.trailing, to: .trailing, of: profilePictureView, withInset: 1) + moderatorIconImageView.pin(.bottom, to: .bottom, of: profilePictureView, withInset: 4.5) + // Separator addSubview(separator) separator.pin(.leading, to: .leading, of: self) separator.pin(.trailing, to: .trailing, of: self) @@ -160,7 +162,7 @@ private extension MentionSelectionView { displayNameLabel.text = mentionCandidate.displayName profilePictureView.hexEncodedPublicKey = mentionCandidate.publicKey profilePictureView.update() - if let server = publicChatServer, let channel = publicChatChannel { + if let server = openGroupServer, let channel = openGroupChannel { let isUserModerator = OpenGroupAPI.isUserModerator(mentionCandidate.publicKey, for: channel, on: server) moderatorIconImageView.isHidden = !isUserModerator } else { @@ -172,7 +174,6 @@ private extension MentionSelectionView { // MARK: - Delegate -@objc(LKMentionSelectionViewDelegate) protocol MentionSelectionViewDelegate { func handleMentionSelected(_ mention: Mention, from view: MentionSelectionView) diff --git a/Session/Meta/Images.xcassets/Loki/Contents.json b/Session/Meta/Images.xcassets/Loki/Contents.json index da4a164c9..73c00596a 100644 --- a/Session/Meta/Images.xcassets/Loki/Contents.json +++ b/Session/Meta/Images.xcassets/Loki/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Session/Meta/Images.xcassets/Loki/Crown.imageset/Contents.json b/Session/Meta/Images.xcassets/Loki/Crown.imageset/Contents.json index 97010b1ad..f3c747220 100644 --- a/Session/Meta/Images.xcassets/Loki/Crown.imageset/Contents.json +++ b/Session/Meta/Images.xcassets/Loki/Crown.imageset/Contents.json @@ -1,12 +1,12 @@ { "images" : [ { - "idiom" : "universal", - "filename" : "crown.pdf" + "filename" : "Crown.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Session/Meta/Images.xcassets/Loki/Crown.imageset/crown.pdf b/Session/Meta/Images.xcassets/Loki/Crown.imageset/crown.pdf index 5e5c424ca..3e067f0b5 100644 Binary files a/Session/Meta/Images.xcassets/Loki/Crown.imageset/crown.pdf and b/Session/Meta/Images.xcassets/Loki/Crown.imageset/crown.pdf differ