Compare commits

...

4 Commits

Author SHA1 Message Date
20af086273 Add NIP-05 favicon to profile names and NIP-05 web of trust feed
Changelog-Added: Added NIP-05 favicon to profile names and NIP-05 web of trust feed
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-05-27 00:54:03 -04:00
Swift Coder
e9c1671d06 Display Circular Indicator on top of media undergoing upload process
Removed existing progress view bar at the top of post view
Added separate stack in PVImageCarouselView for media undergoing the upload process
Changelog-Added: Display uploading indicator in post view
Signed-off-by: Swift Coder <scoder1747@gmail.com>
2025-05-26 17:21:06 -07:00
Daniel D’Aquino
d02847d466 Version bump to 1.15
Changelog-None
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-05-26 11:56:42 -07:00
Daniel D’Aquino
580fa954b2 Add changelog for v1.14
Changelog-None
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-05-26 11:45:09 -07:00
21 changed files with 713 additions and 58 deletions

View File

@@ -1,3 +1,51 @@
## [1.14] - 2025-05-25
### Added
- Added safety reminder to wallets with higher balance (Daniel DAquino)
- Added one-click Coinos wallet setup (Daniel DAquino)
- Add notification setting to hide hellthreads (Terry Yiu)
- Added separated first aid option for relay lists that does not need a contact list reset (Daniel DAquino)
- Added NIP-65 relay list support (Daniel DAquino)
- Added Unicode 16 emoji reactions for iOS 18.4+ by upgrading EmojiPicker (Terry Yiu)
- Added a search interface to the settings screen (SanjaySiddharth)
- Added view introducing users to Zaps (ericholguin)
- Added new wallet view with balance and transactions list (ericholguin)
- Added copy technical info button to user visible errors, so that users can more easily share errors with developers (Daniel DAquino)
- Add dismiss button to wallet high balance reminders (Daniel DAquino)
- Zap receiver information now included for outgoing zaps (Daniel DAquino)
- Added inline note rendering of invoices to pull up wallet selector sheet (Terry Yiu)
- Added route to profile page from wallet tx list (ericholguin)
### Changed
- Added additional information on top of blurred images (SanjaySiddharth)
- Improved robustness of relay list handling (Daniel DAquino)
- Updated image cache for better stability (Daniel DAquino)
- Improved integration with Nostr Wallet Connect wallets (ericholguin)
- Added relay connectivity information to NWC settings (Daniel DAquino)
- Improved handling around NWC responses (Daniel DAquino)
- Added more human visible errors on NWC wallets to aid with troubleshooting (Daniel DAquino)
- Re-enabled note zaps as permitted by the new App Store guidelines (Daniel DAquino)
### Fixed
- Hide future notes from timeline (Terry Yiu)
- Fixed issue where profiles with a NIP-65 relay list would not display on Damus (Daniel DAquino)
- Fix quote notes to include missing q tag (Terry Yiu)
- Fixed issue where the side menu would close when copying the npub (SanjaySiddharth)
- Fixed issue where cached images would be backed up to iCloud (Daniel DAquino)
- Optimized classify_url function (Terry Yiu)
- Fixed note rendering for those that contain previewable items or leading and trailing whitespaces (Terry Yiu)
- Fixed issue where some videos would become unplayable after some time using the app (Daniel DAquino)
[1.14]: https://github.com/damus-io/damus/releases/tag/v1.14
## [1.13.1] - 2025-03-21
### Fixed

View File

