Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
e5c82ec64b
|
|||
| fdbf271432 | |||
| b26eedc633 | |||
| 793970beaf | |||
| 049d9170be | |||
| fd10c5672a | |||
| 37bd9447f0 | |||
| e8457d7486 | |||
| 280297ad35 | |||
| 7da3ead01e |
@@ -14,6 +14,12 @@
|
|||||||
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; };
|
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; };
|
||||||
3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 3A0A30BA2C21397A00F8C9BC /* EmojiPicker */; };
|
3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 3A0A30BA2C21397A00F8C9BC /* EmojiPicker */; };
|
||||||
3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */; };
|
3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */; };
|
||||||
|
3A28D3A12E2F3FB5003C6F82 /* PinnedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A28D3A02E2F3FB5003C6F82 /* PinnedEventView.swift */; };
|
||||||
|
3A28D3A22E2F3FB5003C6F82 /* PinnedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A28D3A02E2F3FB5003C6F82 /* PinnedEventView.swift */; };
|
||||||
|
3A28D3A32E2F3FB5003C6F82 /* PinnedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A28D3A02E2F3FB5003C6F82 /* PinnedEventView.swift */; };
|
||||||
|
3A28D3A62E2F4082003C6F82 /* PinnedHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A28D3A52E2F4082003C6F82 /* PinnedHeaderView.swift */; };
|
||||||
|
3A28D3A72E2F4082003C6F82 /* PinnedHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A28D3A52E2F4082003C6F82 /* PinnedHeaderView.swift */; };
|
||||||
|
3A28D3A82E2F4082003C6F82 /* PinnedHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A28D3A52E2F4082003C6F82 /* PinnedHeaderView.swift */; };
|
||||||
3A2BAC5A2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */; };
|
3A2BAC5A2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */; };
|
||||||
3A2BAC5B2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */; };
|
3A2BAC5B2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */; };
|
||||||
3A2BAC5C2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */; };
|
3A2BAC5C2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */; };
|
||||||
@@ -1869,6 +1875,8 @@
|
|||||||
3A25EF132992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
3A25EF132992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||||
3A25EF142992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
3A25EF142992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||||
3A25EF152992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "el-GR"; path = "el-GR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
3A25EF152992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "el-GR"; path = "el-GR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||||
|
3A28D3A02E2F3FB5003C6F82 /* PinnedEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedEventView.swift; sourceTree = "<group>"; };
|
||||||
|
3A28D3A52E2F4082003C6F82 /* PinnedHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedHeaderView.swift; sourceTree = "<group>"; };
|
||||||
3A2B8B0A296A8982009CC16D /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "en-US"; path = "en-US.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
3A2B8B0A296A8982009CC16D /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "en-US"; path = "en-US.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||||
3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainTimelineHeaderView.swift; sourceTree = "<group>"; };
|
3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainTimelineHeaderView.swift; sourceTree = "<group>"; };
|
||||||
3A2BAC5D2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainPubkeysView.swift; sourceTree = "<group>"; };
|
3A2BAC5D2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainPubkeysView.swift; sourceTree = "<group>"; };
|
||||||
@@ -2784,6 +2792,15 @@
|
|||||||
path = "Empty Views";
|
path = "Empty Views";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
3A28D3A42E2F4053003C6F82 /* Pinned */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
3A28D3A02E2F3FB5003C6F82 /* PinnedEventView.swift */,
|
||||||
|
3A28D3A52E2F4082003C6F82 /* PinnedHeaderView.swift */,
|
||||||
|
);
|
||||||
|
path = Pinned;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
3A515C4E2DF4E0E6002D3B34 /* Tips */ = {
|
3A515C4E2DF4E0E6002D3B34 /* Tips */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -3685,6 +3702,7 @@
|
|||||||
4CC7AAEE297F11B300430951 /* Events */ = {
|
4CC7AAEE297F11B300430951 /* Events */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
3A28D3A42E2F4053003C6F82 /* Pinned */,
|
||||||
5C4FA7FA2DC29C3800CE658C /* FollowPack */,
|
5C4FA7FA2DC29C3800CE658C /* FollowPack */,
|
||||||
5CC852A02BDED9970039FFC5 /* Highlight */,
|
5CC852A02BDED9970039FFC5 /* Highlight */,
|
||||||
4CA927682A290F8F0098A105 /* Components */,
|
4CA927682A290F8F0098A105 /* Components */,
|
||||||
@@ -4698,6 +4716,7 @@
|
|||||||
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
|
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
|
||||||
D7EDED1E2B11797D0018B19C /* LongformEvent.swift in Sources */,
|
D7EDED1E2B11797D0018B19C /* LongformEvent.swift in Sources */,
|
||||||
504323A92A3495B6006AE6DC /* RelayModelCache.swift in Sources */,
|
504323A92A3495B6006AE6DC /* RelayModelCache.swift in Sources */,
|
||||||
|
3A28D3A32E2F3FB5003C6F82 /* PinnedEventView.swift in Sources */,
|
||||||
5C4D9EA72C042FA5005EA0F7 /* HighlightDraftContentView.swift in Sources */,
|
5C4D9EA72C042FA5005EA0F7 /* HighlightDraftContentView.swift in Sources */,
|
||||||
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */,
|
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */,
|
||||||
D7FD12262BD345A700CF195B /* FirstAidSettingsView.swift in Sources */,
|
D7FD12262BD345A700CF195B /* FirstAidSettingsView.swift in Sources */,
|
||||||
@@ -5002,6 +5021,7 @@
|
|||||||
4CC14FF92A741939007AEB17 /* Referenced.swift in Sources */,
|
4CC14FF92A741939007AEB17 /* Referenced.swift in Sources */,
|
||||||
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
|
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
|
||||||
4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */,
|
4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */,
|
||||||
|
3A28D3A62E2F4082003C6F82 /* PinnedHeaderView.swift in Sources */,
|
||||||
D7EFBA372CC322F300F45588 /* DamusVideoControlsView.swift in Sources */,
|
D7EFBA372CC322F300F45588 /* DamusVideoControlsView.swift in Sources */,
|
||||||
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
|
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
|
||||||
D706C5AF2D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */,
|
D706C5AF2D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */,
|
||||||
@@ -5332,6 +5352,7 @@
|
|||||||
82D6FB012CD99F7900C925F4 /* Block.swift in Sources */,
|
82D6FB012CD99F7900C925F4 /* Block.swift in Sources */,
|
||||||
82D6FB022CD99F7900C925F4 /* MigratedTypes.swift in Sources */,
|
82D6FB022CD99F7900C925F4 /* MigratedTypes.swift in Sources */,
|
||||||
82D6FB032CD99F7900C925F4 /* DamusDuration.swift in Sources */,
|
82D6FB032CD99F7900C925F4 /* DamusDuration.swift in Sources */,
|
||||||
|
3A28D3A22E2F3FB5003C6F82 /* PinnedEventView.swift in Sources */,
|
||||||
82D6FB042CD99F7900C925F4 /* SwipeToDismiss.swift in Sources */,
|
82D6FB042CD99F7900C925F4 /* SwipeToDismiss.swift in Sources */,
|
||||||
82D6FB052CD99F7900C925F4 /* MusicController.swift in Sources */,
|
82D6FB052CD99F7900C925F4 /* MusicController.swift in Sources */,
|
||||||
82D6FB062CD99F7900C925F4 /* UserStatusView.swift in Sources */,
|
82D6FB062CD99F7900C925F4 /* UserStatusView.swift in Sources */,
|
||||||
@@ -5490,6 +5511,7 @@
|
|||||||
D74EA08E2D2E271E002290DD /* ErrorView.swift in Sources */,
|
D74EA08E2D2E271E002290DD /* ErrorView.swift in Sources */,
|
||||||
82D6FB952CD99F7900C925F4 /* TranslationService.swift in Sources */,
|
82D6FB952CD99F7900C925F4 /* TranslationService.swift in Sources */,
|
||||||
82D6FB962CD99F7900C925F4 /* DeepLPlan.swift in Sources */,
|
82D6FB962CD99F7900C925F4 /* DeepLPlan.swift in Sources */,
|
||||||
|
3A28D3A72E2F4082003C6F82 /* PinnedHeaderView.swift in Sources */,
|
||||||
82D6FB972CD99F7900C925F4 /* ZapsModel.swift in Sources */,
|
82D6FB972CD99F7900C925F4 /* ZapsModel.swift in Sources */,
|
||||||
82D6FB982CD99F7900C925F4 /* DraftsModel.swift in Sources */,
|
82D6FB982CD99F7900C925F4 /* DraftsModel.swift in Sources */,
|
||||||
82D6FB992CD99F7900C925F4 /* NotificationsModel.swift in Sources */,
|
82D6FB992CD99F7900C925F4 /* NotificationsModel.swift in Sources */,
|
||||||
@@ -5754,6 +5776,7 @@
|
|||||||
D73E5F8B2C6AA6A2007EB227 /* UserStatusSheet.swift in Sources */,
|
D73E5F8B2C6AA6A2007EB227 /* UserStatusSheet.swift in Sources */,
|
||||||
D73E5E282C6A97F4007EB227 /* LoginNotify.swift in Sources */,
|
D73E5E282C6A97F4007EB227 /* LoginNotify.swift in Sources */,
|
||||||
D73E5E292C6A97F4007EB227 /* LogoutNotify.swift in Sources */,
|
D73E5E292C6A97F4007EB227 /* LogoutNotify.swift in Sources */,
|
||||||
|
3A28D3A12E2F3FB5003C6F82 /* PinnedEventView.swift in Sources */,
|
||||||
D73E5E2A2C6A97F4007EB227 /* OnlyZapsNotify.swift in Sources */,
|
D73E5E2A2C6A97F4007EB227 /* OnlyZapsNotify.swift in Sources */,
|
||||||
D73E5E2B2C6A97F4007EB227 /* PostNotify.swift in Sources */,
|
D73E5E2B2C6A97F4007EB227 /* PostNotify.swift in Sources */,
|
||||||
D73E5E2C2C6A97F4007EB227 /* PresentSheetNotify.swift in Sources */,
|
D73E5E2C2C6A97F4007EB227 /* PresentSheetNotify.swift in Sources */,
|
||||||
@@ -5776,6 +5799,7 @@
|
|||||||
D73E5E3A2C6A97F4007EB227 /* SwipeToDismiss.swift in Sources */,
|
D73E5E3A2C6A97F4007EB227 /* SwipeToDismiss.swift in Sources */,
|
||||||
D73E5E3B2C6A97F4007EB227 /* MusicController.swift in Sources */,
|
D73E5E3B2C6A97F4007EB227 /* MusicController.swift in Sources */,
|
||||||
D73E5E3C2C6A97F4007EB227 /* UserStatusView.swift in Sources */,
|
D73E5E3C2C6A97F4007EB227 /* UserStatusView.swift in Sources */,
|
||||||
|
3A28D3A82E2F4082003C6F82 /* PinnedHeaderView.swift in Sources */,
|
||||||
D74EA08F2D2E271E002290DD /* ErrorView.swift in Sources */,
|
D74EA08F2D2E271E002290DD /* ErrorView.swift in Sources */,
|
||||||
D73E5E3E2C6A97F4007EB227 /* SearchHeaderView.swift in Sources */,
|
D73E5E3E2C6A97F4007EB227 /* SearchHeaderView.swift in Sources */,
|
||||||
D73E5E3F2C6A97F4007EB227 /* DamusGradient.swift in Sources */,
|
D73E5E3F2C6A97F4007EB227 /* DamusGradient.swift in Sources */,
|
||||||
|
|||||||
@@ -25,12 +25,13 @@ class ActionBarModel: ObservableObject {
|
|||||||
@Published private(set) var zaps: Int
|
@Published private(set) var zaps: Int
|
||||||
@Published var zap_total: Int64
|
@Published var zap_total: Int64
|
||||||
@Published var replies: Int
|
@Published var replies: Int
|
||||||
|
@Published var relays: Int
|
||||||
|
|
||||||
static func empty() -> ActionBarModel {
|
static func empty() -> ActionBarModel {
|
||||||
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
|
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(likes: Int = 0, boosts: Int = 0, zaps: Int = 0, zap_total: Int64 = 0, replies: Int = 0, our_like: NostrEvent? = nil, our_boost: NostrEvent? = nil, our_zap: Zapping? = nil, our_reply: NostrEvent? = nil, our_quote_repost: NostrEvent? = nil, quote_reposts: Int = 0) {
|
init(likes: Int = 0, boosts: Int = 0, zaps: Int = 0, zap_total: Int64 = 0, replies: Int = 0, our_like: NostrEvent? = nil, our_boost: NostrEvent? = nil, our_zap: Zapping? = nil, our_reply: NostrEvent? = nil, our_quote_repost: NostrEvent? = nil, quote_reposts: Int = 0, relays: Int = 0) {
|
||||||
self.likes = likes
|
self.likes = likes
|
||||||
self.boosts = boosts
|
self.boosts = boosts
|
||||||
self.zaps = zaps
|
self.zaps = zaps
|
||||||
@@ -42,6 +43,7 @@ class ActionBarModel: ObservableObject {
|
|||||||
self.our_reply = our_reply
|
self.our_reply = our_reply
|
||||||
self.our_quote_repost = our_quote_repost
|
self.our_quote_repost = our_quote_repost
|
||||||
self.quote_reposts = quote_reposts
|
self.quote_reposts = quote_reposts
|
||||||
|
self.relays = relays
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(damus: DamusState, evid: NoteId) {
|
func update(damus: DamusState, evid: NoteId) {
|
||||||
@@ -56,11 +58,12 @@ class ActionBarModel: ObservableObject {
|
|||||||
self.our_zap = damus.zaps.our_zaps[evid]?.first
|
self.our_zap = damus.zaps.our_zaps[evid]?.first
|
||||||
self.our_reply = damus.replies.our_reply(evid)
|
self.our_reply = damus.replies.our_reply(evid)
|
||||||
self.our_quote_repost = damus.quote_reposts.our_events[evid]
|
self.our_quote_repost = damus.quote_reposts.our_events[evid]
|
||||||
|
self.relays = (damus.nostrNetwork.pool.seen[evid] ?? []).count
|
||||||
self.objectWillChange.send()
|
self.objectWillChange.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
var is_empty: Bool {
|
var is_empty: Bool {
|
||||||
return likes == 0 && boosts == 0 && zaps == 0
|
return likes == 0 && boosts == 0 && zaps == 0 && quote_reposts == 0 && relays == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
var liked: Bool {
|
var liked: Bool {
|
||||||
|
|||||||
@@ -231,6 +231,8 @@ class HomeModel: ContactsDelegate {
|
|||||||
break
|
break
|
||||||
case .interest_list:
|
case .interest_list:
|
||||||
break // Don't care for now
|
break // Don't care for now
|
||||||
|
case .pinned_notes:
|
||||||
|
break // FIXME(tyiu)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,10 +64,35 @@ enum MentionRef: TagKeys, TagConvertible, Equatable, Hashable {
|
|||||||
switch self {
|
switch self {
|
||||||
case .pubkey(let pubkey): return ["p", pubkey.hex()]
|
case .pubkey(let pubkey): return ["p", pubkey.hex()]
|
||||||
case .note(let noteId): return ["e", noteId.hex()]
|
case .note(let noteId): return ["e", noteId.hex()]
|
||||||
case .nevent(let nevent): return ["e", nevent.noteid.hex()]
|
case .nevent(let nevent):
|
||||||
case .nprofile(let nprofile): return ["p", nprofile.author.hex()]
|
var tagBuilder = ["e", nevent.noteid.hex()]
|
||||||
|
|
||||||
|
let relay = nevent.relays.first
|
||||||
|
if let author = nevent.author?.hex() {
|
||||||
|
tagBuilder.append(relay ?? "")
|
||||||
|
tagBuilder.append(author)
|
||||||
|
} else if let relay {
|
||||||
|
tagBuilder.append(relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tagBuilder
|
||||||
|
case .nprofile(let nprofile):
|
||||||
|
var tagBuilder = ["p", nprofile.author.hex()]
|
||||||
|
|
||||||
|
if let relay = nprofile.relays.first {
|
||||||
|
tagBuilder.append(relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tagBuilder
|
||||||
case .nrelay(let url): return ["r", url]
|
case .nrelay(let url): return ["r", url]
|
||||||
case .naddr(let naddr): return ["a", naddr.kind.description + ":" + naddr.author.hex() + ":" + naddr.identifier.string()]
|
case .naddr(let naddr):
|
||||||
|
var tagBuilder = ["a", "\(naddr.kind.description):\(naddr.author.hex()):\(naddr.identifier.string())"]
|
||||||
|
|
||||||
|
if let relay = naddr.relays.first {
|
||||||
|
tagBuilder.append(relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tagBuilder
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,16 @@ class NostrNetworkManager {
|
|||||||
func connect() {
|
func connect() {
|
||||||
self.userRelayList.connect()
|
self.userRelayList.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func relaysForEvent(event: NostrEvent) -> [RelayURL] {
|
||||||
|
// TODO(tyiu) Ideally this list would be sorted by the event author's outbox relay preferences
|
||||||
|
// and reliability of relays to maximize chances of others finding this event.
|
||||||
|
if let relays = pool.seen[event.id] {
|
||||||
|
return Array(relays)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,11 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, keypair: Keypair) -
|
|||||||
return .separated(render_blocks(blocks: blocks, profiles: profiles, can_hide_last_previewable_refs: true))
|
return .separated(render_blocks(blocks: blocks, profiles: profiles, can_hide_last_previewable_refs: true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME(tyiu): There are a lot of hacks to get this function to render the blocks correctly.
|
||||||
|
// However, the entire note content rendering logic just needs to be rewritten.
|
||||||
|
// Block previews should actually be rendered in the position of the note content where it was found.
|
||||||
|
// Currently, we put some previews at the bottom of the note, which is incorrect as they take things out of
|
||||||
|
// the author's intended context.
|
||||||
func render_blocks(blocks bs: Blocks, profiles: Profiles, can_hide_last_previewable_refs: Bool = false) -> NoteArtifactsSeparated {
|
func render_blocks(blocks bs: Blocks, profiles: Profiles, can_hide_last_previewable_refs: Bool = false) -> NoteArtifactsSeparated {
|
||||||
var invoices: [Invoice] = []
|
var invoices: [Invoice] = []
|
||||||
var urls: [UrlType] = []
|
var urls: [UrlType] = []
|
||||||
@@ -120,6 +125,7 @@ func render_blocks(blocks bs: Blocks, profiles: Profiles, can_hide_last_previewa
|
|||||||
// We should hide whitespace at the end sequence.
|
// We should hide whitespace at the end sequence.
|
||||||
hide_text_index = i
|
hide_text_index = i
|
||||||
} else if case .hashtag = block {
|
} else if case .hashtag = block {
|
||||||
|
// SPECIAL CASE:
|
||||||
// We should keep hashtags at the end sequence but hide all the other previewables around it.
|
// We should keep hashtags at the end sequence but hide all the other previewables around it.
|
||||||
hide_text_index = i
|
hide_text_index = i
|
||||||
} else {
|
} else {
|
||||||
@@ -171,7 +177,14 @@ func render_blocks(blocks bs: Blocks, profiles: Profiles, can_hide_last_previewa
|
|||||||
case .mention(let m):
|
case .mention(let m):
|
||||||
return str + mention_str(m, profiles: profiles)
|
return str + mention_str(m, profiles: profiles)
|
||||||
case .text(let txt):
|
case .text(let txt):
|
||||||
return str + CompatibleText(stringLiteral: reduce_text_block(ind: ind, hide_text_index: hide_text_index, txt: txt))
|
if case .hashtag = blocks[safe: ind+1] {
|
||||||
|
// SPECIAL CASE:
|
||||||
|
// Do not trim whitespaces from suffix if the following block is a hashtag.
|
||||||
|
// This is because of the code further up (see "SPECIAL CASE").
|
||||||
|
return str + CompatibleText(stringLiteral: reduce_text_block(ind: ind, hide_text_index: -1, txt: txt))
|
||||||
|
} else {
|
||||||
|
return str + CompatibleText(stringLiteral: reduce_text_block(ind: ind, hide_text_index: hide_text_index, txt: txt))
|
||||||
|
}
|
||||||
case .relay(let relay):
|
case .relay(let relay):
|
||||||
return str + CompatibleText(stringLiteral: relay)
|
return str + CompatibleText(stringLiteral: relay)
|
||||||
case .hashtag(let htag):
|
case .hashtag(let htag):
|
||||||
|
|||||||
@@ -23,9 +23,16 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private let MAX_SHARE_RELAYS = 4
|
@Published var pinned_notes_list: NostrEvent? = nil
|
||||||
|
var pinned_note_ids: Set<NoteId> {
|
||||||
|
if let pinned_notes_list {
|
||||||
|
return Set(pinned_notes_list.referenced_noterefs.map { $0.note_id })
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
var events: EventHolder
|
var events: EventHolder
|
||||||
|
var pinned_events: EventHolder
|
||||||
let pubkey: Pubkey
|
let pubkey: Pubkey
|
||||||
let damus: DamusState
|
let damus: DamusState
|
||||||
|
|
||||||
@@ -34,6 +41,7 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
var prof_subid = UUID().description
|
var prof_subid = UUID().description
|
||||||
var conversations_subid = UUID().description
|
var conversations_subid = UUID().description
|
||||||
var findRelay_subid = UUID().description
|
var findRelay_subid = UUID().description
|
||||||
|
var pinned_subid = UUID().description
|
||||||
var conversation_events: Set<NoteId> = Set()
|
var conversation_events: Set<NoteId> = Set()
|
||||||
|
|
||||||
init(pubkey: Pubkey, damus: DamusState) {
|
init(pubkey: Pubkey, damus: DamusState) {
|
||||||
@@ -42,6 +50,9 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
self.events = EventHolder(on_queue: { ev in
|
self.events = EventHolder(on_queue: { ev in
|
||||||
preload_events(state: damus, events: [ev])
|
preload_events(state: damus, events: [ev])
|
||||||
})
|
})
|
||||||
|
self.pinned_events = EventHolder(on_queue: { ev in
|
||||||
|
preload_events(state: damus, events: [ev])
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func follows(pubkey: Pubkey) -> Bool {
|
func follows(pubkey: Pubkey) -> Bool {
|
||||||
@@ -76,20 +87,18 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let textKinds: [NostrKind] = [.text, .longform, .highlight]
|
||||||
|
|
||||||
func subscribe() {
|
func subscribe() {
|
||||||
var text_filter = NostrFilter(kinds: [.text, .longform, .highlight])
|
let text_filter = NostrFilter(kinds: textKinds, limit: 500, authors: [pubkey])
|
||||||
var profile_filter = NostrFilter(kinds: [.contacts, .metadata, .boost])
|
let profile_filter = NostrFilter(kinds: [.contacts, .metadata, .boost], authors: [pubkey])
|
||||||
var relay_list_filter = NostrFilter(kinds: [.relay_list], authors: [pubkey])
|
let relay_list_filter = NostrFilter(kinds: [.relay_list], authors: [pubkey])
|
||||||
|
let pinned_notes_filter = NostrFilter(kinds: [.pinned_notes], authors: [pubkey])
|
||||||
profile_filter.authors = [pubkey]
|
|
||||||
|
|
||||||
text_filter.authors = [pubkey]
|
|
||||||
text_filter.limit = 500
|
|
||||||
|
|
||||||
print("subscribing to textlike events from profile \(pubkey) with sub_id \(sub_id)")
|
print("subscribing to textlike events from profile \(pubkey) with sub_id \(sub_id)")
|
||||||
//print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]])
|
//print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]])
|
||||||
damus.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: [text_filter], handler: handle_event)
|
damus.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: [text_filter], handler: handle_event)
|
||||||
damus.nostrNetwork.pool.subscribe(sub_id: prof_subid, filters: [profile_filter, relay_list_filter], handler: handle_event)
|
damus.nostrNetwork.pool.subscribe(sub_id: prof_subid, filters: [profile_filter, relay_list_filter, pinned_notes_filter], handler: handle_event)
|
||||||
|
|
||||||
subscribe_to_conversations()
|
subscribe_to_conversations()
|
||||||
}
|
}
|
||||||
@@ -100,7 +109,7 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let conversation_kinds: [NostrKind] = [.text, .longform, .highlight]
|
let conversation_kinds: [NostrKind] = textKinds
|
||||||
let limit: UInt32 = 500
|
let limit: UInt32 = 500
|
||||||
let conversations_filter_them = NostrFilter(kinds: conversation_kinds, pubkeys: [damus.pubkey], limit: limit, authors: [pubkey])
|
let conversations_filter_them = NostrFilter(kinds: conversation_kinds, pubkeys: [damus.pubkey], limit: limit, authors: [pubkey])
|
||||||
let conversations_filter_us = NostrFilter(kinds: conversation_kinds, pubkeys: [pubkey], limit: limit, authors: [damus.pubkey])
|
let conversations_filter_us = NostrFilter(kinds: conversation_kinds, pubkeys: [pubkey], limit: limit, authors: [damus.pubkey])
|
||||||
@@ -108,6 +117,15 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
damus.nostrNetwork.pool.subscribe(sub_id: conversations_subid, filters: [conversations_filter_them, conversations_filter_us], handler: handle_event)
|
damus.nostrNetwork.pool.subscribe(sub_id: conversations_subid, filters: [conversations_filter_them, conversations_filter_us], handler: handle_event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func subscribe_to_pinned_notes() {
|
||||||
|
guard let pinned_notes_list, pinned_notes_list.referenced_noterefs.first != nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let pinned_filter = NostrFilter(ids: Array(pinned_note_ids), kinds: [.text], authors: [pubkey])
|
||||||
|
damus.nostrNetwork.pool.subscribe(sub_id: pinned_subid, filters: [pinned_filter], handler: handle_event)
|
||||||
|
}
|
||||||
|
|
||||||
func handle_profile_contact_event(_ ev: NostrEvent) {
|
func handle_profile_contact_event(_ ev: NostrEvent) {
|
||||||
process_contact_event(state: damus, ev: ev)
|
process_contact_event(state: damus, ev: ev)
|
||||||
|
|
||||||
@@ -128,11 +146,24 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
if self.events.insert(ev) {
|
if self.events.insert(ev) {
|
||||||
self.objectWillChange.send()
|
self.objectWillChange.send()
|
||||||
}
|
}
|
||||||
|
if pinned_note_ids.contains(ev.id) && self.pinned_events.insert(ev) {
|
||||||
|
self.objectWillChange.send()
|
||||||
|
}
|
||||||
} else if ev.known_kind == .contacts {
|
} else if ev.known_kind == .contacts {
|
||||||
handle_profile_contact_event(ev)
|
handle_profile_contact_event(ev)
|
||||||
}
|
} else if ev.known_kind == .relay_list {
|
||||||
else if ev.known_kind == .relay_list {
|
|
||||||
self.relay_list = try? NIP65.RelayList(event: ev) // Whether another user's list is malformatted is something beyond our control. Probably best to suppress errors
|
self.relay_list = try? NIP65.RelayList(event: ev) // Whether another user's list is malformatted is something beyond our control. Probably best to suppress errors
|
||||||
|
} else if ev.known_kind == .pinned_notes {
|
||||||
|
if let current_ev = self.pinned_notes_list {
|
||||||
|
guard ev.created_at > current_ev.created_at else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pinned_events.incoming.removeAll()
|
||||||
|
pinned_events.events.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pinned_notes_list = ev
|
||||||
|
subscribe_to_pinned_notes()
|
||||||
}
|
}
|
||||||
seen_event.insert(ev.id)
|
seen_event.insert(ev.id)
|
||||||
}
|
}
|
||||||
@@ -150,6 +181,8 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
} else if sub_id == self.pinned_subid {
|
||||||
|
return self.pubkey == ev.pubkey && pinned_note_ids.contains(ev.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.pubkey == ev.pubkey
|
return self.pubkey == ev.pubkey
|
||||||
@@ -160,7 +193,7 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
case .ws_event:
|
case .ws_event:
|
||||||
return
|
return
|
||||||
case .nostr_event(let resp):
|
case .nostr_event(let resp):
|
||||||
guard resp.subid == self.sub_id || resp.subid == self.prof_subid || resp.subid == self.conversations_subid else {
|
guard [self.sub_id, self.prof_subid, self.conversations_subid, self.pinned_subid].contains(resp.subid) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch resp {
|
switch resp {
|
||||||
@@ -181,12 +214,24 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
if resp.subid == self.conversations_subid {
|
if resp.subid == self.conversations_subid {
|
||||||
conversation_events.insert(ev.id)
|
conversation_events.insert(ev.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if resp.subid == self.pinned_subid, self.pinned_events.insert(ev) {
|
||||||
|
self.objectWillChange.send()
|
||||||
|
}
|
||||||
} else if resp.subid == self.conversations_subid && !conversation_events.contains(ev.id) {
|
} else if resp.subid == self.conversations_subid && !conversation_events.contains(ev.id) {
|
||||||
guard relay_filtered_correctly(ev, subid: resp.subid) else {
|
guard relay_filtered_correctly(ev, subid: resp.subid) else {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
conversation_events.insert(ev.id)
|
conversation_events.insert(ev.id)
|
||||||
|
} else if resp.subid == self.pinned_subid {
|
||||||
|
guard relay_filtered_correctly(ev, subid: resp.subid) else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.subid == self.pinned_subid, self.pinned_events.insert(ev) {
|
||||||
|
self.objectWillChange.send()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case .notice:
|
case .notice:
|
||||||
break
|
break
|
||||||
@@ -222,7 +267,7 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getCappedRelayStrings() -> [String] {
|
func getCappedRelayStrings() -> [String] {
|
||||||
return self.relay_urls?.prefix(MAX_SHARE_RELAYS).map { $0.absoluteString } ?? []
|
return self.relay_urls?.prefix(Constants.MAX_SHARE_RELAYS).map { $0.absoluteString } ?? []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -448,17 +448,26 @@ func random_bytes(count: Int) -> Data {
|
|||||||
return Data(bytes: bytes, count: count)
|
return Data(bytes: bytes, count: count)
|
||||||
}
|
}
|
||||||
|
|
||||||
func make_boost_event(keypair: FullKeypair, boosted: NostrEvent) -> NostrEvent? {
|
func make_boost_event(keypair: FullKeypair, boosted: NostrEvent, relayURL: RelayURL?) -> NostrEvent? {
|
||||||
var tags = Array(boosted.referenced_pubkeys).map({ pk in pk.tag })
|
var tags = Array(boosted.referenced_pubkeys).map({ pk in pk.tag })
|
||||||
|
|
||||||
tags.append(["e", boosted.id.hex(), "", "root"])
|
var eTagBuilder = ["e", boosted.id.hex()]
|
||||||
tags.append(["p", boosted.pubkey.hex()])
|
var pTagBuilder = ["p", boosted.pubkey.hex()]
|
||||||
|
|
||||||
|
let relayURLString = relayURL?.absoluteString
|
||||||
|
if let relayURLString {
|
||||||
|
pTagBuilder.append(relayURLString)
|
||||||
|
}
|
||||||
|
eTagBuilder.append(contentsOf: [relayURLString ?? "", "root", boosted.pubkey.hex()])
|
||||||
|
|
||||||
|
tags.append(eTagBuilder)
|
||||||
|
tags.append(pTagBuilder)
|
||||||
|
|
||||||
let content = event_to_json(ev: boosted)
|
let content = event_to_json(ev: boosted)
|
||||||
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 6, tags: tags)
|
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 6, tags: tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
func make_like_event(keypair: FullKeypair, liked: NostrEvent, content: String = "🤙") -> NostrEvent? {
|
func make_like_event(keypair: FullKeypair, liked: NostrEvent, content: String = "🤙", relayURL: RelayURL?) -> NostrEvent? {
|
||||||
var tags = liked.tags.reduce(into: [[String]]()) { ts, tag in
|
var tags = liked.tags.reduce(into: [[String]]()) { ts, tag in
|
||||||
guard tag.count >= 2,
|
guard tag.count >= 2,
|
||||||
(tag[0].matches_char("e") || tag[0].matches_char("p")) else {
|
(tag[0].matches_char("e") || tag[0].matches_char("p")) else {
|
||||||
@@ -467,8 +476,17 @@ func make_like_event(keypair: FullKeypair, liked: NostrEvent, content: String =
|
|||||||
ts.append(tag.strings())
|
ts.append(tag.strings())
|
||||||
}
|
}
|
||||||
|
|
||||||
tags.append(["e", liked.id.hex()])
|
var eTagBuilder = ["e", liked.id.hex()]
|
||||||
tags.append(["p", liked.pubkey.hex()])
|
var pTagBuilder = ["p", liked.pubkey.hex()]
|
||||||
|
|
||||||
|
let relayURLString = relayURL?.absoluteString
|
||||||
|
if let relayURLString {
|
||||||
|
pTagBuilder.append(relayURLString)
|
||||||
|
}
|
||||||
|
eTagBuilder.append(contentsOf: [relayURLString ?? "", liked.pubkey.hex()])
|
||||||
|
|
||||||
|
tags.append(eTagBuilder)
|
||||||
|
tags.append(pTagBuilder)
|
||||||
|
|
||||||
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 7, tags: tags)
|
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 7, tags: tags)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ enum NostrKind: UInt32, Codable {
|
|||||||
case like = 7
|
case like = 7
|
||||||
case chat = 42
|
case chat = 42
|
||||||
case mute_list = 10000
|
case mute_list = 10000
|
||||||
|
case pinned_notes = 10001
|
||||||
case relay_list = 10002
|
case relay_list = 10002
|
||||||
case interest_list = 10015
|
case interest_list = 10015
|
||||||
case list_deprecated = 30000
|
case list_deprecated = 30000
|
||||||
|
|||||||
@@ -19,17 +19,12 @@ struct QueuedRequest {
|
|||||||
let skip_ephemeral: Bool
|
let skip_ephemeral: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SeenEvent: Hashable {
|
|
||||||
let relay_id: RelayURL
|
|
||||||
let evid: NoteId
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establishes and manages connections and subscriptions to a list of relays.
|
/// Establishes and manages connections and subscriptions to a list of relays.
|
||||||
class RelayPool {
|
class RelayPool {
|
||||||
private(set) var relays: [Relay] = []
|
private(set) var relays: [Relay] = []
|
||||||
var handlers: [RelayHandler] = []
|
var handlers: [RelayHandler] = []
|
||||||
var request_queue: [QueuedRequest] = []
|
var request_queue: [QueuedRequest] = []
|
||||||
var seen: Set<SeenEvent> = Set()
|
var seen: [NoteId: Set<RelayURL>] = [:]
|
||||||
var counts: [RelayURL: UInt64] = [:]
|
var counts: [RelayURL: UInt64] = [:]
|
||||||
var ndb: Ndb
|
var ndb: Ndb
|
||||||
/// The keypair used to authenticate with relays
|
/// The keypair used to authenticate with relays
|
||||||
@@ -357,15 +352,12 @@ class RelayPool {
|
|||||||
func record_seen(relay_id: RelayURL, event: NostrConnectionEvent) {
|
func record_seen(relay_id: RelayURL, event: NostrConnectionEvent) {
|
||||||
if case .nostr_event(let ev) = event {
|
if case .nostr_event(let ev) = event {
|
||||||
if case .event(_, let nev) = ev {
|
if case .event(_, let nev) = ev {
|
||||||
let k = SeenEvent(relay_id: relay_id, evid: nev.id)
|
if seen[nev.id]?.contains(relay_id) == true {
|
||||||
if !seen.contains(k) {
|
return
|
||||||
seen.insert(k)
|
|
||||||
if counts[relay_id] == nil {
|
|
||||||
counts[relay_id] = 1
|
|
||||||
} else {
|
|
||||||
counts[relay_id] = (counts[relay_id] ?? 0) + 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
seen[nev.id, default: Set()].insert(relay_id)
|
||||||
|
counts[relay_id, default: 0] += 1
|
||||||
|
notify(.update_stats(note_id: nev.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ struct NEvent : Equatable, Hashable {
|
|||||||
self.author = author
|
self.author = author
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(event: NostrEvent, relays: [String]) {
|
||||||
|
self.init(noteid: event.id, relays: relays, author: event.pubkey, kind: event.kind)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NProfile : Equatable, Hashable {
|
struct NProfile : Equatable, Hashable {
|
||||||
|
|||||||
@@ -45,4 +45,5 @@ class Constants {
|
|||||||
|
|
||||||
// MARK: General constants
|
// MARK: General constants
|
||||||
static let GIF_IMAGE_TYPE: String = "com.compuserve.gif"
|
static let GIF_IMAGE_TYPE: String = "com.compuserve.gif"
|
||||||
|
static let MAX_SHARE_RELAYS = 4
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,6 +218,15 @@ struct EventActionBar: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var event_relay_url_strings: [String] {
|
||||||
|
let relays = damus_state.nostrNetwork.relaysForEvent(event: event)
|
||||||
|
if !relays.isEmpty {
|
||||||
|
return relays.prefix(Constants.MAX_SHARE_RELAYS).map { $0.absoluteString }
|
||||||
|
}
|
||||||
|
|
||||||
|
return userProfile.getCappedRelayStrings()
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
self.content
|
self.content
|
||||||
.onAppear {
|
.onAppear {
|
||||||
@@ -233,7 +242,9 @@ struct EventActionBar: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $show_share_sheet, onDismiss: { self.show_share_sheet = false }) {
|
.sheet(isPresented: $show_share_sheet, onDismiss: { self.show_share_sheet = false }) {
|
||||||
ShareSheet(activityItems: [URL(string: "https://damus.io/" + event.id.bech32)!])
|
if let url = URL(string: "https://damus.io/" + Bech32Object.encode(.nevent(NEvent(event: event, relays: event_relay_url_strings)))) {
|
||||||
|
ShareSheet(activityItems: [url])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $show_repost_action, onDismiss: { self.show_repost_action = false }) {
|
.sheet(isPresented: $show_repost_action, onDismiss: { self.show_repost_action = false }) {
|
||||||
|
|
||||||
@@ -262,7 +273,7 @@ struct EventActionBar: View {
|
|||||||
|
|
||||||
func send_like(emoji: String) {
|
func send_like(emoji: String) {
|
||||||
guard let keypair = damus_state.keypair.to_full(),
|
guard let keypair = damus_state.keypair.to_full(),
|
||||||
let like_ev = make_like_event(keypair: keypair, liked: event, content: emoji) else {
|
let like_ev = make_like_event(keypair: keypair, liked: event, content: emoji, relayURL: damus_state.nostrNetwork.relaysForEvent(event: event).first) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,16 @@ struct EventDetailBar: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(PlainButtonStyle())
|
.buttonStyle(PlainButtonStyle())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bar.relays > 0 {
|
||||||
|
let relays = Array(state.nostrNetwork.pool.seen[target] ?? [])
|
||||||
|
NavigationLink(value: Route.UserRelays(relays: relays)) {
|
||||||
|
let nounString = pluralizedString(key: "relays_count", count: bar.relays)
|
||||||
|
let noun = Text(nounString).foregroundColor(.gray)
|
||||||
|
Text("\(Text(verbatim: bar.relays.formatted()).font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many relays a note was found on. In source English, the first variable is the number of relays, and the second variable is 'Relay' or 'Relays'.")
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ struct RepostAction: View {
|
|||||||
dismiss()
|
dismiss()
|
||||||
|
|
||||||
guard let keypair = self.damus_state.keypair.to_full(),
|
guard let keypair = self.damus_state.keypair.to_full(),
|
||||||
let boost = make_boost_event(keypair: keypair, boosted: self.event) else {
|
let boost = make_boost_event(keypair: keypair, boosted: self.event, relayURL: damus_state.nostrNetwork.relaysForEvent(event: self.event).first) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,15 @@ struct ShareAction: View {
|
|||||||
self._show_share = show_share
|
self._show_share = show_share
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var event_relay_url_strings: [String] {
|
||||||
|
let relays = userProfile.damus.nostrNetwork.relaysForEvent(event: event)
|
||||||
|
if !relays.isEmpty {
|
||||||
|
return relays.prefix(Constants.MAX_SHARE_RELAYS).map { $0.absoluteString }
|
||||||
|
}
|
||||||
|
|
||||||
|
return userProfile.getCappedRelayStrings()
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
||||||
VStack {
|
VStack {
|
||||||
@@ -40,7 +49,7 @@ struct ShareAction: View {
|
|||||||
|
|
||||||
ShareActionButton(img: "link", text: NSLocalizedString("Copy Link", comment: "Button to copy link to note")) {
|
ShareActionButton(img: "link", text: NSLocalizedString("Copy Link", comment: "Button to copy link to note")) {
|
||||||
dismiss()
|
dismiss()
|
||||||
UIPasteboard.general.string = "https://damus.io/" + Bech32Object.encode(.nevent(NEvent(noteid: event.id, relays: userProfile.getCappedRelayStrings())))
|
UIPasteboard.general.string = "https://damus.io/" + Bech32Object.encode(.nevent(NEvent(event: event, relays: event_relay_url_strings)))
|
||||||
}
|
}
|
||||||
|
|
||||||
let bookmarkImg = isBookmarked ? "bookmark.fill" : "bookmark"
|
let bookmarkImg = isBookmarked ? "bookmark.fill" : "bookmark"
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ struct BookmarksView: View {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
InnerTimelineView(events: EventHolder(events: bookmarks, incoming: []), damus: state, filter: noneFilter)
|
InnerTimelineView(events: EventHolder(events: bookmarks, incoming: []), pinned_events: EventHolder(), damus: state, filter: noneFilter)
|
||||||
}
|
}
|
||||||
.padding(.bottom, 10 + tabHeight + getSafeAreaBottom())
|
.padding(.bottom, 10 + tabHeight + getSafeAreaBottom())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ struct ChatEventView: View {
|
|||||||
|
|
||||||
func send_like(emoji: String) {
|
func send_like(emoji: String) {
|
||||||
guard let keypair = damus_state.keypair.to_full(),
|
guard let keypair = damus_state.keypair.to_full(),
|
||||||
let like_ev = make_like_event(keypair: keypair, liked: event, content: emoji) else {
|
let like_ev = make_like_event(keypair: keypair, liked: event, content: emoji, relayURL: damus_state.nostrNetwork.relaysForEvent(event: event).first) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,17 +21,21 @@ struct EventView: View {
|
|||||||
let options: EventViewOptions
|
let options: EventViewOptions
|
||||||
let damus: DamusState
|
let damus: DamusState
|
||||||
let pubkey: Pubkey
|
let pubkey: Pubkey
|
||||||
|
let pinned: Set<NoteId>
|
||||||
|
|
||||||
init(damus: DamusState, event: NostrEvent, pubkey: Pubkey? = nil, options: EventViewOptions = []) {
|
init(damus: DamusState, event: NostrEvent, pubkey: Pubkey? = nil, pinned: Set<NoteId> = [], options: EventViewOptions = []) {
|
||||||
self.event = event
|
self.event = event
|
||||||
self.options = options
|
self.options = options
|
||||||
self.damus = damus
|
self.damus = damus
|
||||||
self.pubkey = pubkey ?? event.pubkey
|
self.pubkey = pubkey ?? event.pubkey
|
||||||
|
self.pinned = pinned
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
if event.known_kind == .boost {
|
if pinned.contains(event.id) {
|
||||||
|
PinnedEventView(damus: damus, event: event, options: options)
|
||||||
|
} else if event.known_kind == .boost {
|
||||||
if let inner_ev = event.get_inner_event(cache: damus.events) {
|
if let inner_ev = event.get_inner_event(cache: damus.events) {
|
||||||
RepostedEvent(damus: damus, event: event, inner_ev: inner_ev, options: options)
|
RepostedEvent(damus: damus, event: event, inner_ev: inner_ev, options: options)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -64,6 +64,15 @@ struct MenuItems: View {
|
|||||||
self.profileModel = profileModel
|
self.profileModel = profileModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var event_relay_url_strings: [String] {
|
||||||
|
let relays = damus_state.nostrNetwork.relaysForEvent(event: event)
|
||||||
|
if !relays.isEmpty {
|
||||||
|
return relays.prefix(Constants.MAX_SHARE_RELAYS).map { $0.absoluteString }
|
||||||
|
}
|
||||||
|
|
||||||
|
return profileModel.getCappedRelayStrings()
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
Button {
|
Button {
|
||||||
@@ -79,7 +88,7 @@ struct MenuItems: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
UIPasteboard.general.string = event.id.bech32
|
UIPasteboard.general.string = Bech32Object.encode(.nevent(NEvent(event: event, relays: event_relay_url_strings)))
|
||||||
} label: {
|
} label: {
|
||||||
Label(NSLocalizedString("Copy note ID", comment: "Context menu option for copying the ID of the note."), image: "note-book")
|
Label(NSLocalizedString("Copy note ID", comment: "Context menu option for copying the ID of the note."), image: "note-book")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ struct FollowPackView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if tab_selection == FollowPackTabSelection.posts {
|
if tab_selection == FollowPackTabSelection.posts {
|
||||||
InnerTimelineView(events: model.events, damus: state, filter: content_filter(event.publicKeys))
|
InnerTimelineView(events: model.events, pinned_events: EventHolder(), damus: state, filter: content_filter(event.publicKeys))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear() {
|
.onAppear() {
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
//
|
||||||
|
// PinnedEventView.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Terry Yiu on 7/21/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct PinnedEventView: View {
|
||||||
|
let damus: DamusState
|
||||||
|
let event: NostrEvent
|
||||||
|
let options: EventViewOptions
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
PinnedHeaderView(damus: damus, pubkey: event.pubkey)
|
||||||
|
.padding(.horizontal)
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
|
||||||
|
TextEvent(damus: damus, event: event, pubkey: event.pubkey, options: options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
PinnedEventView(damus: test_damus_state, event: test_note, options: [])
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
//
|
||||||
|
// PinnedHeaderView.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Terry Yiu on 7/21/25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct PinnedHeaderView: View {
|
||||||
|
let damus: DamusState
|
||||||
|
let pubkey: Pubkey
|
||||||
|
|
||||||
|
init(damus: DamusState, pubkey: Pubkey) {
|
||||||
|
self.damus = damus
|
||||||
|
self.pubkey = pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .center) {
|
||||||
|
Image("pin")
|
||||||
|
.foregroundColor(Color.gray)
|
||||||
|
|
||||||
|
Text("Pinned", comment: "FIXME")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
PinnedHeaderView(damus: test_damus_state, pubkey: test_pubkey)
|
||||||
|
}
|
||||||
@@ -74,7 +74,7 @@ class LoadableNostrEventViewModel: ObservableObject {
|
|||||||
case .zap, .zap_request:
|
case .zap, .zap_request:
|
||||||
guard let zap = await get_zap(from: ev, state: damus_state) else { return .not_found }
|
guard let zap = await get_zap(from: ev, state: damus_state) else { return .not_found }
|
||||||
return .loaded(route: Route.Zaps(target: zap.target))
|
return .loaded(route: Route.Zaps(target: zap.target))
|
||||||
case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .nwc_request, .nwc_response, .http_auth, .status, .relay_list, .follow_list, .interest_list:
|
case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .nwc_request, .nwc_response, .http_auth, .status, .relay_list, .follow_list, .interest_list, .pinned_notes:
|
||||||
return .unknown_or_unsupported_kind
|
return .unknown_or_unsupported_kind
|
||||||
}
|
}
|
||||||
case .naddr(let naddr):
|
case .naddr(let naddr):
|
||||||
|
|||||||
@@ -798,18 +798,18 @@ private func isAlphanumeric(_ char: Character) -> Bool {
|
|||||||
return char.isLetter || char.isNumber
|
return char.isLetter || char.isNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
func nip10_reply_tags(replying_to: NostrEvent, keypair: Keypair) -> [[String]] {
|
func nip10_reply_tags(replying_to: NostrEvent, keypair: Keypair, relayURL: RelayURL?) -> [[String]] {
|
||||||
guard let nip10 = replying_to.thread_reply() else {
|
guard let nip10 = replying_to.thread_reply() else {
|
||||||
// we're replying to a post that isn't in a thread,
|
// we're replying to a post that isn't in a thread,
|
||||||
// just add a single reply-to-root tag
|
// just add a single reply-to-root tag
|
||||||
return [["e", replying_to.id.hex(), "", "root"]]
|
return [["e", replying_to.id.hex(), relayURL?.absoluteString ?? "", "root"]]
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise use the root tag from the parent's nip10 reply and include the note
|
// otherwise use the root tag from the parent's nip10 reply and include the note
|
||||||
// that we are replying to's note id.
|
// that we are replying to's note id.
|
||||||
let tags = [
|
let tags = [
|
||||||
["e", nip10.root.note_id.hex(), nip10.root.relay ?? "", "root"],
|
["e", nip10.root.note_id.hex(), nip10.root.relay ?? "", "root"],
|
||||||
["e", replying_to.id.hex(), "", "reply"]
|
["e", replying_to.id.hex(), relayURL?.absoluteString ?? "", "reply"]
|
||||||
]
|
]
|
||||||
|
|
||||||
return tags
|
return tags
|
||||||
@@ -902,15 +902,19 @@ func build_post(state: DamusState, post: NSAttributedString, action: PostAction,
|
|||||||
switch action {
|
switch action {
|
||||||
case .replying_to(let replying_to):
|
case .replying_to(let replying_to):
|
||||||
// start off with the reply tags
|
// start off with the reply tags
|
||||||
tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair)
|
tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair, relayURL: state.nostrNetwork.relaysForEvent(event: replying_to).first)
|
||||||
|
|
||||||
case .quoting(let ev):
|
case .quoting(let ev):
|
||||||
content.append("\n\nnostr:" + bech32_note_id(ev.id))
|
let relay_urls = state.nostrNetwork.relaysForEvent(event: ev)
|
||||||
|
let nevent = Bech32Object.encode(.nevent(NEvent(event: ev, relays: relay_urls.prefix(4).map { $0.absoluteString })))
|
||||||
|
content.append("\n\nnostr:\(nevent)")
|
||||||
|
|
||||||
tags.append(["q", ev.id.hex()]);
|
if let first_relay = relay_urls.first?.absoluteString {
|
||||||
|
tags.append(["q", ev.id.hex(), first_relay, ev.pubkey.hex()]);
|
||||||
if let quoted_ev = state.events.lookup(ev.id) {
|
tags.append(["p", ev.pubkey.hex(), first_relay])
|
||||||
tags.append(["p", quoted_ev.pubkey.hex()])
|
} else {
|
||||||
|
tags.append(["q", ev.id.hex(), "", ev.pubkey.hex()]);
|
||||||
|
tags.append(["p", ev.pubkey.hex()])
|
||||||
}
|
}
|
||||||
case .posting, .highlighting, .sharing:
|
case .posting, .highlighting, .sharing:
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -464,13 +464,13 @@ struct ProfileView: View {
|
|||||||
.background(colorScheme == .dark ? Color.black : Color.white)
|
.background(colorScheme == .dark ? Color.black : Color.white)
|
||||||
|
|
||||||
if filter_state == FilterState.posts {
|
if filter_state == FilterState.posts {
|
||||||
InnerTimelineView(events: profile.events, damus: damus_state, filter: content_filter(FilterState.posts))
|
InnerTimelineView(events: profile.events, pinned_events: profile.pinned_events, damus: damus_state, filter: content_filter(FilterState.posts))
|
||||||
}
|
}
|
||||||
if filter_state == FilterState.posts_and_replies {
|
if filter_state == FilterState.posts_and_replies {
|
||||||
InnerTimelineView(events: profile.events, damus: damus_state, filter: content_filter(FilterState.posts_and_replies))
|
InnerTimelineView(events: profile.events, pinned_events: profile.pinned_events, damus: damus_state, filter: content_filter(FilterState.posts_and_replies))
|
||||||
}
|
}
|
||||||
if filter_state == FilterState.conversations && !profile.conversation_events.isEmpty {
|
if filter_state == FilterState.conversations && !profile.conversation_events.isEmpty {
|
||||||
InnerTimelineView(events: profile.events, damus: damus_state, filter: content_filter(FilterState.conversations))
|
InnerTimelineView(events: profile.events, pinned_events: EventHolder(), damus: damus_state, filter: content_filter(FilterState.conversations))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, Theme.safeAreaInsets?.left)
|
.padding(.horizontal, Theme.safeAreaInsets?.left)
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ import SwiftUI
|
|||||||
|
|
||||||
struct InnerTimelineView: View {
|
struct InnerTimelineView: View {
|
||||||
@ObservedObject var events: EventHolder
|
@ObservedObject var events: EventHolder
|
||||||
|
@ObservedObject var pinned_events: EventHolder
|
||||||
let state: DamusState
|
let state: DamusState
|
||||||
let filter: (NostrEvent) -> Bool
|
let filter: (NostrEvent) -> Bool
|
||||||
|
|
||||||
init(events: EventHolder, damus: DamusState, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true) {
|
init(events: EventHolder, pinned_events: EventHolder, damus: DamusState, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true) {
|
||||||
self.events = events
|
self.events = events
|
||||||
|
self.pinned_events = pinned_events
|
||||||
self.state = damus
|
self.state = damus
|
||||||
self.filter = apply_mute_rules ? { filter($0) && !damus.mutelist_manager.is_event_muted($0) } : filter
|
self.filter = apply_mute_rules ? { filter($0) && !damus.mutelist_manager.is_event_muted($0) } : filter
|
||||||
}
|
}
|
||||||
@@ -29,7 +31,7 @@ struct InnerTimelineView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
LazyVStack(spacing: 0) {
|
LazyVStack(spacing: 0) {
|
||||||
let events = self.events.events
|
let events = self.pinned_events.events + self.events.events
|
||||||
if events.isEmpty {
|
if events.isEmpty {
|
||||||
EmptyTimelineView()
|
EmptyTimelineView()
|
||||||
} else {
|
} else {
|
||||||
@@ -38,7 +40,7 @@ struct InnerTimelineView: View {
|
|||||||
ForEach(indexed, id: \.0.id) { tup in
|
ForEach(indexed, id: \.0.id) { tup in
|
||||||
let ev = tup.0
|
let ev = tup.0
|
||||||
let ind = tup.1
|
let ind = tup.1
|
||||||
EventView(damus: state, event: ev, options: event_options)
|
EventView(damus: state, event: ev, pinned: Set(self.pinned_events.events.map { $0.id }), options: event_options)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
let event = ev.get_inner_event(cache: state.events) ?? ev
|
let event = ev.get_inner_event(cache: state.events) ?? ev
|
||||||
let thread = ThreadModel(event: event, damus_state: state)
|
let thread = ThreadModel(event: event, damus_state: state)
|
||||||
@@ -69,7 +71,7 @@ struct InnerTimelineView: View {
|
|||||||
|
|
||||||
struct InnerTimelineView_Previews: PreviewProvider {
|
struct InnerTimelineView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
InnerTimelineView(events: test_event_holder, damus: test_damus_state, filter: { _ in true })
|
InnerTimelineView(events: test_event_holder, pinned_events: EventHolder(), damus: test_damus_state, filter: { _ in true })
|
||||||
.frame(width: 300, height: 500)
|
.frame(width: 300, height: 500)
|
||||||
.border(Color.red)
|
.border(Color.red)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ struct TimelineView<Content: View>: View {
|
|||||||
.id("startblock")
|
.id("startblock")
|
||||||
.frame(height: 0)
|
.frame(height: 0)
|
||||||
|
|
||||||
InnerTimelineView(events: events, damus: damus, filter: loading ? { _ in true } : filter, apply_mute_rules: self.apply_mute_rules)
|
InnerTimelineView(events: events, pinned_events: EventHolder(), damus: damus, filter: loading ? { _ in true } : filter, apply_mute_rules: self.apply_mute_rules)
|
||||||
.redacted(reason: loading ? .placeholder : [])
|
.redacted(reason: loading ? .placeholder : [])
|
||||||
.shimmer(loading)
|
.shimmer(loading)
|
||||||
.disabled(loading)
|
.disabled(loading)
|
||||||
|
|||||||
Binary file not shown.
@@ -2,6 +2,22 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>follow_pack_user_count</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
<string>%#@FOLLOW_PACK_USERS@</string>
|
||||||
|
<key>FOLLOW_PACK_USERS</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringFormatSpecTypeKey</key>
|
||||||
|
<string>NSStringPluralRuleType</string>
|
||||||
|
<key>NSStringFormatValueTypeKey</key>
|
||||||
|
<string>d</string>
|
||||||
|
<key>one</key>
|
||||||
|
<string>Nutzer</string>
|
||||||
|
<key>other</key>
|
||||||
|
<string>Nutzer</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
<key>followed_by_three_and_others</key>
|
<key>followed_by_three_and_others</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSStringLocalizedFormatKey</key>
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
@@ -82,6 +98,22 @@
|
|||||||
<string>Imporieren</string>
|
<string>Imporieren</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>notes_from_three_and_others</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
<string>%#@OTHERS@</string>
|
||||||
|
<key>OTHERS</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringFormatSpecTypeKey</key>
|
||||||
|
<string>NSStringPluralRuleType</string>
|
||||||
|
<key>NSStringFormatValueTypeKey</key>
|
||||||
|
<string>d</string>
|
||||||
|
<key>one</key>
|
||||||
|
<string>Notizen von %2$@, %3$@, %4$@ & %1$d weiterem aus deinem vertrauenswürdigen Netzwerk</string>
|
||||||
|
<key>other</key>
|
||||||
|
<string>Notizen von %2$@, %3$@, %4$@ & %1$d weiteren aus deinem vertrauenswürdigen Netzwerk</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
<key>people_reposted_count</key>
|
<key>people_reposted_count</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSStringLocalizedFormatKey</key>
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
|||||||
@@ -168,6 +168,22 @@ class Bech32ObjectTests: XCTestCase {
|
|||||||
XCTAssertEqual(expectedEncoding, actualEncoding)
|
XCTAssertEqual(expectedEncoding, actualEncoding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testTLVEncoding_NeventFromNostrEvent_ValidContent() throws {
|
||||||
|
let relays = ["wss://relay.damus.io", "wss://relay.nostr.band"]
|
||||||
|
let nevent = NEvent(event: test_note, relays: relays)
|
||||||
|
|
||||||
|
XCTAssertEqual(nevent.noteid, test_note.id)
|
||||||
|
XCTAssertEqual(nevent.relays, relays)
|
||||||
|
XCTAssertEqual(nevent.author, test_note.pubkey)
|
||||||
|
XCTAssertEqual(nevent.kind, test_note.kind)
|
||||||
|
|
||||||
|
let expectedEncoding = "nevent1qqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3vamnwvaz7tmjv4kxz7fwdehhxarj9e3xzmnyqgsgydql3q4ka27d9wnlrmus4tvkrnc8ftc4h8h5fgyln54gl0a7dgsrqsqqqqqpppe7n6"
|
||||||
|
|
||||||
|
let actualEncoding = Bech32Object.encode(.nevent(NEvent(event: test_note, relays: relays)))
|
||||||
|
|
||||||
|
XCTAssertEqual(expectedEncoding, actualEncoding)
|
||||||
|
}
|
||||||
|
|
||||||
func testTLVEncoding_NProfileExample_ValidContent() throws {
|
func testTLVEncoding_NProfileExample_ValidContent() throws {
|
||||||
guard let author = try bech32_decode("npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6") else {
|
guard let author = try bech32_decode("npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6") else {
|
||||||
XCTFail()
|
XCTFail()
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class LikeTests: XCTestCase {
|
|||||||
keypair: test_keypair,
|
keypair: test_keypair,
|
||||||
tags: [cindy.tag, bob.tag])!
|
tags: [cindy.tag, bob.tag])!
|
||||||
let id = liked.id
|
let id = liked.id
|
||||||
let like_ev = make_like_event(keypair: test_keypair_full, liked: liked)!
|
let like_ev = make_like_event(keypair: test_keypair_full, liked: liked, relayURL: nil)!
|
||||||
|
|
||||||
XCTAssertTrue(like_ev.referenced_pubkeys.contains(test_keypair.pubkey))
|
XCTAssertTrue(like_ev.referenced_pubkeys.contains(test_keypair.pubkey))
|
||||||
XCTAssertTrue(like_ev.referenced_pubkeys.contains(cindy))
|
XCTAssertTrue(like_ev.referenced_pubkeys.contains(cindy))
|
||||||
@@ -36,12 +36,12 @@ class LikeTests: XCTestCase {
|
|||||||
func testToReactionEmoji() {
|
func testToReactionEmoji() {
|
||||||
let liked = NostrEvent(content: "awesome #[0] post", keypair: test_keypair, tags: [["p", "cindy"], ["e", "bob"]])!
|
let liked = NostrEvent(content: "awesome #[0] post", keypair: test_keypair, tags: [["p", "cindy"], ["e", "bob"]])!
|
||||||
|
|
||||||
let emptyReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "")!
|
let emptyReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "", relayURL: nil)!
|
||||||
let plusReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "+")!
|
let plusReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "+", relayURL: nil)!
|
||||||
let minusReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "-")!
|
let minusReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "-", relayURL: nil)!
|
||||||
let heartReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "❤️")!
|
let heartReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "❤️", relayURL: nil)!
|
||||||
let thumbsUpReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "👍")!
|
let thumbsUpReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "👍", relayURL: nil)!
|
||||||
let shakaReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "🤙")!
|
let shakaReaction = make_like_event(keypair: test_keypair_full, liked: liked, content: "🤙", relayURL: nil)!
|
||||||
|
|
||||||
XCTAssertEqual(to_reaction_emoji(ev: emptyReaction), "❤️")
|
XCTAssertEqual(to_reaction_emoji(ev: emptyReaction), "❤️")
|
||||||
XCTAssertEqual(to_reaction_emoji(ev: plusReaction), "❤️")
|
XCTAssertEqual(to_reaction_emoji(ev: plusReaction), "❤️")
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ final class PostViewTests: XCTestCase {
|
|||||||
func testQuoteRepost() {
|
func testQuoteRepost() {
|
||||||
let post = build_post(state: test_damus_state, post: .init(), action: .quoting(test_note), uploadedMedias: [], pubkeys: [])
|
let post = build_post(state: test_damus_state, post: .init(), action: .quoting(test_note), uploadedMedias: [], pubkeys: [])
|
||||||
|
|
||||||
XCTAssertEqual(post.tags, [["q", test_note.id.hex()]])
|
XCTAssertEqual(post.tags, [["q", test_note.id.hex(), "", jack_keypair.pubkey.hex()], ["p", jack_keypair.pubkey.hex()]])
|
||||||
}
|
}
|
||||||
|
|
||||||
func testBuildPostRecognizesStringsAsNpubs() throws {
|
func testBuildPostRecognizesStringsAsNpubs() throws {
|
||||||
|
|||||||
Reference in New Issue
Block a user