Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,11 @@ struct ChatTextView: NSViewRepresentable {
}
}

class DynamicTextView: NSTextView {
class DynamicTextView: SubmitableTextView {
@objc var placeholderAttributedString: NSAttributedString?

static let padding = NSSize(width: 10, height: 10)

var onSubmit: (() -> Void)?
var onAttachmentPaste: ((NSPasteboard) -> Bool)?

override func paste(_ sender: Any?) {
Expand Down Expand Up @@ -181,6 +180,11 @@ class DynamicTextView: NSTextView {
invalidateIntrinsicContentSize()
}

}

class SubmitableTextView: NSTextView {
var onSubmit: (() -> Void)?

override func performKeyEquivalent(with event: NSEvent) -> Bool {
// Always submit on cmd+enter
if (event.specialKey == .enter || event.specialKey == .carriageReturn)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ protocol MessageContentRowView: NSView {
var onArrowKey: ((TimelineSelectionDirection) -> Void)? { get set }
func configure(content: MatrixRustSDK.MsgLikeContent, matrixClient: MatrixClient?)
func height(for content: MatrixRustSDK.MsgLikeContent, width: CGFloat) -> CGFloat
func configureEditing(_ state: MessageEditState?)
}

extension MessageContentRowView {
func configureEditing(_ state: MessageEditState?) {}
}

/// Media rows deliver downloaded files to their owning timeline controller.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Foundation
import MatrixRustSDK

@MainActor final class MessageEditState {
let id: EventOrTransactionId
var text: String
var selectedRange: NSRange
var isSaving = false
var error: String?
var onChange: (() -> Void)?
var onSave: (() -> Void)?
var onCancel: (() -> Void)?

init(id: EventOrTransactionId, text: String) {
self.id = id
self.text = text
selectedRange = NSRange(location: (text as NSString).length, length: 0)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ final class MessageFileContentView: NSView, MessageMediaPreviewContentView {
size: file.info?.size?.formatted(.byteCount(style: .file))
)

let hasCaption = file.caption?.isEmpty == false || file.formattedCaption != nil
let hasCaption =
captionView.isEditing || file.caption?.isEmpty == false || file.formattedCaption != nil
captionView.isHidden = !hasCaption
captionView.configureCaption(file.caption, formatted: file.formattedCaption)
fileBottomConstraint.isActive = !hasCaption
Expand All @@ -91,6 +92,19 @@ final class MessageFileContentView: NSView, MessageMediaPreviewContentView {
}
}

func configureEditing(_ state: MessageEditState?) {
captionView.configureEditing(state)
let hasCaption =
state != nil || fileContent?.caption?.isEmpty == false
|| fileContent?.formattedCaption != nil
fileBottomConstraint.isActive = false
captionBottomConstraint.isActive = false
captionView.isHidden = !hasCaption
fileBottomConstraint.isActive = !hasCaption
captionBottomConstraint.isActive = hasCaption
needsLayout = true
}

func height(for content: MsgLikeContent, width: CGFloat) -> CGFloat {
guard case .message(let message) = content.kind,
case .file(let file) = message.msgType
Expand All @@ -99,7 +113,8 @@ final class MessageFileContentView: NSView, MessageMediaPreviewContentView {
}

func height(for file: FileMessageContent, width: CGFloat) -> CGFloat {
guard file.caption?.isEmpty == false || file.formattedCaption != nil else { return 36 }
guard captionView.isEditing || file.caption?.isEmpty == false || file.formattedCaption != nil
else { return 36 }
return 36 + 10
+ ceil(
captionView.height(forCaption: file.caption, formatted: file.formattedCaption, width: width)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,23 @@ final class MessageGalleryContentView: NSView, MessageMediaPreviewContentView {
}
attachments = nextAttachments

rebuildComposition()
updateCallbacks()
}

func configureEditing(_ state: MessageEditState?) {
captionView.configureEditing(state)
captionMeasurementView.configureEditing(state)
rebuildComposition()
needsLayout = true
}

private func rebuildComposition() {
guard let gallery else { return }
NSLayoutConstraint.deactivate(compositionConstraints)
compositionConstraints = []
attachmentHeightConstraints = []
captionHeightConstraint = nil
var previous: NSView?
for attachment in attachments {
let view = attachment.view
Expand All @@ -79,7 +96,7 @@ final class MessageGalleryContentView: NSView, MessageMediaPreviewContentView {
previous = view
}

let hasCaption = Self.hasCaption(gallery)
let hasCaption = captionView.isEditing || Self.hasCaption(gallery)
captionView.isHidden = !hasCaption
captionView.configureCaption(gallery.body, formatted: gallery.formatted)
if hasCaption {
Expand All @@ -102,7 +119,6 @@ final class MessageGalleryContentView: NSView, MessageMediaPreviewContentView {
compositionConstraints.append(bottom)
updateHeights()
NSLayoutConstraint.activate(compositionConstraints)
updateCallbacks()
}

override func layout() {
Expand All @@ -123,7 +139,7 @@ final class MessageGalleryContentView: NSView, MessageMediaPreviewContentView {
var height =
itemHeights.reduce(0, +)
+ CGFloat(max(itemHeights.count - 1, 0)) * Self.itemSpacing
if Self.hasCaption(gallery) {
if captionView.isEditing || Self.hasCaption(gallery) {
height +=
(itemHeights.isEmpty ? 0 : Self.captionSpacing)
+ ceil(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ final class MessageHoverOverlayView: NSView {
case reply
case replyInThread
case pin
case edit
}

var onAction: ((Action) -> Void)?
var onMouseExited: ((NSEvent) -> Void)?

private let stack = NSStackView()
private let editButton = HoverActionButton(symbol: "pencil", label: "Edit message")
private let replyButton = HoverActionButton(symbol: "arrowshape.turn.up.left", label: "Reply")
private let threadButton = HoverActionButton(symbol: "ellipsis.message", label: "Reply in thread")

Expand Down Expand Up @@ -74,6 +76,8 @@ final class MessageHoverOverlayView: NSView {
threadButton.onClick = { [weak self] in self?.onAction?(.replyInThread) }
stack.addArrangedSubview(replyButton)
stack.addArrangedSubview(threadButton)
editButton.onClick = { [weak self] in self?.onAction?(.edit) }
stack.addArrangedSubview(editButton)

let pinButton = HoverActionButton(symbol: "pin", label: "Pin")
pinButton.onClick = { [weak self] in self?.onAction?(.pin) }
Expand All @@ -92,7 +96,8 @@ final class MessageHoverOverlayView: NSView {
configure(canReply: true)
}

func configure(canReply: Bool) {
func configure(canReply: Bool, canEdit: Bool = false) {
editButton.isHidden = !canEdit
replyButton.isHidden = !canReply
threadButton.isHidden = !canReply
setFrameSize(fittingSize)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ final class MessageImageContentView: NSView, MessageMediaPreviewContentView {
func configure(image: ImageMessageContent, matrixClient: MatrixClient?) {
self.imageContent = image
self.matrixClient = matrixClient
let hasCaption = image.caption?.isEmpty == false || image.formattedCaption != nil
let hasCaption =
captionView.isEditing || image.caption?.isEmpty == false || image.formattedCaption != nil
captionView.isHidden = !hasCaption
captionView.configureCaption(image.caption, formatted: image.formattedCaption)
imageBottomConstraint.isActive = !hasCaption
Expand All @@ -121,6 +122,19 @@ final class MessageImageContentView: NSView, MessageMediaPreviewContentView {
}
}

func configureEditing(_ state: MessageEditState?) {
captionView.configureEditing(state)
let hasCaption =
state != nil || imageContent?.caption?.isEmpty == false
|| imageContent?.formattedCaption != nil
imageBottomConstraint.isActive = false
captionBottomConstraint.isActive = false
captionView.isHidden = !hasCaption
imageBottomConstraint.isActive = !hasCaption
captionBottomConstraint.isActive = hasCaption
needsLayout = true
}

override func layout() {
super.layout()
guard let imageContent else { return }
Expand All @@ -136,7 +150,8 @@ final class MessageImageContentView: NSView, MessageMediaPreviewContentView {

func height(for image: ImageMessageContent, width: CGFloat) -> CGFloat {
let imageHeight = Self.imageSize(for: image, width: width).height
guard image.caption?.isEmpty == false || image.formattedCaption != nil else {
guard captionView.isEditing || image.caption?.isEmpty == false || image.formattedCaption != nil
else {
return imageHeight
}
return imageHeight + 10
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ final class MessageRowView: NSView {
private static let receiptSpacing: CGFloat = 10

private let timestamp = NSTextField(labelWithString: "")
private let editedLabel = NSTextField(labelWithString: "(edited)")
private let contentView: any MessageContentRowView
private var replyPreview: MessageReplyPreviewView?
private var replyDetails: MatrixRustSDK.EmbeddedEventDetails?
Expand Down Expand Up @@ -81,6 +82,13 @@ final class MessageRowView: NSView {
timestamp.textColor = .secondaryLabelColor
timestamp.alignment = .right
timestamp.translatesAutoresizingMaskIntoConstraints = false

editedLabel.font = .systemFont(ofSize: 10)
editedLabel.textColor = .secondaryLabelColor
editedLabel.alignment = .right
editedLabel.isHidden = true
editedLabel.translatesAutoresizingMaskIntoConstraints = false

contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.onSelectRequest = { [weak self] in
guard let self else { return }
Expand All @@ -101,13 +109,18 @@ final class MessageRowView: NSView {
))

addSubview(timestamp)
addSubview(editedLabel)
addSubview(contentView)

// NSTableView may retain an old encapsulated height while a reused row is reconfigured.
contentBottomToRow.priority = .init(999)
NSLayoutConstraint.activate([
timestamp.leadingAnchor.constraint(equalTo: leadingAnchor),
timestamp.topAnchor.constraint(equalTo: topAnchor, constant: 6),
timestamp.widthAnchor.constraint(equalToConstant: 48),
editedLabel.leadingAnchor.constraint(equalTo: timestamp.leadingAnchor),
editedLabel.trailingAnchor.constraint(equalTo: timestamp.trailingAnchor),
editedLabel.topAnchor.constraint(equalTo: timestamp.bottomAnchor, constant: 1),

contentView.leadingAnchor.constraint(equalTo: timestamp.trailingAnchor, constant: 16),
contentView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10),
Expand All @@ -116,6 +129,10 @@ final class MessageRowView: NSView {
])
}

func configureEditing(_ state: MessageEditState?) {
contentView.configureEditing(state)
}

func configure(
event: MatrixRustSDK.EventTimelineItem,
content: MatrixRustSDK.MsgLikeContent,
Expand All @@ -136,6 +153,7 @@ final class MessageRowView: NSView {

let date = Date(timeIntervalSince1970: Double(event.timestamp) / 1000)
timestamp.stringValue = Self.timeFormatter.string(from: date)
editedLabel.isHidden = !Self.hasBeenEdited(content)
contentView.configure(content: content, matrixClient: matrixClient)
configureReply(details: replyDetails, onClick: onReplyClick)
configureThread(summary: content.threadSummary, onClick: onThreadClick)
Expand Down Expand Up @@ -402,6 +420,14 @@ final class MessageRowView: NSView {
}
}

private static func hasBeenEdited(_ content: MatrixRustSDK.MsgLikeContent) -> Bool {
switch content.kind {
case .message(let message): return message.isEdited
case .poll(_, _, _, _, _, _, let hasBeenEdited): return hasBeenEdited
default: return false
}
}

func height(
for content: MatrixRustSDK.MsgLikeContent, width: CGFloat,
replyDetails: MatrixRustSDK.EmbeddedEventDetails?, reactions: [MatrixRustSDK.Reaction],
Expand Down Expand Up @@ -430,7 +456,7 @@ final class MessageRowView: NSView {
? 0 : MessageReadReceiptsView.rowHeight + Self.receiptSpacing
return max(
ceil(contentView.height(for: content, width: contentWidth)) + replyHeight + threadHeight
+ reactionsHeight + receiptsHeight + 8, 28)
+ reactionsHeight + receiptsHeight + 8, Self.hasBeenEdited(content) ? 40 : 28)
}

@available(*, unavailable)
Expand Down
Loading
Loading