@@ -14,6 +14,12 @@
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; };
3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 3A0A30BA2C21397A00F8C9BC /* EmojiPicker */; };
3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */; };
3A2BAC5A2DD7E4C400EBB4CC /* 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 */; };
3A2BAC5E2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC5D2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift */; };
3A2BAC5F2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC5D2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift */; };
3A2BAC602DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC5D2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift */; };
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */; };
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */; };
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */; };
@@ -22,6 +28,10 @@
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */; };
3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */; };
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; };
3A92C0FE2DE16E9800CEEBAC /* FaviconCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A92C0FD2DE16E9800CEEBAC /* FaviconCache.swift */; };
3A92C0FF2DE16E9800CEEBAC /* FaviconCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A92C0FD2DE16E9800CEEBAC /* FaviconCache.swift */; };
3A92C1002DE16E9800CEEBAC /* FaviconCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A92C0FD2DE16E9800CEEBAC /* FaviconCache.swift */; };
3A92C1022DE17ACA00CEEBAC /* NIP05DomainTimelineHeaderViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A92C1012DE17ACA00CEEBAC /* NIP05DomainTimelineHeaderViewTests.swift */; };
3A96E3FE2D6BCE3800AE1630 /* RepostedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A96E3FD2D6BCE3800AE1630 /* RepostedTests.swift */; };
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
@@ -33,6 +43,15 @@
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685A297633BC00C46468 /* InfoPlist.strings */; };
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685D297633BC00C46468 /* Localizable.strings */; };
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; };
3ACF94382DA9A52F00971A4E /* FaviconFinder in Frameworks */ = {isa = PBXBuildFile; productRef = 3ACF94372DA9A52F00971A4E /* FaviconFinder */; };
3ACF943E2DA9B10800971A4E /* FaviconFinder in Frameworks */ = {isa = PBXBuildFile; productRef = 3ACF943D2DA9B10800971A4E /* FaviconFinder */; };
3ACF94402DA9B11200971A4E /* FaviconFinder in Frameworks */ = {isa = PBXBuildFile; productRef = 3ACF943F2DA9B11200971A4E /* FaviconFinder */; };
3ACF94422DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF94412DA9FCAB00971A4E /* NIP05DomainTimelineView.swift */; };
3ACF94432DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF94412DA9FCAB00971A4E /* NIP05DomainTimelineView.swift */; };
3ACF94442DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF94412DA9FCAB00971A4E /* NIP05DomainTimelineView.swift */; };
3ACF94462DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF94452DAA006500971A4E /* NIP05DomainEventsModel.swift */; };
3ACF94472DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF94452DAA006500971A4E /* NIP05DomainEventsModel.swift */; };
3ACF94482DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF94452DAA006500971A4E /* NIP05DomainEventsModel.swift */; };
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; };
3CCD1E6A2A874C4E0099A953 /* Nip98HTTPAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCD1E692A874C4E0099A953 /* Nip98HTTPAuth.swift */; };
4C011B5E2BD0A56A002F2F9B /* ChatEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C011B5C2BD0A56A002F2F9B /* ChatEventView.swift */; };
@@ -1808,6 +1827,8 @@
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>"; };
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>"; };
3A2BAC5D2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainPubkeysView.swift; sourceTree = "<group>"; };
3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyDescriptionTests.swift; sourceTree = "<group>"; };
3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationUtil.swift; sourceTree = "<group>"; };
3A3040F229A91366008A0F29 /* ProfileViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewTests.swift; sourceTree = "<group>"; };
@@ -1853,6 +1874,8 @@
3A929C20297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A929C21297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A929C22297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "it-IT"; path = "it-IT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A92C0FD2DE16E9800CEEBAC /* FaviconCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconCache.swift; sourceTree = "<group>"; };
3A92C1012DE17ACA00CEEBAC /* NIP05DomainTimelineHeaderViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainTimelineHeaderViewTests.swift; sourceTree = "<group>"; };
3A93342929884CA600D6A8F3 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pl-PL"; path = "pl-PL.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A93342A29884CA600D6A8F3 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pl-PL"; path = "pl-PL.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A93342B29884CA600D6A8F3 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pl-PL"; path = "pl-PL.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -1891,6 +1914,8 @@
3ACB685B297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3ACB685E297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = "<group>"; };
3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeAgoTests.swift; sourceTree = "<group>"; };
3ACF94412DA9FCAB00971A4E /* NIP05DomainTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainTimelineView.swift; sourceTree = "<group>"; };
3ACF94452DAA006500971A4E /* NIP05DomainEventsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainEventsModel.swift; sourceTree = "<group>"; };
3AD14EB529C40F38009D2D9C /* hu-HU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "hu-HU"; path = "hu-HU.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3AD14EB629C40F38009D2D9C /* hu-HU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "hu-HU"; path = "hu-HU.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AD14EB729C40F38009D2D9C /* hu-HU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "hu-HU"; path = "hu-HU.lproj/Localizable.strings"; sourceTree = "<group>"; };
@@ -2617,6 +2642,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
3ACF94382DA9A52F00971A4E /* FaviconFinder in Frameworks */,
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
D7DB1FE42D5A9AC900CF06DA /* CryptoSwift in Frameworks */,
3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */,
@@ -2648,6 +2674,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
3ACF94402DA9B11200971A4E /* FaviconFinder in Frameworks */,
82D6FC862CD9A4A600C925F4 /* MarkdownUI in Frameworks */,
D7DB1FEC2D5A9F6500CF06DA /* CryptoSwift in Frameworks */,
82D6FC8A2CD9A54600C925F4 /* SwipeActions in Frameworks */,
@@ -2664,6 +2691,7 @@
buildActionMask = 2147483647;
files = (
D703D7AF2C670FB700A400EA /* MarkdownUI in Frameworks */,
3ACF943E2DA9B10800971A4E /* FaviconFinder in Frameworks */,
D73E5F9D2C6AA8E3007EB227 /* SwipeActions in Frameworks */,
D7DB1FE82D5A9F5300CF06DA /* CryptoSwift in Frameworks */,
D73E5F762C6A997E007EB227 /* EmojiPicker in Frameworks */,
@@ -2841,6 +2869,7 @@
5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */,
D773BC5E2C6D538500349F0A /* CommentItem.swift */,
D767066E2C8BB3CE00F09726 /* URLHandler.swift */,
3ACF94452DAA006500971A4E /* NIP05DomainEventsModel.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -3255,6 +3284,9 @@
D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */,
D71AD8FC2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift */,
D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */,
3ACF94412DA9FCAB00971A4E /* NIP05DomainTimelineView.swift */,
3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */,
3A2BAC5D2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift */,
);
path = Views;
sourceTree = "<group>";
@@ -3377,6 +3409,7 @@
D74AAFCE2B155D8C006CF0F4 /* ZapDataModel.swift */,
D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */,
D7D09AB42DADCA5600AB170D /* CoinosDeterministicAccountClient.swift */,
3A92C0FD2DE16E9800CEEBAC /* FaviconCache.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -3772,6 +3805,7 @@
4C2D34402BDAF1B300F9FB44 /* NIP10Tests.swift */,
D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */,
3A96E3FD2D6BCE3800AE1630 /* RepostedTests.swift */,
3A92C1012DE17ACA00CEEBAC /* NIP05DomainTimelineHeaderViewTests.swift */,
);
path = damusTests;
sourceTree = "<group>";
@@ -4173,6 +4207,7 @@
D70D90972CDED61800CD0534 /* CodeScanner */,
D7C48C0A2D12DE0C00A3BACF /* SwiftyCrop */,
D7DB1FE32D5A9AC900CF06DA /* CryptoSwift */,
3ACF94372DA9A52F00971A4E /* FaviconFinder */,
);
productName = damus;
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
@@ -4240,6 +4275,7 @@
D7F360282CEBBE34009D34DA /* CodeScanner */,
D7C48C0C2D12E34900A3BACF /* SwiftyCrop */,
D7DB1FEB2D5A9F6500CF06DA /* CryptoSwift */,
3ACF943F2DA9B11200971A4E /* FaviconFinder */,
);
productName = "share extension";
productReference = 82D6FA972CD9820500C925F4 /* ShareExtension.appex */;
@@ -4269,6 +4305,7 @@
D70D909B2CDED7B200CD0534 /* CodeScanner */,
D7C48C0E2D12E35600A3BACF /* SwiftyCrop */,
D7DB1FE72D5A9F5300CF06DA /* CryptoSwift */,
3ACF943D2DA9B10800971A4E /* FaviconFinder */,
);
productName = "highlighter action extension";
productReference = D703D7172C66E47100A400EA /* HighlighterActionExtension.appex */;
@@ -4381,6 +4418,7 @@
D70D90962CDED61800CD0534 /* XCRemoteSwiftPackageReference "CodeScanner" */,
D7C48C092D12DE0C00A3BACF /* XCRemoteSwiftPackageReference "SwiftyCrop" */,
D7DB1FE22D5A9AC900CF06DA /* XCRemoteSwiftPackageReference "CryptoSwift" */,
3ACF94362DA9A52F00971A4E /* XCRemoteSwiftPackageReference "FaviconFinder" */,
);
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
projectDirPath = "";
@@ -4523,6 +4561,7 @@
4C3EA64428FF558100C48A62 /* sha256.c in Sources */,
5C8498032D5D150000F74FEB /* ZapExplainer.swift in Sources */,
504323A72A34915F006AE6DC /* RelayModel.swift in Sources */,
3A2BAC5C2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */,
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */,
4C32B9542A9AD44700DC3548 /* FlatBuffersUtils.swift in Sources */,
D7EDED1C2B1178FE0018B19C /* NoteContent.swift in Sources */,
@@ -4561,6 +4600,7 @@
4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */,
4C363AA228296A7E006E126D /* SearchView.swift in Sources */,
D798D2282B085CDA00234419 /* NdbNote+.swift in Sources */,
3ACF94422DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */,
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */,
4C1253662A76D0FF0004F4B8 /* OnlyZapsNotify.swift in Sources */,
4CA927652A290F1A0098A105 /* TimeDot.swift in Sources */,
@@ -4757,6 +4797,7 @@
4C12535E2A76CA870004F4B8 /* SwitchedTimelineNotify.swift in Sources */,
D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */,
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */,
3ACF94462DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */,
D734B1452CCC19B1000B5C97 /* DamusFullScreenCover.swift in Sources */,
4C4E137D2A76D63600BDD832 /* UnmuteThreadNotify.swift in Sources */,
D706C5B72D602A110027C627 /* QueueableNotify.swift in Sources */,
@@ -4916,6 +4957,7 @@
4CE4F0F229D4FCFA005914DB /* DebouncedOnChange.swift in Sources */,
4C32B9592A9AD44700DC3548 /* Table.swift in Sources */,
4C5D5C9D2A6B2CB40024563C /* AsciiCharacter.swift in Sources */,
3A2BAC5E2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift in Sources */,
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
4C9146FE2A2A87C200DDEA40 /* nostrscript.c in Sources */,
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
@@ -4963,6 +5005,7 @@
4C32B95A2A9AD44700DC3548 /* Verifiable.swift in Sources */,
4C73C5142A4437C10062CAC0 /* ZapUserView.swift in Sources */,
501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */,
3A92C0FE2DE16E9800CEEBAC /* FaviconCache.swift in Sources */,
4C1A9A1D29DDCF9B00516EAC /* NotificationSettingsView.swift in Sources */,
5CC868DD2AA29B3200FB22BA /* NeutralButtonStyle.swift in Sources */,
4C75EFB528049D790006080F /* Relay.swift in Sources */,
@@ -5045,6 +5088,7 @@
4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */,
4C684A552A7E91FE005E6031 /* LargeEventTests.swift in Sources */,
E02B54182B4DFADA0077FF42 /* Bech32ObjectTests.swift in Sources */,
3A92C1022DE17ACA00CEEBAC /* NIP05DomainTimelineHeaderViewTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -5130,6 +5174,7 @@
82D6FAE72CD99F7900C925F4 /* LoginNotify.swift in Sources */,
82D6FAE82CD99F7900C925F4 /* LogoutNotify.swift in Sources */,
D706C5B12D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */,
3ACF94482DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */,
82D6FAE92CD99F7900C925F4 /* NewMutesNotify.swift in Sources */,
82D6FAEA2CD99F7900C925F4 /* NewUnmutesNotify.swift in Sources */,
82D6FAEB2CD99F7900C925F4 /* Notify.swift in Sources */,
@@ -5148,9 +5193,11 @@
82D6FAF62CD99F7900C925F4 /* ZappingNotify.swift in Sources */,
82D6FAF72CD99F7900C925F4 /* MuteNotify.swift in Sources */,
82D6FAF82CD99F7900C925F4 /* RelaysChangedNotify.swift in Sources */,
3A2BAC5B2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */,
82D6FAF92CD99F7900C925F4 /* MuteThreadNotify.swift in Sources */,
82D6FAFA2CD99F7900C925F4 /* UnmuteThreadNotify.swift in Sources */,
82D6FAFB2CD99F7900C925F4 /* ReconnectRelaysNotify.swift in Sources */,
3ACF94432DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */,
82D6FAFC2CD99F7900C925F4 /* PurpleAccountUpdateNotify.swift in Sources */,
82D6FAFD2CD99F7900C925F4 /* IdType.swift in Sources */,
82D6FAFE2CD99F7900C925F4 /* Pubkey.swift in Sources */,
@@ -5403,6 +5450,7 @@
82D6FBED2CD99F7900C925F4 /* MediaView.swift in Sources */,
82D6FBEE2CD99F7900C925F4 /* PurpleViewPrimitives.swift in Sources */,
82D6FBEF2CD99F7900C925F4 /* MarketingContentView.swift in Sources */,
3A2BAC602DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift in Sources */,
82D6FBF02CD99F7900C925F4 /* LogoView.swift in Sources */,
82D6FBF12CD99F7900C925F4 /* IAPProductStateView.swift in Sources */,
82D6FBF22CD99F7900C925F4 /* PurpleBackdrop.swift in Sources */,
@@ -5421,6 +5469,7 @@
82D6FBFF2CD99F7900C925F4 /* NotificationItemView.swift in Sources */,
82D6FC002CD99F7900C925F4 /* ProfilePicturesView.swift in Sources */,
82D6FC012CD99F7900C925F4 /* DamusAppNotificationView.swift in Sources */,
3A92C1002DE16E9800CEEBAC /* FaviconCache.swift in Sources */,
82D6FC022CD99F7900C925F4 /* InnerTimelineView.swift in Sources */,
82D6FC032CD99F7900C925F4 /* PostingTimelineView.swift in Sources */,
82D6FC042CD99F7900C925F4 /* ZapsView.swift in Sources */,
@@ -5636,11 +5685,13 @@
D73E5E682C6A97F4007EB227 /* VectorMath.swift in Sources */,
D73E5E692C6A97F4007EB227 /* RelayBootstrap.swift in Sources */,
D73E5E6A2C6A97F4007EB227 /* RelayModel.swift in Sources */,
3A2BAC5A2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */,
D73E5E6B2C6A97F4007EB227 /* AnyCodable.swift in Sources */,
D73E5E6C2C6A97F4007EB227 /* AnyDecodable.swift in Sources */,
D73E5E6D2C6A97F4007EB227 /* AnyEncodable.swift in Sources */,
D73E5F782C6A9A5C007EB227 /* NdbNote+.swift in Sources */,
D73E5E6E2C6A97F4007EB227 /* NIPURLBuilder.swift in Sources */,
3ACF94472DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */,
D73E5E6F2C6A97F4007EB227 /* TimeAgo.swift in Sources */,
D73E5E702C6A97F4007EB227 /* Parser.swift in Sources */,
D73E5E722C6A97F4007EB227 /* LinkView.swift in Sources */,
@@ -5727,6 +5778,7 @@
D73E5EB92C6A97F4007EB227 /* RelayLog.swift in Sources */,
D73E5EBA2C6A97F4007EB227 /* NostrFilter.swift in Sources */,
D73E5EBB2C6A97F4007EB227 /* Nip98HTTPAuth.swift in Sources */,
3A92C0FF2DE16E9800CEEBAC /* FaviconCache.swift in Sources */,
D73E5EBC2C6A97F4007EB227 /* Relay.swift in Sources */,
D73E5EBD2C6A97F4007EB227 /* NostrRequest.swift in Sources */,
5CB017222D2D985E00A9ED05 /* CoinosButton.swift in Sources */,
@@ -5825,6 +5877,7 @@
D73E5F192C6A97F4007EB227 /* RelayToggle.swift in Sources */,
D73E5F1A2C6A97F4007EB227 /* RelayStatusView.swift in Sources */,
D73E5F1B2C6A97F4007EB227 /* RelayType.swift in Sources */,
3A2BAC5F2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift in Sources */,
D73E5F1C2C6A97F4007EB227 /* SignalView.swift in Sources */,
D73E5F1D2C6A97F4007EB227 /* RelayPicView.swift in Sources */,
D73E5F1E2C6A97F4007EB227 /* UserSearch.swift in Sources */,
@@ -5957,6 +6010,7 @@
D73E5E1B2C6A9672007EB227 /* LikeCounter.swift in Sources */,
D703D7A92C670E5A00A400EA /* refmap.c in Sources */,
D703D77B2C670BF000A400EA /* TableVerifier.swift in Sources */,
3ACF94442DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */,
D703D76D2C670B4500A400EA /* ZapDataModel.swift in Sources */,
D703D79D2C670E0700A400EA /* node_id.c in Sources */,
D703D79B2C670E0000A400EA /* bech32_util.c in Sources */,
@@ -6419,7 +6473,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MACOSX_DEPLOYMENT_TARGET = 12.3;
MARKETING_VERSION = 1.14;
MARKETING_VERSION = 1.15;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@@ -6484,7 +6538,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MACOSX_DEPLOYMENT_TARGET = 12.3;
MARKETING_VERSION = 1.14;
MARKETING_VERSION = 1.15;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
@@ -6939,6 +6993,14 @@
minimumVersion = 0.2.0;
};
};
3ACF94362DA9A52F00971A4E /* XCRemoteSwiftPackageReference "FaviconFinder" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/will-lumley/FaviconFinder.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 5.1.4;
};
};
4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/onevcat/Kingfisher";
@@ -7019,6 +7081,21 @@
package = 3A0A30B92C21397A00F8C9BC /* XCRemoteSwiftPackageReference "EmojiPicker" */;
productName = EmojiPicker;
};
3ACF94372DA9A52F00971A4E /* FaviconFinder */ = {
isa = XCSwiftPackageProductDependency;
package = 3ACF94362DA9A52F00971A4E /* XCRemoteSwiftPackageReference "FaviconFinder" */;
productName = FaviconFinder;
};
3ACF943D2DA9B10800971A4E /* FaviconFinder */ = {
isa = XCSwiftPackageProductDependency;
package = 3ACF94362DA9A52F00971A4E /* XCRemoteSwiftPackageReference "FaviconFinder" */;
productName = FaviconFinder;
};
3ACF943F2DA9B11200971A4E /* FaviconFinder */ = {
isa = XCSwiftPackageProductDependency;
package = 3ACF94362DA9A52F00971A4E /* XCRemoteSwiftPackageReference "FaviconFinder" */;
productName = FaviconFinder;
};
4C06670328FC7EC500038D2A /* Kingfisher */ = {
isa = XCSwiftPackageProductDependency;
package = 4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */;

View File

@@ -1,5 +1,5 @@
{
"originHash" : "06318d35ee2e6bd681b95591e67da33a9461b48a3c652e58bd9d1a6f0d82bdac",
"originHash" : "1fc7e0b44329ba72cd285eeb022b5b92582cd01586b920d243cb0485c2e69dcc",
"pins" : [
{
"identity" : "codescanner",
@@ -35,6 +35,15 @@
"version" : "0.2.0"
}
},
{
"identity" : "faviconfinder",
"kind" : "remoteSourceControl",
"location" : "https://github.com/will-lumley/FaviconFinder.git",
"state" : {
"revision" : "9279f4371f4877ca302ba3bf1015f3f58ae4a56c",
"version" : "5.1.4"
}
},
{
"identity" : "gsplayer",
"kind" : "remoteSourceControl",
@@ -105,6 +114,15 @@
"version" : "0.1.2"
}
},
{
"identity" : "swiftsoup",
"kind" : "remoteSourceControl",
"location" : "https://github.com/scinfu/SwiftSoup.git",
"state" : {
"revision" : "bba848db50462894e7fc0891d018dfecad4ef11e",
"version" : "2.8.7"
}
},
{
"identity" : "swiftycrop",
"kind" : "remoteSourceControl",

View File

@@ -5,27 +5,27 @@
// Created by William Casarin on 2023-01-11.
//
import FaviconFinder
import Kingfisher
import SwiftUI
struct NIP05Badge: View {
let nip05: NIP05
let pubkey: Pubkey
let contacts: Contacts
let damus_state: DamusState
let show_domain: Bool
let profiles: Profiles
let nip05_domain_favicon: FaviconURL?
@Environment(\.openURL) var openURL
init(nip05: NIP05, pubkey: Pubkey, contacts: Contacts, show_domain: Bool, profiles: Profiles) {
init(nip05: NIP05, pubkey: Pubkey, damus_state: DamusState, show_domain: Bool, nip05_domain_favicon: FaviconURL?) {
self.nip05 = nip05
self.pubkey = pubkey
self.contacts = contacts
self.damus_state = damus_state
self.show_domain = show_domain
self.profiles = profiles
self.nip05_domain_favicon = nip05_domain_favicon
}
var nip05_color: Bool {
return use_nip05_color(pubkey: pubkey, contacts: contacts)
return use_nip05_color(pubkey: pubkey, contacts: damus_state.contacts)
}
var Seal: some View {
@@ -44,8 +44,23 @@ struct NIP05Badge: View {
}
}
var domainBadge: some View {
Group {
if let nip05_domain_favicon {
KFImage(nip05_domain_favicon.source)
.imageContext(.favicon, disable_animation: true)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 18, height: 18)
.clipped()
} else {
EmptyView()
}
}
}
var username_matches_nip05: Bool {
guard let name = profiles.lookup(id: pubkey)?.map({ p in p?.name }).value
guard let name = damus_state.profiles.lookup(id: pubkey)?.map({ p in p?.name }).value
else {
return false
}
@@ -65,14 +80,18 @@ struct NIP05Badge: View {
HStack(spacing: 2) {
Seal
if show_domain {
Text(nip05_string)
.nip05_colorized(gradient: nip05_color)
.onTapGesture {
if let nip5url = nip05.siteUrl {
openURL(nip5url)
}
}
Group {
if show_domain {
Text(nip05_string)
.nip05_colorized(gradient: nip05_color)
}
if nip05_domain_favicon != nil {
domainBadge
}
}
.onTapGesture {
damus_state.nav.push(route: Route.NIP05DomainEvents(events: NIP05DomainEventsModel(state: damus_state, domain: nip05.host), nip05_domain_favicon: nip05_domain_favicon))
}
}
@@ -98,13 +117,9 @@ struct NIP05Badge_Previews: PreviewProvider {
static var previews: some View {
let test_state = test_damus_state
VStack {
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles)
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, damus_state: test_state, show_domain: true, nip05_domain_favicon: nil)
NIP05Badge(nip05: NIP05(username: "_", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles)
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles)
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: Contacts(our_pubkey: test_pubkey), show_domain: true, profiles: test_state.profiles)
NIP05Badge(nip05: NIP05(username: "_", host: "jb55.com"), pubkey: test_state.pubkey, damus_state: test_state, show_domain: true, nip05_domain_favicon: nil)
}
}
}

View File

@@ -686,7 +686,8 @@ struct ContentView: View {
video: DamusVideoCoordinator(),
ndb: ndb,
quote_reposts: .init(our_pubkey: pubkey),
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
emoji_provider: DefaultEmojiProvider(showAllVariations: true),
favicon_cache: FaviconCache()
)
home.damus_state = self.damus_state!

View File

@@ -38,6 +38,10 @@ class Contacts {
return friends
}
func get_friend_of_friends_list() -> Set<Pubkey> {
return friend_of_friends
}
func get_followed_hashtags() -> Set<String> {
guard let ev = self.event else { return Set() }
return Set(ev.referenced_hashtags.map({ $0.hashtag }))

View File

@@ -36,9 +36,10 @@ class DamusState: HeadlessDamusState {
var purple: DamusPurple
var push_notification_client: PushNotificationClient
let emoji_provider: EmojiProvider
let favicon_cache: FaviconCache
private(set) var nostrNetwork: NostrNetworkManager
init(keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider) {
init(keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider, favicon_cache: FaviconCache) {
self.keypair = keypair
self.likes = likes
self.boosts = boosts
@@ -68,7 +69,8 @@ class DamusState: HeadlessDamusState {
self.quote_reposts = quote_reposts
self.push_notification_client = PushNotificationClient(keypair: keypair, settings: settings)
self.emoji_provider = emoji_provider
self.favicon_cache = FaviconCache()
let networkManagerDelegate = NostrNetworkManagerDelegate(settings: settings, contacts: contacts, ndb: ndb, keypair: keypair, relayModelCache: relay_model_cache, relayFilters: relay_filters)
self.nostrNetwork = NostrNetworkManager(delegate: networkManagerDelegate)
}
@@ -126,7 +128,8 @@ class DamusState: HeadlessDamusState {
video: DamusVideoCoordinator(),
ndb: ndb,
quote_reposts: .init(our_pubkey: pubkey),
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
emoji_provider: DefaultEmojiProvider(showAllVariations: true),
favicon_cache: FaviconCache()
)
}
@@ -194,7 +197,8 @@ class DamusState: HeadlessDamusState {
video: DamusVideoCoordinator(),
ndb: .empty,
quote_reposts: .init(our_pubkey: empty_pub),
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
emoji_provider: DefaultEmojiProvider(showAllVariations: true),
favicon_cache: FaviconCache()
)
}
}

View File

@@ -0,0 +1,97 @@
//
// NIP05DomainEventsModel.swift
// damus
//
// Created by Terry Yiu on 4/11/25.
//
import FaviconFinder
import Foundation
class NIP05DomainEventsModel: ObservableObject {
let state: DamusState
var events: EventHolder
@Published var loading: Bool = false
let domain: String
var filter: NostrFilter
let sub_id = UUID().description
let profiles_subid = UUID().description
let limit: UInt32 = 500
init(state: DamusState, domain: String) {
self.state = state
self.domain = domain
self.events = EventHolder(on_queue: { ev in
preload_events(state: state, events: [ev])
})
self.filter = NostrFilter()
}
@MainActor func subscribe() {
filter.limit = self.limit
filter.kinds = [.text, .longform, .highlight]
var authors = Set<Pubkey>()
for pubkey in state.contacts.get_friend_of_friends_list() {
let profile_txn = state.profiles.lookup(id: pubkey)
guard let profile = profile_txn?.unsafeUnownedValue,
let nip05_str = profile.nip05,
let nip05 = NIP05.parse(nip05_str),
nip05.host.caseInsensitiveCompare(domain) == .orderedSame else {
continue
}
authors.insert(pubkey)
}
if authors.isEmpty {
return
}
filter.authors = Array(authors)
print("subscribing to notes from friends of friends with '\(domain)' NIP-05 domain with sub_id \(sub_id)")
state.nostrNetwork.pool.register_handler(sub_id: sub_id, handler: handle_event)
loading = true
state.nostrNetwork.pool.send(.subscribe(.init(filters: [filter], sub_id: sub_id)))
}
func unsubscribe() {
state.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
loading = false
print("unsubscribing from notes from friends of friends with '\(domain)' NIP-05 domain with sub_id \(sub_id)")
}
func add_event(_ ev: NostrEvent) {
if !event_matches_filter(ev, filter: filter) {
return
}
guard should_show_event(state: state, ev: ev) else {
return
}
if self.events.insert(ev) {
objectWillChange.send()
}
}
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
let (sub_id, done) = handle_subid_event(pool: state.nostrNetwork.pool, relay_id: relay_id, ev: ev) { sub_id, ev in
if sub_id == self.sub_id && ev.is_textlike && ev.should_show_event {
self.add_event(ev)
}
}
guard done else {
return
}
self.loading = false
if sub_id == self.sub_id {
guard let txn = NdbTxn(ndb: state.ndb) else { return }
load_profiles(context: "search", profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(self.events.all_events), damus_state: state, txn: txn)
}
}
}

View File

@@ -35,6 +35,7 @@ class Profiles {
@MainActor
private var profiles: [Pubkey: ProfileData] = [:]
// Map of validated NIP-05 address to pubkey.
@MainActor
var nip05_pubkey: [String: Pubkey] = [:]

View File

@@ -106,7 +106,8 @@ var test_damus_state: DamusState = ({
video: .init(),
ndb: ndb,
quote_reposts: .init(our_pubkey: our_pubkey),
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
emoji_provider: DefaultEmojiProvider(showAllVariations: true),
favicon_cache: .init()
)
/*

View File

@@ -29,15 +29,15 @@ extension KFOptionSetter {
options.onlyLoadFirstFrame = disable_animation
switch imageContext {
case .pfp:
options.diskCacheExpiration = .days(60)
break
case .banner:
options.diskCacheExpiration = .days(5)
break
case .note:
options.diskCacheExpiration = .days(1)
break
case .pfp, .favicon:
options.diskCacheExpiration = .days(60)
break
case .banner:
options.diskCacheExpiration = .days(5)
break
case .note:
options.diskCacheExpiration = .days(1)
break
}
return self
@@ -82,11 +82,14 @@ enum ImageContext {
case pfp
case banner
case note
case favicon
func maxMebibyteSize() -> Int {
switch self {
case .favicon:
return 512_000 // 500KiB
case .pfp:
return 5_242_880 // 5Mib
return 5_242_880 // 5MiB
case .banner, .note:
return 20_971_520 // 20MiB
}
@@ -94,6 +97,8 @@ enum ImageContext {
func downsampleSize() -> CGSize {
switch self {
case .favicon:
return CGSize(width: 18, height: 18)
case .pfp:
return CGSize(width: 200, height: 200)
case .banner:

View File

@@ -0,0 +1,41 @@
//
// FaviconCache.swift
// damus
//
// Created by Terry Yiu on 5/23/25.
//
import Foundation
import FaviconFinder
class FaviconCache {
private var nip05DomainFavicons: [String: [FaviconURL]] = [:]
@MainActor
func lookup(_ domain: String) async -> [FaviconURL] {
let lowercasedDomain = domain.lowercased()
if let faviconURLs = nip05DomainFavicons[lowercasedDomain] {
return faviconURLs
}
guard let siteURL = URL(string: "https://\(lowercasedDomain)"),
let faviconURLs = try? await FaviconFinder(
url: siteURL,
configuration: .init(
preferredSource: .ico, // Prefer using common favicon .ico filenames at root level to avoid scraping HTML when possible.
preferences: [
.html: FaviconFormatType.appleTouchIcon.rawValue,
.ico: "favicon.ico",
.webApplicationManifestFile: FaviconFormatType.launcherIcon4x.rawValue
]
)
).fetchFaviconURLs()
else {
return []
}
nip05DomainFavicons[lowercasedDomain] = faviconURLs
return faviconURLs
}
}

View File

@@ -5,6 +5,7 @@
// Created by Scott Penrose on 5/7/23.
//
import FaviconFinder
import SwiftUI
enum Route: Hashable {
@@ -46,6 +47,8 @@ enum Route: Hashable {
case Wallet(wallet: WalletModel)
case WalletScanner(result: Binding<WalletScanResult>)
case FollowersYouKnow(friendedFollowers: [Pubkey], followers: FollowersModel)
case NIP05DomainEvents(events: NIP05DomainEventsModel, nip05_domain_favicon: FaviconURL?)
case NIP05DomainPubkeys(domain: String, nip05_domain_favicon: FaviconURL?, pubkeys: [Pubkey])
@ViewBuilder
func view(navigationCoordinator: NavigationCoordinator, damusState: DamusState) -> some View {
@@ -127,6 +130,10 @@ enum Route: Hashable {
FollowersYouKnowView(damus_state: damusState, friended_followers: friendedFollowers, followers: followers)
case .Script(let load_model):
LoadScript(pool: damusState.nostrNetwork.pool, model: load_model)
case .NIP05DomainEvents(let events, let nip05_domain_favicon):
NIP05DomainTimelineView(damus_state: damusState, model: events, nip05_domain_favicon: nip05_domain_favicon)
case .NIP05DomainPubkeys(let domain, let nip05_domain_favicon, let pubkeys):
NIP05DomainPubkeysView(damus_state: damusState, domain: domain, nip05_domain_favicon: nip05_domain_favicon, pubkeys: pubkeys)
}
}
@@ -231,6 +238,12 @@ enum Route: Hashable {
case .Script(let model):
hasher.combine("script")
hasher.combine(model.data.count)
case .NIP05DomainEvents(let events, _):
hasher.combine("nip05DomainEvents")
hasher.combine(events.domain)
case .NIP05DomainPubkeys(let domain, _, _):
hasher.combine("nip05DomainPubkeys")
hasher.combine(domain)
}
}
}

View File

@@ -0,0 +1,51 @@
//
// NIP05DomainPubkeysView.swift
// damus
//
// Created by Terry Yiu on 5/23/25.
//
import FaviconFinder
import Kingfisher
import SwiftUI
struct NIP05DomainPubkeysView: View {
let damus_state: DamusState
let domain: String
let nip05_domain_favicon: FaviconURL?
let pubkeys: [Pubkey]
var body: some View {
ScrollView {
LazyVStack(alignment: .leading) {
ForEach(pubkeys, id: \.self) { pk in
FollowUserView(target: .pubkey(pk), damus_state: damus_state)
}
}
.padding(.horizontal)
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
HStack {
if let nip05_domain_favicon {
KFImage(nip05_domain_favicon.source)
.imageContext(.favicon, disable_animation: true)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 18, height: 18)
.clipped()
}
Text(domain)
.font(.headline)
}
}
}
}
}
#Preview {
let nip05_domain_favicon = FaviconURL(source: URL(string: "https://damus.io/favicon.ico")!, format: .ico, sourceType: .ico)
let pubkeys = [test_pubkey, test_pubkey_2]
NIP05DomainPubkeysView(damus_state: test_damus_state, domain: "damus.io", nip05_domain_favicon: nip05_domain_favicon, pubkeys: pubkeys)
}

View File

@@ -0,0 +1,111 @@
//
// NIP05DomainTimelineHeaderView.swift
// damus
//
// Created by Terry Yiu on 5/16/25.
//
import FaviconFinder
import Kingfisher
import SwiftUI
struct NIP05DomainTimelineHeaderView: View {
let damus_state: DamusState
@ObservedObject var model: NIP05DomainEventsModel
let nip05_domain_favicon: FaviconURL?
@Environment(\.openURL) var openURL
var Icon: some View {
ZStack {
if let nip05_domain_favicon {
KFImage(nip05_domain_favicon.source)
.imageContext(.favicon, disable_animation: true)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 18, height: 18)
.clipped()
} else {
EmptyView()
}
}
}
var friendsOfFriends: [Pubkey] {
// Order it such that the pubkeys that have events come first in the array so that their profile pictures
// show first.
let pubkeys = model.events.all_events.map { $0.pubkey } + (model.filter.authors ?? [])
// Filter out duplicates but retain order, and filter out any that do not have a validated NIP-05.
return (NSMutableOrderedSet(array: pubkeys).array as? [Pubkey] ?? [])
.filter {
damus_state.contacts.is_in_friendosphere($0) && damus_state.profiles.is_validated($0) != nil
}
}
var body: some View {
VStack(alignment: .leading) {
HStack {
if nip05_domain_favicon != nil {
Icon
}
Text(model.domain)
.foregroundStyle(DamusLogoGradient.gradient)
.font(.title.bold())
.onTapGesture {
if let url = URL(string: "https://\(model.domain)") {
openURL(url)
}
}
}
let friendsOfFriends = friendsOfFriends
HStack {
CondensedProfilePicturesView(state: damus_state, pubkeys: friendsOfFriends, maxPictures: 3)
let friendsOfFriendsString = friendsOfFriendsString(friendsOfFriends, ndb: damus_state.ndb)
Text(friendsOfFriendsString)
.font(.subheadline)
.foregroundColor(.gray)
.multilineTextAlignment(.leading)
}
.onTapGesture {
if !friendsOfFriends.isEmpty {
damus_state.nav.push(route: Route.NIP05DomainPubkeys(domain: model.domain, nip05_domain_favicon: nip05_domain_favicon, pubkeys: friendsOfFriends))
}
}
}
}
}
func friendsOfFriendsString(_ friendsOfFriends: [Pubkey], ndb: Ndb, locale: Locale = Locale.current) -> String {
let bundle = bundleForLocale(locale: locale)
let names: [String] = friendsOfFriends.prefix(3).map { pk in
let profile = ndb.lookup_profile(pk)?.unsafeUnownedValue?.profile
return Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 20)
}
switch friendsOfFriends.count {
case 0:
return "No one in your trusted network is associated with this domain."
case 1:
let format = NSLocalizedString("Notes from %@", bundle: bundle, comment: "Text to indicate that notes from one pubkey in our trusted network are shown below.")
return String(format: format, locale: locale, names[0])
case 2:
let format = NSLocalizedString("Notes from %@ & %@", bundle: bundle, comment: "Text to indicate that notes from two pubkeys in our trusted network are shown below.")
return String(format: format, locale: locale, names[0], names[1])
case 3:
let format = NSLocalizedString("Notes from %@, %@ & %@", bundle: bundle, comment: "Text to indicate that notes from three pubkeys in our trusted network are shown below.")
return String(format: format, locale: locale, names[0], names[1], names[2])
default:
let format = localizedStringFormat(key: "notes_from_three_and_others", locale: locale)
return String(format: format, locale: locale, friendsOfFriends.count - 3, names[0], names[1], names[2])
}
}
#Preview {
let model = NIP05DomainEventsModel(state: test_damus_state, domain: "damus.io")
let nip05_domain_favicon = FaviconURL(source: URL(string: "https://damus.io/favicon.ico")!, format: .ico, sourceType: .ico)
NIP05DomainTimelineHeaderView(damus_state: test_damus_state, model: model, nip05_domain_favicon: nip05_domain_favicon)
}

View File

@@ -0,0 +1,64 @@
//
// NIP05DomainTimelineView.swift
// damus
//
// Created by Terry Yiu on 4/11/25.
//
import FaviconFinder
import Kingfisher
import SwiftUI
struct NIP05DomainTimelineView: View {
let damus_state: DamusState
@ObservedObject var model: NIP05DomainEventsModel
let nip05_domain_favicon: FaviconURL?
func nip05_filter(ev: NostrEvent) -> Bool {
damus_state.contacts.is_in_friendosphere(ev.pubkey) && damus_state.profiles.is_validated(ev.pubkey) != nil
}
var contentFilters: ContentFilters {
var filters = Array<(NostrEvent) -> Bool>()
filters.append(contentsOf: ContentFilters.defaults(damus_state: damus_state))
filters.append(nip05_filter)
return ContentFilters(filters: filters)
}
var body: some View {
let height: CGFloat = 250.0
TimelineView(events: model.events, loading: $model.loading, damus: damus_state, show_friend_icon: true, filter: contentFilters.filter(ev:)) {
ZStack(alignment: .leading) {
DamusBackground(maxHeight: height)
.mask(LinearGradient(gradient: Gradient(colors: [.black, .black, .black, .clear]), startPoint: .top, endPoint: .bottom))
NIP05DomainTimelineHeaderView(damus_state: damus_state, model: model, nip05_domain_favicon: nip05_domain_favicon)
.padding(.leading, 30)
.padding(.top, 30)
}
}
.ignoresSafeArea()
.padding(.bottom, tabHeight)
.onAppear {
guard model.events.all_events.isEmpty else { return }
model.subscribe()
if let pubkeys = model.filter.authors {
for pubkey in pubkeys {
check_nip05_validity(pubkey: pubkey, profiles: damus_state.profiles)
}
}
}
.onDisappear {
model.unsubscribe()
}
}
}
#Preview {
let damus_state = test_damus_state
let model = NIP05DomainEventsModel(state: damus_state, domain: "damus.io")
let nip05_domain_favicon = FaviconURL(source: URL(string: "https://damus.io/favicon.ico")!, format: .ico, sourceType: .ico)
NIP05DomainTimelineView(damus_state: test_damus_state, model: model, nip05_domain_favicon: nip05_domain_favicon)
}

View File

@@ -79,6 +79,7 @@ struct PostView: View {
var autoSaveModel: AutoSaveIndicatorView.AutoSaveViewModel
@State var preUploadedMedia: [PreUploadedMedia] = []
@State var mediaUploadUnderProgress: MediaUpload? = nil
@StateObject var image_upload: ImageUploadModel = ImageUploadModel()
@StateObject var tagModel: TagModel = TagModel()
@@ -330,11 +331,6 @@ struct PostView: View {
PostButton
}
if let progress = image_upload.progress {
ProgressView(value: progress, total: 1.0)
.progressViewStyle(.linear)
}
Divider()
.foregroundColor(DamusColors.neutral3)
.padding(.top, 5)
@@ -346,6 +342,7 @@ struct PostView: View {
@discardableResult
func handle_upload(media: MediaUpload) async -> Bool {
mediaUploadUnderProgress = media
let uploader = damus_state.settings.default_media_uploader
let img = getImage(media: media)
@@ -354,6 +351,7 @@ struct PostView: View {
async let blurhash = calculate_blurhash(img: img)
let res = await image_upload.start(media: media, uploader: uploader, mediaType: .normal, keypair: damus_state.keypair)
mediaUploadUnderProgress = nil
switch res {
case .success(let url):
guard let url = URL(string: url) else {
@@ -401,10 +399,13 @@ struct PostView: View {
}
.id("post")
PVImageCarouselView(media: $uploadedMedias, deviceWidth: deviceSize.size.width)
.onChange(of: uploadedMedias) { media in
post_changed(post: post, media: media)
}
PVImageCarouselView(media: $uploadedMedias,
mediaUnderProgress: $mediaUploadUnderProgress,
imageUploadModel: image_upload,
deviceWidth: deviceSize.size.width)
.onChange(of: uploadedMedias) { media in
post_changed(post: post, media: media)
}
if case .quoting(let ev) = action {
BuilderEventView(damus: damus_state, event: ev)
@@ -620,6 +621,8 @@ struct PostView_Previews: PreviewProvider {
struct PVImageCarouselView: View {
@Binding var media: [UploadedMedia]
@Binding var mediaUnderProgress: MediaUpload?
@ObservedObject var imageUploadModel: ImageUploadModel
let deviceWidth: CGFloat
@@ -667,6 +670,25 @@ struct PVImageCarouselView: View {
.padding(.bottom, 35)
}
}
if let mediaUP = mediaUnderProgress, let progress = imageUploadModel.progress {
ZStack {
// Media under upload-progress
Image(uiImage: getImage(media: mediaUP))
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: media.count == 0 ? deviceWidth * 0.8 : 250, height: media.count == 0 ? 400 : 250)
.cornerRadius(10)
.opacity(0.3)
.padding()
// Circle showing progress on top of media
Circle()
.trim(from: 0, to: CGFloat(progress))
.stroke(Color.damusPurple, lineWidth: 5.0)
.rotationEffect(.degrees(-90))
.frame(width: 30, height: 30)
.padding()
}
}
}
.padding()
}

View File

@@ -5,6 +5,7 @@
// Created by William Casarin on 2022-04-16.
//
import FaviconFinder
import SwiftUI
enum FriendType {
@@ -43,6 +44,7 @@ struct ProfileName: View {
@State var nip05: NIP05?
@State var donation: Int?
@State var purple_account: DamusPurple.Account?
@State var nip05_domain_favicon: FaviconURL?
init(pubkey: Pubkey, prefix: String = "", damus: DamusState, show_nip5_domain: Bool = true, supporterBadgeStyle: SupporterBadge.Style = .compact) {
self.pubkey = pubkey
@@ -61,7 +63,7 @@ struct ProfileName: View {
var current_nip05: NIP05? {
nip05 ?? damus_state.profiles.is_validated(pubkey)
}
func current_display_name(profile: Profile?) -> DisplayName {
return display_name ?? Profile.displayName(profile: profile, pubkey: pubkey)
}
@@ -101,7 +103,7 @@ struct ProfileName: View {
.fontWeight(prefix == "@" ? .none : .bold)
if let nip05 = current_nip05 {
NIP05Badge(nip05: nip05, pubkey: pubkey, contacts: damus_state.contacts, show_domain: show_nip5_domain, profiles: damus_state.profiles)
NIP05Badge(nip05: nip05, pubkey: pubkey, damus_state: damus_state, show_domain: show_nip5_domain, nip05_domain_favicon: nip05_domain_favicon)
}
if let friend = friend_type, current_nip05 == nil {
@@ -118,9 +120,15 @@ struct ProfileName: View {
}
.task {
if damus_state.purple.enable_purple {
self.purple_account = try? await damus_state.purple.get_maybe_cached_account(pubkey: pubkey)
}
if damus_state.purple.enable_purple {
self.purple_account = try? await damus_state.purple.get_maybe_cached_account(pubkey: pubkey)
}
}
.task {
if let domain = current_nip05?.host {
self.nip05_domain_favicon = try? await damus_state.favicon_cache.lookup(domain)
.largest()
}
}
.onReceive(handle_notify(.profile_updated)) { update in
if update.pubkey != pubkey {
@@ -151,6 +159,24 @@ struct ProfileName: View {
let nip05 = damus_state.profiles.is_validated(pubkey)
if nip05 != self.nip05 {
self.nip05 = nip05
if let domain = nip05?.host {
Task {
let favicon = try? await damus_state.favicon_cache.lookup(domain)
.filter {
if let size = $0.size {
return size.width <= 128 && size.height <= 128
} else {
return true
}
}
.largest()
await MainActor.run {
self.nip05_domain_favicon = favicon
}
}
}
}
if donation != profile.damus_donation {

View File

@@ -82,6 +82,22 @@
<string>Imports</string>
</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>Notes from %2$@, %3$@, %4$@ &amp; %1$d other in your trusted network</string>
<key>other</key>
<string>Notes from %2$@, %3$@, %4$@ &amp; %1$d others in your trusted network</string>
</dict>
</dict>
<key>people_reposted_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>

View File

@@ -49,7 +49,8 @@ func generate_test_damus_state(
video: .init(),
ndb: ndb,
quote_reposts: .init(our_pubkey: our_pubkey),
emoji_provider: DefaultEmojiProvider(showAllVariations: false)
emoji_provider: DefaultEmojiProvider(showAllVariations: false),
favicon_cache: .init()
)
return damus

View File

@@ -0,0 +1,39 @@
//
// NIP05DomainTimelineHeaderViewTests.swift
// damusTests
//
// Created by Terry Yiu on 5/23/25.
//
import XCTest
@testable import damus
final class NIP05DomainTimelineHeaderViewTests: XCTestCase {
let enUsLocale = Locale(identifier: "en-US")
func testFriendsOfFriendsString() throws {
let pk1 = test_pubkey
let pk2 = test_pubkey_2
let pk3 = Pubkey(hex: "b42e44b555013239a0d5dcdb09ebde0857cd8a5a57efbba5a2b6ac78833cb9f0")!
let pk4 = Pubkey(hex: "cc590e46363d0fa66bb27081368d01f169b8ffc7c614629d4e9eef6c88b38670")!
let pk5 = Pubkey(hex: "f2aa579bb998627e04a8f553842a09446360c9d708c6141dd119c479f6ab9d29")!
let ndb = Ndb(path: Ndb.db_path)!
let damus_name = "17ldvg64:nq5mhr77"
XCTAssertEqual(friendsOfFriendsString([pk1], ndb: ndb, locale: enUsLocale), "Notes from \(damus_name)")
XCTAssertEqual(friendsOfFriendsString([pk1, pk2], ndb: ndb, locale: enUsLocale), "Notes from \(damus_name) & 1rppft3m:4qxhsgnj")
XCTAssertEqual(friendsOfFriendsString([pk1, pk2, pk3], ndb: ndb, locale: enUsLocale), "Notes from \(damus_name), 1rppft3m:4qxhsgnj & 1kshyfd2:cq04aze0")
XCTAssertEqual(friendsOfFriendsString([pk1, pk2, pk3, pk4,], ndb: ndb, locale: enUsLocale), "Notes from \(damus_name), 1rppft3m:4qxhsgnj, 1kshyfd2:cq04aze0 & 1 other in your trusted network")
XCTAssertEqual(friendsOfFriendsString([pk1, pk2, pk3, pk4, pk5], ndb: ndb, locale: enUsLocale), "Notes from \(damus_name), 1rppft3m:4qxhsgnj, 1kshyfd2:cq04aze0 & 2 others in your trusted network")
let pubkeys = [pk1, pk2, pk3, pk4, pk5, pk1, pk2, pk3, pk4, pk5]
Bundle.main.localizations.map { Locale(identifier: $0) }.forEach {
for count in 1...10 {
XCTAssertNoThrow(friendsOfFriendsString(pubkeys.prefix(count).map { $0 }, ndb: ndb, locale: $0))
}
}
}
}