Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion NextcloudTalk.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3387,7 +3387,7 @@
repositoryURL = "https://github.com/nextcloud-deps/CDMarkdownKit.git";
requirement = {
kind = revision;
revision = 4b328bc7bbf4a822b56a6b925fb087275cca2ed4;
revision = 632cd89fb53fcb6bf04d53219e984bdf279ea19a;
};
};
1F45A1142A01D6EC005FE87D /* XCRemoteSwiftPackageReference "SDWebImage" */ = {
Expand Down
56 changes: 53 additions & 3 deletions NextcloudTalk/Chat/Chat views/MessageBodyTextView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ class MessageBodyTextView: UITextView, UITextViewDelegate {
self.isEditable = false
self.isScrollEnabled = false
self.delegate = self

let codeBlockGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleCodeBlockTap(_:)))

// Don't swallow the touch, links and the message context menu need to see it as well
codeBlockGestureRecognizer.cancelsTouchesInView = false
self.addGestureRecognizer(codeBlockGestureRecognizer)
}

override func awakeFromNib() {
Expand Down Expand Up @@ -105,13 +111,57 @@ class MessageBodyTextView: UITextView, UITextViewDelegate {
return true
}

guard let attributedText = self.attributedText, let startIndex = self.characterIndex(at: point), startIndex < attributedText.length
else { return false }

if attributedText.attribute(.link, at: startIndex, effectiveRange: nil) != nil {
return true
}

// Code blocks need to receive touches as well, to be able to open them in a scrollable view
return self.codeBlockRange(at: point) != nil
}

// MARK: - Code blocks

private func characterIndex(at point: CGPoint) -> Int? {
guard let position = self.closestPosition(to: point),
let range = self.tokenizer.rangeEnclosingPosition(position, with: .character, inDirection: .layout(.left))
else { return false }
else { return nil }

return self.offset(from: self.beginningOfDocument, to: range.start)
}

private func codeBlockRange(at point: CGPoint) -> NSRange? {
guard let attributedText = self.attributedText, let index = self.characterIndex(at: point), index < attributedText.length
else { return nil }

var effectiveRange = NSRange()

// Inline code is styled like a code block, only the attribute set by the parser tells them apart
guard attributedText.attribute(.syntaxBlock, at: index, effectiveRange: &effectiveRange) != nil else { return nil }

return effectiveRange
}

@objc private func handleCodeBlockTap(_ gestureRecognizer: UITapGestureRecognizer) {
let point = gestureRecognizer.location(in: self)

guard let attributedText = self.attributedText, let index = self.characterIndex(at: point), index < attributedText.length,
// A detected link inside a code block is handled by the text view itself
attributedText.attribute(.link, at: index, effectiveRange: nil) == nil,
let range = self.codeBlockRange(at: point)
else { return }

// The block keeps the newline before the closing fence, but leading spaces are part of the code
let code = attributedText.attributedSubstring(from: range).string.trimmingCharacters(in: .newlines)

guard !code.isEmpty else { return }

let startIndex = self.offset(from: self.beginningOfDocument, to: range.start)
let codeViewController = GithubPermalinkViewController(codeBlock: code)
let navigationController = UINavigationController(rootViewController: codeViewController)

return self.attributedText.attribute(.link, at: startIndex, effectiveRange: nil) != nil
NCUserInterfaceController.sharedInstance().mainViewController.present(navigationController, animated: true)
}

// MARK: - UITextView delegate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import SwiftyAttributes
private var repo = ""
private var filePath = ""
private var lineNumberWidth: CGFloat = 0
private var codeBlock: String?

init(url: String,
sourceWithLineNumbers: NSAttributedString,
Expand All @@ -45,6 +46,17 @@ import SwiftyAttributes
self.lineNumberWidth = lineNumberWidth
}

// A code block of a chat message, shown without line numbers and without any repository details
init(codeBlock: String) {
super.init(nibName: "GithubPermalinkViewController", bundle: nil)

self.codeBlock = codeBlock
self.sourceWithoutLineNumbers = NSAttributedString(string: codeBlock, attributes: [
.font: UIFont.monospacedPreferredFont(forTextStyle: .body),
.foregroundColor: UIColor.label
])
}

required init?(coder: NSCoder) {
super.init(coder: coder)
}
Expand All @@ -58,15 +70,19 @@ import SwiftyAttributes

NCAppBranding.styleViewController(self)

self.navigationItem.title = NSLocalizedString("Source code", comment: "")
self.navigationItem.title = NSLocalizedString("Source code", comment: "Title of a view showing the source code of a file or a code block")

self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(self.cancelButtonPressed))
if #unavailable(iOS 26.0) {
self.navigationItem.leftBarButtonItem?.tintColor = NCAppBranding.themeTextColor()
}

let githubButton = UIBarButtonItem(image: UIImage(named: "github")?.withRenderingMode(.alwaysTemplate), style: .plain, target: self, action: #selector(githubButtonPressed))
self.navigationItem.rightBarButtonItem = githubButton
if self.codeBlock != nil {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "doc.on.doc"), style: .plain, target: self, action: #selector(copyButtonPressed))
} else {
let githubButton = UIBarButtonItem(image: UIImage(named: "github")?.withRenderingMode(.alwaysTemplate), style: .plain, target: self, action: #selector(githubButtonPressed))
self.navigationItem.rightBarButtonItem = githubButton
}

if #unavailable(iOS 26.0) {
self.navigationItem.rightBarButtonItem?.tintColor = NCAppBranding.themeTextColor()
Expand Down Expand Up @@ -99,6 +115,21 @@ import SwiftyAttributes
// Take safe-area padding of 10 into account here
self.scrollViewLeftConstraint.constant = self.lineNumberWidth + 10

if self.codeBlock != nil {
// A code block has no repository details to show, remove the labels and start with the source at the top.
// Removing the labels also removes the constraints the scroll views had to them, so those are set up again.
self.ownerLabel.removeFromSuperview()
self.repoLabel.removeFromSuperview()
self.fileLabel.removeFromSuperview()

NSLayoutConstraint.activate([
self.sourceWithNumbersScrollView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 10),
self.sourceWithoutNumbersScrollView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 10)
])

return
}

var formattedOwner = NSLocalizedString("Owner", comment: "Owner of a repository").attributedString + ": ".attributedString
formattedOwner = formattedOwner.withFont(fontSemibold).withTextColor(.secondaryLabel)
formattedOwner += self.owner.withFont(font)
Expand All @@ -115,6 +146,11 @@ import SwiftyAttributes
self.fileLabel.attributedText = formattedPath
}

func copyButtonPressed() {
UIPasteboard.general.string = self.codeBlock
NotificationPresenter.shared().present(text: NSLocalizedString("Code copied", comment: "Shown after the code of a code block was copied to the clipboard"), dismissAfterDelay: 5.0, includedStyle: .dark)
}

func githubButtonPressed() {
if let url = self.url {
NCUtils.openLinkInBrowser(link: url)
Expand Down
5 changes: 4 additions & 1 deletion NextcloudTalk/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,9 @@
/* Code block */
"Code" = "Code";

/* Shown after the code of a code block was copied to the clipboard */
"Code copied" = "Code copied";

/* No comment provided by engineer. */
"Configuration" = "Configuration";

Expand Down Expand Up @@ -2328,7 +2331,7 @@
/* Title for conversations sorting options */
"Sort conversations" = "Sort conversations";

/* No comment provided by engineer. */
/* Title of a view showing the source code of a file or a code block */
"Source code" = "Source code";

/* speaker = Loudspeaker, device */
Expand Down
Loading