mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Add permalink detection and custom attributed string attributes (#536)
* Pass full attributed string to TimelineItems and let the UI level handle blockquote coalescing * Add permalink detection and attribute embedding within the attributed string
This commit is contained in:
@@ -245,14 +245,14 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
case .react:
|
||||
callback?(.displayEmojiPicker(itemId: item.id))
|
||||
case .copy:
|
||||
UIPasteboard.general.string = item.text
|
||||
UIPasteboard.general.string = item.body
|
||||
case .edit:
|
||||
state.bindings.composerFocused = true
|
||||
state.bindings.composerText = item.text
|
||||
state.bindings.composerText = item.body
|
||||
state.composerMode = .edit(originalItemId: item.id)
|
||||
case .quote:
|
||||
state.bindings.composerFocused = true
|
||||
state.bindings.composerText = "> \(item.text)"
|
||||
state.bindings.composerText = "> \(item.body)"
|
||||
case .copyPermalink:
|
||||
do {
|
||||
let permalink = try PermalinkBuilder.permalinkTo(eventIdentifier: item.id, roomIdentifier: timelineController.roomID)
|
||||
|
||||
@@ -22,10 +22,10 @@ struct EmoteRoomTimelineView: View {
|
||||
|
||||
var body: some View {
|
||||
TimelineStyler(timelineItem: timelineItem) {
|
||||
if let attributedComponents = timelineItem.attributedComponents {
|
||||
FormattedBodyText(attributedComponents: attributedComponents)
|
||||
if let attributedString = timelineItem.formattedBody {
|
||||
FormattedBodyText(attributedString: attributedString)
|
||||
} else {
|
||||
FormattedBodyText(text: timelineItem.text)
|
||||
FormattedBodyText(text: timelineItem.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ struct EmoteRoomTimelineView_Previews: PreviewProvider {
|
||||
|
||||
private static func itemWith(text: String, timestamp: String, senderId: String) -> EmoteRoomTimelineItem {
|
||||
EmoteRoomTimelineItem(id: UUID().uuidString,
|
||||
text: text,
|
||||
body: text,
|
||||
timestamp: timestamp,
|
||||
groupState: .single,
|
||||
isOutgoing: false,
|
||||
|
||||
@@ -21,7 +21,7 @@ struct EncryptedRoomTimelineView: View {
|
||||
|
||||
var body: some View {
|
||||
TimelineStyler(timelineItem: timelineItem) {
|
||||
Label(timelineItem.text, systemImage: "lock.shield")
|
||||
Label(timelineItem.body, systemImage: "lock.shield")
|
||||
.labelStyle(RoomTimelineViewLabelStyle())
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,7 @@ struct EncryptedRoomTimelineView_Previews: PreviewProvider {
|
||||
|
||||
private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> EncryptedRoomTimelineItem {
|
||||
EncryptedRoomTimelineItem(id: UUID().uuidString,
|
||||
text: text,
|
||||
body: text,
|
||||
encryptionType: .unknown,
|
||||
timestamp: timestamp,
|
||||
groupState: .single,
|
||||
|
||||
@@ -25,7 +25,7 @@ struct FileRoomTimelineView: View {
|
||||
HStack {
|
||||
Image(systemName: "doc.text.fill")
|
||||
.foregroundColor(.element.primaryContent)
|
||||
FormattedBodyText(text: timelineItem.text)
|
||||
FormattedBodyText(text: timelineItem.body)
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 6)
|
||||
@@ -44,7 +44,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider {
|
||||
static var body: some View {
|
||||
VStack(spacing: 20.0) {
|
||||
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "document.pdf",
|
||||
body: "document.pdf",
|
||||
timestamp: "Now",
|
||||
groupState: .single,
|
||||
isOutgoing: false,
|
||||
@@ -54,7 +54,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider {
|
||||
thumbnailSource: nil))
|
||||
|
||||
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "document.docx",
|
||||
body: "document.docx",
|
||||
timestamp: "Now",
|
||||
groupState: .single,
|
||||
isOutgoing: false,
|
||||
@@ -64,7 +64,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider {
|
||||
thumbnailSource: nil))
|
||||
|
||||
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "document.txt",
|
||||
body: "document.txt",
|
||||
timestamp: "Now",
|
||||
groupState: .single,
|
||||
isOutgoing: false,
|
||||
|
||||
@@ -76,7 +76,11 @@ struct FormattedBodyTextBubbleLayout: Layout {
|
||||
struct FormattedBodyText: View {
|
||||
@Environment(\.timelineStyle) private var timelineStyle
|
||||
|
||||
let attributedComponents: [AttributedStringBuilderComponent]
|
||||
private let attributedComponents: [AttributedStringBuilderComponent]
|
||||
|
||||
init(attributedString: AttributedString) {
|
||||
attributedComponents = attributedString.formattedComponents
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if timelineStyle == .bubbles {
|
||||
@@ -155,7 +159,7 @@ struct FormattedBodyText: View {
|
||||
|
||||
extension FormattedBodyText {
|
||||
init(text: String) {
|
||||
attributedComponents = [.init(attributedString: AttributedString(text), isBlockquote: false, isReply: false)]
|
||||
self.init(attributedString: AttributedString(text))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,10 +208,8 @@ struct FormattedBodyText_Previews: PreviewProvider {
|
||||
|
||||
VStack(alignment: .leading, spacing: 24.0) {
|
||||
ForEach(htmlStrings, id: \.self) { htmlString in
|
||||
let attributedString = attributedStringBuilder.fromHTML(htmlString)
|
||||
|
||||
if let components = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedString) {
|
||||
FormattedBodyText(attributedComponents: components)
|
||||
if let attributedString = attributedStringBuilder.fromHTML(htmlString) {
|
||||
FormattedBodyText(attributedString: attributedString)
|
||||
.previewBubble()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
||||
static var body: some View {
|
||||
VStack(spacing: 20.0) {
|
||||
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Some image",
|
||||
body: "Some image",
|
||||
timestamp: "Now",
|
||||
groupState: .single,
|
||||
isOutgoing: false,
|
||||
@@ -81,7 +81,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
||||
source: nil))
|
||||
|
||||
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Some other image",
|
||||
body: "Some other image",
|
||||
timestamp: "Now",
|
||||
groupState: .single,
|
||||
isOutgoing: false,
|
||||
@@ -90,7 +90,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
||||
source: nil))
|
||||
|
||||
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Blurhashed image",
|
||||
body: "Blurhashed image",
|
||||
timestamp: "Now",
|
||||
groupState: .single,
|
||||
isOutgoing: false,
|
||||
|
||||
@@ -26,14 +26,15 @@ struct NoticeRoomTimelineView: View {
|
||||
// adds additional padding so the spacing between the icon and text is inconsistent.
|
||||
|
||||
// Spacing: 6 = label spacing - formatted text padding
|
||||
HStack(alignment: .firstTextBaseline, spacing: 6) {
|
||||
|
||||
HStack(alignment: .firstTextBaseline, spacing: 6.0) {
|
||||
Image(systemName: "info.bubble").padding(.top, 2.0)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
|
||||
if let attributedComponents = timelineItem.attributedComponents {
|
||||
FormattedBodyText(attributedComponents: attributedComponents)
|
||||
if let attributedString = timelineItem.formattedBody {
|
||||
FormattedBodyText(attributedString: attributedString)
|
||||
} else {
|
||||
FormattedBodyText(text: timelineItem.text)
|
||||
FormattedBodyText(text: timelineItem.body)
|
||||
}
|
||||
}
|
||||
.padding(.leading, 4) // Trailing padding is provided by FormattedBodyText
|
||||
@@ -63,7 +64,7 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider {
|
||||
|
||||
private static func itemWith(text: String, timestamp: String, senderId: String) -> NoticeRoomTimelineItem {
|
||||
NoticeRoomTimelineItem(id: UUID().uuidString,
|
||||
text: text,
|
||||
body: text,
|
||||
timestamp: timestamp,
|
||||
groupState: .single,
|
||||
isOutgoing: false,
|
||||
|
||||
@@ -43,7 +43,7 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
RoomTimelineViewProvider.separator(.init(text: "Today"))
|
||||
RoomTimelineViewProvider.text(.init(id: "",
|
||||
text: "This is another message",
|
||||
body: "This is another message",
|
||||
timestamp: "",
|
||||
groupState: .single,
|
||||
isOutgoing: true,
|
||||
@@ -54,7 +54,7 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider {
|
||||
|
||||
RoomTimelineViewProvider.separator(.init(text: "Today"))
|
||||
RoomTimelineViewProvider.text(.init(id: "",
|
||||
text: "This is a message",
|
||||
body: "This is a message",
|
||||
timestamp: "",
|
||||
groupState: .single,
|
||||
isOutgoing: false,
|
||||
|
||||
@@ -22,7 +22,7 @@ struct RedactedRoomTimelineView: View {
|
||||
|
||||
var body: some View {
|
||||
TimelineStyler(timelineItem: timelineItem) {
|
||||
Label(timelineItem.text, systemImage: "trash")
|
||||
Label(timelineItem.body, systemImage: "trash")
|
||||
.labelStyle(RoomTimelineViewLabelStyle())
|
||||
.imageScale(.small) // Smaller icon so that the bubble remains rounded on the outside.
|
||||
}
|
||||
@@ -43,7 +43,7 @@ struct RedactedRoomTimelineView_Previews: PreviewProvider {
|
||||
|
||||
private static func itemWith(text: String, timestamp: String, senderId: String) -> RedactedRoomTimelineItem {
|
||||
RedactedRoomTimelineItem(id: UUID().uuidString,
|
||||
text: text,
|
||||
body: text,
|
||||
timestamp: timestamp,
|
||||
groupState: .single,
|
||||
isOutgoing: false,
|
||||
|
||||
@@ -20,7 +20,7 @@ struct StateRoomTimelineView: View {
|
||||
let timelineItem: StateRoomTimelineItem
|
||||
|
||||
var body: some View {
|
||||
Text(timelineItem.text)
|
||||
Text(timelineItem.body)
|
||||
.font(.element.footnote)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
@@ -41,7 +41,7 @@ struct StateRoomTimelineView_Previews: PreviewProvider {
|
||||
}
|
||||
|
||||
static let item = StateRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Alice joined",
|
||||
body: "Alice joined",
|
||||
timestamp: "Now",
|
||||
groupState: .beginning,
|
||||
isOutgoing: false,
|
||||
|
||||
@@ -31,7 +31,7 @@ struct StickerRoomTimelineView: View {
|
||||
.frame(maxHeight: 300)
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
}
|
||||
.accessibilityLabel(timelineItem.text)
|
||||
.accessibilityLabel(timelineItem.body)
|
||||
}
|
||||
|
||||
private var placeholder: some View {
|
||||
@@ -57,7 +57,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
|
||||
static var body: some View {
|
||||
VStack(spacing: 20.0) {
|
||||
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Some image",
|
||||
body: "Some image",
|
||||
timestamp: "Now",
|
||||
groupState: .single,
|
||||
isOutgoing: false,
|
||||
@@ -66,7 +66,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
|
||||
imageURL: URL.picturesDirectory))
|
||||
|
||||
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Some other image",
|
||||
body: "Some other image",
|
||||
timestamp: "Now",
|
||||
groupState: .single,
|
||||
isOutgoing: false,
|
||||
@@ -75,7 +75,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
|
||||
imageURL: URL.picturesDirectory))
|
||||
|
||||
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Blurhashed image",
|
||||
body: "Blurhashed image",
|
||||
timestamp: "Now",
|
||||
groupState: .single,
|
||||
isOutgoing: false,
|
||||
|
||||
@@ -22,10 +22,10 @@ struct TextRoomTimelineView: View {
|
||||
|
||||
var body: some View {
|
||||
TimelineStyler(timelineItem: timelineItem) {
|
||||
if let attributedComponents = timelineItem.attributedComponents {
|
||||
FormattedBodyText(attributedComponents: attributedComponents)
|
||||
if let attributedString = timelineItem.formattedBody {
|
||||
FormattedBodyText(attributedString: attributedString)
|
||||
} else {
|
||||
FormattedBodyText(text: timelineItem.text)
|
||||
FormattedBodyText(text: timelineItem.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ struct TextRoomTimelineView_Previews: PreviewProvider {
|
||||
|
||||
private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> TextRoomTimelineItem {
|
||||
TextRoomTimelineItem(id: UUID().uuidString,
|
||||
text: text,
|
||||
body: text,
|
||||
timestamp: timestamp,
|
||||
groupState: .single,
|
||||
isOutgoing: isOutgoing,
|
||||
|
||||
@@ -23,7 +23,7 @@ struct UnsupportedRoomTimelineView: View {
|
||||
TimelineStyler(timelineItem: timelineItem) {
|
||||
Label {
|
||||
VStack(alignment: .leading) {
|
||||
Text("\(timelineItem.text): \(timelineItem.eventType)")
|
||||
Text("\(timelineItem.body): \(timelineItem.eventType)")
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Text(timelineItem.error)
|
||||
@@ -62,7 +62,7 @@ struct UnsupportedRoomTimelineView_Previews: PreviewProvider {
|
||||
|
||||
private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> UnsupportedRoomTimelineItem {
|
||||
UnsupportedRoomTimelineItem(id: UUID().uuidString,
|
||||
text: text,
|
||||
body: text,
|
||||
eventType: "Some Event Type",
|
||||
error: "Something went wrong",
|
||||
timestamp: timestamp,
|
||||
|
||||
@@ -67,7 +67,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
|
||||
static var body: some View {
|
||||
VStack(spacing: 20.0) {
|
||||
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Some video",
|
||||
body: "Some video",
|
||||
timestamp: "Now",
|
||||
groupState: .single,
|
||||
isOutgoing: false,
|
||||
@@ -78,7 +78,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
|
||||
thumbnailSource: nil))
|
||||
|
||||
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Some other video",
|
||||
body: "Some other video",
|
||||
timestamp: "Now",
|
||||
groupState: .single,
|
||||
isOutgoing: false,
|
||||
@@ -89,7 +89,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
|
||||
thumbnailSource: nil))
|
||||
|
||||
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Blurhashed video",
|
||||
body: "Blurhashed video",
|
||||
timestamp: "Now",
|
||||
groupState: .single,
|
||||
isOutgoing: false,
|
||||
|
||||
Reference in New Issue
Block a user