Bring local notification logic into the push notification target
This commit brings key local notification logic into the notification extension target to allow the extension to reuse much of the functionality surrounding the processing and formatting of notifications. More specifically, the functions `process_local_notification` and `create_local_notification` were brought into the extension target. This will enable us to reuse much of the pre-existing notification logic (and avoid having to reimplement all of that) However, those functions had high dependencies on other parts of the code, so significant refactorings were needed to make this happen: - `create_local_notification` and `process_local_notification` had its function signatures changed to avoid the need to `DamusState` (which pulls too many other dependecies) - Other necessary dependencies, such as `Profiles`, `UserSettingsStore` had to be pulled into the extension target. Subsequently, sub-dependencies of those items had to be pulled in as well - In several cases, files were split to avoid pulling too many dependencies (e.g. Some Model files depended on some functions in View files, so in those cases I moved those functions into their own separate file to avoid pulling in view logic into the extension target) - Notification processing logic was changed a bit to remove dependency on `EventCache` in favor of using ndb directly (As instructed in a TODO comment in EventCache, and because EventCache has too many other dependencies) tldr: A LOT of things were moved around, a bit of logic was changed around local notifications to avoid using `EventCache`, but otherwise this commit is meant to be a no-op without any new features or user-facing functional changes. Testing ------- Device: iPhone 15 Pro iOS: 17.0.1 Damus: This commit Coverage: 1. Ran unit tests to check for regressions (none detected) 2. Launched the app and navigated around and did some interactions to perform a quick functional smoke test (no regressions found) 3. Sent a few push notifications to check they still work as expected (PASS) Signed-off-by: Daniel D’Aquino <daniel@daquino.me> Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
committed by
William Casarin
parent
4171252b18
commit
88f938d11c
@@ -300,70 +300,13 @@ struct NoteContentView: View {
|
||||
|
||||
}
|
||||
|
||||
func attributed_string_attach_icon(_ astr: inout AttributedString, img: UIImage) {
|
||||
let wrapped = icon_attributed_string(img: img)
|
||||
astr.append(wrapped)
|
||||
}
|
||||
|
||||
func icon_attributed_string(img: UIImage) -> AttributedString {
|
||||
let attachment = NSTextAttachment()
|
||||
attachment.image = img
|
||||
let attachmentString = NSAttributedString(attachment: attachment)
|
||||
return AttributedString(attachmentString)
|
||||
}
|
||||
|
||||
func url_str(_ url: URL) -> CompatibleText {
|
||||
var attributedString = AttributedString(stringLiteral: url.absoluteString)
|
||||
attributedString.link = url
|
||||
attributedString.foregroundColor = DamusColors.purple
|
||||
class NoteArtifactsParts {
|
||||
var parts: [ArtifactPart]
|
||||
var words: Int
|
||||
|
||||
return CompatibleText(attributed: attributedString)
|
||||
}
|
||||
|
||||
func mention_str(_ m: Mention<MentionRef>, profiles: Profiles) -> CompatibleText {
|
||||
switch m.ref {
|
||||
case .pubkey(let pk):
|
||||
let npub = bech32_pubkey(pk)
|
||||
let profile_txn = profiles.lookup(id: pk)
|
||||
let profile = profile_txn.unsafeUnownedValue
|
||||
let disp = Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 50)
|
||||
var attributedString = AttributedString(stringLiteral: "@\(disp)")
|
||||
attributedString.link = URL(string: "damus:nostr:\(npub)")
|
||||
attributedString.foregroundColor = DamusColors.purple
|
||||
|
||||
return CompatibleText(attributed: attributedString)
|
||||
case .note(let note_id):
|
||||
let bevid = bech32_note_id(note_id)
|
||||
var attributedString = AttributedString(stringLiteral: "@\(abbrev_pubkey(bevid))")
|
||||
attributedString.link = URL(string: "damus:nostr:\(bevid)")
|
||||
attributedString.foregroundColor = DamusColors.purple
|
||||
|
||||
return CompatibleText(attributed: attributedString)
|
||||
}
|
||||
}
|
||||
|
||||
struct LongformContent {
|
||||
let markdown: MarkdownContent
|
||||
let words: Int
|
||||
|
||||
init(_ markdown: String) {
|
||||
let blocks = [BlockNode].init(markdown: markdown)
|
||||
self.markdown = MarkdownContent(blocks: blocks)
|
||||
self.words = count_markdown_words(blocks: blocks)
|
||||
}
|
||||
}
|
||||
|
||||
enum NoteArtifacts {
|
||||
case separated(NoteArtifactsSeparated)
|
||||
case longform(LongformContent)
|
||||
|
||||
var images: [URL] {
|
||||
switch self {
|
||||
case .separated(let arts):
|
||||
return arts.images
|
||||
case .longform:
|
||||
return []
|
||||
}
|
||||
init(parts: [ArtifactPart], words: Int) {
|
||||
self.parts = parts
|
||||
self.words = words
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,83 +324,6 @@ enum ArtifactPart {
|
||||
}
|
||||
}
|
||||
|
||||
class NoteArtifactsParts {
|
||||
var parts: [ArtifactPart]
|
||||
var words: Int
|
||||
|
||||
init(parts: [ArtifactPart], words: Int) {
|
||||
self.parts = parts
|
||||
self.words = words
|
||||
}
|
||||
}
|
||||
|
||||
struct NoteArtifactsSeparated: Equatable {
|
||||
static func == (lhs: NoteArtifactsSeparated, rhs: NoteArtifactsSeparated) -> Bool {
|
||||
return lhs.content == rhs.content
|
||||
}
|
||||
|
||||
let content: CompatibleText
|
||||
let words: Int
|
||||
let urls: [UrlType]
|
||||
let invoices: [Invoice]
|
||||
|
||||
var media: [MediaUrl] {
|
||||
return urls.compactMap { url in url.is_media }
|
||||
}
|
||||
|
||||
var images: [URL] {
|
||||
return urls.compactMap { url in url.is_img }
|
||||
}
|
||||
|
||||
var links: [URL] {
|
||||
return urls.compactMap { url in url.is_link }
|
||||
}
|
||||
|
||||
static func just_content(_ content: String) -> NoteArtifactsSeparated {
|
||||
let txt = CompatibleText(attributed: AttributedString(stringLiteral: content))
|
||||
return NoteArtifactsSeparated(content: txt, words: 0, urls: [], invoices: [])
|
||||
}
|
||||
}
|
||||
|
||||
enum NoteArtifactState {
|
||||
case not_loaded
|
||||
case loading
|
||||
case loaded(NoteArtifacts)
|
||||
|
||||
var artifacts: NoteArtifacts? {
|
||||
if case .loaded(let artifacts) = self {
|
||||
return artifacts
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var should_preload: Bool {
|
||||
switch self {
|
||||
case .loaded:
|
||||
return false
|
||||
case .loading:
|
||||
return false
|
||||
case .not_loaded:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func note_artifact_is_separated(kind: NostrKind?) -> Bool {
|
||||
return kind != .longform
|
||||
}
|
||||
|
||||
func render_note_content(ev: NostrEvent, profiles: Profiles, keypair: Keypair) -> NoteArtifacts {
|
||||
let blocks = ev.blocks(keypair)
|
||||
|
||||
if ev.known_kind == .longform {
|
||||
return .longform(LongformContent(ev.content))
|
||||
}
|
||||
|
||||
return .separated(render_blocks(blocks: blocks, profiles: profiles))
|
||||
}
|
||||
|
||||
fileprivate func artifact_part_last_text_ind(parts: [ArtifactPart]) -> (Int, Text)? {
|
||||
let ind = parts.count - 1
|
||||
if ind < 0 {
|
||||
@@ -471,175 +337,6 @@ fileprivate func artifact_part_last_text_ind(parts: [ArtifactPart]) -> (Int, Tex
|
||||
return (ind, txt)
|
||||
}
|
||||
|
||||
func reduce_text_block(blocks: [Block], ind: Int, txt: String, one_note_ref: Bool) -> String {
|
||||
var trimmed = txt
|
||||
|
||||
if let prev = blocks[safe: ind-1],
|
||||
case .url(let u) = prev,
|
||||
classify_url(u).is_media != nil {
|
||||
trimmed = " " + trim_prefix(trimmed)
|
||||
}
|
||||
|
||||
if let next = blocks[safe: ind+1] {
|
||||
if case .url(let u) = next, classify_url(u).is_media != nil {
|
||||
trimmed = trim_suffix(trimmed)
|
||||
} else if case .mention(let m) = next,
|
||||
case .note = m.ref,
|
||||
one_note_ref {
|
||||
trimmed = trim_suffix(trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
return trimmed
|
||||
}
|
||||
|
||||
func render_blocks(blocks bs: Blocks, profiles: Profiles) -> NoteArtifactsSeparated {
|
||||
var invoices: [Invoice] = []
|
||||
var urls: [UrlType] = []
|
||||
let blocks = bs.blocks
|
||||
|
||||
let one_note_ref = blocks
|
||||
.filter({
|
||||
if case .mention(let mention) = $0,
|
||||
case .note = mention.ref {
|
||||
return true
|
||||
}
|
||||
else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
.count == 1
|
||||
|
||||
var ind: Int = -1
|
||||
let txt: CompatibleText = blocks.reduce(CompatibleText()) { str, block in
|
||||
ind = ind + 1
|
||||
|
||||
switch block {
|
||||
case .mention(let m):
|
||||
if case .note = m.ref, one_note_ref {
|
||||
return str
|
||||
}
|
||||
return str + mention_str(m, profiles: profiles)
|
||||
case .text(let txt):
|
||||
return str + CompatibleText(stringLiteral: reduce_text_block(blocks: blocks, ind: ind, txt: txt, one_note_ref: one_note_ref))
|
||||
|
||||
case .relay(let relay):
|
||||
return str + CompatibleText(stringLiteral: relay)
|
||||
|
||||
case .hashtag(let htag):
|
||||
return str + hashtag_str(htag)
|
||||
case .invoice(let invoice):
|
||||
invoices.append(invoice)
|
||||
return str
|
||||
case .url(let url):
|
||||
let url_type = classify_url(url)
|
||||
switch url_type {
|
||||
case .media:
|
||||
urls.append(url_type)
|
||||
return str
|
||||
case .link(let url):
|
||||
urls.append(url_type)
|
||||
return str + url_str(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NoteArtifactsSeparated(content: txt, words: bs.words, urls: urls, invoices: invoices)
|
||||
}
|
||||
|
||||
enum MediaUrl {
|
||||
case image(URL)
|
||||
case video(URL)
|
||||
|
||||
var url: URL {
|
||||
switch self {
|
||||
case .image(let url):
|
||||
return url
|
||||
case .video(let url):
|
||||
return url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum UrlType {
|
||||
case media(MediaUrl)
|
||||
case link(URL)
|
||||
|
||||
var url: URL {
|
||||
switch self {
|
||||
case .media(let media_url):
|
||||
switch media_url {
|
||||
case .image(let url):
|
||||
return url
|
||||
case .video(let url):
|
||||
return url
|
||||
}
|
||||
case .link(let url):
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
var is_video: URL? {
|
||||
switch self {
|
||||
case .media(let media_url):
|
||||
switch media_url {
|
||||
case .image:
|
||||
return nil
|
||||
case .video(let url):
|
||||
return url
|
||||
}
|
||||
case .link:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var is_img: URL? {
|
||||
switch self {
|
||||
case .media(let media_url):
|
||||
switch media_url {
|
||||
case .image(let url):
|
||||
return url
|
||||
case .video:
|
||||
return nil
|
||||
}
|
||||
case .link:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var is_link: URL? {
|
||||
switch self {
|
||||
case .media:
|
||||
return nil
|
||||
case .link(let url):
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
var is_media: MediaUrl? {
|
||||
switch self {
|
||||
case .media(let murl):
|
||||
return murl
|
||||
case .link:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func classify_url(_ url: URL) -> UrlType {
|
||||
let str = url.lastPathComponent.lowercased()
|
||||
|
||||
if str.hasSuffix(".png") || str.hasSuffix(".jpg") || str.hasSuffix(".jpeg") || str.hasSuffix(".gif") || str.hasSuffix(".webp") {
|
||||
return .media(.image(url))
|
||||
}
|
||||
|
||||
if str.hasSuffix(".mp4") || str.hasSuffix(".mov") || str.hasSuffix(".m3u8") {
|
||||
return .media(.video(url))
|
||||
}
|
||||
|
||||
return .link(url)
|
||||
}
|
||||
|
||||
func lookup_cached_preview_size(previews: PreviewCache, evid: NoteId) -> CGFloat? {
|
||||
guard case .value(let cached) = previews.lookup(evid) else {
|
||||
return nil
|
||||
@@ -652,16 +349,6 @@ func lookup_cached_preview_size(previews: PreviewCache, evid: NoteId) -> CGFloat
|
||||
return height
|
||||
}
|
||||
|
||||
// trim suffix whitespace and newlines
|
||||
func trim_suffix(_ str: String) -> String {
|
||||
return str.replacingOccurrences(of: "\\s+$", with: "", options: .regularExpression)
|
||||
}
|
||||
|
||||
// trim prefix whitespace and newlines
|
||||
func trim_prefix(_ str: String) -> String {
|
||||
return str.replacingOccurrences(of: "^\\s+", with: "", options: .regularExpression)
|
||||
}
|
||||
|
||||
struct NoteContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let state = test_damus_state
|
||||
@@ -687,39 +374,6 @@ struct NoteContentView_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func count_words(_ s: String) -> Int {
|
||||
return s.components(separatedBy: .whitespacesAndNewlines).count
|
||||
}
|
||||
|
||||
func count_inline_nodes_words(nodes: [InlineNode]) -> Int {
|
||||
return nodes.reduce(0) { words, node in
|
||||
switch node {
|
||||
case .text(let words):
|
||||
return count_words(words)
|
||||
case .emphasis(let children):
|
||||
return words + count_inline_nodes_words(nodes: children)
|
||||
case .strong(let children):
|
||||
return words + count_inline_nodes_words(nodes: children)
|
||||
case .strikethrough(let children):
|
||||
return words + count_inline_nodes_words(nodes: children)
|
||||
case .softBreak, .lineBreak, .code, .html, .image, .link:
|
||||
return words
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func count_markdown_words(blocks: [BlockNode]) -> Int {
|
||||
return blocks.reduce(0) { words, block in
|
||||
switch block {
|
||||
case .paragraph(let content):
|
||||
return words + count_inline_nodes_words(nodes: content)
|
||||
case .blockquote, .bulletedList, .numberedList, .taskList, .codeBlock, .htmlBlock, .heading, .table, .thematicBreak:
|
||||
return words
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func separate_images(ev: NostrEvent, keypair: Keypair) -> [MediaUrl]? {
|
||||
let urlBlocks: [URL] = ev.blocks(keypair).blocks.reduce(into: []) { urls, block in
|
||||
guard case .url(let url) = block else {
|
||||
|
||||
Reference in New Issue
Block a user