Inline image loading
Changelog-Added: Added inline image loading Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670028FC7C5900038D2A /* RelayView.swift */; };
|
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670028FC7C5900038D2A /* RelayView.swift */; };
|
||||||
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4C06670328FC7EC500038D2A /* Kingfisher */; };
|
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4C06670328FC7EC500038D2A /* Kingfisher */; };
|
||||||
|
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670528FCB08600038D2A /* ImageCarousel.swift */; };
|
||||||
4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F8B280F5FCA000448DE /* ChatroomView.swift */; };
|
4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F8B280F5FCA000448DE /* ChatroomView.swift */; };
|
||||||
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F8E280F640A000448DE /* ThreadModel.swift */; };
|
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F8E280F640A000448DE /* ThreadModel.swift */; };
|
||||||
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F90280F6528000448DE /* ChatView.swift */; };
|
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F90280F6528000448DE /* ChatView.swift */; };
|
||||||
@@ -128,6 +129,7 @@
|
|||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
4C06670028FC7C5900038D2A /* RelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayView.swift; sourceTree = "<group>"; };
|
4C06670028FC7C5900038D2A /* RelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayView.swift; sourceTree = "<group>"; };
|
||||||
|
4C06670528FCB08600038D2A /* ImageCarousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCarousel.swift; sourceTree = "<group>"; };
|
||||||
4C0A3F8B280F5FCA000448DE /* ChatroomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatroomView.swift; sourceTree = "<group>"; };
|
4C0A3F8B280F5FCA000448DE /* ChatroomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatroomView.swift; sourceTree = "<group>"; };
|
||||||
4C0A3F8E280F640A000448DE /* ThreadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadModel.swift; sourceTree = "<group>"; };
|
4C0A3F8E280F640A000448DE /* ThreadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadModel.swift; sourceTree = "<group>"; };
|
||||||
4C0A3F90280F6528000448DE /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; };
|
4C0A3F90280F6528000448DE /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; };
|
||||||
@@ -378,6 +380,7 @@
|
|||||||
children = (
|
children = (
|
||||||
4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */,
|
4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */,
|
||||||
4CD7641A28A1641400B6928F /* EndBlock.swift */,
|
4CD7641A28A1641400B6928F /* EndBlock.swift */,
|
||||||
|
4C06670528FCB08600038D2A /* ImageCarousel.swift */,
|
||||||
);
|
);
|
||||||
path = Components;
|
path = Components;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -654,6 +657,7 @@
|
|||||||
4C216F32286E388800040376 /* DMChatView.swift in Sources */,
|
4C216F32286E388800040376 /* DMChatView.swift in Sources */,
|
||||||
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
|
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
|
||||||
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
|
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
|
||||||
|
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
|
||||||
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
||||||
4C363A9C282838B9006E126D /* EventRef.swift in Sources */,
|
4C363A9C282838B9006E126D /* EventRef.swift in Sources */,
|
||||||
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */,
|
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */,
|
||||||
|
|||||||
69
damus/Components/ImageCarousel.swift
Normal file
69
damus/Components/ImageCarousel.swift
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
//
|
||||||
|
// ImageCarousel.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2022-10-16.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
struct ImageViewer: View {
|
||||||
|
let urls: [URL]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
TabView {
|
||||||
|
ForEach(urls, id: \.absoluteString) { url in
|
||||||
|
VStack{
|
||||||
|
Text(url.lastPathComponent)
|
||||||
|
|
||||||
|
KFImage(url)
|
||||||
|
.loadDiskFileSynchronously()
|
||||||
|
.scaleFactor(UIScreen.main.scale)
|
||||||
|
.fade(duration: 0.1)
|
||||||
|
.tabItem {
|
||||||
|
Text(url.absoluteString)
|
||||||
|
}
|
||||||
|
.id(url.absoluteString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tabViewStyle(PageTabViewStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ImageCarousel: View {
|
||||||
|
var urls: [URL]
|
||||||
|
|
||||||
|
@State var open_sheet: Bool = false
|
||||||
|
@State var current_url: URL? = nil
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
TabView {
|
||||||
|
ForEach(urls, id: \.absoluteString) { url in
|
||||||
|
KFImage(url)
|
||||||
|
.loadDiskFileSynchronously()
|
||||||
|
.scaleFactor(UIScreen.main.scale)
|
||||||
|
.fade(duration: 0.1)
|
||||||
|
.tabItem {
|
||||||
|
Text(url.absoluteString)
|
||||||
|
}
|
||||||
|
.id(url.absoluteString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $open_sheet) {
|
||||||
|
ImageViewer(urls: urls)
|
||||||
|
}
|
||||||
|
.frame(height: 200)
|
||||||
|
.onTapGesture {
|
||||||
|
open_sheet = true
|
||||||
|
}
|
||||||
|
.tabViewStyle(PageTabViewStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ImageCarousel_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
ImageCarousel(urls: [URL(string: "https://jb55.com/red-me.jpg")!])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Starscream
|
import Starscream
|
||||||
//import Kingfisher
|
import Kingfisher
|
||||||
|
|
||||||
let BOOTSTRAP_RELAYS = [
|
let BOOTSTRAP_RELAYS = [
|
||||||
"wss://relay.damus.io",
|
"wss://relay.damus.io",
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ func build_mention_indices(_ blocks: [Block], type: MentionType) -> Set<Int> {
|
|||||||
return
|
return
|
||||||
case .hashtag:
|
case .hashtag:
|
||||||
return
|
return
|
||||||
|
case .url:
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ enum Block {
|
|||||||
case text(String)
|
case text(String)
|
||||||
case mention(Mention)
|
case mention(Mention)
|
||||||
case hashtag(String)
|
case hashtag(String)
|
||||||
|
case url(URL)
|
||||||
|
|
||||||
var is_hashtag: String? {
|
var is_hashtag: String? {
|
||||||
if case .hashtag(let htag) = self {
|
if case .hashtag(let htag) = self {
|
||||||
@@ -45,6 +46,14 @@ enum Block {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var is_url: URL? {
|
||||||
|
if case .url(let url) = self {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var is_text: String? {
|
var is_text: String? {
|
||||||
if case .text(let txt) = self {
|
if case .text(let txt) = self {
|
||||||
return txt
|
return txt
|
||||||
@@ -69,6 +78,8 @@ func render_blocks(blocks: [Block]) -> String {
|
|||||||
return str + txt
|
return str + txt
|
||||||
case .hashtag(let htag):
|
case .hashtag(let htag):
|
||||||
return str + "#" + htag
|
return str + "#" + htag
|
||||||
|
case .url(let url):
|
||||||
|
return str + url.absoluteString
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,21 +94,43 @@ func parse_mentions(content: String, tags: [[String]]) -> [Block] {
|
|||||||
var starting_from: Int = 0
|
var starting_from: Int = 0
|
||||||
|
|
||||||
while p.pos < content.count {
|
while p.pos < content.count {
|
||||||
if !consume_until(p, match: { $0 == "#" }) {
|
if !consume_until(p, match: { !$0.isWhitespace}) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
let pre_mention = p.pos
|
let pre_mention = p.pos
|
||||||
if let mention = parse_mention(p, tags: tags) {
|
|
||||||
blocks.append(parse_textblock(str: p.str, from: starting_from, to: pre_mention))
|
let c = peek_char(p, 0)
|
||||||
blocks.append(.mention(mention))
|
let pr = peek_char(p, -1)
|
||||||
starting_from = p.pos
|
|
||||||
} else if let hashtag = parse_hashtag(p) {
|
if c == "#" {
|
||||||
blocks.append(parse_textblock(str: p.str, from: starting_from, to: pre_mention))
|
if let mention = parse_mention(p, tags: tags) {
|
||||||
blocks.append(.hashtag(hashtag))
|
blocks.append(parse_textblock(str: p.str, from: starting_from, to: pre_mention))
|
||||||
starting_from = p.pos
|
blocks.append(.mention(mention))
|
||||||
|
starting_from = p.pos
|
||||||
|
} else if let hashtag = parse_hashtag(p) {
|
||||||
|
blocks.append(parse_textblock(str: p.str, from: starting_from, to: pre_mention))
|
||||||
|
blocks.append(.hashtag(hashtag))
|
||||||
|
starting_from = p.pos
|
||||||
|
} else {
|
||||||
|
if !consume_until(p, match: { $0.isWhitespace }) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if c == "h" && (pr == nil || pr!.isWhitespace) {
|
||||||
|
if let url = parse_url(p) {
|
||||||
|
blocks.append(parse_textblock(str: p.str, from: starting_from, to: pre_mention))
|
||||||
|
blocks.append(.url(url))
|
||||||
|
starting_from = p.pos
|
||||||
|
} else {
|
||||||
|
if !consume_until(p, match: { $0.isWhitespace }) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
p.pos += 1
|
if !consume_until(p, match: { $0.isWhitespace }) {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,6 +178,37 @@ func is_punctuation(_ c: Character) -> Bool {
|
|||||||
return c.isWhitespace || c.isPunctuation
|
return c.isWhitespace || c.isPunctuation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parse_url(_ p: Parser) -> URL? {
|
||||||
|
let start = p.pos
|
||||||
|
|
||||||
|
if !parse_str(p, "http") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if parse_char(p, "s") {
|
||||||
|
if !parse_str(p, "://") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !parse_str(p, "://") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !consume_until(p, match: { c in c.isWhitespace }, end_ok: true) {
|
||||||
|
p.pos = start
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let url_str = String(substring(p.str, start: start, end: p.pos))
|
||||||
|
guard let url = URL(string: url_str) else {
|
||||||
|
p.pos = start
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
func parse_hashtag(_ p: Parser) -> String? {
|
func parse_hashtag(_ p: Parser) -> String? {
|
||||||
let start = p.pos
|
let start = p.pos
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,15 @@ func parse_str(_ p: Parser, _ s: String) -> Bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func peek_char(_ p: Parser, _ i: Int) -> Character? {
|
||||||
|
let offset = p.pos + i
|
||||||
|
if offset < 0 || offset > p.str.count {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let ind = p.str.index(p.str.startIndex, offsetBy: offset)
|
||||||
|
return p.str[ind]
|
||||||
|
}
|
||||||
|
|
||||||
func parse_char(_ p: Parser, _ c: Character) -> Bool {
|
func parse_char(_ p: Parser, _ c: Character) -> Bool {
|
||||||
if p.pos >= p.str.count {
|
if p.pos >= p.str.count {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ struct ChatView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, content: event.content)
|
NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, show_images: true, content: event.content)
|
||||||
|
|
||||||
if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey {
|
if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey {
|
||||||
let bar = make_actionbar_model(ev: event, damus: damus_state)
|
let bar = make_actionbar_model(ev: event, damus: damus_state)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ struct DMView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, content: event.get_content(damus_state.keypair.privkey))
|
NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, show_images: true, content: event.get_content(damus_state.keypair.privkey))
|
||||||
.foregroundColor(is_ours ? Color.white : Color.primary)
|
.foregroundColor(is_ours ? Color.white : Color.primary)
|
||||||
.padding(10)
|
.padding(10)
|
||||||
.background(is_ours ? Color.accentColor : Color.secondary.opacity(0.15))
|
.background(is_ours ? Color.accentColor : Color.secondary.opacity(0.15))
|
||||||
|
|||||||
@@ -40,20 +40,6 @@ struct EventActionBar: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack(alignment: .bottom) {
|
|
||||||
Text("\(bar.likes > 0 ? "\(bar.likes)" : "")")
|
|
||||||
.font(.footnote)
|
|
||||||
.foregroundColor(bar.liked ? Color.red : Color.gray)
|
|
||||||
|
|
||||||
EventActionButton(img: bar.liked ? "heart.fill" : "heart", col: bar.liked ? Color.red : nil) {
|
|
||||||
if bar.liked {
|
|
||||||
notify(.delete, bar.our_like)
|
|
||||||
} else {
|
|
||||||
send_like()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack(alignment: .bottom) {
|
HStack(alignment: .bottom) {
|
||||||
Text("\(bar.boosts > 0 ? "\(bar.boosts)" : "")")
|
Text("\(bar.boosts > 0 ? "\(bar.boosts)" : "")")
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
@@ -68,6 +54,21 @@ struct EventActionBar: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HStack(alignment: .bottom) {
|
||||||
|
Text("\(bar.likes > 0 ? "\(bar.likes)" : "")")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(bar.liked ? Color.red : Color.gray)
|
||||||
|
|
||||||
|
EventActionButton(img: bar.liked ? "heart.fill" : "heart", col: bar.liked ? Color.red : nil) {
|
||||||
|
if bar.liked {
|
||||||
|
notify(.delete, bar.our_like)
|
||||||
|
} else {
|
||||||
|
send_like()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
HStack(alignment: .bottom) {
|
HStack(alignment: .bottom) {
|
||||||
Text("\(bar.tips > 0 ? "\(bar.tips)" : "")")
|
Text("\(bar.tips > 0 ? "\(bar.tips)" : "")")
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
@@ -81,6 +82,7 @@ struct EventActionBar: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
.padding(.top, 1)
|
.padding(.top, 1)
|
||||||
.alert("Boost", isPresented: $confirm_boost) {
|
.alert("Boost", isPresented: $confirm_boost) {
|
||||||
|
|||||||
@@ -127,13 +127,13 @@ func scroll_after_load(thread: ThreadModel, proxy: ScrollViewProxy) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
struct EventDetailView_Previews: PreviewProvider {
|
struct EventDetailView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
EventDetailView(event: NostrEvent(content: "Hello", pubkey: "Guy"), profile: nil)
|
let state = test_damus_state()
|
||||||
|
let tm = ThreadModel(evid: "4da698ceac09a16cdb439276fa3d13ef8f6620ffb45d11b76b3f103483c2d0b0", damus_state: state)
|
||||||
|
EventDetailView(damus: state, thread: tm)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
/// Find the entire reply path for the active event
|
/// Find the entire reply path for the active event
|
||||||
func make_reply_map(active: NostrEvent, events: [NostrEvent], privkey: String?) -> [String: ()]
|
func make_reply_map(active: NostrEvent, events: [NostrEvent], privkey: String?) -> [String: ()]
|
||||||
|
|||||||
@@ -129,9 +129,8 @@ struct EventView: View {
|
|||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
|
|
||||||
NoteContentView(privkey: damus.keypair.privkey, event: event, profiles: damus.profiles, content: content)
|
NoteContentView(privkey: damus.keypair.privkey, event: event, profiles: damus.profiles, show_images: true, content: content)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.textSelection(.enabled)
|
|
||||||
|
|
||||||
if has_action_bar {
|
if has_action_bar {
|
||||||
let bar = make_actionbar_model(ev: event, damus: damus)
|
let bar = make_actionbar_model(ev: event, damus: damus)
|
||||||
@@ -146,7 +145,7 @@ struct EventView: View {
|
|||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.background(event_validity_color(event.validity))
|
.background(event_validity_color(event.validity))
|
||||||
.id(event.id)
|
.id(event.id)
|
||||||
.frame(minHeight: PFP_SIZE)
|
.frame(maxWidth: .infinity, minHeight: PFP_SIZE)
|
||||||
.padding([.bottom], 4)
|
.padding([.bottom], 4)
|
||||||
.event_context_menu(event, privkey: damus.keypair.privkey)
|
.event_context_menu(event, privkey: damus.keypair.privkey)
|
||||||
}
|
}
|
||||||
@@ -269,3 +268,8 @@ func make_actionbar_model(ev: NostrEvent, damus: DamusState) -> ActionBarModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct EventView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
EventView(damus: test_damus_state(), event: NostrEvent(content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool", pubkey: "pk"), show_friend_icon: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,9 +8,10 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -> String {
|
func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -> (String, [URL]) {
|
||||||
let blocks = ev.blocks(privkey)
|
let blocks = ev.blocks(privkey)
|
||||||
return blocks.reduce("") { str, block in
|
var img_urls: [URL] = []
|
||||||
|
let txt = blocks.reduce("") { str, block in
|
||||||
switch block {
|
switch block {
|
||||||
case .mention(let m):
|
case .mention(let m):
|
||||||
return str + mention_str(m, profiles: profiles)
|
return str + mention_str(m, profiles: profiles)
|
||||||
@@ -18,8 +19,20 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -
|
|||||||
return str + txt
|
return str + txt
|
||||||
case .hashtag(let htag):
|
case .hashtag(let htag):
|
||||||
return str + hashtag_str(htag)
|
return str + hashtag_str(htag)
|
||||||
|
case .url(let url):
|
||||||
|
if is_image_url(url) {
|
||||||
|
img_urls.append(url)
|
||||||
|
}
|
||||||
|
return str + url.absoluteString
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (txt, img_urls)
|
||||||
|
}
|
||||||
|
|
||||||
|
func is_image_url(_ url: URL) -> Bool {
|
||||||
|
let str = url.lastPathComponent
|
||||||
|
return str.hasSuffix("png") || str.hasSuffix("jpg") || str.hasSuffix("jpeg")
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NoteContentView: View {
|
struct NoteContentView: View {
|
||||||
@@ -27,23 +40,33 @@ struct NoteContentView: View {
|
|||||||
let event: NostrEvent
|
let event: NostrEvent
|
||||||
let profiles: Profiles
|
let profiles: Profiles
|
||||||
|
|
||||||
|
let show_images: Bool
|
||||||
|
|
||||||
@State var content: String
|
@State var content: String
|
||||||
|
@State var images: [URL] = []
|
||||||
|
|
||||||
func MainContent() -> some View {
|
func MainContent() -> some View {
|
||||||
let md_opts: AttributedString.MarkdownParsingOptions =
|
let md_opts: AttributedString.MarkdownParsingOptions =
|
||||||
.init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
|
.init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
|
||||||
|
|
||||||
guard let txt = try? AttributedString(markdown: content, options: md_opts) else {
|
return VStack(alignment: .leading) {
|
||||||
return Text(content)
|
if let txt = try? AttributedString(markdown: content, options: md_opts) {
|
||||||
|
Text(txt)
|
||||||
|
} else {
|
||||||
|
Text(content)
|
||||||
|
}
|
||||||
|
if show_images && images.count > 0 {
|
||||||
|
ImageCarousel(urls: images)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Text(txt)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
MainContent()
|
MainContent()
|
||||||
.onAppear() {
|
.onAppear() {
|
||||||
self.content = render_note_content(ev: event, profiles: profiles, privkey: privkey)
|
let (txt, images) = render_note_content(ev: event, profiles: profiles, privkey: privkey)
|
||||||
|
self.content = txt
|
||||||
|
self.images = images
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.profile_updated)) { notif in
|
.onReceive(handle_notify(.profile_updated)) { notif in
|
||||||
let profile = notif.object as! ProfileUpdate
|
let profile = notif.object as! ProfileUpdate
|
||||||
@@ -52,10 +75,13 @@ struct NoteContentView: View {
|
|||||||
switch block {
|
switch block {
|
||||||
case .mention(let m):
|
case .mention(let m):
|
||||||
if m.type == .pubkey && m.ref.ref_id == profile.pubkey {
|
if m.type == .pubkey && m.ref.ref_id == profile.pubkey {
|
||||||
content = render_note_content(ev: event, profiles: profiles, privkey: privkey)
|
let (txt, images) = render_note_content(ev: event, profiles: profiles, privkey: privkey)
|
||||||
|
self.content = txt
|
||||||
|
self.images = images
|
||||||
}
|
}
|
||||||
case .text: return
|
case .text: return
|
||||||
case .hashtag: return
|
case .hashtag: return
|
||||||
|
case .url: return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,10 +106,10 @@ func mention_str(_ m: Mention, profiles: Profiles) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
struct NoteContentView_Previews: PreviewProvider {
|
struct NoteContentView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
NoteContentView()
|
let state = test_damus_state()
|
||||||
|
let content = "hi there https://jb55.com/s/Oct12-150217.png 5739a762ef6124dd.jpg"
|
||||||
|
NoteContentView(privkey: "", event: NostrEvent(content: content, pubkey: "pk"), profiles: state.profiles, show_images: true, content: content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|||||||
@@ -56,9 +56,8 @@ struct ProfilePicView: View {
|
|||||||
Group {
|
Group {
|
||||||
let pic = picture ?? profiles.lookup(id: pubkey)?.picture ?? robohash(pubkey)
|
let pic = picture ?? profiles.lookup(id: pubkey)?.picture ?? robohash(pubkey)
|
||||||
let url = URL(string: pic)
|
let url = URL(string: pic)
|
||||||
let processor = /*DownsamplingImageProcessor(size: CGSize(width: size, height: size))
|
let processor = ResizingImageProcessor(referenceSize: CGSize(width: size, height: size))
|
||||||
|>*/ ResizingImageProcessor(referenceSize: CGSize(width: size, height: size))
|
|
||||||
|> RoundCornerImageProcessor(cornerRadius: 20)
|
|
||||||
KFImage.url(url)
|
KFImage.url(url)
|
||||||
.placeholder { _ in
|
.placeholder { _ in
|
||||||
Placeholder
|
Placeholder
|
||||||
@@ -67,6 +66,7 @@ struct ProfilePicView: View {
|
|||||||
.scaleFactor(UIScreen.main.scale)
|
.scaleFactor(UIScreen.main.scale)
|
||||||
.loadDiskFileSynchronously()
|
.loadDiskFileSynchronously()
|
||||||
.fade(duration: 0.1)
|
.fade(duration: 0.1)
|
||||||
|
.clipShape(Circle())
|
||||||
.overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
|
.overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ struct ReplyQuoteView: View {
|
|||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
}
|
}
|
||||||
|
|
||||||
NoteContentView(privkey: privkey, event: event, profiles: profiles, content: event.content)
|
NoteContentView(privkey: privkey, event: event, profiles: profiles, show_images: false, content: event.content)
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ class ReplyTests: XCTestCase {
|
|||||||
XCTAssertNil(ref.is_reply)
|
XCTAssertNil(ref.is_reply)
|
||||||
XCTAssertNil(ref.is_thread_id)
|
XCTAssertNil(ref.is_thread_id)
|
||||||
XCTAssertNil(ref.is_direct_reply)
|
XCTAssertNil(ref.is_direct_reply)
|
||||||
XCTAssertEqual(ref.is_mention!.type, .event)
|
XCTAssertEqual(ref.is_mention?.type, .event)
|
||||||
XCTAssertEqual(ref.is_mention!.ref.ref_id, "event_id")
|
XCTAssertEqual(ref.is_mention?.ref.ref_id, "event_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testUrlAnchorsAreNotHashtags() {
|
func testUrlAnchorsAreNotHashtags() {
|
||||||
@@ -96,11 +96,11 @@ class ReplyTests: XCTestCase {
|
|||||||
XCTAssertNotNil(event_refs[0].is_thread_id)
|
XCTAssertNotNil(event_refs[0].is_thread_id)
|
||||||
XCTAssertNotNil(event_refs[0].is_reply)
|
XCTAssertNotNil(event_refs[0].is_reply)
|
||||||
XCTAssertNotNil(event_refs[0].is_direct_reply)
|
XCTAssertNotNil(event_refs[0].is_direct_reply)
|
||||||
XCTAssertEqual(event_refs[0].is_reply!.ref_id, "thread_id")
|
XCTAssertEqual(event_refs[0].is_reply?.ref_id, "thread_id")
|
||||||
XCTAssertEqual(event_refs[0].is_thread_id!.ref_id, "thread_id")
|
XCTAssertEqual(event_refs[0].is_thread_id?.ref_id, "thread_id")
|
||||||
XCTAssertNotNil(event_refs[1].is_mention)
|
XCTAssertNotNil(event_refs[1].is_mention)
|
||||||
XCTAssertEqual(event_refs[1].is_mention!.type, .event)
|
XCTAssertEqual(event_refs[1].is_mention?.type, .event)
|
||||||
XCTAssertEqual(event_refs[1].is_mention!.ref.ref_id, "mentioned_id")
|
XCTAssertEqual(event_refs[1].is_mention?.ref.ref_id, "mentioned_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testEmptyMention() throws {
|
func testEmptyMention() throws {
|
||||||
|
|||||||
@@ -64,6 +64,33 @@ class damusTests: XCTestCase {
|
|||||||
XCTAssertNotNil(parsed[0].is_text)
|
XCTAssertNotNil(parsed[0].is_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testParseUrl() {
|
||||||
|
let parsed = parse_mentions(content: "a https://jb55.com b", tags: [])
|
||||||
|
|
||||||
|
XCTAssertNotNil(parsed)
|
||||||
|
XCTAssertEqual(parsed.count, 3)
|
||||||
|
XCTAssertEqual(parsed[1].is_url?.absoluteString, "https://jb55.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testParseUrlEnd() {
|
||||||
|
let parsed = parse_mentions(content: "a https://jb55.com", tags: [])
|
||||||
|
|
||||||
|
XCTAssertNotNil(parsed)
|
||||||
|
XCTAssertEqual(parsed.count, 2)
|
||||||
|
XCTAssertEqual(parsed[0].is_text, "a ")
|
||||||
|
XCTAssertEqual(parsed[1].is_url?.absoluteString, "https://jb55.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testParseUrlStart() {
|
||||||
|
let parsed = parse_mentions(content: "https://jb55.com br", tags: [])
|
||||||
|
|
||||||
|
XCTAssertNotNil(parsed)
|
||||||
|
XCTAssertEqual(parsed.count, 3)
|
||||||
|
XCTAssertEqual(parsed[0].is_text, "")
|
||||||
|
XCTAssertEqual(parsed[1].is_url?.absoluteString, "https://jb55.com")
|
||||||
|
XCTAssertEqual(parsed[2].is_text, " br")
|
||||||
|
}
|
||||||
|
|
||||||
func testParseMentionBlank() {
|
func testParseMentionBlank() {
|
||||||
let parsed = parse_mentions(content: "", tags: [["e", "event_id"]])
|
let parsed = parse_mentions(content: "", tags: [["e", "event_id"]])
|
||||||
|
|
||||||
@@ -91,9 +118,9 @@ class damusTests: XCTestCase {
|
|||||||
|
|
||||||
XCTAssertNotNil(parsed)
|
XCTAssertNotNil(parsed)
|
||||||
XCTAssertEqual(parsed.count, 3)
|
XCTAssertEqual(parsed.count, 3)
|
||||||
XCTAssertEqual(parsed[0].is_text!, "some hashtag ")
|
XCTAssertEqual(parsed[0].is_text, "some hashtag ")
|
||||||
XCTAssertEqual(parsed[1].is_hashtag!, "bitcoin")
|
XCTAssertEqual(parsed[1].is_hashtag, "bitcoin")
|
||||||
XCTAssertEqual(parsed[2].is_text!, " derp")
|
XCTAssertEqual(parsed[2].is_text, " derp")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testParseHashtagEnd() {
|
func testParseHashtagEnd() {
|
||||||
@@ -101,8 +128,8 @@ class damusTests: XCTestCase {
|
|||||||
|
|
||||||
XCTAssertNotNil(parsed)
|
XCTAssertNotNil(parsed)
|
||||||
XCTAssertEqual(parsed.count, 2)
|
XCTAssertEqual(parsed.count, 2)
|
||||||
XCTAssertEqual(parsed[0].is_text!, "some hashtag ")
|
XCTAssertEqual(parsed[0].is_text, "some hashtag ")
|
||||||
XCTAssertEqual(parsed[1].is_hashtag!, "bitcoin")
|
XCTAssertEqual(parsed[1].is_hashtag, "bitcoin")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testParseMentionOnlyText() {
|
func testParseMentionOnlyText() {
|
||||||
@@ -110,7 +137,7 @@ class damusTests: XCTestCase {
|
|||||||
|
|
||||||
XCTAssertNotNil(parsed)
|
XCTAssertNotNil(parsed)
|
||||||
XCTAssertEqual(parsed.count, 1)
|
XCTAssertEqual(parsed.count, 1)
|
||||||
XCTAssertEqual(parsed[0].is_text!, "there is no mention here")
|
XCTAssertEqual(parsed[0].is_text, "there is no mention here")
|
||||||
|
|
||||||
guard case .text(let txt) = parsed[0] else {
|
guard case .text(let txt) = parsed[0] else {
|
||||||
XCTAssertTrue(false)
|
XCTAssertTrue(false)
|
||||||
|
|||||||
Reference in New Issue
Block a user