Compare commits

..

1 Commits

Author SHA1 Message Date
tyiu a72f964111 Fix localized string for privacy access description for photos 2023-01-21 16:01:31 -05:00
375 changed files with 6929 additions and 28293 deletions
+8 -22
View File
@@ -1,6 +1,4 @@
name: Run Test Suite
run-name: Testing ${{ github.ref }} by @${{ github.actor }}
name: Test
on:
push:
branches:
@@ -8,24 +6,12 @@ on:
pull_request:
branches:
- "*"
jobs:
run_tests:
runs-on: macos-12
strategy:
matrix:
include:
- xcode: "14.2"
ios: "16.2"
name: Test iOS (${{ matrix.ios }})
test:
name: Run Tests
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Select Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ${{ matrix.xcode }}
- name: Run Tests
run: xcodebuild test -scheme damus -project damus.xcodeproj -destination 'platform=iOS Simulator,name=iPhone 14,OS=${{ matrix.ios }}' | xcpretty && exit ${PIPESTATUS[0]}
- name: Checkout repository
uses: actions/checkout@v1
- name: Running Tests
run: xcodebuild test -scheme damus -project damus.xcodeproj -destination 'platform=iOS Simulator,name=iPhone 14,OS=16.0' | xcpretty && exit ${PIPESTATUS[0]}
-2
View File
@@ -1,2 +0,0 @@
translations/
*.lproj/
+2 -506
View File
@@ -1,515 +1,10 @@
## [1.4.1-3] - 2023-04-05
### Added
- Added text truncation settings (William Casarin)
### Changed
- Rename block to mute (William Casarin)
### Fixed
- Reduce chopping of images (mainvolume)
- Fix some notification settings not saving (William Casarin)
- Fix broken camera uploads (again) (Joel Klabo)
[1.4.1-3]: https://github.com/damus-io/damus/releases/tag/v1.4.1-3
## [1.4.1-2] - 2023-04-04
### Added
- Reply counts (William Casarin)
- Add option to only show notification from people you follow (Swift)
- Added local notifications for other events (Swift)
- Show a custom view when tagged user isn't found (ericholguin)
- Show referenced notes in DMs (William Casarin)
### Changed
- Show full bleed images on selected events in threads (William Casarin)
- Improvement to square image displaying (mainvolume)
### Fixed
- Fix broken website links that have missing https:// prefixes (William Casarin)
- Get around CCP bootstrap relay banning by caching user's relays as their bootstrap relays (William Casarin)
[1.4.1-2]: https://github.com/damus-io/damus/releases/tag/v1.4.1-2
## [1.4.1] - 2023-04-03
### Added
- Profile Picture Upload (Joel Klabo)
- Enable offline posting (William Casarin)
- Add auto-translation caching to ruduce api usage (Terry Yiu)
- Added support for gif uploads (Swift)
- Add a Divider in the Follows List for Large Screens (Joel Klabo)
- Upload Photos and Videos from Camera (Joel Klabo)
- Added ability to lookup users by nip05 identifiers (William Casarin)
### Changed
- Only truncate timeline text if enabled in settings (William Casarin)
- Make mentions wide in notifications like in timeline (William Casarin)
- Broadcast events you are replying to (William Casarin)
- Broadcast now also broadcasts event user's profile (William Casarin)
- Improved look of reply view (ericholguin)
- Remove gradient in some places for visibility (ericholguin)
### Fixed
- Fix cropped images (mainvolume)
- Truncate long text in notification items (William Casarin)
- Restore missing reply description on selected events (William Casarin)
- Show sent DMs immediately (William Casarin)
- Fixed size of translated text (William Casarin)
- Fix crash when reposting (William Casarin)
- Fix unclickable image dismiss button (OlegAba)
[1.4.1]: https://github.com/damus-io/damus/releases/tag/v1.4.1
## [1.4.0] - 2023-03-27
### Added
- Local zap notifications (Swift)
- Add support for video uploads (Swift)
- Auto Translation (Terry Yiu)
- Portuguese (Brazil) translations (Andressa Munturo)
- Spanish (Spain) translations (Max Pleb)
- Vietnamese translations (ShiryoRyo)
### Fixed
- Fixed small notification hit boxes (Terry Yiu)
[1.4.0]: https://github.com/damus-io/damus/releases/tag/v1.4.0
## [1.3.0-7] - 2023-03-24
- New experimental timeline view
[1.3.0-7]: https://github.com/damus-io/damus/releases/tag/v1.3.0-7
## [1.3.0-6] - 2023-03-21
### Fixed
- Fix bug where nostr: links and QRs stopped working (William Casarin)
[1.3.0-6]: https://github.com/damus-io/damus/releases/tag/v1.3.0-6
## [1.3.0-5] - 2023-03-20
### Added
- Add Time Ago to DM View (Joel Klabo)
### Fixed
- Fixed internal links opening in other nostr clients (William Casarin)
- Remove authentication for copying npub (Swift)
[1.3.0-5]: https://github.com/damus-io/damus/releases/tag/v1.3.0-5
## [1.3.0-4] - 2023-03-17
### Changed
- It's much easier to tag users in replies and posts (William Casarin)
### Fixed
- Fix bug where small black text appears during image upload (William Casarin)
[1.3.0-4]: https://github.com/damus-io/damus/releases/tag/v1.3.0-4
## [1.3.0-3] - 2023-03-17
### Fixed
- Fix image upload url delay after progress bar disappears (William Casarin)
- Fix issue where damus stops trying to reconnect (William Casarin)
[1.3.0-3]: https://github.com/damus-io/damus/releases/tag/v1.3.0-3
## [1.3.0-2] - 2023-03-16
### Added
- Add image uploader (Swift)
- Add option to always show images (never blur) (William Casarin)
- Canadian French (Pierre - synoptic_okubo)
- Hungarian translations (Zoltan)
- Korean translations (sogoagain)
- Swedish translations (Pextar)
### Changed
- Fixed embedded note popping (William Casarin)
- Bump notification limit from 100 to 500 (William Casarin)
### Fixed
- Fix zap button preventing scrolling (William Casarin)
[1.3.0-2]: https://github.com/damus-io/damus/releases/tag/v1.3.0-2
## [1.3.0] - 2023-03-15
### Added
- Extend user tagging search to all local profiles (William Casarin)
- Vibrate when a zap is received (Swift)
- New and Improved Share sheet (ericholguin)
- Bulgarian translations (elsat)
- Persian translations (Mahdi Taghizadeh)
- Ukrainian translations (Valeriia Khudiakova, Tony B)
### Changed
- Reduce battery usage by using exp backoff on connections (Bryan Montz)
- Don't show both realname and username if they are the same (William Casarin)
- Show error on invalid lightning tip address (Swift)
- Make DM Content More Visible (Joel Klabo)
- Remove spaces from hashtag searches (gladiusKatana)
### Fixed
- Show @ mentions for users with display_names and no username (William Casarin)
- Make user search case insensitive (William Casarin)
- Fix repost button sometimes not working (OlegAba)
- Don't show follows you for your own profile (benthecarman)
- Fix json appearing in profile searches (gladiusKatana)
- Fix unexpected font size when posting (Bryan Montz)
- Fix keyboard sticking issues (OlegAba)
- Fixed tab bar background color on macOS (Joel Klabo)
- Fix some links getting interpreted as images (gladiusKatana)
[1.3.0]: https://github.com/damus-io/damus/releases/tag/v1.3.0
## [1.2.0-4] - 2023-03-05
### Added
- Add ellipsis button to notes (ericholguin)
### Changed
- Immediately search for events and profiles (William Casarin)
- Use long-press for custom zaps (William Casarin)
- Make shaka animation smoother (Swift)
### Fixed
- Fixed hit detection bugs on profile page (OlegAba)
- Fix disappearing text on Thread view (Bryan Montz)
- Render links in notification summaries (Joel Klabo)
- Don't show notifications from ourselves (William Casarin)
- Fix issue where navbar back button would show the wrong text (Jack Chakany)
- Fix case sensitivity when searching hashtags (randymcmillan)
- Fix issue where opening reposts shows json (William Casarin)
[1.2.0-4]: https://github.com/damus-io/damus/releases/tag/v1.2.0-4
## [1.2.0-3] - 2023-03-04
### Added
- Add additional info to recommended relay view (ericholguin)
- Add shaka animation (Swift)
- Add option to disable image animation (OlegAba)
- Add additional warning when deleting account (ericholguin)
- Threads now load instantly and are cached (William Casarin)
### Fixed
- Wrap long profile display names (OlegAba)
- Fixed weird scaling on profile pictures (OlegAba)
- Fixed width of copy pubkey on profile page (Joel Klabo)
- Make damus purple use more consistent in mentions (Joel Klabo)
[1.2.0-3]: https://github.com/damus-io/damus/releases/tag/v1.2.0-3
## [1.1.0-10] - 2023-03-01
### Added
- Truncate large posts and add a show more button (OlegAba)
- Private Zaps (William Casarin)
### Fixed
- Fix default zap amount setting not getting updated (William Casarin)
- Fix issue where keyboard covers custom zap comment (William Casarin)
[1.1.0-10]: https://github.com/damus-io/damus/releases/tag/v1.1.0-10
## [1.1.0-9] - 2023-02-26
### Added
- Customized zaps (William Casarin)
- Add new Notifications View (William Casarin)
- Bookmarking (Joel Klabo)
- Chinese, Traditional (Hong Kong) translations (rasputin)
- Chinese, Traditional (Taiwan) translations (rasputin)
### Changed
- No more inline npubs when tagging users (Swift)
### Fixed
- Fix alignment of side menu labels (Joel Klabo)
- Fix duplicated participants in reply-to view (Joel Klabo)
- Load missing profiles in Zaps view (William Casarin)
- Fix memory leak with inline videos (William Casarin)
- Eliminate popping when scrolling (William Casarin)
[1.1.0-9]: https://github.com/damus-io/damus/releases/tag/v1.1.0-9
## [1.1.0-3] - 2023-02-20
### Added
- Add a "load more" button instead of always inserting events in timelines (William Casarin)
- Added the ability to select text on posts (OlegAba)
- Added Posts or Post & Replies selector to Profile (ericholguin)
- Improved profile navbar (OlegAba)
- Czech translations (Martin Gabrhel)
- Indonesian translations (johnybergzy)
- Russian translations (Tony B)
### Changed
- Rename global feed to universe (William Casarin)
- Improve look of post view (ericholguin)
- Added a 20MB content length limit for all image files (OlegAba)
- Improved EventActionBar button spacing (Bryan Montz)
- Polished profile key copy buttons, added animation (Bryan Montz)
- Format large numbers of action bar actions (Joel Klabo)
- Improved blur on images, especially in dark mode (Bryan Montz)
### Fixed
- Remove trailing slash when adding a relay (middlingphys)
- Scroll to top of events instead of the bottom (OlegAba)
- Fix lag on startup when you have lots of DMs (William Casarin)
- Fix an issues where dm notifications appear without any new events (William Casarin)
- Fix some hangs when scrolling by images (OlegAba)
- Force default zap amount text field to accept only numbers (Terry Yiu)
[1.1.0-3]: https://github.com/damus-io/damus/releases/tag/v1.1.0-3
## [1.1.0-2] - 2023-02-14
### Added
- Save drafts to posts, replies and DMs (Terry Yiu)
### Fixed
- Ensure stats get updated in realtime on action bars (William Casarin)
- Fix reposts not getting counted properly (William Casarin)
- Fix a bug where zaps on other people's posts weren't showing (William Casarin)
- Fix punctuation getting included in some urls (Gert Goet)
- Improve language detection (Terry Yiu)
- Fix some animated image crashes (William Casarin)
[1.1.0-2]: https://github.com/damus-io/damus/releases/tag/v1.1.0-2
## [1.0.0-15] - 2023-02-10
### Added
- Relay Filtering (William Casarin)
- Add password autofill on account login and creation (Terry Yiu)
- Show if relay is paid (William Casarin)
- Add "Follows You" indicator on profile (William Casarin)
- Add screen to select individual relays when posting/broadcasting (Andrii Sievrikov)
- Relay Detail View (Joel Klabo)
- Warn when attempting to post an nsec key (Terry Yiu)
- DeepL translation integration (Terry Yiu)
- Use local authentication (faceid) to access private key (Andrii Sievrikov)
- Add accessibility labels to action bar (Bryan Montz)
- Copy invoice button (Joel Klabo)
- Receive Lightning Zaps (William Casarin)
- Allow text selection in bio (Suhail Saqan)
- Chinese, Simplified (China mainland) translations (haolong, rasputin)
- Dutch translations (Heimen Stoffels - Vistaus)
- Greek translations (milicode)
- Japanese translations (akiomik, foxytanuki, Guetsu Ren - Nighthaven, h3y6e, middlingphys)
### Changed
- Show "Follow Back" button on profile page (William Casarin)
- When on your profile page, open relay view instead for your own relays (Terry Yiu)
- Updated QR code view, include profile image, etc (ericholguin)
- Clicking relay numbers now goes to relay config (radixrat)
### Fixed
- Load zaps, likes and reposts when you open a thread (William Casarin)
- Fix bug where sidebar navigation fails to pop when switching timelines (William Casarin)
- Use lnaddress before lnurl for tip addresses to avoid Anigma scamming (William Casarin)
- Fix sidebar navigation bugs (OlegAba)
- Fix issue where navigation fails pop to root when switching timelines (William Casarin)
- Make @ mentions case insensitive (William Casarin)
- Fix some lnurls not getting decoded properly (William Casarin)
- Hide incoming DMs from blocked users (William Casarin)
- Hide blocked users from search results (William Casarin)
- Fix Cash App invoice payments (Rob Seward)
- DM Padding (OlegAba)
- Check for broken lnurls (William Casarin)
[1.0.0-15]: https://github.com/damus-io/damus/releases/tag/v1.0.0-15
## [1.0.0-13] - 2023-01-30
### Added
- LibreTranslate note translations (Terry Yiu)
- Added support for account deletion (William Casarin)
- User tagging and autocompletion in posts (Swift)
- Polish translations (pysiak)
### Changed
- Remove redundant logout button from settings (Jonathan Milligan)
- Moved relay config to its own sidebar entry (William Casarin)
- New stylized tabs (ericholguin)
### Fixed
- Fix hidden profile action sheet when clicking ... (William Casarin)
- Fixed height of DM input (Terry Yiu)
- Fixed bug where copying pubkey from context menu only copied your own pubkey (Terry Yiu)
[1.0.0-13]: https://github.com/damus-io/damus/releases/tag/v1.0.0-13
## [1.0.0-12] - 2023-01-28
### Added
- Arabic translations (Barodane)
- Portuguese translations (Antonio Chagas)
- Add QRCode view for sharing your pubkey (ericholguin)
- Added nostr: uri handling (William Casarin)
### Changed
- Remove markdown link support from posts (Joel Klabo)
### Fixed
- Fixed crash on some SVG profile pictures (OlegAba)
- Localization fixes
- Don't allow blocking yourself (Terry)
- Hide muted users from global (William Casarin)
- Fixed profiles sometimes not loading from other clients (William Casarin)
- Fixed bug where `spam` was always the report type (William Casarin)
[1.0.0-12]: https://github.com/damus-io/damus/releases/tag/v1.0.0-12
## [1.0.0-11] - 2023-01-25
### Added
- Reposts view (Terry Yiu)
- Italian translations (Nicolò Carcagnì)
- Latvian translations (SYX)
- Added ability to block users (William Casarin)
- Added a way to report content (William Casarin)
- Stretchable profile cover header (Swift)
### Changed
- Bump pfp/banner animated fize size limit to 5MiB/20MiB (William Casarin)
- Updated default boostrap relays (Ricardo Arturo Cabral Mejía)
### Fixed
- allow ws:// relays again (Steven Briscoe)
[1.0.0-11]: https://github.com/damus-io/damus/releases/tag/v1.0.0-11
## [1.0.0-8] - 2023-01-22
### Added
- Show website on profiles (William Casarin)
- Add the ability to choose participants when replying (Joel Klabo)
- German translations (Gregor, Peter Gerstbach)
- Turkish translations (Taylan Benli)
- French (France) translations (Solobalbo)
- Add DM Message Requests (William Casarin)
### Fixed
- Fix commands and emojis getting included in hashtags (William Casarin)
- Fix duplicate post buttons when swiping tabs (Thomas Rademaker)
- Show embedded note references (William Casarin)
[1.0.0-8]: https://github.com/damus-io/damus/releases/tag/v1.0.0-8
## [1.0.0-7] - 2023-01-20
### Added
- Drastically improved image viewer (OlegAba)
- Added pinch to zoom on images (Swift)
- Add Latin American Spanish translations (Nicolás Valencia)
- Add Latin American Spanish translations (William Casarin)
- Added SVG profile picture support (OlegAba)
@@ -922,3 +417,4 @@
[0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2
+6 -19
View File
@@ -1,4 +1,3 @@
[![Run Test Suite](https://github.com/damus-io/damus/actions/workflows/run-tests.yaml/badge.svg?branch=master)](https://github.com/damus-io/damus/actions/workflows/run-tests.yaml)
# damus
@@ -26,7 +25,7 @@ damus implements the following [Nostr Implementation Possibilities][nips]
## Getting Started on Damus
### Damus iOS
1) Get the Damus app on the iOS App Store: https://apps.apple.com/ca/app/damus/id1628663131
1) Get the Damus app on TestFlight: https://testflight.apple.com/join/CLwjLxWl
#### ⚙️ Settings (gear icon, top right)
- Relays: You can add more relays to send your notes to by tapping the "+".
@@ -49,7 +48,7 @@ damus implements the following [Nostr Implementation Possibilities][nips]
4. Add @ direcly followed by the pubkey (e.g., `@npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s`)
- You can also long-press a Note to grab their User ID aka pubkey or Note ID to link directly to a Note.
- Currently you can't delete your Notes in the iOS app
- Share images by pasting the image url which you can grab from imgbb, imgur, etc. (i.e., `https://i.ibb.co/2SHZbwm/alpha60.jpg`). Currently images only load for people you follow in the 🏠 Personal Feed. Images are not automatically loaded in 🔍 Global Feed
- Share images by pasting the image url which you can grab from imgbb, imgur, etc. (i.e., `(https://i.ibb.co/2SHZbwm/alpha60.jpg)`). Currently images only load for people you follow in the 🏠 Personal Feed. Images are not automatically loaded in 🔍 Global Feed
- Engaging with Notes
- 💬 Replying to a Note: Tap the chat icon underneath the note. This will show up in the users notifications and in your 🏠 Personal and 🔍 Global Feeds
- ♺ Reposts: Tap the repost icon which will show up in your 🏠 Personal and 🔍 Global Feeds
@@ -92,23 +91,15 @@ damus implements the following [Nostr Implementation Possibilities][nips]
## Contributing
Contributors welcome!
### Code
[Email patches][git-send-email] to jb55@jb55.com are preferred, but I accept PRs on GitHub as well.
Contributors welcome! [Email patches][git-send-email] to jb55@jb55.com are preferred, but I accept PRs on github as well.
[git-send-email]: http://git-send-email.io
### Translations
## git log bot
Translators welcome! Join the [Transifex][transifex] project.
npub1fjtdwclt9lspjy8huu3qklr7eklp5uq90u6yh8mec290pqxraccqlufnas
All user-facing strings must have a comment in order to provide context to translators. If a SwiftUI component has a `comment` parameter, use that. Otherwise, wrap your string with `NSLocalizedString` with the `comment` field populated.
[transifex]: https://explore.transifex.com/damus/damus-ios/
### Awards
### Awards
There may be nostr badges awarded for contributors in the future... :)
@@ -116,7 +107,3 @@ First contributors:
1. @randymcmillan
2. @jcarucci27
### git log bot
npub1fjtdwclt9lspjy8huu3qklr7eklp5uq90u6yh8mec290pqxraccqlufnas
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,6 @@
/* Bundle display name */
"CFBundleDisplayName" = "Damus";
/* Bundle name */
"CFBundleName" = "damus";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "Granting Damus access to your photos allows you to save images.";
@@ -5,7 +5,7 @@
<key>collapsed_event_view_other_notes</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@NOTES@</string>
<string>··· %#@NOTES@ ···</string>
<key>NOTES</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
@@ -13,9 +13,9 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>... %d diğer not ...</string>
<string>%d other note</string>
<key>other</key>
<string>... %d diğer notlar ...</string>
<string>%d other notes</string>
</dict>
</dict>
<key>followers_count</key>
@@ -29,9 +29,9 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Takipçi</string>
<string>Follower</string>
<key>other</key>
<string>Takipçi</string>
<string>Followers</string>
</dict>
</dict>
<key>reactions_count</key>
@@ -45,9 +45,9 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Tepki</string>
<string>Reaction</string>
<key>other</key>
<string>Tepki</string>
<string>Reactions</string>
</dict>
</dict>
<key>relays_count</key>
@@ -61,41 +61,45 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Röle</string>
<string>Relay</string>
<key>other</key>
<string>Röle</string>
<string>Relays</string>
</dict>
</dict>
<key>replying_to_one_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@OTHERS@</string>
<string>Replying to %@%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string></string>
<key>one</key>
<string>%2$@ &amp; %1$d diğer'lara yanıt</string>
<string> &amp; %d other</string>
<key>other</key>
<string>%2$@ &amp; %1$d ve diğerlerine yanıt</string>
<string> &amp; %d others</string>
</dict>
</dict>
<key>replying_to_two_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@OTHERS@</string>
<string>Replying to %@, %@%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string></string>
<key>one</key>
<string>%2$@, %3$@ &amp; %1$d diğer'a yanıt</string>
<string> &amp; %d other</string>
<key>other</key>
<string>%2$@, %3$@ &amp; %1$d ve diğerlerine yanıt</string>
<string> &amp; %d others</string>
</dict>
</dict>
<key>reposts_count</key>
@@ -109,9 +113,9 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Yineleme</string>
<string>Repost</string>
<key>other</key>
<string>Yineleme</string>
<string>Reposts</string>
</dict>
</dict>
<key>sats_count</key>
@@ -130,20 +134,20 @@
<string>%2$@ sats</string>
</dict>
</dict>
<key>zaps_count</key>
<key>tips_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@ZAPS@</string>
<key>ZAPS</key>
<string>%#@TIPS@</string>
<key>TIPS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Zap</string>
<string>Tip</string>
<key>other</key>
<string>Zaps</string>
<string>Tips</string>
</dict>
</dict>
</dict>
@@ -3,10 +3,10 @@
"project" : "damus.xcodeproj",
"targetLocale" : "en-US",
"toolInfo" : {
"toolBuildNumber" : "14E222b",
"toolBuildNumber" : "14C18",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "14.3"
"toolVersion" : "14.2"
},
"version" : "1.0"
}
File diff suppressed because it is too large Load Diff
+2 -30
View File
@@ -22,14 +22,6 @@ static inline int is_whitespace(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
}
static inline int is_boundary(char c) {
return !isalnum(c);
}
static inline int is_invalid_url_ending(char c) {
return c == '!' || c == '?' || c == ')' || c == '.' || c == ',' || c == ';';
}
static void make_cursor(struct cursor *c, const u8 *content, size_t len)
{
c->start = content;
@@ -37,33 +29,16 @@ static void make_cursor(struct cursor *c, const u8 *content, size_t len)
c->p = content;
}
static int consume_until_boundary(struct cursor *cur) {
char c;
while (cur->p < cur->end) {
c = *cur->p;
if (is_boundary(c))
return 1;
cur->p++;
}
return 1;
}
static int consume_until_whitespace(struct cursor *cur, int or_end) {
char c;
bool consumedAtLeastOne = false;
while (cur->p < cur->end) {
c = *cur->p;
if (is_whitespace(c))
return consumedAtLeastOne;
return 1;
cur->p++;
consumedAtLeastOne = true;
}
return or_end;
@@ -170,7 +145,7 @@ static int parse_hashtag(struct cursor *cur, struct block *block) {
return 0;
}
consume_until_boundary(cur);
consume_until_whitespace(cur, 1);
block->type = BLOCK_HASHTAG;
block->block.str.start = (const char*)(start + 1);
@@ -225,9 +200,6 @@ static int parse_url(struct cursor *cur, struct block *block) {
return 0;
}
// strip any unwanted characters
while(is_invalid_url_ending(peek_char(cur, -1))) cur->p--;
block->type = BLOCK_URL;
block->block.str.start = (const char *)start;
block->block.str.end = (const char *)cur->p;
File diff suppressed because it is too large Load Diff
@@ -5,8 +5,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/onevcat/Kingfisher",
"state" : {
"revision" : "415b1d97fb38bda1e5a6b2dde63354720832110b",
"version" : "7.6.1"
"revision" : "017f94ccfdacabb1ae7f45b75b4217b24c06e6ac",
"version" : "7.4.0"
}
},
{
@@ -26,6 +26,14 @@
"version" : "4.0.4"
}
},
{
"identity" : "svgkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SVGKit/SVGKit",
"state" : {
"revision" : "e1f13e27b1e4c0ffe20e7d8d3984bf49c2a584d0"
}
},
{
"identity" : "vault",
"kind" : "remoteSourceControl",
@@ -1,98 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1420"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
BuildableName = "damus.app"
BlueprintName = "damus"
ReferencedContainer = "container:damus.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CE6DEF227F7A08200C66700"
BuildableName = "damusTests.xctest"
BlueprintName = "damusTests"
ReferencedContainer = "container:damus.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CE6DEFC27F7A08200C66700"
BuildableName = "damusUITests.xctest"
BlueprintName = "damusUITests"
ReferencedContainer = "container:damus.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
BuildableName = "damus.app"
BlueprintName = "damus"
ReferencedContainer = "container:damus.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
BuildableName = "damus.app"
BlueprintName = "damus"
ReferencedContainer = "container:damus.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -11,6 +11,24 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xC5",
"green" : "0x43",
"red" : "0xCC"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -1,38 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xF4",
"green" : "0xEE",
"red" : "0xEE"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x1E",
"green" : "0x1C",
"red" : "0x1C"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -11,6 +11,24 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x00",
"green" : "0x00",
"red" : "0x00"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,6 +11,24 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0x4D",
"red" : "0x4B"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,6 +11,24 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x1E",
"green" : "0x1C",
"red" : "0x1C"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,6 +11,24 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x4F",
"green" : "0xC3",
"red" : "0x66"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,6 +11,24 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xF4",
"green" : "0xEE",
"red" : "0xEE"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,6 +11,24 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x5F",
"green" : "0x5F",
"red" : "0x5F"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,6 +11,24 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xC5",
"green" : "0x43",
"red" : "0xCC"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,6 +11,24 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFF"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "bitcoin-logo.svg",
"filename" : "ic-copy.png",
"idiom" : "universal",
"scale" : "1x"
},
Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "ic-key.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

