Inline image loading
Changelog-Added: Added inline image loading Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
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 Starscream
|
||||
//import Kingfisher
|
||||
import Kingfisher
|
||||
|
||||
let BOOTSTRAP_RELAYS = [
|
||||
"wss://relay.damus.io",
|
||||
|
||||
@@ -80,6 +80,8 @@ func build_mention_indices(_ blocks: [Block], type: MentionType) -> Set<Int> {
|
||||
return
|
||||
case .hashtag:
|
||||
return
|
||||
case .url:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ enum Block {
|
||||
case text(String)
|
||||
case mention(Mention)
|
||||
case hashtag(String)
|
||||
case url(URL)
|
||||
|
||||
var is_hashtag: String? {
|
||||
if case .hashtag(let htag) = self {
|
||||
@@ -45,6 +46,14 @@ enum Block {
|
||||
return nil
|
||||
}
|
||||
|
||||
var is_url: URL? {
|
||||
if case .url(let url) = self {
|
||||
return url
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var is_text: String? {
|
||||
if case .text(let txt) = self {
|
||||
return txt
|
||||
@@ -69,6 +78,8 @@ func render_blocks(blocks: [Block]) -> String {
|
||||
return str + txt
|
||||
case .hashtag(let 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
|
||||
|
||||
while p.pos < content.count {
|
||||
if !consume_until(p, match: { $0 == "#" }) {
|
||||
if !consume_until(p, match: { !$0.isWhitespace}) {
|
||||
break
|
||||
}
|
||||
|
||||
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))
|
||||
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
|
||||
|
||||
let c = peek_char(p, 0)
|
||||
let pr = peek_char(p, -1)
|
||||
|
||||
if c == "#" {
|
||||
if let mention = parse_mention(p, tags: tags) {
|
||||
blocks.append(parse_textblock(str: p.str, from: starting_from, to: pre_mention))
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
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? {
|
||||
let start = p.pos
|
||||
|
||||
|
||||
@@ -55,6 +55,15 @@ func parse_str(_ p: Parser, _ s: String) -> Bool {
|
||||
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 {
|
||||
if p.pos >= p.str.count {
|
||||
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 {
|
||||
let bar = make_actionbar_model(ev: event, damus: damus_state)
|
||||
|
||||
@@ -21,7 +21,7 @@ struct DMView: View {
|
||||
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)
|
||||
.padding(10)
|
||||
.background(is_ours ? Color.accentColor : Color.secondary.opacity(0.15))
|
||||
|
||||
@@ -39,6 +39,20 @@ struct EventActionBar: View {
|
||||
notify(.reply, event)
|
||||
}
|
||||
}
|
||||
|
||||
HStack(alignment: .bottom) {
|
||||
Text("\(bar.boosts > 0 ? "\(bar.boosts)" : "")")
|
||||
.font(.footnote)
|
||||
.foregroundColor(bar.boosted ? Color.green : Color.gray)
|
||||
|
||||
EventActionButton(img: "arrow.2.squarepath", col: bar.boosted ? Color.green : nil) {
|
||||
if bar.boosted {
|
||||
notify(.delete, bar.our_boost)
|
||||
} else {
|
||||
self.confirm_boost = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HStack(alignment: .bottom) {
|
||||
Text("\(bar.likes > 0 ? "\(bar.likes)" : "")")
|
||||
@@ -53,21 +67,8 @@ struct EventActionBar: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HStack(alignment: .bottom) {
|
||||
Text("\(bar.boosts > 0 ? "\(bar.boosts)" : "")")
|
||||
.font(.footnote)
|
||||
.foregroundColor(bar.boosted ? Color.green : Color.gray)
|
||||
|
||||
EventActionButton(img: "arrow.2.squarepath", col: bar.boosted ? Color.green : nil) {
|
||||
if bar.boosted {
|
||||
notify(.delete, bar.our_boost)
|
||||
} else {
|
||||
self.confirm_boost = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
HStack(alignment: .bottom) {
|
||||
Text("\(bar.tips > 0 ? "\(bar.tips)" : "")")
|
||||
.font(.footnote)
|
||||
@@ -81,6 +82,7 @@ struct EventActionBar: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
.padding(.top, 1)
|
||||
.alert("Boost", isPresented: $confirm_boost) {
|
||||
|
||||
@@ -127,13 +127,13 @@ func scroll_after_load(thread: ThreadModel, proxy: ScrollViewProxy) {
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
struct EventDetailView_Previews: PreviewProvider {
|
||||
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
|
||||
func make_reply_map(active: NostrEvent, events: [NostrEvent], privkey: String?) -> [String: ()]
|
||||
|
||||
@@ -129,9 +129,8 @@ struct EventView: View {
|
||||
.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)
|
||||
.textSelection(.enabled)
|
||||
|
||||
if has_action_bar {
|
||||
let bar = make_actionbar_model(ev: event, damus: damus)
|
||||
@@ -146,7 +145,7 @@ struct EventView: View {
|
||||
.contentShape(Rectangle())
|
||||
.background(event_validity_color(event.validity))
|
||||
.id(event.id)
|
||||
.frame(minHeight: PFP_SIZE)
|
||||
.frame(maxWidth: .infinity, minHeight: PFP_SIZE)
|
||||
.padding([.bottom], 4)
|
||||
.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
|
||||
|
||||
|
||||
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)
|
||||
return blocks.reduce("") { str, block in
|
||||
var img_urls: [URL] = []
|
||||
let txt = blocks.reduce("") { str, block in
|
||||
switch block {
|
||||
case .mention(let m):
|
||||
return str + mention_str(m, profiles: profiles)
|
||||
@@ -18,8 +19,20 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -
|
||||
return str + txt
|
||||
case .hashtag(let 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 {
|
||||
@@ -27,23 +40,33 @@ struct NoteContentView: View {
|
||||
let event: NostrEvent
|
||||
let profiles: Profiles
|
||||
|
||||
let show_images: Bool
|
||||
|
||||
@State var content: String
|
||||
@State var images: [URL] = []
|
||||
|
||||
func MainContent() -> some View {
|
||||
let md_opts: AttributedString.MarkdownParsingOptions =
|
||||
.init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
|
||||
|
||||
guard let txt = try? AttributedString(markdown: content, options: md_opts) else {
|
||||
return Text(content)
|
||||
return VStack(alignment: .leading) {
|
||||
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 {
|
||||
MainContent()
|
||||
.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
|
||||
let profile = notif.object as! ProfileUpdate
|
||||
@@ -52,10 +75,13 @@ struct NoteContentView: View {
|
||||
switch block {
|
||||
case .mention(let m):
|
||||
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 .hashtag: return
|
||||
case .url: return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,10 +106,10 @@ func mention_str(_ m: Mention, profiles: Profiles) -> String {
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
struct NoteContentView_Previews: PreviewProvider {
|
||||
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 {
|
||||
let pic = picture ?? profiles.lookup(id: pubkey)?.picture ?? robohash(pubkey)
|
||||
let url = URL(string: pic)
|
||||
let processor = /*DownsamplingImageProcessor(size: CGSize(width: size, height: size))
|
||||
|>*/ ResizingImageProcessor(referenceSize: CGSize(width: size, height: size))
|
||||
|> RoundCornerImageProcessor(cornerRadius: 20)
|
||||
let processor = ResizingImageProcessor(referenceSize: CGSize(width: size, height: size))
|
||||
|
||||
KFImage.url(url)
|
||||
.placeholder { _ in
|
||||
Placeholder
|
||||
@@ -67,6 +66,7 @@ struct ProfilePicView: View {
|
||||
.scaleFactor(UIScreen.main.scale)
|
||||
.loadDiskFileSynchronously()
|
||||
.fade(duration: 0.1)
|
||||
.clipShape(Circle())
|
||||
.overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ struct ReplyQuoteView: View {
|
||||
.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)
|
||||
.foregroundColor(.accentColor)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user