@@ -0,0 +1,52 @@
{
"images" : [
{
"filename" : "ic-message-black.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "ic-message-white 1.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "ic-nipverified.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 950 B

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "ic-qr.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 252 B

@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "profile-banner.jpeg",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
+10 -1
View File
@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "bbw.jpg",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20px" height="20px" viewBox="0 0 20 20" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(96.862745%,57.647059%,10.196078%);fill-opacity:1;" d="M 19.699219 12.417969 C 18.363281 17.777344 12.9375 21.035156 7.582031 19.699219 C 2.226562 18.363281 -1.035156 12.9375 0.300781 7.582031 C 1.636719 2.222656 7.0625 -1.035156 12.417969 0.300781 C 17.773438 1.632812 21.035156 7.0625 19.699219 12.417969 Z M 19.699219 12.417969 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 14.410156 8.574219 C 14.609375 7.246094 13.59375 6.53125 12.210938 6.050781 L 12.660156 4.25 L 11.5625 3.976562 L 11.125 5.730469 C 10.835938 5.660156 10.539062 5.589844 10.246094 5.523438 L 10.6875 3.757812 L 9.589844 3.484375 L 9.140625 5.285156 C 8.902344 5.230469 8.667969 5.179688 8.4375 5.121094 L 8.441406 5.117188 L 6.925781 4.738281 L 6.636719 5.910156 C 6.636719 5.910156 7.449219 6.097656 7.433594 6.109375 C 7.875 6.21875 7.957031 6.511719 7.941406 6.746094 L 7.429688 8.800781 C 7.460938 8.808594 7.5 8.820312 7.546875 8.835938 L 7.429688 8.808594 L 6.710938 11.683594 C 6.65625 11.820312 6.519531 12.023438 6.210938 11.945312 C 6.21875 11.960938 5.410156 11.746094 5.410156 11.746094 L 4.867188 13 L 6.296875 13.359375 C 6.5625 13.425781 6.820312 13.492188 7.078125 13.558594 L 6.621094 15.382812 L 7.71875 15.65625 L 8.167969 13.851562 C 8.46875 13.933594 8.757812 14.007812 9.042969 14.078125 L 8.59375 15.875 L 9.691406 16.148438 L 10.144531 14.328125 C 12.015625 14.683594 13.425781 14.539062 14.015625 12.847656 C 14.492188 11.484375 13.992188 10.699219 13.007812 10.1875 C 13.726562 10.019531 14.265625 9.550781 14.410156 8.574219 Z M 11.902344 12.089844 C 11.5625 13.453125 9.269531 12.71875 8.523438 12.53125 L 9.128906 10.117188 C 9.871094 10.300781 12.253906 10.667969 11.902344 12.089844 Z M 12.242188 8.554688 C 11.933594 9.796875 10.023438 9.164062 9.402344 9.011719 L 9.949219 6.820312 C 10.570312 6.976562 12.5625 7.261719 12.242188 8.554688 Z M 12.242188 8.554688 "/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

+10 -1
View File
@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "bitcoin-p2p.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
+10 -1
View File
@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "blixt-wallet.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
+10 -1
View File
@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "bluewallet.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
+10 -1
View File
@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "breez.jpg",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
+10 -1
View File
@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "cashapp.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
+10 -1
View File
@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "digital-nomad.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "encrypted-message.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "ic-lightning.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 458 B

+21
View File
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "ic-tick.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

+10 -1
View File
@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "lnlink.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
+10 -1
View File
@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "damus-nobg.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
+10 -1
View File
@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "muun.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
+10 -1
View File
@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "phoenix.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
+10 -1
View File
@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "river.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
+10 -1
View File
@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "strike.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
+10 -1
View File
@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "undercover.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "walletofsatoshi.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
+10 -1
View File
@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "zebedee.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
+10 -1
View File
@@ -2,7 +2,16 @@
"images" : [
{
"filename" : "zeus.png",
"idiom" : "universal"
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
-61
View File
@@ -1,61 +0,0 @@
//
// CustomPicker.swift
// damus
//
// Created by Eric Holguin on 1/22/23.
//
import SwiftUI
let RECTANGLE_GRADIENT = LinearGradient(gradient: Gradient(colors: [
DamusColors.purple,
DamusColors.blue
]), startPoint: .leading, endPoint: .trailing)
struct CustomPicker<SelectionValue: Hashable, Content: View>: View {
@Environment(\.colorScheme) var colorScheme
@Namespace var picker
@Binding var selection: SelectionValue
@ViewBuilder let content: Content
public var body: some View {
let contentMirror = Mirror(reflecting: content)
let blocksCount = Mirror(reflecting: contentMirror.descendant("value")!).children.count
HStack {
ForEach(0..<blocksCount, id: \.self) { index in
let tupleBlock = contentMirror.descendant("value", ".\(index)")
let text = Mirror(reflecting: tupleBlock!).descendant("content") as! Text
let tag = Mirror(reflecting: tupleBlock!).descendant("modifier", "value", "tagged") as! SelectionValue
Button {
withAnimation(.spring()) {
selection = tag
}
} label: {
text
.padding(EdgeInsets(top: 15, leading: 0, bottom: 10, trailing: 0))
.font(.system(size: 14, weight: .heavy))
}
.background(
Group {
if tag == selection {
Rectangle().fill(RECTANGLE_GRADIENT).frame(height: 2.5)
.matchedGeometryEffect(id: "selector", in: picker)
.cornerRadius(2.5)
}
},
alignment: .bottom
)
.frame(maxWidth: .infinity)
.accentColor(tag == selection ? textColor() : .gray)
}
}
.background(Color(UIColor.systemBackground))
}
func textColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white
}
}
-22
View File
@@ -1,22 +0,0 @@
//
// DamusColors.swift
// damus
//
// Created by William Casarin on 2023-03-27.
//
import Foundation
import SwiftUI
class DamusColors {
static let adaptableGrey = Color("DamusAdaptableGrey")
static let white = Color("DamusWhite")
static let black = Color("DamusBlack")
static let lightGrey = Color("DamusLightGrey")
static let mediumGrey = Color("DamusMediumGrey")
static let darkGrey = Color("DamusDarkGrey")
static let green = Color("DamusGreen")
static let purple = Color("DamusPurple")
static let blue = Color("DamusBlue")
}
-39
View File
@@ -1,39 +0,0 @@
//
// Highlight.swift
// damus
//
// Created by William Casarin on 2023-01-23.
//
import Foundation
import SwiftUI
enum Highlight {
case none
case main
case reply
case custom(Color, Float)
var is_main: Bool {
if case .main = self {
return true
}
return false
}
var is_none: Bool {
if case .none = self {
return true
}
return false
}
var is_replied_to: Bool {
switch self {
case .reply: return true
default: return false
}
}
}
-44
View File
@@ -1,44 +0,0 @@
//
// IconLabel.swift
// damus
//
// Created by William Casarin on 2023-04-05.
//
import SwiftUI
import UIKit
struct IconLabel: View {
let text: String
let img_name: String
let img_color: Color
init(_ text: String, img_name: String, color: Color) {
self.text = text
self.img_name = img_name
self.img_color = color
}
var body: some View {
HStack(spacing: 0) {
Image(systemName: img_name)
.foregroundColor(img_color)
.frame(width: 20)
.padding([.trailing], 20)
Text(text)
}
}}
struct IconLabel_Previews: PreviewProvider {
static var previews: some View {
Form {
Section {
IconLabel(NSLocalizedString("Keys", comment: "Settings section for managing keys"), img_name: "key.fill", color: .orange)
IconLabel(NSLocalizedString("Local Notifications", comment: "Section header for damus local notifications user configuration"), img_name: "bell.fill", color: .blue)
IconLabel(NSLocalizedString("Appearance", comment: "Section header for text and appearance settings"), img_name: "textformat", color: .red)
}
}
}
}
+253 -114
View File
@@ -31,43 +31,243 @@ struct ShareSheet: UIViewControllerRepresentable {
}
}
enum ImageShape {
case square
case landscape
case portrait
case unknown
struct ImageContextMenuModifier: ViewModifier {
let url: URL?
let image: UIImage?
@Binding var showShareSheet: Bool
func body(content: Content) -> some View {
return content.contextMenu {
Button {
UIPasteboard.general.url = url
} label: {
Label(NSLocalizedString("Copy Image URL", comment: "Context menu option to copy the URL of an image into clipboard."), systemImage: "doc.on.doc")
}
if let someImage = image {
Button {
UIPasteboard.general.image = someImage
} label: {
Label(NSLocalizedString("Copy Image", comment: "Context menu option to copy an image into clipboard."), systemImage: "photo.on.rectangle")
}
Button {
UIImageWriteToSavedPhotosAlbum(someImage, nil, nil, nil)
} label: {
Label(NSLocalizedString("Save Image", comment: "Context menu option to save an image."), systemImage: "square.and.arrow.down")
}
}
Button {
showShareSheet = true
} label: {
Label(NSLocalizedString("Share", comment: "Button to share an image."), systemImage: "square.and.arrow.up")
}
}
}
}
private struct ImageContainerView: View {
@ObservedObject var imageModel: KFImageModel
@State private var image: UIImage?
@State private var showShareSheet = false
init(url: URL?) {
self.imageModel = KFImageModel(
url: url,
fallbackUrl: nil,
maxByteSize: 2000000, // 2 MB
downsampleSize: CGSize(width: 400, height: 400)
)
}
private struct ImageHandler: ImageModifier {
@Binding var handler: UIImage?
func modify(_ image: UIImage) -> UIImage {
handler = image
return image
}
}
var body: some View {
KFAnimatedImage(imageModel.url)
.callbackQueue(.dispatch(.global(qos: .background)))
.processingQueue(.dispatch(.global(qos: .background)))
.cacheOriginalImage()
.configure { view in
view.framePreloadCount = 1
}
.scaleFactor(UIScreen.main.scale)
.loadDiskFileSynchronously()
.fade(duration: 0.1)
.imageModifier(ImageHandler(handler: $image))
.onFailure { _ in
imageModel.downloadFailed()
}
.id(imageModel.refreshID)
.clipped()
.modifier(ImageContextMenuModifier(url: imageModel.url, image: image, showShareSheet: $showShareSheet))
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: [imageModel.url])
}
// TODO: Update ImageCarousel with serializer and processor
// .serialize(by: imageModel.serializer)
// .setProcessor(imageModel.processor)
}
}
struct ZoomableScrollView<Content: View>: UIViewRepresentable {
private var content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
func makeUIView(context: Context) -> UIScrollView {
let scrollView = UIScrollView()
scrollView.delegate = context.coordinator
scrollView.maximumZoomScale = 20
scrollView.minimumZoomScale = 1
scrollView.bouncesZoom = true
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
let hostedView = context.coordinator.hostingController.view!
hostedView.translatesAutoresizingMaskIntoConstraints = true
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostedView.frame = scrollView.bounds
hostedView.backgroundColor = .clear
scrollView.addSubview(hostedView)
return scrollView
}
func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: self.content))
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
context.coordinator.hostingController.rootView = self.content
assert(context.coordinator.hostingController.view.superview == uiView)
}
class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>
init(hostingController: UIHostingController<Content>) {
self.hostingController = hostingController
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}
}
}
struct ImageView: View {
let urls: [URL?]
@Environment(\.presentationMode) var presentationMode
@State private var selectedIndex = 0
@State var showMenu = true
var navBarView: some View {
VStack {
HStack {
Text(urls[selectedIndex]?.lastPathComponent ?? "")
.bold()
Spacer()
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Image(systemName: "xmark")
})
}
.padding()
Divider()
.ignoresSafeArea()
}
.background(.regularMaterial)
}
var tabViewIndicator: some View {
HStack(spacing: 10) {
ForEach(urls.indices, id: \.self) { index in
Capsule()
.fill(index == selectedIndex ? Color(UIColor.label) : Color.secondary)
.frame(width: 7, height: 7)
}
}
.padding()
.background(.regularMaterial)
.clipShape(Capsule())
}
var body: some View {
ZStack {
Color(.systemBackground)
.ignoresSafeArea()
TabView(selection: $selectedIndex) {
ForEach(urls.indices, id: \.self) { index in
ZoomableScrollView {
ImageContainerView(url: urls[index])
.aspectRatio(contentMode: .fit)
}
.ignoresSafeArea()
.tag(index)
.modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
presentationMode.wrappedValue.dismiss()
}))
}
}
.ignoresSafeArea()
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.onChange(of: selectedIndex, perform: { _ in
showMenu = true
})
.onTapGesture {
showMenu.toggle()
}
.overlay(
VStack {
if showMenu {
navBarView
Spacer()
if (urls.count > 1) {
tabViewIndicator
}
}
}
.animation(.easeInOut, value: showMenu)
.padding(
.bottom,
UIApplication
.shared
.connectedScenes
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
.first { $0.isKeyWindow }?.safeAreaInsets.bottom
)
)
}
}
}
struct ImageCarousel: View {
var urls: [URL]
let evid: String
let previews: PreviewCache
@State private var open_sheet: Bool = false
@State private var current_url: URL? = nil
@State private var image_fill: ImageFill? = nil
@State private var fillHeight: CGFloat = 350
@State private var maxHeight: CGFloat = UIScreen.main.bounds.height * 0.85
init(previews: PreviewCache, evid: String, urls: [URL]) {
_open_sheet = State(initialValue: false)
_current_url = State(initialValue: nil)
_image_fill = State(initialValue: previews.lookup_image_meta(evid))
self.urls = urls
self.evid = evid
self.previews = previews
}
var filling: Bool {
image_fill?.filling == true
}
var height: CGFloat {
image_fill?.height ?? 0
}
@State var open_sheet: Bool = false
@State var current_url: URL? = nil
var body: some View {
TabView {
@@ -75,32 +275,34 @@ struct ImageCarousel: View {
Rectangle()
.foregroundColor(Color.clear)
.overlay {
GeometryReader { geo in
KFAnimatedImage(url)
.callbackQueue(.dispatch(.global(qos:.background)))
.backgroundDecode(true)
.imageContext(.note)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
KFAnimatedImage(url)
.callbackQueue(.dispatch(.global(qos: .background)))
.processingQueue(.dispatch(.global(qos: .background)))
.cacheOriginalImage()
.loadDiskFileSynchronously()
.scaleFactor(UIScreen.main.scale)
.fade(duration: 0.1)
.configure { view in
view.framePreloadCount = 3
}
.aspectRatio(contentMode: .fit)
.tabItem {
Text(url.absoluteString)
}
.id(url.absoluteString)
.contextMenu {
Button(NSLocalizedString("Copy Image", comment: "Context menu option to copy an image to clipboard.")) {
UIPasteboard.general.string = url.absoluteString
}
.imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in
previews.cache_image_meta(evid: evid, image_fill: fill)
image_fill = fill
}
.aspectRatio(contentMode: filling ? .fill : .fit)
.tabItem {
Text(url.absoluteString)
}
.id(url.absoluteString)
}
}
}
}
}
.cornerRadius(10)
.fullScreenCover(isPresented: $open_sheet) {
ImageView(urls: urls)
}
.frame(height: height)
.frame(height: 200)
.onTapGesture {
open_sheet = true
}
@@ -108,71 +310,8 @@ struct ImageCarousel: View {
}
}
// MARK: - Image Modifier
extension KFOptionSetter {
/// Sets a block to get image size
///
/// - Parameter block: The block which is used to read the image object.
/// - Returns: `Self` value after read size
public func imageFill(for size: CGSize, max: CGFloat, fill: CGFloat, block: @escaping (ImageFill) throws -> Void) -> Self {
let modifier = AnyImageModifier { image -> KFCrossPlatformImage in
let img_size = image.size
let geo_size = size
let fill = ImageFill.calculate_image_fill(geo_size: geo_size,
img_size: img_size,
maxHeight: max,
fillHeight: fill)
DispatchQueue.main.async { [block, fill] in
try? block(fill)
}
return image
}
options.imageModifier = modifier
return self
}
}
public struct ImageFill {
let filling: Bool?
let height: CGFloat
static func determine_image_shape(_ size: CGSize) -> ImageShape {
guard size.height > 0 else {
return .unknown
}
let imageRatio = size.width / size.height
switch imageRatio {
case 1.0: return .square
case ..<1.0: return .portrait
case 1.0...: return .landscape
default: return .unknown
}
}
static func calculate_image_fill(geo_size: CGSize, img_size: CGSize, maxHeight: CGFloat, fillHeight: CGFloat) -> ImageFill {
let shape = determine_image_shape(img_size)
let xfactor = geo_size.width / img_size.width
let scaled = img_size.height * xfactor
// calculate scaled image height
// set scale factor and constrain images to minimum 150
// and animations to scaled factor for dynamic size adjustment
switch shape {
case .portrait, .landscape:
let filling = scaled > maxHeight
let height = filling ? fillHeight : scaled
return ImageFill(filling: filling, height: height)
case .square, .unknown:
return ImageFill(filling: nil, height: scaled)
}
}
}
struct ImageCarousel_Previews: PreviewProvider {
static var previews: some View {
ImageCarousel(previews: test_damus_state().previews, evid: "evid", urls: [URL(string: "https://jb55.com/red-me.jpg")!,URL(string: "https://jb55.com/red-me.jpg")!])
ImageCarousel(urls: [URL(string: "https://jb55.com/red-me.jpg")!,URL(string: "https://jb55.com/red-me.jpg")!])
}
}
+59 -81
View File
@@ -7,84 +7,6 @@
import SwiftUI
struct InvoiceView: View {
@Environment(\.colorScheme) var colorScheme
@Environment(\.openURL) private var openURL
let our_pubkey: String
let invoice: Invoice
@State var showing_select_wallet: Bool = false
@State var copied = false
var CopyButton: some View {
Button {
copied = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
copied = false
}
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
UIPasteboard.general.string = invoice.string
} label: {
if !copied {
Image(systemName: "doc.on.clipboard")
.foregroundColor(.gray)
} else {
Image(systemName: "checkmark.circle")
.foregroundColor(DamusColors.green)
}
}
}
var PayButton: some View {
Button {
if should_show_wallet_selector(our_pubkey) {
showing_select_wallet = true
} else {
open_with_wallet(wallet: get_default_wallet(our_pubkey).model, invoice: invoice.string)
}
} label: {
RoundedRectangle(cornerRadius: 20, style: .circular)
.foregroundColor(colorScheme == .light ? .black : .white)
.overlay {
Text("Pay", comment: "Button to pay a Lightning invoice.")
.fontWeight(.medium)
.foregroundColor(colorScheme == .light ? .white : .black)
}
}
.onTapGesture {
// Temporary solution so that the "pay" button can be clicked (Yes we need an empty tap gesture)
print("pay button tap")
}
}
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 10)
.foregroundColor(.secondary.opacity(0.1))
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("", systemImage: "bolt.fill")
.foregroundColor(.orange)
Text("Lightning Invoice", comment: "Indicates that the view is for paying a Lightning invoice.")
Spacer()
CopyButton
}
Divider()
Text(invoice.description_string)
Text(invoice.amount.amount_sats_str())
.font(.title)
PayButton
.frame(height: 50)
.zIndex(10.0)
}
.padding(30)
}
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: our_pubkey, invoice: invoice.string)
}
}
}
func open_with_wallet(wallet: Wallet.Model, invoice: String) {
if let url = URL(string: "\(wallet.link)\(invoice)"), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
@@ -106,12 +28,68 @@ func open_with_wallet(wallet: Wallet.Model, invoice: String) {
}
}
struct InvoiceView: View {
@Environment(\.colorScheme) var colorScheme
@Environment(\.openURL) private var openURL
let invoice: Invoice
@State var showing_select_wallet: Bool = false
@ObservedObject var user_settings = UserSettingsStore()
var PayButton: some View {
Button {
if user_settings.show_wallet_selector {
showing_select_wallet = true
} else {
open_with_wallet(wallet: user_settings.default_wallet.model, invoice: invoice.string)
}
} label: {
RoundedRectangle(cornerRadius: 20)
.foregroundColor(colorScheme == .light ? .black : .white)
.overlay {
Text("Pay", comment: "Button to pay a Lightning invoice.")
.fontWeight(.medium)
.foregroundColor(colorScheme == .light ? .white : .black)
}
}
//.buttonStyle(.bordered)
.onTapGesture {
// Temporary solution so that the "pay" button can be clicked (Yes we need an empty tap gesture)
}
}
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 10)
.foregroundColor(.secondary.opacity(0.1))
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("", systemImage: "bolt.fill")
.foregroundColor(.orange)
Text("Lightning Invoice", comment: "Indicates that the view is for paying a Lightning invoice.")
}
Divider()
Text(invoice.description)
Text(invoice.amount.amount_sats_str())
.font(.title)
PayButton
.frame(height: 50)
.zIndex(10.0)
}
.padding(30)
}
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
SelectWalletView(showingSelectWallet: $showing_select_wallet, invoice: invoice.string).environmentObject(user_settings)
}
}
}
let test_invoice = Invoice(description: .description("this is a description"), amount: .specific(10000), string: "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r", expiry: 604800, payment_hash: Data(), created_at: 1666139119)
let test_invoice = Invoice(description: "this is a description", amount: .specific(10000), string: "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r", expiry: 604800, payment_hash: Data(), created_at: 1666139119)
struct InvoiceView_Previews: PreviewProvider {
static var previews: some View {
InvoiceView(our_pubkey: "", invoice: test_invoice)
.frame(width: 300, height: 200)
InvoiceView(invoice: test_invoice)
.frame(width: 200, height: 200)
}
}
+2 -3
View File
@@ -8,7 +8,6 @@
import SwiftUI
struct InvoicesView: View {
let our_pubkey: String
var invoices: [Invoice]
@State var open_sheet: Bool = false
@@ -17,7 +16,7 @@ struct InvoicesView: View {
var body: some View {
TabView {
ForEach(invoices, id: \.string) { invoice in
InvoiceView(our_pubkey: our_pubkey, invoice: invoice)
InvoiceView(invoice: invoice)
.tabItem {
Text(invoice.string)
}
@@ -31,7 +30,7 @@ struct InvoicesView: View {
struct InvoicesView_Previews: PreviewProvider {
static var previews: some View {
InvoicesView(our_pubkey: "", invoices: [Invoice.init(description: .description("description"), amount: .specific(10000), string: "invstr", expiry: 100000, payment_hash: Data(), created_at: 1000000)])
InvoicesView(invoices: [Invoice.init(description: "description", amount: .specific(10000), string: "invstr", expiry: 100000, payment_hash: Data(), created_at: 1000000)])
.frame(width: 300)
}
}
+9 -34
View File
@@ -24,33 +24,19 @@ struct NIP05Badge: View {
self.clickable = clickable
}
var nip05_color: Bool {
return use_nip05_color(pubkey: pubkey, contacts: contacts)
}
var Seal: some View {
Group {
if nip05_color {
LINEAR_GRADIENT
.mask(Image(systemName: "checkmark.seal.fill")
.resizable()
).frame(width: 14, height: 14)
} else {
Image(systemName: "checkmark.seal.fill")
.font(.footnote)
.foregroundColor(.gray)
}
}
var nip05_color: Color {
return get_nip05_color(pubkey: pubkey, contacts: contacts)
}
var body: some View {
HStack(spacing: 2) {
Seal
Image(systemName: "checkmark.seal.fill")
.font(.footnote)
.foregroundColor(nip05_color)
if show_domain {
if clickable {
Text(nip05.host)
.nip05_colorized(gradient: nip05_color)
.foregroundColor(nip05_color)
.onTapGesture {
if let nip5url = nip05.siteUrl {
openURL(nip5url)
@@ -58,7 +44,7 @@ struct NIP05Badge: View {
}
} else {
Text(nip05.host)
.foregroundColor(.gray)
.foregroundColor(nip05_color)
}
}
}
@@ -66,19 +52,8 @@ struct NIP05Badge: View {
}
}
extension View {
func nip05_colorized(gradient: Bool) -> some View {
if gradient {
return AnyView(self.foregroundStyle(LINEAR_GRADIENT))
} else {
return AnyView(self.foregroundColor(.gray))
}
}
}
func use_nip05_color(pubkey: String, contacts: Contacts) -> Bool {
return contacts.is_friend_or_self(pubkey) ? true : false
func get_nip05_color(pubkey: String, contacts: Contacts) -> Color {
return contacts.is_friend_or_self(pubkey) ? .accentColor : .gray
}
struct NIP05Badge_Previews: PreviewProvider {
+2
View File
@@ -15,10 +15,12 @@ struct Reposted: View {
var body: some View {
HStack(alignment: .center) {
Image(systemName: "arrow.2.squarepath")
.font(.footnote)
.foregroundColor(Color.gray)
ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true, show_nip5_domain: false)
.foregroundColor(Color.gray)
Text("Reposted", comment: "Text indicating that the post was reposted (i.e. re-shared).")
.font(.footnote)
.foregroundColor(Color.gray)
}
}
-102
View File
@@ -1,102 +0,0 @@
//
// SelectableText.swift
// damus
//
// Created by Oleg Abalonski on 2/16/23.
//
import UIKit
import SwiftUI
struct SelectableText: View {
let attributedString: AttributedString
@State private var selectedTextHeight: CGFloat = .zero
@State private var selectedTextWidth: CGFloat = .zero
let size: EventViewKind
var body: some View {
GeometryReader { geo in
TextViewRepresentable(
attributedString: attributedString,
textColor: UIColor.label,
font: eventviewsize_to_uifont(size),
fixedWidth: selectedTextWidth,
height: $selectedTextHeight
)
.padding([.leading, .trailing], -1.0)
.onAppear {
self.selectedTextWidth = geo.size.width
}
.onChange(of: geo.size) { newSize in
self.selectedTextWidth = newSize.width
}
}
.frame(height: selectedTextHeight)
}
}
fileprivate struct TextViewRepresentable: UIViewRepresentable {
let attributedString: AttributedString
let textColor: UIColor
let font: UIFont
let fixedWidth: CGFloat
@Binding var height: CGFloat
func makeUIView(context: UIViewRepresentableContext<Self>) -> UITextView {
let view = UITextView()
view.isEditable = false
view.dataDetectorTypes = .all
view.isSelectable = true
view.backgroundColor = .clear
view.textContainer.lineFragmentPadding = 0
view.textContainerInset = .zero
view.textContainerInset.left = 1.0
view.textContainerInset.right = 1.0
return view
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<Self>) {
let mutableAttributedString = createNSAttributedString()
uiView.attributedText = mutableAttributedString
let newHeight = mutableAttributedString.height(containerWidth: fixedWidth)
DispatchQueue.main.async {
height = newHeight
}
}
func createNSAttributedString() -> NSMutableAttributedString {
let mutableAttributedString = NSMutableAttributedString(attributedString)
let myAttribute = [
NSAttributedString.Key.font: font,
NSAttributedString.Key.foregroundColor: textColor
]
mutableAttributedString.addAttributes(
myAttribute,
range: NSRange.init(location: 0, length: mutableAttributedString.length)
)
return mutableAttributedString
}
}
fileprivate extension NSAttributedString {
func height(containerWidth: CGFloat) -> CGFloat {
let rect = self.boundingRect(
with: CGSize.init(width: containerWidth, height: CGFloat.greatestFiniteMagnitude),
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil
)
return ceil(rect.size.height)
}
}
-22
View File
@@ -1,22 +0,0 @@
//
// ThiccDivider.swift
// damus
//
// Created by William Casarin on 2023-04-03.
//
import SwiftUI
struct ThiccDivider: View {
var body: some View {
Rectangle()
.frame(height: 4)
.foregroundColor(DamusColors.adaptableGrey)
}
}
struct ThiccDivider_Previews: PreviewProvider {
static var previews: some View {
ThiccDivider()
}
}
-141
View File
@@ -1,141 +0,0 @@
//
// TranslateButton.swift
// damus
//
// Created by William Casarin on 2023-02-02.
//
import SwiftUI
import NaturalLanguage
struct TranslateView: View {
let damus_state: DamusState
let event: NostrEvent
let size: EventViewKind
@State var checkingTranslationStatus: Bool = false
@State var translatable: Bool = true
@State var noteLanguage: String?
@State var show_translated_note: Bool
@State var translated_artifacts: NoteArtifacts?
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind) {
self.damus_state = damus_state
self.event = event
self.size = size
self._noteLanguage = State(initialValue: damus_state.translations.detectLanguage(event, state: damus_state))
if let translationWithLanguage = damus_state.translations.cachedTranslation(event) {
self._noteLanguage = State(initialValue: translationWithLanguage.language)
let translatedBlocks = event.get_blocks(content: translationWithLanguage.translation)
self._translated_artifacts = State.init(initialValue: render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey))
} else {
self._translated_artifacts = State(initialValue: nil)
}
self._show_translated_note = State(initialValue: damus_state.settings.auto_translate)
}
var TranslateButton: some View {
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
show_translated_note = true
processTranslation()
}
.translate_button_style()
}
func processTranslation() {
guard noteLanguage != nil && !checkingTranslationStatus && translatable else {
return
}
checkingTranslationStatus = true
show_translated_note = true
Task {
let translationWithLanguage = await damus_state.translations.translate(event, state: damus_state)
DispatchQueue.main.async {
guard translationWithLanguage != nil else {
noteLanguage = damus_state.translations.targetLanguage
checkingTranslationStatus = false
show_translated_note = false
translatable = false
return
}
noteLanguage = translationWithLanguage!.language
// Render translated note.
let translatedBlocks = event.get_blocks(content: translationWithLanguage!.translation)
translated_artifacts = render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
translatable = true
checkingTranslationStatus = false
}
}
}
func Translated(lang: String, artifacts: NoteArtifacts) -> some View {
return Group {
Button(String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang)) {
show_translated_note = false
}
.translate_button_style()
SelectableText(attributedString: artifacts.content, size: self.size)
}
}
func MainContent(note_lang: String) -> some View {
return Group {
if translatable {
let languageName = Locale.current.localizedString(forLanguageCode: note_lang)
if let languageName, let translated_artifacts, show_translated_note {
Translated(lang: languageName, artifacts: translated_artifacts)
} else if !damus_state.settings.auto_translate {
TranslateButton
} else {
EmptyView()
}
} else {
EmptyView()
}
}
}
var body: some View {
Group {
if let note_lang = noteLanguage, note_lang != damus_state.translations.targetLanguage {
MainContent(note_lang: note_lang)
.task {
if show_translated_note {
processTranslation()
}
}
} else {
Text("")
}
}
}
}
extension View {
func translate_button_style() -> some View {
return self
.font(.footnote)
.contentShape(Rectangle())
.padding([.top, .bottom], 10)
}
}
struct TranslateView_Previews: PreviewProvider {
static var previews: some View {
let ds = test_damus_state()
TranslateView(damus_state: ds, event: test_event, size: .normal)
}
}
-38
View File
@@ -1,38 +0,0 @@
//
// UserView.swift
// damus
//
// Created by William Casarin on 2023-01-25.
//
import SwiftUI
struct UserView: View {
let damus_state: DamusState
let pubkey: String
var body: some View {
NavigationLink(destination: ProfileView(damus_state: damus_state, pubkey: pubkey)) {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey)
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false)
if let about = profile?.about {
Text(about)
.lineLimit(3)
.font(.footnote)
}
}
Spacer()
}
.buttonStyle(PlainButtonStyle())
}
}
struct UserView_Previews: PreviewProvider {
static var previews: some View {
UserView(damus_state: test_damus_state(), pubkey: "pk")
}
}
-39
View File
@@ -1,39 +0,0 @@
//
// WebsiteLink.swift
// damus
//
// Created by William Casarin on 2023-01-22.
//
import SwiftUI
struct WebsiteLink: View {
let url: URL
@Environment(\.openURL) var openURL
var body: some View {
HStack {
Image(systemName: "link")
.foregroundColor(.gray)
.font(.footnote)
Button(action: {
openURL(url)
}, label: {
Text(link_text)
.font(.footnote)
.foregroundColor(.accentColor)
})
}
}
var link_text: String {
url.host ?? url.absoluteString
}
}
struct WebsiteLink_Previews: PreviewProvider {
static var previews: some View {
WebsiteLink(url: URL(string: "https://jb55.com")!)
}
}
-194
View File
@@ -1,194 +0,0 @@
//
// ZapButton.swift
// damus
//
// Created by William Casarin on 2023-01-17.
//
import SwiftUI
enum ZappingEventType {
case failed(ZappingError)
case got_zap_invoice(String)
}
enum ZappingError {
case fetching_invoice
case bad_lnurl
}
struct ZappingEvent {
let is_custom: Bool
let type: ZappingEventType
let event: NostrEvent
}
struct ZapButton: View {
let damus_state: DamusState
let event: NostrEvent
let lnurl: String
@ObservedObject var bar: ActionBarModel
@State var zapping: Bool = false
@State var invoice: String = ""
@State var slider_value: Double = 0.0
@State var slider_visible: Bool = false
@State var showing_select_wallet: Bool = false
@State var showing_zap_customizer: Bool = false
@State var is_charging: Bool = false
var zap_img: String {
if bar.zapped {
return "bolt.fill"
}
if !zapping {
return "bolt"
}
return "bolt.horizontal.fill"
}
var zap_color: Color? {
if bar.zapped {
return Color.orange
}
if is_charging {
return Color.yellow
}
if !zapping {
return nil
}
return Color.yellow
}
var body: some View {
HStack(spacing: 4) {
Button(action: {
}, label: {
Image(systemName: zap_img)
.foregroundColor(zap_color == nil ? Color.gray : zap_color!)
.font(.footnote.weight(.medium))
})
.simultaneousGesture(LongPressGesture().onEnded {_ in
guard !zapping else {
return
}
self.showing_zap_customizer = true
})
.highPriorityGesture(TapGesture().onEnded {_ in
guard !zapping else {
return
}
send_zap(damus_state: damus_state, event: event, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: ZapType.pub)
self.zapping = true
})
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
if bar.zap_total > 0 {
Text(verbatim: format_msats_abbrev(bar.zap_total))
.font(.footnote)
.foregroundColor(bar.zapped ? Color.orange : Color.gray)
}
}
.sheet(isPresented: $showing_zap_customizer) {
CustomizeZapView(state: damus_state, event: event, lnurl: lnurl)
}
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: invoice)
}
.onReceive(handle_notify(.zapping)) { notif in
let zap_ev = notif.object as! ZappingEvent
guard zap_ev.event.id == self.event.id else {
return
}
guard !zap_ev.is_custom else {
return
}
switch zap_ev.type {
case .failed:
break
case .got_zap_invoice(let inv):
if should_show_wallet_selector(damus_state.pubkey) {
self.invoice = inv
self.showing_select_wallet = true
} else {
open_with_wallet(wallet: get_default_wallet(damus_state.pubkey).model, invoice: inv)
}
}
self.zapping = false
}
}
}
struct ZapButton_Previews: PreviewProvider {
static var previews: some View {
let bar = ActionBarModel(likes: 0, boosts: 0, zaps: 10, zap_total: 15623414, replies: 2, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
ZapButton(damus_state: test_damus_state(), event: test_event, lnurl: "lnurl", bar: bar)
}
}
func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) {
guard let keypair = damus_state.keypair.to_full() else {
return
}
// Only take the first 10 because reasons
let relays = Array(damus_state.pool.descriptors.prefix(10))
let target = ZapTarget.note(id: event.id, author: event.pubkey)
let content = comment ?? ""
let zapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type)
Task {
var mpayreq = damus_state.lnurls.lookup(target.pubkey)
if mpayreq == nil {
mpayreq = await fetch_static_payreq(lnurl)
}
guard let payreq = mpayreq else {
// TODO: show error
DispatchQueue.main.async {
let typ = ZappingEventType.failed(.bad_lnurl)
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
notify(.zapping, ev)
}
return
}
DispatchQueue.main.async {
damus_state.lnurls.endpoints[target.pubkey] = payreq
}
let zap_amount = amount_sats ?? get_default_zap_amount(pubkey: damus_state.pubkey) ?? 1000
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount, zap_type: zap_type, comment: comment) else {
DispatchQueue.main.async {
let typ = ZappingEventType.failed(.fetching_invoice)
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
notify(.zapping, ev)
}
return
}
DispatchQueue.main.async {
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), event: event)
notify(.zapping, ev)
}
}
return
}
-152
View File
@@ -1,152 +0,0 @@
//
// ZoomableScrollView.swift
// damus
//
// Created by Oleg Abalonski on 1/25/23.
//
import SwiftUI
struct ZoomableScrollView<Content: View>: UIViewRepresentable {
private var content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
func makeUIView(context: Context) -> UIScrollView {
let scrollView = GesturedScrollView()
scrollView.delegate = context.coordinator
scrollView.maximumZoomScale = 20
scrollView.minimumZoomScale = 1
scrollView.bouncesZoom = true
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
let hostedView = context.coordinator.hostingController.view!
hostedView.translatesAutoresizingMaskIntoConstraints = true
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostedView.frame = scrollView.bounds
hostedView.backgroundColor = .clear
scrollView.addSubview(hostedView)
return scrollView
}
func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: self.content, ignoreSafeArea: true))
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
context.coordinator.hostingController.rootView = self.content
assert(context.coordinator.hostingController.view.superview == uiView)
}
class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>
init(hostingController: UIHostingController<Content>) {
self.hostingController = hostingController
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
let viewSize = hostingController.view.frame.size
guard let imageSize = scrollView.subviews[0].subviews.last?.frame.size else { return }
if scrollView.zoomScale > 1 {
let ratioW = viewSize.width / imageSize.width
let ratioH = viewSize.height / imageSize.height
let ratio = ratioW < ratioH ? ratioW:ratioH
let newWidth = imageSize.width * ratio
let newHeight = imageSize.height * ratio
let left = 0.5 * (newWidth * scrollView.zoomScale > viewSize.width ? (newWidth - viewSize.width) : (scrollView.frame.width - scrollView.contentSize.width))
let top = 0.5 * (newHeight * scrollView.zoomScale > viewSize.height ? (newHeight - viewSize.height) : (scrollView.frame.height - scrollView.contentSize.height))
scrollView.contentInset = UIEdgeInsets(top: top, left: left, bottom: top, right: left)
} else {
scrollView.contentInset = .zero
}
}
}
}
fileprivate class GesturedScrollView: UIScrollView, UIGestureRecognizerDelegate {
let doubleTapGesture: UITapGestureRecognizer
override init(frame: CGRect) {
doubleTapGesture = UITapGestureRecognizer()
super.init(frame: frame)
doubleTapGesture.addTarget(self, action: #selector(handleDoubleTap))
doubleTapGesture.numberOfTapsRequired = 2
addGestureRecognizer(doubleTapGesture)
doubleTapGesture.delegate = self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func handleDoubleTap(_ gesture: UITapGestureRecognizer) {
if self.zoomScale == 1 {
let pointInView = gesture.location(in: self.subviews.first)
let newZoomScale = self.maximumZoomScale / 4.0
let scrollViewSize = self.bounds.size
let width = scrollViewSize.width / newZoomScale
let height = scrollViewSize.height / newZoomScale
let originX = pointInView.x - (width / 2.0)
let originY = pointInView.y - (height / 2.0)
let zoomRect = CGRect(x: originX, y: originY, width: width, height: height)
self.zoom(to: zoomRect, animated: true)
} else {
self.setZoomScale(self.minimumZoomScale, animated: true)
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return gestureRecognizer == doubleTapGesture
}
}
fileprivate extension UIHostingController {
convenience init(rootView: Content, ignoreSafeArea: Bool) {
self.init(rootView: rootView)
if ignoreSafeArea {
disableSafeArea()
}
}
func disableSafeArea() {
guard let viewClass = object_getClass(view) else { return }
let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea")
if let viewSubclass = NSClassFromString(viewSubclassName) {
object_setClass(view, viewSubclass)
}
else {
guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return }
guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return }
if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) {
let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in
return .zero
}
class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method))
}
objc_registerClassPair(viewSubclass)
object_setClass(view, viewSubclass)
}
}
}
+120 -378
View File
@@ -7,27 +7,31 @@
import SwiftUI
import Starscream
import Kingfisher
var BOOTSTRAP_RELAYS = [
"wss://relay.damus.io",
"wss://nostr-relay.wlvs.space",
"wss://nostr.fmt.wiz.biz",
"wss://relay.nostr.bg",
"wss://nostr.oxtr.dev",
"wss://nostr.v0l.io",
"wss://brb.io",
]
struct TimestampedProfile {
let profile: Profile
let timestamp: Int64
let event: NostrEvent
}
enum Sheets: Identifiable {
case post
case report(ReportTarget)
case reply(NostrEvent)
case event(NostrEvent)
case filter
var id: String {
switch self {
case .report: return "report"
case .post: return "post"
case .reply(let ev): return "reply-" + ev.id
case .event(let ev): return "event-" + ev.id
case .filter: return "filter"
}
}
}
@@ -67,24 +71,19 @@ struct ContentView: View {
@State var damus_state: DamusState? = nil
@State var selected_timeline: Timeline? = .home
@State var is_thread_open: Bool = false
@State var is_deleted_account: Bool = false
@State var is_profile_open: Bool = false
@State var event: NostrEvent? = nil
@State var active_profile: String? = nil
@State var active_search: NostrFilter? = nil
@State var active_event: NostrEvent? = nil
@State var active_event_id: String? = nil
@State var profile_open: Bool = false
@State var thread_open: Bool = false
@State var search_open: Bool = false
@State var muting: String? = nil
@State var confirm_mute: Bool = false
@State var user_muted_confirm: Bool = false
@State var confirm_overwrite_mutelist: Bool = false
@State var current_boost: NostrEvent? = nil
@State var filter_state : FilterState = .posts_and_replies
@State private var isSideBarOpened = false
@StateObject var home: HomeModel = HomeModel()
@StateObject var user_settings = UserSettingsStore()
// connect retry timer
let timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect()
@@ -94,30 +93,21 @@ struct ContentView: View {
var PostingTimelineView: some View {
VStack {
ZStack {
TabView(selection: $filter_state) {
contentTimelineView(filter: FilterState.posts.filter)
.tag(FilterState.posts)
.id(FilterState.posts)
contentTimelineView(filter: FilterState.posts_and_replies.filter)
.tag(FilterState.posts_and_replies)
.id(FilterState.posts_and_replies)
}
.tabViewStyle(.page(indexDisplayMode: .never))
if privkey != nil {
PostButtonContainer(is_left_handed: damus_state?.settings.left_handed ?? false) {
self.active_sheet = .post
}
}
TabView(selection: $filter_state) {
contentTimelineView(filter: FilterState.posts.filter)
.tag(FilterState.posts)
.id(FilterState.posts)
contentTimelineView(filter: FilterState.posts_and_replies.filter)
.tag(FilterState.posts_and_replies)
.id(FilterState.posts_and_replies)
}
.tabViewStyle(.page(indexDisplayMode: .never))
}
.safeAreaInset(edge: .top, spacing: 0) {
VStack(spacing: 0) {
CustomPicker(selection: $filter_state, content: {
Text("Posts", comment: "Label for filter for seeing only posts (instead of posts and replies).").tag(FilterState.posts)
Text("Posts & Replies", comment: "Label for filter for seeing posts and replies (instead of only posts).").tag(FilterState.posts_and_replies)
})
FiltersView
//.frame(maxWidth: 275)
.padding()
Divider()
.frame(height: 1)
}
@@ -128,21 +118,24 @@ struct ContentView: View {
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
ZStack {
if let damus = self.damus_state {
TimelineView(events: home.events, loading: $home.loading, damus: damus, show_friend_icon: false, filter: filter)
TimelineView(events: $home.events, loading: $home.loading, damus: damus, show_friend_icon: false, filter: filter)
}
if privkey != nil {
PostButtonContainer(userSettings: user_settings) {
self.active_sheet = .post
}
}
}
}
func popToRoot() {
profile_open = false
thread_open = false
search_open = false
isSideBarOpened = false
}
var timelineNavItem: Text {
return Text(timeline_name(selected_timeline))
.bold()
var FiltersView: some View {
VStack{
Picker(NSLocalizedString("Filter State", comment: "Filter state for seeing either only posts, or posts & replies."), selection: $filter_state) {
Text("Posts", comment: "Label for filter for seeing only posts (instead of posts and replies).").tag(FilterState.posts)
Text("Posts & Replies", comment: "Label for filter for seeing posts and replies (instead of only posts).").tag(FilterState.posts_and_replies)
}
.pickerStyle(.segmented)
}
}
func MainContent(damus: DamusState) -> some View {
@@ -150,30 +143,22 @@ struct ContentView: View {
NavigationLink(destination: MaybeProfileView, isActive: $profile_open) {
EmptyView()
}
if let active_event {
let thread = ThreadModel(event: active_event, damus_state: damus_state!)
NavigationLink(destination: ThreadView(state: damus_state!, thread: thread), isActive: $thread_open) {
EmptyView()
}
NavigationLink(destination: MaybeThreadView, isActive: $thread_open) {
EmptyView()
}
NavigationLink(destination: MaybeSearchView, isActive: $search_open) {
EmptyView()
}
switch selected_timeline {
case .search:
if #available(iOS 16.0, *) {
SearchHomeView(damus_state: damus_state!, model: SearchHomeModel(damus_state: damus_state!))
.scrollDismissesKeyboard(.immediately)
} else {
// Fallback on earlier versions
SearchHomeView(damus_state: damus_state!, model: SearchHomeModel(damus_state: damus_state!))
}
SearchHomeView(damus_state: damus_state!, model: SearchHomeModel(damus_state: damus_state!))
case .home:
PostingTimelineView
case .notifications:
NotificationsView(state: damus, notifications: home.notifications)
TimelineView(events: $home.notifications, loading: $home.loading, damus: damus, show_friend_icon: true, filter: { _ in true })
.navigationTitle(NSLocalizedString("Notifications", comment: "Navigation title for notifications."))
case .dms:
DirectMessagesView(damus_state: damus_state!)
@@ -183,31 +168,46 @@ struct ContentView: View {
EmptyView()
}
}
.navigationBarTitle(timeline_name(selected_timeline), displayMode: .inline)
.navigationBarTitle(selected_timeline == .home ? NSLocalizedString("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.") : NSLocalizedString("Global", comment: "Navigation bar title for Global view where posts from all connected relay servers appear."), displayMode: .inline)
.toolbar {
ToolbarItem(placement: .principal) {
VStack {
if selected_timeline == .home {
Image("damus-home")
.resizable()
.frame(width:30,height:30)
.shadow(color: DamusColors.purple, radius: 2)
.opacity(isSideBarOpened ? 0 : 1)
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
} else {
timelineNavItem
.opacity(isSideBarOpened ? 0 : 1)
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
}
switch selected_timeline {
case .home:
Image("damus-home")
.resizable()
.frame(width:30,height:30)
.shadow(color: Color("DamusPurple"), radius: 2)
case .dms:
Text("DM", comment: "Toolbar label for DM view, which is the English abbreviation for Direct Message.")
.bold()
case .notifications:
Text("Notifications", comment: "Toolbar label for Notifications view.")
.bold()
case .search:
Text("Global", comment: "Toolbar label for Global view where posts from all connected relay servers appear.")
.bold()
case .none:
Text("", comment: "Toolbar label for unknown views. This label would be displayed only if a new timeline view is added but a toolbar label was not explicitly assigned to it yet.")
}
}
}
.ignoresSafeArea(.keyboard)
}
var MaybeSearchView: some View {
Group {
if let search = self.active_search {
SearchView(appstate: damus_state!, search: SearchModel(contacts: damus_state!.contacts, pool: damus_state!.pool, search: search))
SearchView(appstate: damus_state!, search: SearchModel(pool: damus_state!.pool, search: search))
} else {
EmptyView()
}
}
}
var MaybeThreadView: some View {
Group {
if let evid = self.active_event_id {
BuildThreadV2View(damus: damus_state!, event_id: evid)
} else {
EmptyView()
}
@@ -226,100 +226,60 @@ struct ContentView: View {
}
}
func MaybeReportView(target: ReportTarget) -> some View {
Group {
if let damus_state {
if let sec = damus_state.keypair.privkey {
ReportView(postbox: damus_state.postbox, target: target, privkey: sec)
} else {
EmptyView()
}
} else {
EmptyView()
}
}
}
var body: some View {
VStack(alignment: .leading, spacing: 0) {
if let damus = self.damus_state {
NavigationView {
TabView { // Prevents navbar appearance change on scroll
MainContent(damus: damus)
.toolbar() {
ToolbarItem(placement: .navigationBarLeading) {
Button {
isSideBarOpened.toggle()
} label: {
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles)
.opacity(isSideBarOpened ? 0 : 1)
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
ZStack {
VStack {
MainContent(damus: damus)
.toolbar() {
ToolbarItem(placement: .navigationBarLeading) {
Button {
isSideBarOpened.toggle()
} label: {
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles)
}
}
.disabled(isSideBarOpened)
}
ToolbarItem(placement: .navigationBarTrailing) {
HStack(alignment: .center) {
if home.signal.signal != home.signal.max_signal {
NavigationLink(destination: RelayConfigView(state: damus_state!)) {
ToolbarItem(placement: .navigationBarTrailing) {
HStack(alignment: .center) {
if home.signal.signal != home.signal.max_signal {
Text("\(home.signal.signal)/\(home.signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
.font(.callout)
.foregroundColor(.gray)
}
}
// maybe expand this to other timelines in the future
if selected_timeline == .search {
Button(action: {
//isFilterVisible.toggle()
self.active_sheet = .filter
}) {
// checklist, checklist.checked, lisdt.bullet, list.bullet.circle, line.3.horizontal.decrease..., line.3.horizontail.decrease
Label(NSLocalizedString("Filter", comment: "Button label text for filtering relay servers."), systemImage: "line.3.horizontal.decrease")
.foregroundColor(.gray)
//.contentShape(Rectangle())
}
}
}
}
}
}
Color.clear
.overlay(
SideMenuView(damus_state: damus, isSidebarVisible: $isSideBarOpened)
)
}
.tabViewStyle(.page(indexDisplayMode: .never))
.overlay(
SideMenuView(damus_state: damus, isSidebarVisible: $isSideBarOpened.animation())
)
.navigationBarHidden(isSideBarOpened ? true: false) // Would prefer a different way of doing this.
}
.navigationViewStyle(.stack)
TabBar(new_events: $home.new_events, selected: $selected_timeline, isSidebarVisible: $isSideBarOpened, action: switch_timeline)
.padding([.bottom], 8)
.background(Color(uiColor: .systemBackground).ignoresSafeArea())
}
}
.ignoresSafeArea(.keyboard)
.onAppear() {
self.connect()
//KingfisherManager.shared.cache.clearDiskCache()
setup_notifications()
}
.sheet(item: $active_sheet) { item in
switch item {
case .report(let target):
MaybeReportView(target: target)
case .post:
PostView(replying_to: nil, damus_state: damus_state!)
PostView(replying_to: nil, references: [])
case .reply(let event):
PostView(replying_to: event, damus_state: damus_state!)
case .event:
EventDetailView()
case .filter:
let timeline = selected_timeline ?? .home
if #available(iOS 16.0, *) {
RelayFilterView(state: damus_state!, timeline: timeline)
.presentationDetents([.height(550)])
.presentationDragIndicator(.visible)
} else {
RelayFilterView(state: damus_state!, timeline: timeline)
}
ReplyView(replying_to: event, damus: damus_state!)
}
}
.onOpenURL { url in
@@ -333,11 +293,7 @@ struct ContentView: View {
active_profile = ref.ref_id
profile_open = true
} else if ref.key == "e" {
find_event(state: damus_state!, evid: ref.ref_id, search_type: .event, find_from: nil) { ev in
if let ev {
active_event = ev
}
}
active_event_id = ref.ref_id
thread_open = true
}
case .filter(let filt):
@@ -349,7 +305,12 @@ struct ContentView: View {
}
.onReceive(handle_notify(.boost)) { notif in
current_boost = (notif.object as? NostrEvent)
guard let privkey = self.privkey else {
return
}
let ev = notif.object as! NostrEvent
let boost = make_boost_event(pubkey: pubkey, privkey: privkey, boosted: ev)
self.damus_state?.pool.send(.event(boost))
}
.onReceive(handle_notify(.open_thread)) { obj in
//let ev = obj.object as! NostrEvent
@@ -362,27 +323,9 @@ struct ContentView: View {
}
.onReceive(handle_notify(.like)) { like in
}
.onReceive(handle_notify(.deleted_account)) { notif in
self.is_deleted_account = true
}
.onReceive(handle_notify(.report)) { notif in
let target = notif.object as! ReportTarget
self.active_sheet = .report(target)
}
.onReceive(handle_notify(.mute)) { notif in
let pubkey = notif.object as! String
self.muting = pubkey
self.confirm_mute = true
}
.onReceive(handle_notify(.broadcast_event)) { obj in
let ev = obj.object as! NostrEvent
guard let ds = self.damus_state else {
return
}
ds.postbox.send(ev)
if let profile = ds.profiles.profiles[ev.pubkey] {
ds.postbox.send(profile.event)
}
self.damus_state?.pool.send(.event(ev))
}
.onReceive(handle_notify(.unfollow)) { notif in
guard let privkey = self.privkey else {
@@ -396,7 +339,7 @@ struct ContentView: View {
let target = notif.object as! FollowTarget
let pk = target.pubkey
if let ev = unfollow_user(postbox: damus.postbox,
if let ev = unfollow_user(pool: damus.pool,
our_contacts: damus.contacts.event,
pubkey: damus.pubkey,
privkey: privkey,
@@ -443,20 +386,9 @@ struct ContentView: View {
let post_res = obj.object as! NostrPostResult
switch post_res {
case .post(let post):
//let post = tup.0
//let to_relays = tup.1
print("post \(post.content)")
let new_ev = post_to_event(post: post, privkey: privkey, pubkey: pubkey)
guard let ds = self.damus_state else {
return
}
ds.postbox.send(new_ev)
for eref in new_ev.referenced_ids.prefix(3) {
// also broadcast at most 3 referenced events
if let ev = ds.events.lookup(eref.ref_id) {
ds.postbox.send(ev)
}
}
self.damus_state?.pool.send(.event(new_ev))
case .cancel:
active_sheet = nil
print("post cancelled")
@@ -465,112 +397,9 @@ struct ContentView: View {
.onReceive(timer) { n in
self.damus_state?.pool.connect_to_disconnected()
}
.onReceive(handle_notify(.new_mutes)) { notif in
home.filter_muted()
}
.alert(NSLocalizedString("Deleted Account", comment: "Alert message to indicate this is a deleted account"), isPresented: $is_deleted_account) {
Button(NSLocalizedString("Logout", comment: "Button to close the alert that informs that the current account has been deleted.")) {
is_deleted_account = false
notify(.logout, ())
}
}
.alert(NSLocalizedString("User muted", comment: "Alert message to indicate the user has been muted"), isPresented: $user_muted_confirm, actions: {
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to muted a user was successful.")) {
user_muted_confirm = false
}
}, message: {
if let pubkey = self.muting {
let profile = damus_state!.profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: profile, pubkey: pubkey).username
Text("\(name) has been muted", comment: "Alert message that informs a user was muted.")
} else {
Text("User has been muted", comment: "Alert message that informs a user was d.")
}
})
.alert(NSLocalizedString("Create new mutelist", comment: "Title of alert prompting the user to create a new mutelist."), isPresented: $confirm_overwrite_mutelist, actions: {
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of alert that creates a new mutelist.")) {
confirm_overwrite_mutelist = false
confirm_mute = false
}
Button(NSLocalizedString("Yes, Overwrite", comment: "Text of button that confirms to overwrite the existing mutelist.")) {
guard let ds = damus_state else {
return
}
guard let keypair = ds.keypair.to_full() else {
return
}
guard let pubkey = muting else {
return
}
guard let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: pubkey) else {
return
}
damus_state?.contacts.set_mutelist(mutelist)
ds.postbox.send(mutelist)
confirm_overwrite_mutelist = false
confirm_mute = false
user_muted_confirm = true
}
}, message: {
Text("No mute list found, create a new one? This will overwrite any previous mute lists.", comment: "Alert message prompt that asks if the user wants to create a new mute list, overwriting previous mute lists.")
})
.alert(NSLocalizedString("Mute User", comment: "Title of alert for muting a user."), isPresented: $confirm_mute, actions: {
Button(NSLocalizedString("Cancel", comment: "Alert button to cancel out of alert for muting a user."), role: .cancel) {
confirm_mute = false
}
Button(NSLocalizedString("Mute", comment: "Alert button to mute a user."), role: .destructive) {
guard let ds = damus_state else {
return
}
if ds.contacts.mutelist == nil {
confirm_overwrite_mutelist = true
} else {
guard let keypair = ds.keypair.to_full() else {
return
}
guard let pubkey = muting else {
return
}
guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.contacts.mutelist, to_add: pubkey) else {
return
}
damus_state?.contacts.set_mutelist(ev)
ds.postbox.send(ev)
}
}
}, message: {
if let pubkey = muting {
let profile = damus_state?.profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: profile, pubkey: pubkey).username
Text("Mute \(name)?", comment: "Alert message prompt to ask if a user should be muted.")
} else {
Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.")
}
})
.alert(NSLocalizedString("Repost", comment: "Title of alert for confirming to repost a post."), isPresented: $current_boost.mappedToBool()) {
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of reposting a post.")) {
current_boost = nil
}
Button(NSLocalizedString("Repost", comment: "Button to confirm reposting a post.")) {
if let current_boost {
self.damus_state?.pool.send(.event(current_boost))
}
}
} message: {
Text("Are you sure you want to repost this?", comment: "Alert message to ask if user wants to repost a post.")
}
}
func switch_timeline(_ timeline: Timeline) {
self.popToRoot()
NotificationCenter.default.post(name: .switched_timeline, object: timeline)
if timeline == self.selected_timeline {
@@ -596,42 +425,21 @@ struct ContentView: View {
func connect() {
let pool = RelayPool()
let metadatas = RelayMetadatas()
let relay_filters = RelayFilters(our_pubkey: pubkey)
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
let new_relay_filters = load_relay_filters(pubkey) == nil
for relay in bootstrap_relays {
if let url = URL(string: relay) {
add_new_relay(relay_filters: relay_filters, metadatas: metadatas, pool: pool, url: url, info: .rw, new_relay_filters: new_relay_filters)
}
for relay in BOOTSTRAP_RELAYS {
add_relay(pool, relay)
}
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
let settings = UserSettingsStore()
self.damus_state = DamusState(pool: pool,
keypair: keypair,
likes: EventCounter(our_pubkey: pubkey),
boosts: EventCounter(our_pubkey: pubkey),
contacts: Contacts(our_pubkey: pubkey),
tips: TipCounter(our_pubkey: pubkey),
profiles: Profiles(),
dms: home.dms,
previews: PreviewCache(),
zaps: Zaps(our_pubkey: pubkey),
lnurls: LNUrls(),
settings: settings,
relay_filters: relay_filters,
relay_metadata: metadatas,
drafts: Drafts(),
events: EventCache(),
bookmarks: BookmarksManager(pubkey: pubkey),
postbox: PostBox(pool: pool),
bootstrap_relays: bootstrap_relays,
replies: ReplyCounter(our_pubkey: pubkey),
translations: Translations(settings)
self.damus_state = DamusState(pool: pool, keypair: keypair,
likes: EventCounter(our_pubkey: pubkey),
boosts: EventCounter(our_pubkey: pubkey),
contacts: Contacts(our_pubkey: pubkey),
tips: TipCounter(our_pubkey: pubkey),
profiles: Profiles(),
dms: home.dms,
previews: PreviewCache()
)
home.damus_state = self.damus_state!
@@ -780,69 +588,3 @@ func setup_notifications() {
}
}
func find_event(state: DamusState, evid: String, search_type: SearchType, find_from: [String]?, callback: @escaping (NostrEvent?) -> ()) {
if let ev = state.events.lookup(evid) {
callback(ev)
return
}
let subid = UUID().description
var has_event = false
var filter = search_type == .event ? NostrFilter.filter_ids([ evid ]) : NostrFilter.filter_authors([ evid ])
if search_type == .profile {
filter.kinds = [0]
}
filter.limit = 1
var attempts = 0
state.pool.subscribe_to(sub_id: subid, filters: [filter], to: find_from) { relay_id, res in
guard case .nostr_event(let ev) = res else {
return
}
guard ev.subid == subid else {
return
}
switch ev {
case .ok:
break
case .event(_, let ev):
has_event = true
callback(ev)
state.pool.unsubscribe(sub_id: subid)
case .eose:
if !has_event {
attempts += 1
if attempts == state.pool.descriptors.count / 2 {
callback(nil)
}
state.pool.unsubscribe(sub_id: subid, to: [relay_id])
}
case .notice(_):
break
}
}
}
func timeline_name(_ timeline: Timeline?) -> String {
guard let timeline else {
return ""
}
switch timeline {
case .home:
return NSLocalizedString("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.")
case .notifications:
return NSLocalizedString("Notifications", comment: "Toolbar label for Notifications view.")
case .search:
return NSLocalizedString("Universe 🛸", comment: "Toolbar label for the universal view where posts from all connected relay servers appear.")
case .dms:
return NSLocalizedString("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
}
}
+1 -14
View File
@@ -14,16 +14,6 @@
<string>nostr</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLName</key>
<string>io.damus</string>
<key>CFBundleURLSchemes</key>
<array>
<string>damus</string>
</array>
</dict>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
@@ -34,6 +24,7 @@
<string>zeusln</string>
<string>zebedee</string>
<string>lightning</string>
<string>squarecash</string>
<string>phoenix</string>
<string>lnlink</string>
<string>strike</string>
@@ -46,9 +37,5 @@
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSCameraUsageDescription</key>
<string>Damus needs access to your camera if you want to upload photos from it</string>
<key>NSMicrophoneUsageDescription</key>
<string>Damus needs access to your microphone if you want to upload recorded videos from it</string>
</dict>
</plist>
+9 -32
View File
@@ -11,59 +11,36 @@ import Foundation
class ActionBarModel: ObservableObject {
@Published var our_like: NostrEvent?
@Published var our_boost: NostrEvent?
@Published var our_reply: NostrEvent?
@Published var our_zap: Zap?
@Published var our_tip: NostrEvent?
@Published var likes: Int
@Published var boosts: Int
@Published var zaps: Int
@Published var zap_total: Int64
@Published var replies: Int
@Published var tips: Int64
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, tips: 0, our_like: nil, our_boost: nil, our_tip: nil)
}
init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, replies: Int, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zap?, our_reply: NostrEvent?) {
init(likes: Int, boosts: Int, tips: Int64, our_like: NostrEvent?, our_boost: NostrEvent?, our_tip: NostrEvent?) {
self.likes = likes
self.boosts = boosts
self.zaps = zaps
self.replies = replies
self.zap_total = zap_total
self.tips = tips
self.our_like = our_like
self.our_boost = our_boost
self.our_zap = our_zap
self.our_reply = our_reply
}
func update(damus: DamusState, evid: String) {
self.likes = damus.likes.counts[evid] ?? 0
self.boosts = damus.boosts.counts[evid] ?? 0
self.zaps = damus.zaps.event_counts[evid] ?? 0
self.replies = damus.replies.get_replies(evid)
self.zap_total = damus.zaps.event_totals[evid] ?? 0
self.our_like = damus.likes.our_events[evid]
self.our_boost = damus.boosts.our_events[evid]
self.our_zap = damus.zaps.our_zaps[evid]?.first
self.our_reply = damus.replies.our_reply(evid)
self.objectWillChange.send()
self.our_tip = our_tip
}
var is_empty: Bool {
return likes == 0 && boosts == 0 && zaps == 0
return likes == 0 && boosts == 0 && tips == 0
}
var zapped: Bool {
return our_zap != nil
var tipped: Bool {
return our_tip != nil
}
var liked: Bool {
return our_like != nil
}
var replied: Bool {
return our_reply != nil
}
var boosted: Bool {
return our_boost != nil
}
-71
View File
@@ -1,71 +0,0 @@
//
// BookmarksManager.swift
// damus
//
// Created by Joel Klabo on 2/18/23.
//
import Foundation
fileprivate func get_bookmarks_key(pubkey: String) -> String {
pk_setting_key(pubkey, key: "bookmarks")
}
func load_bookmarks(pubkey: String) -> [NostrEvent] {
let key = get_bookmarks_key(pubkey: pubkey)
return (UserDefaults.standard.stringArray(forKey: key) ?? []).compactMap {
event_from_json(dat: $0)
}
}
func save_bookmarks(pubkey: String, current_value: [NostrEvent], value: [NostrEvent]) -> Bool {
let uniq_bookmarks = Array(Set(value))
if uniq_bookmarks != current_value {
let encoded = uniq_bookmarks.map(event_to_json)
UserDefaults.standard.set(encoded, forKey: get_bookmarks_key(pubkey: pubkey))
return true
}
return false
}
class BookmarksManager: ObservableObject {
private let userDefaults = UserDefaults.standard
private let pubkey: String
private var _bookmarks: [NostrEvent]
var bookmarks: [NostrEvent] {
get {
return _bookmarks
}
set {
if save_bookmarks(pubkey: pubkey, current_value: _bookmarks, value: newValue) {
self._bookmarks = newValue
self.objectWillChange.send()
}
}
}
init(pubkey: String) {
self._bookmarks = load_bookmarks(pubkey: pubkey)
self.pubkey = pubkey
}
func isBookmarked(_ ev: NostrEvent) -> Bool {
return bookmarks.contains(ev)
}
func updateBookmark(_ ev: NostrEvent) {
if isBookmarked(ev) {
bookmarks = bookmarks.filter { $0 != ev }
} else {
bookmarks.append(ev)
}
}
func clearAll() {
bookmarks = []
}
}
+2 -40
View File
@@ -11,51 +11,13 @@ import Foundation
class Contacts {
private var friends: Set<String> = Set()
private var friend_of_friends: Set<String> = Set()
private var muted: Set<String> = Set()
let our_pubkey: String
var event: NostrEvent?
var mutelist: NostrEvent?
init(our_pubkey: String) {
self.our_pubkey = our_pubkey
}
func is_muted(_ pk: String) -> Bool {
return muted.contains(pk)
}
func set_mutelist(_ ev: NostrEvent) {
let oldlist = self.mutelist
self.mutelist = ev
let old = Set(oldlist?.referenced_pubkeys.map({ $0.ref_id }) ?? [])
let new = Set(ev.referenced_pubkeys.map({ $0.ref_id }))
let diff = old.symmetricDifference(new)
var new_mutes = Array<String>()
var new_unmutes = Array<String>()
for d in diff {
if new.contains(d) {
new_mutes.append(d)
} else {
new_unmutes.append(d)
}
}
// TODO: set local mutelist here
self.muted = Set(ev.referenced_pubkeys.map({ $0.ref_id }))
if new_mutes.count > 0 {
notify(.new_mutes, new_mutes)
}
if new_unmutes.count > 0 {
notify(.new_unmutes, new_unmutes)
}
}
func get_friendosphere() -> [String] {
var fs = get_friend_list()
fs.append(contentsOf: get_friend_of_friend_list())
@@ -140,7 +102,7 @@ func follow_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, pri
return ev
}
func unfollow_user(postbox: PostBox, our_contacts: NostrEvent?, pubkey: String, privkey: String, unfollow: String) -> NostrEvent? {
func unfollow_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, privkey: String, unfollow: String) -> NostrEvent? {
guard let cs = our_contacts else {
return nil
}
@@ -149,7 +111,7 @@ func unfollow_user(postbox: PostBox, our_contacts: NostrEvent?, pubkey: String,
ev.calculate_id()
ev.sign(privkey: privkey)
postbox.send(ev)
pool.send(.event(ev))
return ev
}
-1
View File
@@ -14,7 +14,6 @@ class CreateAccountModel: ObservableObject {
@Published var about: String = ""
@Published var pubkey: String = ""
@Published var privkey: String = ""
@Published var profile_image: String? = nil
var pubkey_bech32: String {
return bech32_pubkey(self.pubkey) ?? ""
+4 -15
View File
@@ -18,28 +18,17 @@ struct DamusState {
let profiles: Profiles
let dms: DirectMessagesModel
let previews: PreviewCache
let zaps: Zaps
let lnurls: LNUrls
let settings: UserSettingsStore
let relay_filters: RelayFilters
let relay_metadata: RelayMetadatas
let drafts: Drafts
let events: EventCache
let bookmarks: BookmarksManager
let postbox: PostBox
let bootstrap_relays: [String]
let replies: ReplyCounter
let translations: Translations
var pubkey: String {
return keypair.pubkey
}
var is_privkey_user: Bool {
keypair.privkey != nil
}
static var empty: DamusState {
let settings = UserSettingsStore()
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), translations: Translations(settings))
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache())
}
}
-35
View File
@@ -1,35 +0,0 @@
//
// DeepLPlan.swift
// damus
//
// Created by Terry Yiu on 2/3/23.
//
import Foundation
enum DeepLPlan: String, CaseIterable, Identifiable {
var id: String { self.rawValue }
struct Model: Identifiable, Hashable {
var id: String { self.tag }
var tag: String
var displayName: String
var url: String
}
case free
case pro
var model: Model {
switch self {
case .free:
return .init(tag: self.rawValue, displayName: NSLocalizedString("Free", comment: "Dropdown option for selecting Free plan for DeepL translation service."), url: "https://api-free.deepl.com")
case .pro:
return .init(tag: self.rawValue, displayName: NSLocalizedString("Pro", comment: "Dropdown option for selecting Pro plan for DeepL translation service."), url: "https://api.deepl.com")
}
}
static var allModels: [Model] {
return Self.allCases.map { $0.model }
}
}
-4
View File
@@ -13,8 +13,6 @@ class DirectMessageModel: ObservableObject {
is_request = determine_is_request()
}
}
@Published var draft: String
var is_request: Bool
var our_pubkey: String
@@ -33,13 +31,11 @@ class DirectMessageModel: ObservableObject {
self.events = events
self.is_request = false
self.our_pubkey = our_pubkey
self.draft = ""
}
init(our_pubkey: String) {
self.events = []
self.is_request = false
self.our_pubkey = our_pubkey
self.draft = ""
}
}
-13
View File
@@ -1,13 +0,0 @@
//
// DraftModel.swift
// damus
//
// Created by Terry Yiu on 2/12/23.
//
import Foundation
class Drafts: ObservableObject {
@Published var post: NSMutableAttributedString = NSMutableAttributedString(string: "")
@Published var replies: [NostrEvent: NSMutableAttributedString] = [:]
}
+2 -58
View File
@@ -9,65 +9,9 @@ import Foundation
class EventsModel: ObservableObject {
let state: DamusState
let target: String
let kind: NostrKind
let sub_id = UUID().uuidString
let profiles_id = UUID().uuidString
var has_event: Set<String> = Set()
@Published var events: [NostrEvent] = []
init(state: DamusState, target: String, kind: NostrKind) {
self.state = state
self.target = target
self.kind = kind
}
private func get_filter() -> NostrFilter {
var filter = NostrFilter.filter_kinds([kind.rawValue])
filter.referenced_ids = [target]
filter.limit = 500
return filter
}
func subscribe() {
state.pool.subscribe(sub_id: sub_id,
filters: [get_filter()],
handler: handle_nostr_event)
}
func unsubscribe() {
state.pool.unsubscribe(sub_id: sub_id)
}
private func handle_event(relay_id: String, ev: NostrEvent) {
guard ev.kind == kind.rawValue else {
return
}
guard last_etag(tags: ev.tags) == target else {
return
}
if insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { a, b in a.created_at < b.created_at } ) {
objectWillChange.send()
}
}
func handle_nostr_event(relay_id: String, ev: NostrConnectionEvent) {
guard case .nostr_event(let nev) = ev else {
return
}
switch nev {
case .event(_, let ev):
handle_event(relay_id: relay_id, ev: ev)
case .notice(_):
break
case .ok:
break
case .eose(_):
load_profiles(profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state)
}
init() {
}
}
+6 -5
View File
@@ -51,7 +51,11 @@ class FollowersModel: ObservableObject {
if has_contact.contains(ev.pubkey) {
return
}
process_contact_event(state: damus_state, ev: ev)
process_contact_event(
pool: damus_state.pool,
contacts: damus_state.contacts,
pubkey: damus_state.pubkey, ev: ev
)
contacts?.append(ev.pubkey)
has_contact.insert(ev.pubkey)
}
@@ -82,7 +86,7 @@ class FollowersModel: ObservableObject {
if ev.known_kind == .contacts {
handle_contact_event(ev)
} else if ev.known_kind == .metadata {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
process_metadata_event(profiles: damus_state.profiles, ev: ev)
}
case .notice(let msg):
@@ -94,9 +98,6 @@ class FollowersModel: ObservableObject {
} else if sub_id == self.profiles_id {
damus_state.pool.unsubscribe(sub_id: profiles_id, to: [relay_id])
}
case .ok:
break
}
}
}
+1 -3
View File
@@ -58,11 +58,9 @@ class FollowingModel {
break
case .nostr_event(let nev):
switch nev {
case .ok:
break
case .event(_, let ev):
if ev.kind == 0 {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
process_metadata_event(profiles: damus_state.profiles, ev: ev)
}
case .notice(let msg):
print("followingmodel notice: \(msg)")
+59 -439
View File
@@ -38,9 +38,6 @@ class HomeModel: ObservableObject {
var channels: [String: NostrEvent] = [:]
var last_event_of_kind: [String: [Int: NostrEvent]] = [:]
var done_init: Bool = false
var incoming_dms: [NostrEvent] = []
let dm_debouncer = Debouncer(interval: 0.5)
var should_debounce_dms = true
let home_subid = UUID().description
let contacts_subid = UUID().description
@@ -50,33 +47,25 @@ class HomeModel: ObservableObject {
let profiles_subid = UUID().description
@Published var new_events: NewEventsBits = NewEventsBits()
@Published var notifications = NotificationsModel()
@Published var notifications: [NostrEvent] = []
@Published var dms: DirectMessagesModel
@Published var events = EventHolder()
@Published var events: [NostrEvent] = []
@Published var loading: Bool = false
@Published var signal: SignalModel = SignalModel()
init() {
self.damus_state = DamusState.empty
self.dms = DirectMessagesModel(our_pubkey: "")
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
}
init(damus_state: DamusState) {
self.damus_state = damus_state
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
self.setup_debouncer()
}
var pool: RelayPool {
return damus_state.pool
}
func setup_debouncer() {
// turn off debouncer after initial load
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
self.should_debounce_dms = false
}
}
func has_sub_id_event(sub_id: String, ev_id: String) -> Bool {
if !has_event.keys.contains(sub_id) {
@@ -109,8 +98,6 @@ class HomeModel: ObservableObject {
handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
case .metadata:
handle_metadata_event(ev)
case .list:
handle_list_event(ev)
case .boost:
handle_boost_event(sub_id: sub_id, ev)
case .like:
@@ -123,75 +110,9 @@ class HomeModel: ObservableObject {
handle_channel_create(ev)
case .channel_meta:
handle_channel_meta(ev)
case .zap:
handle_zap_event(ev)
case .zap_request:
break
}
}
func handle_zap_event_with_zapper(profiles: Profiles, ev: NostrEvent, our_keypair: Keypair, zapper: String) {
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
return
}
damus_state.zaps.add_zap(zap: zap)
guard zap.target.pubkey == our_keypair.pubkey else {
return
}
if !notifications.insert_zap(zap) {
return
}
if handle_last_event(ev: ev, timeline: .notifications) {
if damus_state.settings.zap_vibration {
// Generate zap vibration
zap_vibrate(zap_amount: zap.invoice.amount)
}
if damus_state.settings.zap_notification {
// Create in-app local notification for zap received.
create_in_app_zap_notification(profiles: profiles, zap: zap)
}
}
return
}
func handle_zap_event(_ ev: NostrEvent) {
// These are zap notifications
guard let ptag = event_tag(ev, name: "p") else {
return
}
let our_keypair = damus_state.keypair
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) {
handle_zap_event_with_zapper(profiles: self.damus_state.profiles, ev: ev, our_keypair: our_keypair, zapper: local_zapper)
return
}
guard let profile = damus_state.profiles.lookup(id: ptag) else {
return
}
guard let lnurl = profile.lnurl else {
return
}
Task {
guard let zapper = await fetch_zapper_from_lnurl(lnurl) else {
return
}
DispatchQueue.main.async {
self.damus_state.profiles.zappers[ptag] = zapper
self.handle_zap_event_with_zapper(profiles: self.damus_state.profiles, ev: ev, our_keypair: our_keypair, zapper: zapper)
}
}
}
func handle_channel_create(_ ev: NostrEvent) {
guard ev.is_valid else {
return
@@ -203,12 +124,6 @@ class HomeModel: ObservableObject {
func handle_channel_meta(_ ev: NostrEvent) {
}
func filter_muted() {
events.filter { !damus_state.contacts.is_muted($0.pubkey) }
self.dms.dms = dms.dms.filter { !damus_state.contacts.is_muted($0.0) }
notifications.filter { !damus_state.contacts.is_muted($0.pubkey) }
}
func handle_delete_event(_ ev: NostrEvent) {
guard ev.is_valid else {
return
@@ -218,7 +133,7 @@ class HomeModel: ObservableObject {
}
func handle_contact_event(sub_id: String, relay_id: String, ev: NostrEvent) {
process_contact_event(state: self.damus_state, ev: ev)
process_contact_event(pool: damus_state.pool, contacts: damus_state.contacts, pubkey: damus_state.pubkey, ev: ev)
if sub_id == init_subid {
pool.send(.unsubscribe(init_subid), to: [relay_id])
@@ -238,7 +153,7 @@ class HomeModel: ObservableObject {
guard inner_ev.is_valid else {
return
}
if inner_ev.is_textlike {
handle_text_event(sub_id: sub_id, ev)
}
@@ -254,7 +169,6 @@ class HomeModel: ObservableObject {
case .success(let n):
let boosted = Counted(event: ev, id: e, total: n)
notify(.boosted, boosted)
notify(.update_stats, e)
}
}
@@ -264,14 +178,14 @@ class HomeModel: ObservableObject {
return
}
// CHECK SIGS ON THESE
switch damus_state.likes.add_event(ev, target: e.ref_id) {
case .already_counted:
break
case .success(let n):
handle_notification(ev: ev)
let liked = Counted(event: ev, id: e.ref_id, total: n)
notify(.liked, liked)
notify(.update_stats, e.ref_id)
}
}
@@ -312,7 +226,7 @@ class HomeModel: ObservableObject {
switch ev {
case .event(let sub_id, let ev):
// globally handle likes
let always_process = sub_id == notifications_subid || sub_id == contacts_subid || sub_id == home_subid || sub_id == dms_subid || sub_id == init_subid || ev.known_kind == .like || ev.known_kind == .boost || ev.known_kind == .zap || ev.known_kind == .contacts || ev.known_kind == .metadata
let always_process = sub_id == notifications_subid || sub_id == contacts_subid || sub_id == home_subid || sub_id == dms_subid || sub_id == init_subid || ev.known_kind == .like || ev.known_kind == .contacts || ev.known_kind == .metadata
if !always_process {
// TODO: other views like threads might have their own sub ids, so ignore those events... or should we?
return
@@ -326,20 +240,15 @@ class HomeModel: ObservableObject {
case .eose(let sub_id):
if sub_id == dms_subid {
var dms = dms.dms.flatMap { $0.1.events }
dms.append(contentsOf: incoming_dms)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(dms), damus_state: damus_state)
let dms = dms.dms.flatMap { $0.1.events }
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: dms, damus_state: damus_state)
} else if sub_id == notifications_subid {
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_keys(notifications.uniq_pubkeys()), damus_state: damus_state)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: notifications, damus_state: damus_state)
}
self.loading = false
break
case .ok:
break
}
}
}
@@ -365,11 +274,7 @@ class HomeModel: ObservableObject {
var our_contacts_filter = NostrFilter.filter_kinds([3, 0])
our_contacts_filter.authors = [damus_state.pubkey]
var our_blocklist_filter = NostrFilter.filter_kinds([30000])
our_blocklist_filter.parameter = ["mute"]
our_blocklist_filter.authors = [damus_state.pubkey]
var dms_filter = NostrFilter.filter_kinds([
NostrKind.dm.rawValue,
])
@@ -387,6 +292,7 @@ class HomeModel: ObservableObject {
// TODO: separate likes?
var home_filter = NostrFilter.filter_kinds([
NostrKind.text.rawValue,
NostrKind.chat.rawValue,
NostrKind.like.rawValue,
NostrKind.boost.rawValue,
])
@@ -396,16 +302,16 @@ class HomeModel: ObservableObject {
var notifications_filter = NostrFilter.filter_kinds([
NostrKind.text.rawValue,
NostrKind.chat.rawValue,
NostrKind.like.rawValue,
NostrKind.boost.rawValue,
NostrKind.zap.rawValue,
])
notifications_filter.pubkeys = [damus_state.pubkey]
notifications_filter.limit = 500
notifications_filter.limit = 100
var home_filters = [home_filter]
var notifications_filters = [notifications_filter]
var contacts_filters = [contacts_filter, our_contacts_filter, our_blocklist_filter]
var contacts_filters = [contacts_filter, our_contacts_filter]
var dms_filters = [dms_filter, our_dms_filter]
let last_of_kind = relay_id.flatMap { last_event_of_kind[$0] } ?? [:]
@@ -429,32 +335,9 @@ class HomeModel: ObservableObject {
pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)))
}
}
func handle_list_event(_ ev: NostrEvent) {
// we only care about our lists
guard ev.pubkey == damus_state.pubkey else {
return
}
if let mutelist = damus_state.contacts.mutelist {
if ev.created_at <= mutelist.created_at {
return
}
}
guard let name = get_referenced_ids(tags: ev.tags, key: "d").first else {
return
}
guard name.ref_id == "mute" else {
return
}
damus_state.contacts.set_mutelist(ev)
}
func handle_metadata_event(_ ev: NostrEvent) {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
process_metadata_event(profiles: damus_state.profiles, ev: ev)
}
func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? {
@@ -465,93 +348,52 @@ class HomeModel: ObservableObject {
return m[kind]
}
func handle_notification(ev: NostrEvent) {
// don't show notifications from ourselves
guard ev.pubkey != damus_state.pubkey else {
return
}
guard event_has_our_pubkey(ev, our_pubkey: self.damus_state.pubkey) else {
return
}
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
if !insert_uniq_sorted_event(events: &notifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) {
return
}
damus_state.events.insert(ev)
if let inner_ev = ev.inner_event {
damus_state.events.insert(inner_ev)
}
if !notifications.insert_event(ev) {
return
}
if handle_last_event(ev: ev, timeline: .notifications) {
process_local_notification(damus_state: damus_state, event: ev)
}
}
@discardableResult
func handle_last_event(ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) -> Bool {
handle_last_event(ev: ev, timeline: .notifications)
}
func handle_last_event(ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) {
if let new_bits = handle_last_events(new_events: self.new_events, ev: ev, timeline: timeline, shouldNotify: shouldNotify) {
new_events = new_bits
return true
} else {
return false
}
}
func insert_home_event(_ ev: NostrEvent) {
if events.insert(ev) {
func insert_home_event(_ ev: NostrEvent) -> Bool {
let ok = insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at })
if ok {
handle_last_event(ev: ev, timeline: .home)
}
return ok
}
func should_hide_event(_ ev: NostrEvent) -> Bool {
return !ev.should_show_event
}
func handle_text_event(sub_id: String, _ ev: NostrEvent) {
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
if should_hide_event(ev) {
return
}
damus_state.replies.count_replies(ev)
damus_state.events.insert(ev)
if sub_id == home_subid {
insert_home_event(ev)
let _ = insert_home_event(ev)
} else if sub_id == notifications_subid {
handle_notification(ev: ev)
}
}
func handle_dm(_ ev: NostrEvent) {
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
return
}
if !should_debounce_dms {
self.incoming_dms.append(ev)
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
self.new_events = notifs
}
self.incoming_dms = []
return
}
incoming_dms.append(ev)
dm_debouncer.debounce { [self] in
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
self.new_events = notifs
if damus_state.settings.dm_notification,
let displayName = damus_state.profiles.lookup(id: self.incoming_dms.last!.pubkey)?.display_name {
create_local_notification(displayName: displayName, conversation: "You have received a direct message", type: .dm)
}
}
self.incoming_dms = []
if let notifs = handle_incoming_dm(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, ev: ev) {
self.new_events = notifs
}
}
}
@@ -657,17 +499,10 @@ func print_filters(relay_id: String?, filters groups: [[NostrFilter]]) {
print("-----")
}
func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEvent) {
func process_metadata_event(profiles: Profiles, ev: NostrEvent) {
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
return
}
if our_pubkey == ev.pubkey && (profile.deleted ?? false) {
DispatchQueue.main.async {
notify(.deleted_account, ())
}
return
}
var old_nip05: String? = nil
if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) {
@@ -678,7 +513,7 @@ func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEve
}
}
let tprof = TimestampedProfile(profile: profile, timestamp: ev.created_at, event: ev)
let tprof = TimestampedProfile(profile: profile, timestamp: ev.created_at)
profiles.add(id: ev.pubkey, profile: tprof)
if let nip05 = profile.nip05, old_nip05 != profile.nip05 {
@@ -690,7 +525,6 @@ func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEve
DispatchQueue.main.async {
profiles.validated[ev.pubkey] = validated
profiles.nip05_pubkey[nip05] = ev.pubkey
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
}
}
@@ -698,14 +532,14 @@ func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEve
// load pfps asap
let picture = tprof.profile.picture ?? robohash(ev.pubkey)
if URL(string: picture) != nil {
if let _ = URL(string: picture) {
DispatchQueue.main.async {
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
}
}
let banner = tprof.profile.banner ?? ""
if URL(string: banner) != nil {
if let _ = URL(string: banner) {
DispatchQueue.main.async {
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
}
@@ -718,33 +552,33 @@ func robohash(_ pk: String) -> String {
return "https://robohash.org/" + pk
}
func load_our_stuff(state: DamusState, ev: NostrEvent) {
guard ev.pubkey == state.pubkey else {
func load_our_stuff(pool: RelayPool, contacts: Contacts, pubkey: String, ev: NostrEvent) {
guard ev.pubkey == pubkey else {
return
}
// only use new stuff
if let current_ev = state.contacts.event {
if let current_ev = contacts.event {
guard ev.created_at > current_ev.created_at else {
return
}
}
let m_old_ev = state.contacts.event
state.contacts.event = ev
let m_old_ev = contacts.event
contacts.event = ev
load_our_contacts(contacts: state.contacts, our_pubkey: state.pubkey, m_old_ev: m_old_ev, ev: ev)
load_our_relays(state: state, m_old_ev: m_old_ev, ev: ev)
load_our_contacts(contacts: contacts, our_pubkey: pubkey, m_old_ev: m_old_ev, ev: ev)
load_our_relays(contacts: contacts, our_pubkey: pubkey, pool: pool, m_old_ev: m_old_ev, ev: ev)
}
func process_contact_event(state: DamusState, ev: NostrEvent) {
load_our_stuff(state: state, ev: ev)
add_contact_if_friend(contacts: state.contacts, ev: ev)
func process_contact_event(pool: RelayPool, contacts: Contacts, pubkey: String, ev: NostrEvent) {
load_our_stuff(pool: pool, contacts: contacts, pubkey: pubkey, ev: ev)
add_contact_if_friend(contacts: contacts, ev: ev)
}
func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
func load_our_relays(contacts: Contacts, our_pubkey: String, pool: RelayPool, m_old_ev: NostrEvent?, ev: NostrEvent) {
let bootstrap_dict: [String: RelayInfo] = [:]
let old_decoded = m_old_ev.flatMap { decode_json_relays($0.content) } ?? state.bootstrap_relays.reduce(into: bootstrap_dict) { (d, r) in
let old_decoded = m_old_ev.flatMap { decode_json_relays($0.content) } ?? BOOTSTRAP_RELAYS.reduce(into: bootstrap_dict) { (d, r) in
d[r] = .rw
}
@@ -766,79 +600,25 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
let diff = old.symmetricDifference(new)
let new_relay_filters = load_relay_filters(state.pubkey) == nil
for d in diff {
changed = true
if new.contains(d) {
if let url = URL(string: d) {
add_new_relay(relay_filters: state.relay_filters, metadatas: state.relay_metadata, pool: state.pool, url: url, info: decoded[d] ?? .rw, new_relay_filters: new_relay_filters)
try? pool.add_relay(url, info: decoded[d] ?? .rw)
}
} else {
state.pool.remove_relay(d)
pool.remove_relay(d)
}
}
if changed {
save_bootstrap_relays(pubkey: state.pubkey, relays: Array(new))
notify(.relays_changed, ())
}
}
func add_new_relay(relay_filters: RelayFilters, metadatas: RelayMetadatas, pool: RelayPool, url: URL, info: RelayInfo, new_relay_filters: Bool) {
try? pool.add_relay(url, info: info)
let relay_id = url.absoluteString
guard metadatas.lookup(relay_id: relay_id) == nil else {
return
}
Task.detached(priority: .background) {
guard let meta = try? await fetch_relay_metadata(relay_id: relay_id) else {
return
}
DispatchQueue.main.async {
metadatas.insert(relay_id: relay_id, metadata: meta)
// if this is the first time adding filters, we should filter non-paid relays
if new_relay_filters && !meta.is_paid {
relay_filters.insert(timeline: .search, relay_id: relay_id)
}
}
}
}
func fetch_relay_metadata(relay_id: String) async throws -> RelayMetadata? {
var urlString = relay_id.replacingOccurrences(of: "wss://", with: "https://")
urlString = urlString.replacingOccurrences(of: "ws://", with: "http://")
guard let url = URL(string: urlString) else {
return nil
}
var request = URLRequest(url: url)
request.setValue("application/nostr+json", forHTTPHeaderField: "Accept")
var res: (Data, URLResponse)? = nil
res = try await URLSession.shared.data(for: request)
guard let data = res?.0 else {
return nil
}
let nip11 = try JSONDecoder().decode(RelayMetadata.self, from: data)
return nip11
}
func process_relay_metadata() {
}
@discardableResult
func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesModel, prev_events: NewEventsBits) -> (Bool, NewEventsBits?) {
func handle_incoming_dm(prev_events: NewEventsBits, dms: DirectMessagesModel, our_pubkey: String, ev: NostrEvent) -> NewEventsBits? {
var inserted = false
var found = false
let ours = ev.pubkey == our_pubkey
var i = 0
@@ -865,34 +645,15 @@ func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesM
}
if !found {
inserted = true
let model = DirectMessageModel(events: [ev], our_pubkey: our_pubkey)
dms.dms.append((the_pk, model))
inserted = true
}
var new_bits: NewEventsBits? = nil
if inserted {
new_bits = handle_last_events(new_events: prev_events, ev: ev, timeline: .dms, shouldNotify: !ours)
}
return (inserted, new_bits)
}
@discardableResult
func handle_incoming_dms(prev_events: NewEventsBits, dms: DirectMessagesModel, our_pubkey: String, evs: [NostrEvent]) -> NewEventsBits? {
var inserted = false
var new_events: NewEventsBits? = nil
for ev in evs {
let res = handle_incoming_dm(ev: ev, our_pubkey: our_pubkey, dms: dms, prev_events: prev_events)
inserted = res.0 || inserted
if let new = res.1 {
new_events = new
}
}
if inserted {
new_events = handle_last_events(new_events: prev_events, ev: ev, timeline: .dms, shouldNotify: !ours)
dms.dms = dms.dms.filter({ $0.1.events.count > 0 }).sorted { a, b in
return a.1.events.last!.created_at > b.1.events.last!.created_at
}
@@ -927,144 +688,3 @@ func event_has_our_pubkey(_ ev: NostrEvent, our_pubkey: String) -> Bool {
return false
}
func should_show_event(contacts: Contacts, ev: NostrEvent) -> Bool {
if contacts.is_muted(ev.pubkey) {
return false
}
return ev.should_show_event
}
func zap_vibrate(zap_amount: Int64) {
let sats = zap_amount / 1000
var vibration_generator: UIImpactFeedbackGenerator
if sats >= 10000 {
vibration_generator = UIImpactFeedbackGenerator(style: .heavy)
} else if sats >= 1000 {
vibration_generator = UIImpactFeedbackGenerator(style: .medium)
} else {
vibration_generator = UIImpactFeedbackGenerator(style: .light)
}
vibration_generator.impactOccurred()
}
func zap_notification_title(_ zap: Zap) -> String {
if zap.private_request != nil {
return NSLocalizedString("Private Zap", comment: "Title of notification when a private zap is received.")
} else {
return NSLocalizedString("Zap", comment: "Title of notification when a non-private zap is received.")
}
}
func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) -> String {
let src = zap.private_request ?? zap.request.ev
let anon = event_is_anonymous(ev: src)
let pk = anon ? "anon" : src.pubkey
let profile = profiles.lookup(id: pk)
let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0))
let formattedSats = format_msats_abbrev(zap.invoice.amount)
let name = Profile.displayName(profile: profile, pubkey: pk).display_name
if src.content.isEmpty {
let format = localizedStringFormat(key: "zap_notification_no_message", locale: locale)
return String(format: format, locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats, name)
} else {
let format = localizedStringFormat(key: "zap_notification_with_message", locale: locale)
return String(format: format, locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats, name, src.content)
}
}
func create_in_app_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) {
let content = UNMutableNotificationContent()
content.title = zap_notification_title(zap)
content.body = zap_notification_body(profiles: profiles, zap: zap, locale: locale)
content.sound = UNNotificationSound.default
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let request = UNNotificationRequest(identifier: "myZapNotification", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("Error: \(error)")
} else {
print("Local notification scheduled")
}
}
}
func process_local_notification(damus_state: DamusState, event ev: NostrEvent) {
guard let type = ev.known_kind else {
return
}
if damus_state.settings.notification_only_from_following,
damus_state.contacts.follow_state(ev.pubkey) != .follows
{
return
}
if type == .text && damus_state.settings.mention_notification {
for block in ev.blocks(damus_state.keypair.privkey) {
if case .mention(let mention) = block, mention.ref.ref_id == damus_state.keypair.pubkey,
let displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name {
let justContent = NSAttributedString(render_note_content(ev: ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey).content).string
create_local_notification(displayName: displayName, conversation: justContent, type: type)
}
}
} else if type == .boost && damus_state.settings.repost_notification,
let displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name {
if let inner_ev = ev.inner_event {
create_local_notification(displayName: displayName, conversation: inner_ev.content, type: type)
}
} else if type == .like && damus_state.settings.like_notification,
let displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name,
let e_ref = ev.referenced_ids.first?.ref_id,
let content = damus_state.events.lookup(e_ref)?.content {
create_local_notification(displayName: displayName, conversation: content, type: type)
}
}
func create_local_notification(displayName: String, conversation: String, type: NostrKind) {
let content = UNMutableNotificationContent()
var title = ""
var identifier = ""
switch type {
case .text:
title = String(format: NSLocalizedString("Mentioned by %@", comment: "Mentioned by heading in local notification"), displayName)
identifier = "myMentionNotification"
case .boost:
title = String(format: NSLocalizedString("Reposted by %@", comment: "Reposted by heading in local notification"), displayName)
identifier = "myBoostNotification"
case .like:
title = String(format: NSLocalizedString("Liked by %@", comment: "Liked by heading in local notification"), displayName)
identifier = "myLikeNotification"
case .dm:
title = String(format: NSLocalizedString("DM by %@", comment: "DM by heading in local notification"), displayName)
identifier = "myDMNotification"
default:
break
}
content.title = title
content.body = conversation
content.sound = UNNotificationSound.default
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("Error: \(error)")
} else {
print("Local notification scheduled")
}
}
}
-54
View File
@@ -1,54 +0,0 @@
//
// ImageUploadModel.swift
// damus
//
// Created by William Casarin on 2023-03-16.
//
import Foundation
import UIKit
enum MediaUpload {
case image(URL)
case video(URL)
var genericFileName: String {
"damus_generic_filename.\(file_extension)"
}
var file_extension: String {
switch self {
case .image(let url):
return url.pathExtension
case .video(let url):
return url.pathExtension
}
}
var is_image: Bool {
if case .image = self {
return true
}
return false
}
}
class ImageUploadModel: NSObject, URLSessionTaskDelegate, ObservableObject {
@Published var progress: Double? = nil
func start(media: MediaUpload, uploader: MediaUploader) async -> ImageUploadResult {
let res = await create_upload_request(mediaToUpload: media, mediaUploader: uploader, progress: self)
DispatchQueue.main.async {
self.progress = nil
}
return res
}
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
DispatchQueue.main.async {
self.progress = Double(totalBytesSent) / Double(totalBytesExpectedToSend)
}
}
}
+108
View File
@@ -0,0 +1,108 @@
//
// KFImageModel.swift
// damus
//
// Created by Oleg Abalonski on 1/11/23.
//
import Foundation
import Kingfisher
import SVGKit
class KFImageModel: ObservableObject {
let url: URL?
let fallbackUrl: URL?
let processor: ImageProcessor
let serializer: CacheSerializer
@Published var refreshID = ""
init(url: URL?, fallbackUrl: URL?, maxByteSize: Int, downsampleSize: CGSize) {
self.url = url
self.fallbackUrl = fallbackUrl
self.processor = CustomImageProcessor(maxSize: maxByteSize, downsampleSize: downsampleSize)
self.serializer = CustomCacheSerializer(maxSize: maxByteSize, downsampleSize: downsampleSize)
}
func refresh() -> Void {
DispatchQueue.main.async {
self.refreshID = UUID().uuidString
}
}
func cache(_ image: UIImage, forKey key: String) -> Void {
KingfisherManager.shared.cache.store(image, forKey: key, processorIdentifier: processor.identifier) { _ in
self.refresh()
}
}
func downloadFailed() -> Void {
guard let url = url, let fallbackUrl = fallbackUrl else { return }
DispatchQueue.global(qos: .background).async {
KingfisherManager.shared.downloader.downloadImage(with: fallbackUrl) { result in
var fallbackImage: UIImage {
switch result {
case .success(let imageLoadingResult):
return imageLoadingResult.image
case .failure(let error):
print(error)
return UIImage()
}
}
self.cache(fallbackImage, forKey: url.absoluteString)
}
}
}
}
struct CustomImageProcessor: ImageProcessor {
let maxSize: Int
let downsampleSize: CGSize
let identifier = "com.damus.customimageprocessor"
func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
switch item {
case .image(_):
// This case will never run
return DefaultImageProcessor.default.process(item: item, options: options)
case .data(let data):
// Handle large image size
if data.count > maxSize {
return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
}
// Handle SVG image
if let svgImage = SVGKImage(data: data), let image = svgImage.uiImage {
return image.kf.scaled(to: options.scaleFactor)
}
return DefaultImageProcessor.default.process(item: item, options: options)
}
}
}
struct CustomCacheSerializer: CacheSerializer {
let maxSize: Int
let downsampleSize: CGSize
func data(with image: Kingfisher.KFCrossPlatformImage, original: Data?) -> Data? {
return DefaultCacheSerializer.default.data(with: image, original: original)
}
func image(with data: Data, options: Kingfisher.KingfisherParsedOptionsInfo) -> Kingfisher.KFCrossPlatformImage? {
if data.count > maxSize {
return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
}
return DefaultCacheSerializer.default.image(with: data, options: options)
}
}
-41
View File
@@ -1,41 +0,0 @@
//
// LibreTranslateServer.swift
// damus
//
// Created by Terry Yiu on 1/21/23.
//
import Foundation
enum LibreTranslateServer: String, CaseIterable, Identifiable {
var id: String { self.rawValue }
struct Model: Identifiable, Hashable {
var id: String { self.tag }
var tag: String
var displayName: String
var url: String?
}
case argosopentech
case terraprint
case vern
case custom
var model: Model {
switch self {
case .argosopentech:
return .init(tag: self.rawValue, displayName: "translate.argosopentech.com", url: "https://translate.argosopentech.com")
case .terraprint:
return .init(tag: self.rawValue, displayName: "translate.terraprint.co", url: "https://translate.terraprint.co")
case .vern:
return .init(tag: self.rawValue, displayName: "lt.vern.cc", url: "https://lt.vern.cc")
case .custom:
return .init(tag: self.rawValue, displayName: NSLocalizedString("Custom", comment: "Dropdown option for selecting a custom translation server."), url: nil)
}
}
static var allModels: [Model] {
return Self.allCases.map { $0.model }
}
}
+5 -4
View File
@@ -7,10 +7,6 @@
import Foundation
enum CountResult {
case already_counted
case success(Int)
}
class EventCounter {
var counts: [String: Int] = [:]
@@ -18,6 +14,11 @@ class EventCounter {
var our_events: [String: NostrEvent] = [:]
var our_pubkey: String
enum CountResult {
case already_counted
case success(Int)
}
init (our_pubkey: String) {
self.our_pubkey = our_pubkey
}
+16 -110
View File
@@ -32,30 +32,13 @@ struct IdBlock: Identifiable {
let block: Block
}
typealias Invoice = LightningInvoice<Amount>
typealias ZapInvoice = LightningInvoice<Int64>
enum InvoiceDescription {
case description(String)
case description_hash(Data)
}
struct LightningInvoice<T> {
let description: InvoiceDescription
let amount: T
struct Invoice {
let description: String
let amount: Amount
let string: String
let expiry: UInt64
let payment_hash: Data
let created_at: UInt64
var description_string: String {
switch description {
case .description(let string):
return string
case .description_hash:
return ""
}
}
}
enum Block {
@@ -94,14 +77,6 @@ enum Block {
return nil
}
var is_note_mention: Bool {
guard case .mention(let mention) = self else {
return false
}
return mention.type == .event
}
var is_mention: Bool {
if case .mention = self {
return true
@@ -214,78 +189,20 @@ enum Amount: Equatable {
case .any:
return NSLocalizedString("Any", comment: "Any amount of sats")
case .specific(let amt):
return format_msats(amt)
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
numberFormatter.minimumFractionDigits = 0
numberFormatter.maximumFractionDigits = 3
numberFormatter.roundingMode = .down
let sats = NSNumber(value: (Double(amt) / 1000.0))
let formattedSats = numberFormatter.string(from: sats) ?? sats.stringValue
return String(format: NSLocalizedString("sats_count", comment: "Amount of sats."), sats.decimalValue as NSDecimalNumber, formattedSats)
}
}
}
func format_actions_abbrev(_ actions: Int) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.positiveSuffix = "m"
formatter.positivePrefix = ""
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 3
formatter.roundingMode = .down
formatter.roundingIncrement = 0.1
formatter.multiplier = 1
if actions >= 1_000_000 {
formatter.positiveSuffix = "m"
formatter.multiplier = 0.000001
} else if actions >= 1000 {
formatter.positiveSuffix = "k"
formatter.multiplier = 0.001
} else {
return "\(actions)"
}
let actions = NSNumber(value: actions)
return formatter.string(from: actions) ?? "\(actions)"
}
func format_msats_abbrev(_ msats: Int64) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.positiveSuffix = "m"
formatter.positivePrefix = ""
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 3
formatter.roundingMode = .down
formatter.roundingIncrement = 0.1
formatter.multiplier = 1
let sats = NSNumber(value: (Double(msats) / 1000.0))
if msats >= 1_000_000*1000 {
formatter.positiveSuffix = "m"
formatter.multiplier = 0.000001
} else if msats >= 1000*1000 {
formatter.positiveSuffix = "k"
formatter.multiplier = 0.001
} else {
return sats.stringValue
}
return formatter.string(from: sats) ?? sats.stringValue
}
func format_msats(_ msat: Int64, locale: Locale = Locale.current) -> String {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
numberFormatter.minimumFractionDigits = 0
numberFormatter.maximumFractionDigits = 3
numberFormatter.roundingMode = .down
numberFormatter.locale = locale
let sats = NSNumber(value: (Double(msat) / 1000.0))
let formattedSats = numberFormatter.string(from: sats) ?? sats.stringValue
let format = localizedStringFormat(key: "sats_count", locale: locale)
return String(format: format, locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats)
}
func convert_invoice_block(_ b: invoice_block) -> Block? {
guard let invstr = strblock_to_string(b.invstr) else {
return nil
@@ -295,8 +212,9 @@ func convert_invoice_block(_ b: invoice_block) -> Block? {
return nil
}
guard let description = convert_invoice_description(b11: b11) else {
return nil
var description = ""
if b11.description != nil {
description = String(cString: b11.description)
}
let amount: Amount = maybe_pointee(b11.msat).map { .specific(Int64($0.millisatoshis)) } ?? .any
@@ -307,18 +225,6 @@ func convert_invoice_block(_ b: invoice_block) -> Block? {
return .invoice(Invoice(description: description, amount: amount, string: invstr, expiry: b11.expiry, payment_hash: payment_hash, created_at: created_at))
}
func convert_invoice_description(b11: bolt11) -> InvoiceDescription? {
if let desc = b11.description {
return .description(String(cString: desc))
}
if var deschash = maybe_pointee(b11.description_hash) {
return .description_hash(Data(bytes: &deschash, count: 32))
}
return nil
}
func convert_mention_block(ind: Int32, tags: [[String]]) -> Block?
{
let ind = Int(ind)
-18
View File
@@ -1,18 +0,0 @@
//
// ListModel.swift
// damus
//
// Created by William Casarin on 2023-01-25.
//
import Foundation
/*
class MutelistModel: ObservableObject {
let contacts: Contacts
@Published var users: [String]
}
*/
@@ -1,32 +0,0 @@
//
// ReactionGroup.swift
// damus
//
// Created by William Casarin on 2023-02-21.
//
import Foundation
class EventGroup {
var events: [NostrEvent]
var last_event_at: Int64 {
guard let first = self.events.first else {
return 0
}
return first.created_at
}
init() {
self.events = []
}
init(events: [NostrEvent]) {
self.events = events
}
func insert(_ ev: NostrEvent) -> Bool {
return insert_uniq_sorted_event_created(events: &events, new_ev: ev)
}
}
-59
View File
@@ -1,59 +0,0 @@
//
// ZapGroup.swift
// damus
//
// Created by William Casarin on 2023-02-21.
//
import Foundation
class ZapGroup {
var zaps: [Zap]
var msat_total: Int64
var zappers: Set<String>
var last_event_at: Int64 {
guard let first = zaps.first else {
return 0
}
return first.event.created_at
}
func zap_requests() -> [NostrEvent] {
zaps.map { z in
if let priv = z.private_request {
return priv
} else {
return z.request.ev
}
}
}
init(zaps: [Zap]) {
self.zaps = zaps
self.msat_total = 0
self.zappers = Set()
}
init() {
self.zaps = []
self.msat_total = 0
self.zappers = Set()
}
func insert(_ zap: Zap) -> Bool {
if !insert_uniq_sorted_zap_by_created(zaps: &zaps, new_zap: zap) {
return false
}
msat_total += zap.invoice.amount
if !zappers.contains(zap.request.ev.pubkey) {
zappers.insert(zap.request.ev.pubkey)
}
return true
}
}
-320
View File
@@ -1,320 +0,0 @@
//
// NotificationsModel.swift
// damus
//
// Created by William Casarin on 2023-02-21.
//
import Foundation
enum NotificationItem {
case repost(String, EventGroup)
case reaction(String, EventGroup)
case profile_zap(ZapGroup)
case event_zap(String, ZapGroup)
case reply(NostrEvent)
var is_reply: NostrEvent? {
if case .reply(let ev) = self {
return ev
}
return nil
}
var is_zap: ZapGroup? {
switch self {
case .profile_zap(let zapgrp):
return zapgrp
case .event_zap(_, let zapgrp):
return zapgrp
case .reaction:
return nil
case .reply:
return nil
case .repost:
return nil
}
}
var id: String {
switch self {
case .repost(let evid, _):
return "repost_" + evid
case .reaction(let evid, _):
return "reaction_" + evid
case .profile_zap:
return "profile_zap"
case .event_zap(let evid, _):
return "event_zap_" + evid
case .reply(let ev):
return "reply_" + ev.id
}
}
var last_event_at: Int64 {
switch self {
case .reaction(_, let evgrp):
return evgrp.last_event_at
case .repost(_, let evgrp):
return evgrp.last_event_at
case .profile_zap(let zapgrp):
return zapgrp.last_event_at
case .event_zap(_, let zapgrp):
return zapgrp.last_event_at
case .reply(let reply):
return reply.created_at
}
}
}
class NotificationsModel: ObservableObject, ScrollQueue {
var incoming_zaps: [Zap]
var incoming_events: [NostrEvent]
var should_queue: Bool
// mappings from events to
var zaps: [String: ZapGroup]
var profile_zaps: ZapGroup
var reactions: [String: EventGroup]
var reposts: [String: EventGroup]
var replies: [NostrEvent]
var has_reply: Set<String>
@Published var notifications: [NotificationItem]
init() {
self.zaps = [:]
self.reactions = [:]
self.reposts = [:]
self.replies = []
self.has_reply = Set()
self.should_queue = true
self.incoming_zaps = []
self.incoming_events = []
self.profile_zaps = ZapGroup()
self.notifications = []
}
func set_should_queue(_ val: Bool) {
self.should_queue = val
}
func uniq_pubkeys() -> [String] {
var pks = Set<String>()
for ev in incoming_events {
pks.insert(ev.pubkey)
}
for grp in reposts {
for ev in grp.value.events {
pks.insert(ev.pubkey)
}
}
for ev in replies {
pks.insert(ev.pubkey)
}
for zap in incoming_zaps {
pks.insert(zap.request.ev.pubkey)
}
return Array(pks)
}
func build_notifications() -> [NotificationItem] {
var notifs: [NotificationItem] = []
for el in zaps {
let evid = el.key
let zapgrp = el.value
let notif: NotificationItem = .event_zap(evid, zapgrp)
notifs.append(notif)
}
if !profile_zaps.zaps.isEmpty {
notifs.append(.profile_zap(profile_zaps))
}
for el in reposts {
let evid = el.key
let evgrp = el.value
notifs.append(.repost(evid, evgrp))
}
for el in reactions {
let evid = el.key
let evgrp = el.value
notifs.append(.reaction(evid, evgrp))
}
for reply in replies {
notifs.append(.reply(reply))
}
notifs.sort { $0.last_event_at > $1.last_event_at }
return notifs
}
private func insert_repost(_ ev: NostrEvent) -> Bool {
guard let reposted_ev = ev.inner_event else {
return false
}
let id = reposted_ev.id
if let evgrp = self.reposts[id] {
return evgrp.insert(ev)
} else {
let evgrp = EventGroup()
self.reposts[id] = evgrp
return evgrp.insert(ev)
}
}
private func insert_text(_ ev: NostrEvent) -> Bool {
guard !has_reply.contains(ev.id) else {
return false
}
has_reply.insert(ev.id)
replies.append(ev)
return true
}
private func insert_reaction(_ ev: NostrEvent) -> Bool {
guard let ref_id = ev.referenced_ids.last else {
return false
}
let id = ref_id.id
if let evgrp = self.reactions[id] {
return evgrp.insert(ev)
} else {
let evgrp = EventGroup()
self.reactions[id] = evgrp
return evgrp.insert(ev)
}
}
private func insert_event_immediate(_ ev: NostrEvent) -> Bool {
if ev.known_kind == .boost {
return insert_repost(ev)
} else if ev.known_kind == .like {
return insert_reaction(ev)
} else if ev.known_kind == .text {
return insert_text(ev)
}
return false
}
private func insert_zap_immediate(_ zap: Zap) -> Bool {
switch zap.target {
case .note(let notezt):
let id = notezt.note_id
if let zapgrp = self.zaps[notezt.note_id] {
return zapgrp.insert(zap)
} else {
let zapgrp = ZapGroup()
self.zaps[id] = zapgrp
return zapgrp.insert(zap)
}
case .profile:
return profile_zaps.insert(zap)
}
}
func insert_event(_ ev: NostrEvent) -> Bool {
if should_queue {
return insert_uniq_sorted_event_created(events: &incoming_events, new_ev: ev)
}
if insert_event_immediate(ev) {
self.notifications = build_notifications()
return true
}
return false
}
func insert_zap(_ zap: Zap) -> Bool {
if should_queue {
return insert_uniq_sorted_zap_by_created(zaps: &incoming_zaps, new_zap: zap)
}
if insert_zap_immediate(zap) {
self.notifications = build_notifications()
return true
}
return false
}
func filter(_ isIncluded: (NostrEvent) -> Bool) {
var changed = false
var count = 0
count = incoming_events.count
incoming_events = incoming_events.filter(isIncluded)
changed = changed || incoming_events.count != count
count = profile_zaps.zaps.count
profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request.ev) }
changed = changed || profile_zaps.zaps.count != count
for el in reactions {
count = el.value.events.count
el.value.events = el.value.events.filter(isIncluded)
changed = changed || el.value.events.count != count
}
for el in reposts {
count = el.value.events.count
el.value.events = el.value.events.filter(isIncluded)
changed = changed || el.value.events.count != count
}
for el in zaps {
count = el.value.zaps.count
el.value.zaps = el.value.zaps.filter {
isIncluded($0.request.ev)
}
changed = changed || el.value.zaps.count != count
}
count = replies.count
replies = replies.filter(isIncluded)
changed = changed || replies.count != count
if changed {
self.notifications = build_notifications()
}
}
func flush() -> Bool {
var inserted = false
for zap in incoming_zaps {
inserted = insert_zap_immediate(zap) || inserted
}
for event in incoming_events {
inserted = insert_event_immediate(event) || inserted
}
if inserted {
self.notifications = build_notifications()
}
return inserted
}
}
+8 -33
View File
@@ -8,38 +8,18 @@
import Foundation
class ProfileModel: ObservableObject, Equatable {
var events: EventHolder = EventHolder()
@Published var events: [NostrEvent] = []
@Published var contacts: NostrEvent? = nil
@Published var following: Int = 0
@Published var relays: [String: RelayInfo]? = nil
@Published var progress: Int = 0
let pubkey: String
let damus: DamusState
var seen_event: Set<String> = Set()
var sub_id = UUID().description
var prof_subid = UUID().description
func follows(pubkey: String) -> Bool {
guard let contacts = self.contacts else {
return false
}
for tag in contacts.tags {
guard tag.count >= 2 && tag[0] == "p" else {
continue
}
if tag[1] == pubkey {
return true
}
}
return false
}
func get_follow_target() -> FollowTarget {
if let contacts = contacts {
return .contact(contacts)
@@ -90,7 +70,7 @@ class ProfileModel: ObservableObject, Equatable {
}
func handle_profile_contact_event(_ ev: NostrEvent) {
process_contact_event(state: damus, ev: ev)
process_contact_event(pool: damus.pool, contacts: damus.contacts, pubkey: damus.pubkey, ev: ev)
// only use new stuff
if let current_ev = self.contacts {
@@ -113,13 +93,11 @@ class ProfileModel: ObservableObject, Equatable {
return
}
if ev.is_textlike || ev.known_kind == .boost {
if self.events.insert(ev) {
self.objectWillChange.send()
}
let _ = insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at})
} else if ev.known_kind == .contacts {
handle_profile_contact_event(ev)
} else if ev.known_kind == .metadata {
process_metadata_event(our_pubkey: damus.pubkey, profiles: damus.profiles, ev: ev)
process_metadata_event(profiles: damus.profiles, ev: ev)
}
seen_event.insert(ev.id)
}
@@ -129,18 +107,15 @@ class ProfileModel: ObservableObject, Equatable {
case .ws_event:
return
case .nostr_event(let resp):
guard resp.subid == self.sub_id || resp.subid == self.prof_subid else {
return
}
switch resp {
case .ok:
break
case .event(_, let ev):
case .event(let sid, let ev):
if sid != self.sub_id && sid != self.prof_subid {
return
}
add_event(ev)
case .notice(let notice):
notify(.notice, notice)
case .eose:
progress += 1
break
}
}
+65 -3
View File
@@ -8,9 +8,71 @@
import Foundation
final class ReactionsModel: EventsModel {
class ReactionsModel: ObservableObject {
let state: DamusState
let target: String
let sub_id: String
let profiles_id: String
init(state: DamusState, target: String) {
super.init(state: state, target: target, kind: .like)
@Published var reactions: [NostrEvent]
init (state: DamusState, target: String) {
self.state = state
self.target = target
self.sub_id = UUID().description
self.profiles_id = UUID().description
self.reactions = []
}
func get_filter() -> NostrFilter {
var filter = NostrFilter.filter_kinds([7])
filter.referenced_ids = [target]
filter.limit = 500
return filter
}
func subscribe() {
let filter = get_filter()
let filters = [filter]
self.state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_nostr_event)
}
func unsubscribe() {
self.state.pool.unsubscribe(sub_id: sub_id)
}
func handle_event(relay_id: String, ev: NostrEvent) {
guard ev.kind == 7 else {
return
}
guard let reacted_to = last_etag(tags: ev.tags) else {
return
}
guard reacted_to == self.target else {
return
}
if insert_uniq_sorted_event(events: &self.reactions, new_ev: ev, cmp: { a, b in a.created_at < b.created_at } ) {
objectWillChange.send()
}
}
func handle_nostr_event(relay_id: String, ev: NostrConnectionEvent) {
guard case .nostr_event(let nev) = ev else {
return
}
switch nev {
case .event(_, let ev):
handle_event(relay_id: relay_id, ev: ev)
case .notice(_):
break
case .eose(_):
load_profiles(profiles_subid: profiles_id, relay_id: relay_id, events: reactions, damus_state: state)
break
}
}
}

Some files were not shown because too many files have changed in this diff Show More