Compare commits
1 Commits
qr-code-sc
...
tyiu/local
| Author | SHA1 | Date | |
|---|---|---|---|
|
b6c811dedf
|
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,35 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: 'Bug: '
|
|
||||||
labels: bug, Needs recreation
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**What happens**
|
|
||||||
When I perform action ___, _____ happens.
|
|
||||||
|
|
||||||
**What I expect to happen**
|
|
||||||
I expect _______ to happen.
|
|
||||||
|
|
||||||
**Link to noteID, npub**
|
|
||||||
Provide link to relevant noteID, npub etc.
|
|
||||||
|
|
||||||
**Screenshots/video recording**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
|
|
||||||
** Versions **
|
|
||||||
Damus version: [e.g. 1.7.2 (1()]
|
|
||||||
Operating system version: [e.g. iOS 17.2.1]
|
|
||||||
Device: e.g. iPhone 13 Pro
|
|
||||||
|
|
||||||
**Steps To Reproduce**
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
1. Open Damus
|
|
||||||
2. Tap on ___
|
|
||||||
3. Action ____
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
||||||
27
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,27 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: 'Feature Request:'
|
|
||||||
labels: feature
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Have a go at filling out the User Story template below
|
|
||||||
|
|
||||||
As a Damus user who is _____________, I would like to _________________, so that I achieve ___________.
|
|
||||||
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
** When does this problem happen? **
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
||||||
32
.github/workflows/run-tests.yaml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: Run Test Suite
|
||||||
|
run-name: Testing ${{ github.ref }} by @${{ github.actor }}
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "master"
|
||||||
|
- "ci"
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run_tests:
|
||||||
|
runs-on: macos-12
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- xcode: "14.2"
|
||||||
|
ios: "16.2"
|
||||||
|
|
||||||
|
name: Test iOS (${{ matrix.ios }})
|
||||||
|
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]}
|
||||||
1
.mailmap
@@ -4,4 +4,3 @@ Suhail Saqan <suhail.saqan@gmail.com> <43693074+suhailsaqan@users.noreply.github
|
|||||||
cr0bar <cr0bar@cr0.bar> <cr0bar@users.noreply.github.com>
|
cr0bar <cr0bar@cr0.bar> <cr0bar@users.noreply.github.com>
|
||||||
Swift <scoder1747@gmail.com> <120697811+scoder1747@users.noreply.github.com>
|
Swift <scoder1747@gmail.com> <120697811+scoder1747@users.noreply.github.com>
|
||||||
Daniel D'Aquino <daniel@daquino.me> <patches@damus.io>
|
Daniel D'Aquino <daniel@daquino.me> <patches@damus.io>
|
||||||
Transifex <transifex@transifex.com> <43880903+transifex-integration[bot]@users.noreply.github.com>
|
|
||||||
|
|||||||
263
CHANGELOG.md
@@ -1,264 +1,3 @@
|
|||||||
## [1.9 (14)] - 2024-07-14
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Completely new threads experience that is easier and more pleasant to use (Daniel D’Aquino)
|
|
||||||
- Add emoji search to emoji picker (Terry Yiu)
|
|
||||||
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Added first aid contact damus support email (alltheseas)
|
|
||||||
- Disable mutiny wallet button (William Casarin)
|
|
||||||
- Make friends show up first when searching for profiles (Terry Yiu)
|
|
||||||
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix crash on profile page when there are profile updates (William Casarin)
|
|
||||||
- Fix crash when adding duplicate mute items (William Casarin)
|
|
||||||
- Fix pretty bad crash when building flatbuffer profiles (William Casarin)
|
|
||||||
- Fix reactions view to not show reactions from replies on parent note (Terry Yiu)
|
|
||||||
- Fix missing Mute button in profile view menu (Terry Yiu)
|
|
||||||
- Fixed wallet not disconnecting when a user logs out (ericholguin)
|
|
||||||
- Fix stale feed issue when follow list is too big (Daniel D’Aquino)
|
|
||||||
|
|
||||||
[1.9 (14)]: https://github.com/damus-io/damus/releases/tag/v1.9-14
|
|
||||||
|
|
||||||
## [1.8] - 2024-05-11
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Added nip10 marker replies (William Casarin)
|
|
||||||
- Add marker nip10 support when reading notes (William Casarin)
|
|
||||||
- Added title image and tags to longform events (ericholguin)
|
|
||||||
- Add First Aid solution for users who do not have a contact list created for their account (Daniel D’Aquino)
|
|
||||||
- Relay fees metadata (ericholguin)
|
|
||||||
- Added callbackuri for a better ux when connecting mutiny wallet nwc (ericholguin)
|
|
||||||
- Add event content preview to the full screen carousel (Daniel D’Aquino)
|
|
||||||
- Show list of quoted reposts in threads (William Casarin)
|
|
||||||
- Proxy Tags are now viewable on Selected Events (ericholguin)
|
|
||||||
- Connect to Mutiny Wallet Button (ericholguin)
|
|
||||||
- Add ability to mute words, add new mutelist interface (Charlie) (William Casarin)
|
|
||||||
- Add ability to mute hashtag from SearchView (Charlie Fish)
|
|
||||||
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Change reactions to use a native looking emoji picker (Terry Yiu)
|
|
||||||
- Relay detail design (ericholguin)
|
|
||||||
- Updated Zeus logo (ericholguin)
|
|
||||||
- Improve UX around video playback (Daniel D’Aquino)
|
|
||||||
- Moved paste nwc button to main wallet view (ericholguin)
|
|
||||||
- Errors with an NWC will show as an alert (ericholguin)
|
|
||||||
- Relay config view user interface (ericholguin)
|
|
||||||
- Always strip GPS data from images (kernelkind)
|
|
||||||
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix thread bug where a quote isn't picked up as a reply (William Casarin)
|
|
||||||
- Fixed threads not loading sometimes (William Casarin)
|
|
||||||
- Fixed issue where some replies were including the q tag (William Casarin)
|
|
||||||
- Fixed issue where timeline was scrolling when it isn't supposed to (William Casarin)
|
|
||||||
- Fix issue where bootstrap relays would inadvertently be added to the user's list on connectivity issues (Daniel D’Aquino)
|
|
||||||
- Fix broken GIF uploads (Daniel D’Aquino)
|
|
||||||
- Fix ghost notifications caused by Purple impending expiration notifications (Daniel D’Aquino)
|
|
||||||
- Improve reliability of contact list creation during onboarding (Daniel D’Aquino)
|
|
||||||
- Fix emoji reactions being cut off (ericholguin)
|
|
||||||
- Fix image indicators to limit number of dots to not spill screen beyond visible margins (ericholguin)
|
|
||||||
- Fix bug that would cause connection issues with relays defined with a trailing slash URL, and an inability to delete them. (Daniel D’Aquino)
|
|
||||||
- Issue where NWC Scanner view would not dismiss after a failed scan/paste (ericholguin)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[1.8]: https://github.com/damus-io/damus/releases/tag/v1.8
|
|
||||||
|
|
||||||
## [1.7-rc2] - 2024-02-28
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Add support for Apple In-App purchases (Daniel D’Aquino)
|
|
||||||
- Notification reminders for Damus Purple impending expiration (Daniel D’Aquino)
|
|
||||||
- Damus Purple membership! (William Casarin)
|
|
||||||
- Fixed minor spacing and padding issues in onboarding views (ericholguin)
|
|
||||||
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Disable inline text suggestions on 17.0 as they interfere with mention generation (William Casarin)
|
|
||||||
- EULA is not shown by default (ericholguin)
|
|
||||||
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix welcome screen not showing if the user enters the app directly after a successful checkout without going through the link (Daniel D’Aquino)
|
|
||||||
- Fix profile not updating bug (William Casarin)
|
|
||||||
- Fix nostrscripts not loading (William Casarin)
|
|
||||||
- Fix crash when accessing cached purple accounts (William Casarin)
|
|
||||||
- Hide member signup date on reposts (kernelkind)
|
|
||||||
- Fixed previews not rendering (ericholguin)
|
|
||||||
- Fix load media formatting on small screens (kernelkind)
|
|
||||||
- Fix shared nevents that are too long (kernelkind)
|
|
||||||
- Fix many nostrdb transaction related crashes (William Casarin)
|
|
||||||
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
- Removed copying public key action (ericholguin)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[1.7-rc2]: https://github.com/damus-io/damus/releases/tag/v1.7-rc2
|
|
||||||
|
|
||||||
## [1.7-2] - 2024-01-24
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- New fulltext search engine (William Casarin)
|
|
||||||
|
|
||||||
- Add "Always show onboarding suggestions" developer setting (Daniel D’Aquino)
|
|
||||||
- Add NIP-42 relay auth support (Charlie Fish)
|
|
||||||
- Add ability to hide suggested hashtags (ericholguin)
|
|
||||||
- Add ability to mute hashtag from SearchView (Charlie Fish)
|
|
||||||
- Add ability to preview media taken with camera (Suhail Saqan)
|
|
||||||
- Add ability to search for naddr, nprofiles, nevents (kernelkind)
|
|
||||||
- Add experimental push notification support (Daniel D’Aquino)
|
|
||||||
- Add naddr link support (kernelkind)
|
|
||||||
- Add regional relay recommendations to Relay configuration view (currently for Japanese users only) (Daniel D’Aquino)
|
|
||||||
- Add regional relays for Germany (Daniel D’Aquino)
|
|
||||||
- Add regional relays for Thailand (Daniel D’Aquino)
|
|
||||||
- Added a custom camera view (Suhail Saqan)
|
|
||||||
- Always convert damus.io links to inline mentions (William Casarin)
|
|
||||||
- Unfurl profile name on remote push notifications (Daniel D’Aquino)
|
|
||||||
- Zap notification support for push notifications (Daniel D’Aquino)
|
|
||||||
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Generate nprofile/nevent links in share menus (kernelkind)
|
|
||||||
- Improve push notification support to match local notification support (Daniel D’Aquino)
|
|
||||||
- Move mute thread in menu so it's not clicked by accident (alltheseas)
|
|
||||||
- Prioritize friends when autocompleting (Charlie Fish)
|
|
||||||
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Add workaround to fix note language recognition and reduce wasteful translation requests (Terry Yiu)
|
|
||||||
- Allow mentioning users with punctuation characters in their names (kernelkind)
|
|
||||||
- Fix broken mentions when there is text is directly after (kernelkind)
|
|
||||||
- Fix crash on very large notes (Daniel D’Aquino)
|
|
||||||
- Fix crash when logging out and switching accounts (William Casarin)
|
|
||||||
- Fix duplicate notes getting written to nostrdb (William Casarin)
|
|
||||||
- Fix issue where adding relays might not work on corrupted contact lists (Charlie Fish)
|
|
||||||
- Fix onboarding post view not being dismissed under certain conditions (Daniel D’Aquino)
|
|
||||||
- Fix performance issue with gifs (William Casarin)
|
|
||||||
- Fix persistent local notifications even after logout (William Casarin)
|
|
||||||
- Fixed bug where sometimes notes from other profiles appear on profile pages (Charlie Fish)
|
|
||||||
- Remove extra space at the end of DM messages (kernelkind)
|
|
||||||
- Save current viewed image index when switching to fullscreen (kernelkind)
|
|
||||||
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
- Removed old nsec key warning, nsec automatically convert to npub when posting (kernelkind)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[1.7-2]: https://github.com/damus-io/damus/releases/tag/v1.7-2
|
|
||||||
## [1.6-25] - 2023-10-31
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Tap to dismiss keyboard on user status view (ericholguin)
|
|
||||||
- Add setting that allows users to optionally disable the new profile action sheet feature (Daniel D’Aquino)
|
|
||||||
- Add follow button to profile action sheet (Daniel D’Aquino)
|
|
||||||
- Added reaction counters to nostrdb (William Casarin)
|
|
||||||
- Record when profile is last fetched in nostrdb (William Casarin)
|
|
||||||
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Automatically load extra regional Japanese relays during account creation if user's region is set to Japan. (Daniel D’Aquino)
|
|
||||||
- Updated customize zap view (ericholguin)
|
|
||||||
- Users are now notified when you quote repost them (William Casarin)
|
|
||||||
- Save bandwidth by only fetching new profiles after a certain amount of time (William Casarin)
|
|
||||||
- Zap button on profile action sheet now zaps with a single click, while a long press brings custom zap view (Daniel D’Aquino)
|
|
||||||
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Use white font color in qrcode view (ericholguin)
|
|
||||||
- Fixed an issue where zapping would silently fail on default settings if the user does not have a lightning wallet preinstalled on their device. (Daniel D’Aquino)
|
|
||||||
|
|
||||||
|
|
||||||
[1.6-25]: https://github.com/damus-io/damus/releases/tag/v1.6-25
|
|
||||||
## [1.6-24] - 2023-10-22 - AppStore Rejection Cope
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Improve discoverability of profile zaps with zappability badges and profile action sheets (Daniel D’Aquino)
|
|
||||||
- Add suggested hashtags to universe view (Daniel D’Aquino)
|
|
||||||
- Suggest first post during onboarding (Daniel D’Aquino)
|
|
||||||
- Add expiry date for images in cache to be auto-deleted after a preset time to save space on storage (Daniel D’Aquino)
|
|
||||||
- Add QR scan nsec logins. (Jericho Hasselbush)
|
|
||||||
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Improved status view design (ericholguin)
|
|
||||||
- Improve clear cache functionality (Daniel D’Aquino)
|
|
||||||
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Reduce size of event menu hitbox (William Casarin)
|
|
||||||
- Do not show DMs from muted users (Daniel D’Aquino)
|
|
||||||
- Add more spacing between display name and username, and prefix username with `@` character (Daniel D’Aquino)
|
|
||||||
- Broadcast quoted notes when posting a note with quotes (Daniel D’Aquino)
|
|
||||||
|
|
||||||
|
|
||||||
[1.6-24]: https://github.com/damus-io/damus/releases/tag/v1.6-24
|
|
||||||
|
|
||||||
## [1.6-23] - 2023-10-06 - Appstore Release
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Added merch store button to sidebar menu (Daniel D’Aquino)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Damus icon now opens sidebar (Daniel D’Aquino)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Stop tab buttons from causing the root view to scroll to the top unless user is coming from another tab or already at the root view (Daniel D’Aquino)
|
|
||||||
- Fix profiles not updating (William Casarin)
|
|
||||||
- Fix issue where relays with trailing slashes cannot be removed (#1531) (Daniel D’Aquino)
|
|
||||||
|
|
||||||
|
|
||||||
[1.6-23]: https://github.com/damus-io/damus/releases/tag/v1.6-23
|
|
||||||
|
|
||||||
## [1.6-20] - 2023-10-04
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Improve UX around clearing cache (Daniel D’Aquino)
|
|
||||||
- Show muted thread replies at the bottom of the thread view (#1522) (Daniel D’Aquino)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix situations where the note composer cursor gets stuck in one place after tagging a user (Daniel D’Aquino)
|
|
||||||
- Fix some note composer issues, such as when copying/pasting larger text, and make the post composer more robust. (Daniel D’Aquino)
|
|
||||||
- Apply filters to hashtag search timeline view (Daniel D’Aquino)
|
|
||||||
- Hide quoted or reposted notes from people whom the user has muted. (#1216) (Daniel D’Aquino)
|
|
||||||
- Fix profile not updating (William Casarin)
|
|
||||||
- Fix small graphical toolbar bug when scrolling profiles (Daniel D’Aquino)
|
|
||||||
- Fix localization issues and export strings for translation (Terry Yiu)
|
|
||||||
|
|
||||||
|
|
||||||
[1.6-20]: https://github.com/damus-io/damus/releases/tag/v1.6-20
|
|
||||||
|
|
||||||
## [1.6-18] - 2023-09-21
|
## [1.6-18] - 2023-09-21
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -1819,3 +1558,5 @@
|
|||||||
|
|
||||||
|
|
||||||
[0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2
|
[0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>com.apple.security.app-sandbox</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.application-groups</key>
|
|
||||||
<array>
|
|
||||||
<string>group.com.damus</string>
|
|
||||||
</array>
|
|
||||||
<key>com.apple.security.network.client</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>NSExtension</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSExtensionPointIdentifier</key>
|
|
||||||
<string>com.apple.usernotifications.service</string>
|
|
||||||
<key>NSExtensionPrincipalClass</key>
|
|
||||||
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
//
|
|
||||||
// NotificationExtensionState.swift
|
|
||||||
// DamusNotificationService
|
|
||||||
//
|
|
||||||
// Created by Daniel D’Aquino on 2023-11-27.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct NotificationExtensionState: HeadlessDamusState {
|
|
||||||
let ndb: Ndb
|
|
||||||
let settings: UserSettingsStore
|
|
||||||
let contacts: Contacts
|
|
||||||
let mutelist_manager: MutelistManager
|
|
||||||
let keypair: Keypair
|
|
||||||
let profiles: Profiles
|
|
||||||
let zaps: Zaps
|
|
||||||
let lnurls: LNUrls
|
|
||||||
|
|
||||||
init?() {
|
|
||||||
guard let ndb = try? Ndb(owns_db_file: false) else { return nil }
|
|
||||||
self.ndb = ndb
|
|
||||||
|
|
||||||
guard let keypair = get_saved_keypair() else { return nil }
|
|
||||||
|
|
||||||
// dumb stuff needed for property wrappers
|
|
||||||
UserSettingsStore.pubkey = keypair.pubkey
|
|
||||||
self.settings = UserSettingsStore()
|
|
||||||
|
|
||||||
self.contacts = Contacts(our_pubkey: keypair.pubkey)
|
|
||||||
self.mutelist_manager = MutelistManager(user_keypair: keypair)
|
|
||||||
self.keypair = keypair
|
|
||||||
self.profiles = Profiles(ndb: ndb)
|
|
||||||
self.zaps = Zaps(our_pubkey: keypair.pubkey)
|
|
||||||
self.lnurls = LNUrls()
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func add_zap(zap: Zapping) -> Bool {
|
|
||||||
// store generic zap mapping
|
|
||||||
self.zaps.add_zap(zap: zap)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
//
|
|
||||||
// NotificationFormatter.swift
|
|
||||||
// DamusNotificationService
|
|
||||||
//
|
|
||||||
// Created by Daniel D’Aquino on 2023-11-13.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import UserNotifications
|
|
||||||
|
|
||||||
struct NotificationFormatter {
|
|
||||||
static var shared = NotificationFormatter()
|
|
||||||
|
|
||||||
// MARK: - Formatting with NdbNote
|
|
||||||
|
|
||||||
func format_message(event: NdbNote) -> UNMutableNotificationContent? {
|
|
||||||
let content = UNMutableNotificationContent()
|
|
||||||
if let event_json_data = try? JSONEncoder().encode(event), // Must be encoded, as the notification completion handler requires this object to conform to `NSSecureCoding`
|
|
||||||
let event_json_string = String(data: event_json_data, encoding: .utf8) {
|
|
||||||
content.userInfo = [
|
|
||||||
NDB_NOTE_JSON_USER_INFO_KEY: event_json_string
|
|
||||||
]
|
|
||||||
}
|
|
||||||
switch event.known_kind {
|
|
||||||
case .text:
|
|
||||||
content.title = NSLocalizedString("Someone posted a note", comment: "Title label for push notification where someone posted a note")
|
|
||||||
content.body = event.content
|
|
||||||
break
|
|
||||||
case .dm:
|
|
||||||
content.title = NSLocalizedString("New message", comment: "Title label for push notifications where a direct message was sent to the user")
|
|
||||||
content.body = NSLocalizedString("(Contents are encrypted)", comment: "Label on push notification indicating that the contents of the message are encrypted")
|
|
||||||
break
|
|
||||||
case .like:
|
|
||||||
guard let reactionEmoji = to_reaction_emoji(ev: event) else {
|
|
||||||
content.title = NSLocalizedString("Someone reacted to your note", comment: "Generic title label for push notifications where someone reacted to the user's post")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
content.title = NSLocalizedString("New note reaction", comment: "Title label for push notifications where someone reacted to the user's post with a specific emoji")
|
|
||||||
content.body = String(format: NSLocalizedString("Someone reacted to your note with %@", comment: "Body label for push notifications where someone reacted to the user's post with a specific emoji"), reactionEmoji)
|
|
||||||
break
|
|
||||||
case .zap:
|
|
||||||
content.title = NSLocalizedString("Someone zapped you ⚡️", comment: "Title label for a push notification where someone zapped the user")
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Formatting with LocalNotification
|
|
||||||
|
|
||||||
func format_message(displayName: String, notify: LocalNotification) -> (content: UNMutableNotificationContent, identifier: String)? {
|
|
||||||
let content = UNMutableNotificationContent()
|
|
||||||
var title = ""
|
|
||||||
var identifier = ""
|
|
||||||
|
|
||||||
switch notify.type {
|
|
||||||
case .mention:
|
|
||||||
title = String(format: NSLocalizedString("Mentioned by %@", comment: "Mentioned by heading in local notification"), displayName)
|
|
||||||
identifier = "myMentionNotification"
|
|
||||||
case .repost:
|
|
||||||
title = String(format: NSLocalizedString("Reposted by %@", comment: "Reposted by heading in local notification"), displayName)
|
|
||||||
identifier = "myBoostNotification"
|
|
||||||
case .like:
|
|
||||||
title = String(format: NSLocalizedString("%@ reacted with %@", comment: "Reacted by heading in local notification"), displayName, to_reaction_emoji(ev: notify.event) ?? "")
|
|
||||||
identifier = "myLikeNotification"
|
|
||||||
case .dm:
|
|
||||||
title = displayName
|
|
||||||
identifier = "myDMNotification"
|
|
||||||
case .zap, .profile_zap:
|
|
||||||
// not handled here. Try `format_message(displayName: String, notify: LocalNotification, state: HeadlessDamusState) async -> (content: UNMutableNotificationContent, identifier: String)?`
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
content.title = title
|
|
||||||
content.body = notify.content
|
|
||||||
content.sound = UNNotificationSound.default
|
|
||||||
content.userInfo = notify.to_lossy().to_user_info()
|
|
||||||
|
|
||||||
return (content, identifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
func format_message(displayName: String, notify: LocalNotification, state: HeadlessDamusState) async -> (content: UNMutableNotificationContent, identifier: String)? {
|
|
||||||
// Try sync method first and return if it works
|
|
||||||
if let sync_formatted_message = self.format_message(displayName: displayName, notify: notify) {
|
|
||||||
return sync_formatted_message
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it does not work, try async formatting methods
|
|
||||||
let content = UNMutableNotificationContent()
|
|
||||||
|
|
||||||
switch notify.type {
|
|
||||||
case .zap, .profile_zap:
|
|
||||||
guard let zap = await get_zap(from: notify.event, state: state) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
content.title = Self.zap_notification_title(zap)
|
|
||||||
content.body = Self.zap_notification_body(profiles: state.profiles, zap: zap)
|
|
||||||
content.sound = UNNotificationSound.default
|
|
||||||
content.userInfo = LossyLocalNotification(type: .zap, mention: .note(notify.event.id)).to_user_info()
|
|
||||||
return (content, "myZapNotification")
|
|
||||||
default:
|
|
||||||
// The sync method should have taken care of this.
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Formatting zap utility notifications
|
|
||||||
|
|
||||||
static 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.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) -> String {
|
|
||||||
let src = zap.request.ev
|
|
||||||
let pk = zap.is_anon ? ANON_PUBKEY : src.pubkey
|
|
||||||
|
|
||||||
let profile_txn = profiles.lookup(id: pk)
|
|
||||||
let profile = profile_txn?.unsafeUnownedValue
|
|
||||||
let name = Profile.displayName(profile: profile, pubkey: pk).displayName.truncate(maxLength: 50)
|
|
||||||
|
|
||||||
let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0))
|
|
||||||
let formattedSats = format_msats_abbrev(zap.invoice.amount)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
//
|
|
||||||
// NotificationService.swift
|
|
||||||
// DamusNotificationService
|
|
||||||
//
|
|
||||||
// Created by Daniel D’Aquino on 2023-11-10.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UserNotifications
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
class NotificationService: UNNotificationServiceExtension {
|
|
||||||
|
|
||||||
var contentHandler: ((UNNotificationContent) -> Void)?
|
|
||||||
var bestAttemptContent: UNMutableNotificationContent?
|
|
||||||
|
|
||||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
|
||||||
self.contentHandler = contentHandler
|
|
||||||
|
|
||||||
guard let nostr_event_json = request.content.userInfo["nostr_event"] as? String,
|
|
||||||
let nostr_event = NdbNote.owned_from_json(json: nostr_event_json)
|
|
||||||
else {
|
|
||||||
// No nostr event detected. Just display the original notification
|
|
||||||
contentHandler(request.content)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log that we got a push notification
|
|
||||||
Log.debug("Got nostr event push notification from pubkey %s", for: .push_notifications, nostr_event.pubkey.hex())
|
|
||||||
|
|
||||||
guard let state = NotificationExtensionState(),
|
|
||||||
let display_name = state.ndb.lookup_profile(nostr_event.pubkey)?.unsafeUnownedValue?.profile?.display_name // We are not holding the txn here.
|
|
||||||
else {
|
|
||||||
// Something failed to initialize so let's go for the next best thing
|
|
||||||
guard let improved_content = NotificationFormatter.shared.format_message(event: nostr_event) else {
|
|
||||||
// We cannot format this nostr event. Suppress notification.
|
|
||||||
contentHandler(UNNotificationContent())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
contentHandler(improved_content)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't show notification details that match mute list.
|
|
||||||
// TODO: Remove this code block once we get notification suppression entitlement from Apple. It will be covered by the `guard should_display_notification` block
|
|
||||||
if state.mutelist_manager.is_event_muted(nostr_event) {
|
|
||||||
// We cannot really suppress muted notifications until we have the notification supression entitlement.
|
|
||||||
// The best we can do if we ever get those muted notifications (which we generally won't due to server-side processing) is to obscure the details
|
|
||||||
let content = UNMutableNotificationContent()
|
|
||||||
content.title = NSLocalizedString("Muted event", comment: "Title for a push notification which has been muted")
|
|
||||||
content.body = NSLocalizedString("This is an event that has been muted according to your mute list rules. We cannot suppress this notification, but we obscured the details to respect your preferences", comment: "Description for a push notification which has been muted, and explanation that we cannot suppress it")
|
|
||||||
content.sound = UNNotificationSound.default
|
|
||||||
contentHandler(content)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard should_display_notification(state: state, event: nostr_event, mode: .push) else {
|
|
||||||
// We should not display notification for this event. Suppress notification.
|
|
||||||
// contentHandler(UNNotificationContent())
|
|
||||||
// TODO: We cannot really suppress until we have the notification supression entitlement. Show the raw notification
|
|
||||||
contentHandler(request.content)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let notification_object = generate_local_notification_object(from: nostr_event, state: state) else {
|
|
||||||
// We could not process this notification. Probably an unsupported nostr event kind. Suppress.
|
|
||||||
// contentHandler(UNNotificationContent())
|
|
||||||
// TODO: We cannot really suppress until we have the notification supression entitlement. Show the raw notification
|
|
||||||
contentHandler(request.content)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Task {
|
|
||||||
if let (improvedContent, _) = await NotificationFormatter.shared.format_message(displayName: display_name, notify: notification_object, state: state) {
|
|
||||||
contentHandler(improvedContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func serviceExtensionTimeWillExpire() {
|
|
||||||
// Called just before the extension will be terminated by the system.
|
|
||||||
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
|
|
||||||
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
|
|
||||||
contentHandler(bestAttemptContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>NSPrivacyCollectedDataTypes</key>
|
|
||||||
<array/>
|
|
||||||
<key>NSPrivacyAccessedAPITypes</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>NSPrivacyAccessedAPIType</key>
|
|
||||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
|
||||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
|
||||||
<array>
|
|
||||||
<string>1C8F.1</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>NSPrivacyAccessedAPIType</key>
|
|
||||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
|
||||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
|
||||||
<array>
|
|
||||||
<string>C617.1</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>NSPrivacyCollectedDataTypes</key>
|
|
||||||
<array/>
|
|
||||||
<key>NSPrivacyAccessedAPITypes</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>NSPrivacyAccessedAPIType</key>
|
|
||||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
|
||||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
|
||||||
<array>
|
|
||||||
<string>1C8F.1</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>NSPrivacyAccessedAPIType</key>
|
|
||||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
|
||||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
|
||||||
<array>
|
|
||||||
<string>C617.1</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
125
Purple.storekit
@@ -1,125 +0,0 @@
|
|||||||
{
|
|
||||||
"identifier" : "64C21A2D",
|
|
||||||
"nonRenewingSubscriptions" : [
|
|
||||||
|
|
||||||
],
|
|
||||||
"products" : [
|
|
||||||
|
|
||||||
],
|
|
||||||
"settings" : {
|
|
||||||
"_applicationInternalID" : "1628663131",
|
|
||||||
"_developerTeamID" : "XK7H4JAB3D",
|
|
||||||
"_failTransactionsEnabled" : false,
|
|
||||||
"_lastSynchronizedDate" : 704848066.26849198,
|
|
||||||
"_locale" : "en_US",
|
|
||||||
"_storefront" : "USA",
|
|
||||||
"_storeKitErrors" : [
|
|
||||||
{
|
|
||||||
"current" : null,
|
|
||||||
"enabled" : false,
|
|
||||||
"name" : "Load Products"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"current" : null,
|
|
||||||
"enabled" : false,
|
|
||||||
"name" : "Purchase"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"current" : null,
|
|
||||||
"enabled" : false,
|
|
||||||
"name" : "Verification"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"current" : null,
|
|
||||||
"enabled" : false,
|
|
||||||
"name" : "App Store Sync"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"current" : null,
|
|
||||||
"enabled" : false,
|
|
||||||
"name" : "Subscription Status"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"current" : null,
|
|
||||||
"enabled" : false,
|
|
||||||
"name" : "App Transaction"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"current" : null,
|
|
||||||
"enabled" : false,
|
|
||||||
"name" : "Manage Subscriptions Sheet"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"current" : null,
|
|
||||||
"enabled" : false,
|
|
||||||
"name" : "Refund Request Sheet"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"current" : null,
|
|
||||||
"enabled" : false,
|
|
||||||
"name" : "Offer Code Redeem Sheet"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"subscriptionGroups" : [
|
|
||||||
{
|
|
||||||
"id" : "21283177",
|
|
||||||
"localizations" : [
|
|
||||||
|
|
||||||
],
|
|
||||||
"name" : "Purple",
|
|
||||||
"subscriptions" : [
|
|
||||||
{
|
|
||||||
"adHocOffers" : [
|
|
||||||
|
|
||||||
],
|
|
||||||
"codeOffers" : [
|
|
||||||
|
|
||||||
],
|
|
||||||
"displayPrice" : "6.99",
|
|
||||||
"familyShareable" : false,
|
|
||||||
"groupNumber" : 1,
|
|
||||||
"internalID" : "6446591615",
|
|
||||||
"introductoryOffer" : null,
|
|
||||||
"localizations" : [
|
|
||||||
{
|
|
||||||
"description" : "Support damus development with Damus Purple!",
|
|
||||||
"displayName" : "Damus Purple",
|
|
||||||
"locale" : "en_CA"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"productID" : "purple",
|
|
||||||
"recurringSubscriptionPeriod" : "P1M",
|
|
||||||
"referenceName" : "Purple",
|
|
||||||
"subscriptionGroupID" : "21283177",
|
|
||||||
"type" : "RecurringSubscription"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"adHocOffers" : [
|
|
||||||
|
|
||||||
],
|
|
||||||
"codeOffers" : [
|
|
||||||
|
|
||||||
],
|
|
||||||
"displayPrice" : "69.99",
|
|
||||||
"familyShareable" : false,
|
|
||||||
"groupNumber" : 2,
|
|
||||||
"internalID" : "6448764101",
|
|
||||||
"introductoryOffer" : null,
|
|
||||||
"localizations" : [
|
|
||||||
|
|
||||||
],
|
|
||||||
"productID" : "purpleyearly",
|
|
||||||
"recurringSubscriptionPeriod" : "P1Y",
|
|
||||||
"referenceName" : "Purple Yearly",
|
|
||||||
"subscriptionGroupID" : "21283177",
|
|
||||||
"type" : "RecurringSubscription"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"version" : {
|
|
||||||
"major" : 3,
|
|
||||||
"minor" : 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
50
README.md
@@ -8,50 +8,20 @@ A twitter-like [nostr][nostr] client for iPhone, iPad and MacOS.
|
|||||||
|
|
||||||
[nostr]: https://github.com/fiatjaf/nostr
|
[nostr]: https://github.com/fiatjaf/nostr
|
||||||
|
|
||||||
## How is Damus better than twitter?
|
|
||||||
There are no toxic algorithms.\
|
|
||||||
You can send or receive zaps (satoshis) without asking for permission.\
|
|
||||||
[There is no central database](https://fiatjaf.com/nostr.html). Therefore, Damus is censorship resistant.\
|
|
||||||
There are no ads.\
|
|
||||||
You don't have to reveal sensitive personal information to sign up.\
|
|
||||||
No email is required. \
|
|
||||||
No phone number is required. \
|
|
||||||
Damus is free and open source software. \
|
|
||||||
There is no Big Tech moat. Therefore, seamless interoperability with thousands or millions of other nostr apps is possible, and is how [Damus and nostr win](https://www.youtube.com/watch?v=qTixqS-W1yo).
|
|
||||||
|
|
||||||
## If there are no ads, how is Damus funded?
|
|
||||||
Damus offers a paid subscription 🟣 purple 🟣 https://damus.io/purple/. \
|
|
||||||
Initial benefits include a unique subscriber number, subscriber badge, and auto-translate powered by DeepL.
|
|
||||||
|
|
||||||
Damus has also graciously received donations or grants from hundreds of Damus users, [Opensats](https://opensats.org/), and the [Human Rights Foundation](https://hrf.org/).
|
|
||||||
|
|
||||||
## Spec Compliance
|
## Spec Compliance
|
||||||
|
|
||||||
damus implements the following [Nostr Implementation Possibilities][nips]
|
damus implements the following [Nostr Implementation Possibilities][nips]
|
||||||
|
|
||||||
- [NIP-01: Basic protocol flow][nip01]
|
- [NIP-01: Basic protocol flow][nip01]
|
||||||
- [NIP-04: Encrypted direct message][nip04]
|
|
||||||
- [NIP-08: Mentions][nip08]
|
- [NIP-08: Mentions][nip08]
|
||||||
- [NIP-10: Reply conventions][nip10]
|
- [NIP-10: Reply conventions][nip10]
|
||||||
- [NIP-12: Generic tag queries (hashtags)][nip12]
|
- [NIP-12: Generic tag queries (hashtags)][nip12]
|
||||||
- [NIP-19: bech32-encoded entities][NIP19]
|
|
||||||
- [NIP-21: nostr: URI scheme][NIP21]
|
|
||||||
- [NIP-25: Reactions][NIP25]
|
|
||||||
- [NIP-42: Authentication of clients to relays][nip42]
|
|
||||||
- [NIP-56: Reporting][nip56]
|
|
||||||
|
|
||||||
[nips]: https://github.com/nostr-protocol/nips
|
[nips]: https://github.com/nostr-protocol/nips
|
||||||
[nip01]: https://github.com/nostr-protocol/nips/blob/master/01.md
|
[nip01]: https://github.com/nostr-protocol/nips/blob/master/01.md
|
||||||
[nip04]: https://github.com/nostr-protocol/nips/blob/master/04.md
|
|
||||||
[nip08]: https://github.com/nostr-protocol/nips/blob/master/08.md
|
[nip08]: https://github.com/nostr-protocol/nips/blob/master/08.md
|
||||||
[nip10]: https://github.com/nostr-protocol/nips/blob/master/10.md
|
[nip10]: https://github.com/nostr-protocol/nips/blob/master/10.md
|
||||||
[nip12]: https://github.com/nostr-protocol/nips/blob/master/12.md
|
[nip12]: https://github.com/nostr-protocol/nips/blob/master/12.md
|
||||||
[nip19]: https://github.com/nostr-protocol/nips/blob/master/19.md
|
|
||||||
[nip21]: https://github.com/nostr-protocol/nips/blob/master/21.md
|
|
||||||
[nip25]: https://github.com/nostr-protocol/nips/blob/master/25.md
|
|
||||||
[nip42]: https://github.com/nostr-protocol/nips/blob/master/42.md
|
|
||||||
[nip56]: https://github.com/nostr-protocol/nips/blob/master/56.md
|
|
||||||
|
|
||||||
|
|
||||||
## Getting Started on Damus
|
## Getting Started on Damus
|
||||||
|
|
||||||
@@ -62,7 +32,7 @@ damus implements the following [Nostr Implementation Possibilities][nips]
|
|||||||
- Relays: You can add more relays to send your notes to by tapping the "+".
|
- Relays: You can add more relays to send your notes to by tapping the "+".
|
||||||
- Find more relays to add: https://nostr.info/relays/
|
- Find more relays to add: https://nostr.info/relays/
|
||||||
- Public Key (pubkey): Your public, personal address and how people can find and tag you
|
- Public Key (pubkey): Your public, personal address and how people can find and tag you
|
||||||
- Secret Key: Your *private* key unique to you. Never share your private key publicly and share with other clients at your own risk!
|
- Secret Key: Your *private* key unique to you. Never share your private key publically and share with other clients at your own risk!
|
||||||
- Save your keys somewhere safe
|
- Save your keys somewhere safe
|
||||||
- Log out
|
- Log out
|
||||||
|
|
||||||
@@ -76,15 +46,19 @@ damus implements the following [Nostr Implementation Possibilities][nips]
|
|||||||
1. Search their username in the search bar at the top of the 🔍 Global Feed and click their profile
|
1. Search their username in the search bar at the top of the 🔍 Global Feed and click their profile
|
||||||
2. Tap the 🔑 icon which will copy their pubkey to your clipboard
|
2. Tap the 🔑 icon which will copy their pubkey to your clipboard
|
||||||
3. Go back to your 🏠 Personal Feed and tap the blue + button to compose your Note
|
3. Go back to your 🏠 Personal Feed and tap the blue + button to compose your Note
|
||||||
4. Add @ directly followed by the pubkey (e.g., `@npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s`)
|
4. Add @ direcly followed by the pubkey (e.g., `@npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s`)
|
||||||
- You can also tap the ellipsis menu of a Note (three dots in top right of note) to grab their User ID aka pubkey or Note ID to link directly to a Note.
|
- 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
|
- Currently you can't delete your Notes in the iOS app
|
||||||
- Share images by pasting the image url which you can grab from nostr.build, 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
|
- 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
|
- 💬 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
|
- ♺ Reposts: Tap the repost icon which will show up in your 🏠 Personal and 🔍 Global Feeds
|
||||||
- ♡ Likes: Tap the heart icon. Users will not get a notification, and cannot see who liked their note (currently, web clients can see your pfp only)
|
- ♡ Likes: Tap the heart icon. Users will not get a notification, and cannot see who liked their note (currently, web clients can see your pfp only)
|
||||||
|
- Formatting Notes (may not format as intended in other web clients)
|
||||||
|
- Italics: 1 asterisk `*italic*`
|
||||||
|
- Bold: 2 asterisk `**bold**`
|
||||||
|
- Strikethrough: 1 tildes `~strikethrough~`
|
||||||
|
- Code: 1 back-tick `` `code` ``
|
||||||
|
|
||||||
#### 💬 Encrypted DMs (chat app, bottom navigation)
|
#### 💬 Encrypted DMs (chat app, bottom navigation)
|
||||||
- Tap the chat icon and you'll notice there's nothing to see at first. Go to a user profile and tap the 💬 chat icon next to the follow button to begin a DM
|
- Tap the chat icon and you'll notice there's nothing to see at first. Go to a user profile and tap the 💬 chat icon next to the follow button to begin a DM
|
||||||
@@ -102,9 +76,7 @@ damus implements the following [Nostr Implementation Possibilities][nips]
|
|||||||
4. For PFP, insert a URL containing your image (support video: https://cdn.jb55.com/vid/pfp-editor.mp4)
|
4. For PFP, insert a URL containing your image (support video: https://cdn.jb55.com/vid/pfp-editor.mp4)
|
||||||
5. Save
|
5. Save
|
||||||
|
|
||||||
|
|
||||||
#### ⚡️ Request Sats
|
#### ⚡️ Request Sats
|
||||||
Paste an invoice from your favorite LN wallet.
|
|
||||||
(Sats or Satoshis are the smallest denomination of bitcoin)
|
(Sats or Satoshis are the smallest denomination of bitcoin)
|
||||||
|
|
||||||
**Alby (browser extension)**
|
**Alby (browser extension)**
|
||||||
@@ -145,8 +117,6 @@ Your internet protocol (IP) address is exposed to the relays you connect to, and
|
|||||||
|
|
||||||
The relay also learns which public keys you are requesting, meaning your public key will be tied to your IP address.
|
The relay also learns which public keys you are requesting, meaning your public key will be tied to your IP address.
|
||||||
|
|
||||||
It is public information which other profiles (npubs) you are exchanging DMs with. The content of the DMs is encrypted.
|
|
||||||
|
|
||||||
### Translations
|
### Translations
|
||||||
|
|
||||||
Translators welcome! Join the [Transifex][transifex] project.
|
Translators welcome! Join the [Transifex][transifex] project.
|
||||||
@@ -157,10 +127,8 @@ All user-facing strings must have a comment in order to provide context to trans
|
|||||||
|
|
||||||
### Awards
|
### Awards
|
||||||
|
|
||||||
Damus lead dev and founder Will awards developers with satoshis!
|
|
||||||
There may be nostr badges awarded for contributors in the future... :)
|
There may be nostr badges awarded for contributors in the future... :)
|
||||||
|
|
||||||
|
|
||||||
First contributors:
|
First contributors:
|
||||||
|
|
||||||
1. @randymcmillan
|
1. @randymcmillan
|
||||||
|
|||||||
@@ -96,16 +96,6 @@ static inline void copy_cursor(struct cursor *src, struct cursor *dest)
|
|||||||
dest->end = src->end;
|
dest->end = src->end;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline int cursor_skip(struct cursor *cursor, int n)
|
|
||||||
{
|
|
||||||
if (cursor->p + n >= cursor->end)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
cursor->p += n;
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline int pull_byte(struct cursor *cursor, u8 *c)
|
static inline int pull_byte(struct cursor *cursor, u8 *c)
|
||||||
{
|
{
|
||||||
if (unlikely(cursor->p >= cursor->end))
|
if (unlikely(cursor->p >= cursor->end))
|
||||||
@@ -369,20 +359,6 @@ static inline int push_sized_str(struct cursor *cursor, const char *str, int len
|
|||||||
return cursor_push(cursor, (u8*)str, len);
|
return cursor_push(cursor, (u8*)str, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline int cursor_push_lowercase(struct cursor *cur, const char *str, int len)
|
|
||||||
{
|
|
||||||
int i;
|
|
||||||
|
|
||||||
if (unlikely(cur->p + len >= cur->end))
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
for (i = 0; i < len; i++)
|
|
||||||
cur->p[i] = tolower(str[i]);
|
|
||||||
|
|
||||||
cur->p += len;
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline int cursor_push_str(struct cursor *cursor, const char *str)
|
static inline int cursor_push_str(struct cursor *cursor, const char *str)
|
||||||
{
|
{
|
||||||
return cursor_push(cursor, (u8*)str, (int)strlen(str));
|
return cursor_push(cursor, (u8*)str, (int)strlen(str));
|
||||||
@@ -485,37 +461,11 @@ static inline int parse_str(struct cursor *cur, const char *str) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline int is_whitespace(int c) {
|
static inline int is_whitespace(char c) {
|
||||||
return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
|
return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static inline int is_underscore(char c) {
|
||||||
static inline int next_char_is_whitespace(unsigned char *curChar, unsigned char *endChar) {
|
|
||||||
unsigned char * next = curChar + 1;
|
|
||||||
if(next > endChar) return 0;
|
|
||||||
else if(next == endChar) return 1;
|
|
||||||
return is_whitespace(*next);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int char_disallowed_at_end_url(char c){
|
|
||||||
return c == '.' || c == ',';
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline int is_final_url_char(unsigned char *curChar, unsigned char *endChar){
|
|
||||||
if(is_whitespace(*curChar)){
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
else if(next_char_is_whitespace(curChar, endChar)) {
|
|
||||||
// next char is whitespace so this char could be the final char in the url
|
|
||||||
return char_disallowed_at_end_url(*curChar);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
// next char isn't whitespace so it can't be a final char
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline int is_underscore(int c) {
|
|
||||||
return c == '_';
|
return c == '_';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -696,23 +646,6 @@ static inline int consume_until_whitespace(struct cursor *cur, int or_end) {
|
|||||||
return or_end;
|
return or_end;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline int consume_until_end_url(struct cursor *cur, int or_end) {
|
|
||||||
char c;
|
|
||||||
int consumedAtLeastOne = 0;
|
|
||||||
|
|
||||||
while (cur->p < cur->end) {
|
|
||||||
c = *cur->p;
|
|
||||||
|
|
||||||
if (is_final_url_char(cur->p, cur->end))
|
|
||||||
return consumedAtLeastOne;
|
|
||||||
|
|
||||||
cur->p++;
|
|
||||||
consumedAtLeastOne = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return or_end;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline int consume_until_non_alphanumeric(struct cursor *cur, int or_end) {
|
static inline int consume_until_non_alphanumeric(struct cursor *cur, int or_end) {
|
||||||
char c;
|
char c;
|
||||||
int consumedAtLeastOne = 0;
|
int consumedAtLeastOne = 0;
|
||||||
@@ -730,17 +663,4 @@ static inline int consume_until_non_alphanumeric(struct cursor *cur, int or_end)
|
|||||||
return or_end;
|
return or_end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static inline int cursor_memset(struct cursor *cursor, unsigned char c, int n)
|
|
||||||
{
|
|
||||||
if (cursor->p + n >= cursor->end)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
memset(cursor->p, c, n);
|
|
||||||
cursor->p += n;
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ static int consume_url_fragment(struct cursor *cur)
|
|||||||
|
|
||||||
cur->p++;
|
cur->p++;
|
||||||
|
|
||||||
return consume_until_end_url(cur, 1);
|
return consume_until_whitespace(cur, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int consume_url_path(struct cursor *cur)
|
static int consume_url_path(struct cursor *cur)
|
||||||
@@ -134,7 +134,7 @@ static int consume_url_path(struct cursor *cur)
|
|||||||
while (cur->p < cur->end) {
|
while (cur->p < cur->end) {
|
||||||
c = *cur->p;
|
c = *cur->p;
|
||||||
|
|
||||||
if (c == '?' || c == '#' || is_final_url_char(cur->p, cur->end)) {
|
if (c == '?' || c == '#' || is_whitespace(c)) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ static int consume_url_host(struct cursor *cur)
|
|||||||
while (cur->p < cur->end) {
|
while (cur->p < cur->end) {
|
||||||
c = *cur->p;
|
c = *cur->p;
|
||||||
// TODO: handle IDNs
|
// TODO: handle IDNs
|
||||||
if ((is_alphanumeric(c) || c == '.' || c == '-') && !is_final_url_char(cur->p, cur->end))
|
if (is_alphanumeric(c) || c == '.' || c == '-')
|
||||||
{
|
{
|
||||||
count++;
|
count++;
|
||||||
cur->p++;
|
cur->p++;
|
||||||
@@ -169,9 +169,6 @@ static int consume_url_host(struct cursor *cur)
|
|||||||
|
|
||||||
static int parse_url(struct cursor *cur, struct note_block *block) {
|
static int parse_url(struct cursor *cur, struct note_block *block) {
|
||||||
u8 *start = cur->p;
|
u8 *start = cur->p;
|
||||||
u8 *host;
|
|
||||||
int host_len;
|
|
||||||
struct cursor path_cur;
|
|
||||||
|
|
||||||
if (!parse_str(cur, "http"))
|
if (!parse_str(cur, "http"))
|
||||||
return 0;
|
return 0;
|
||||||
@@ -188,30 +185,10 @@ static int parse_url(struct cursor *cur, struct note_block *block) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure to save the hostname. We will use this to detect damus.io links
|
if (!(consume_url_host(cur) &&
|
||||||
host = cur->p;
|
consume_url_path(cur) &&
|
||||||
|
consume_url_fragment(cur)))
|
||||||
if (!consume_url_host(cur)) {
|
{
|
||||||
cur->p = start;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the length of the host string
|
|
||||||
host_len = (int)(cur->p - host);
|
|
||||||
|
|
||||||
// save the current parse state so that we can continue from here when
|
|
||||||
// parsing the bech32 in the damus.io link if we have it
|
|
||||||
copy_cursor(cur, &path_cur);
|
|
||||||
|
|
||||||
// skip leading /
|
|
||||||
cursor_skip(&path_cur, 1);
|
|
||||||
|
|
||||||
if (!consume_url_path(cur)) {
|
|
||||||
cur->p = start;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!consume_url_fragment(cur)) {
|
|
||||||
cur->p = start;
|
cur->p = start;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -226,19 +203,6 @@ static int parse_url(struct cursor *cur, struct note_block *block) {
|
|||||||
cur->p--;
|
cur->p--;
|
||||||
}
|
}
|
||||||
|
|
||||||
// save the bech32 string pos in case we hit a damus.io link
|
|
||||||
block->block.str.start = (const char *)path_cur.p;
|
|
||||||
|
|
||||||
// if we have a damus link, make it a mention
|
|
||||||
if (host_len == 8
|
|
||||||
&& !strncmp((const char *)host, "damus.io", 8)
|
|
||||||
&& parse_nostr_bech32(&path_cur, &block->block.mention_bech32.bech32))
|
|
||||||
{
|
|
||||||
block->block.str.end = (const char *)path_cur.p;
|
|
||||||
block->type = BLOCK_MENTION_BECH32;
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
block->type = BLOCK_URL;
|
block->type = BLOCK_URL;
|
||||||
block->block.str.start = (const char *)start;
|
block->block.str.start = (const char *)start;
|
||||||
block->block.str.end = (const char *)cur->p;
|
block->block.str.end = (const char *)cur->p;
|
||||||
|
|||||||
@@ -7,10 +7,8 @@
|
|||||||
|
|
||||||
#include "nostr_bech32.h"
|
#include "nostr_bech32.h"
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include "endian.h"
|
|
||||||
#include "cursor.h"
|
#include "cursor.h"
|
||||||
#include "bech32.h"
|
#include "bech32.h"
|
||||||
#include <stdbool.h>
|
|
||||||
|
|
||||||
#define MAX_TLVS 16
|
#define MAX_TLVS 16
|
||||||
|
|
||||||
@@ -147,11 +145,6 @@ static int tlvs_to_relays(struct nostr_tlvs *tlvs, struct relays *relays) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static uint32_t decode_tlv_u32(const uint8_t *bytes) {
|
|
||||||
beint32_t *be32_bytes = (beint32_t*)bytes;
|
|
||||||
return be32_to_cpu(*be32_bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int parse_nostr_bech32_nevent(struct cursor *cur, struct bech32_nevent *nevent) {
|
static int parse_nostr_bech32_nevent(struct cursor *cur, struct bech32_nevent *nevent) {
|
||||||
struct nostr_tlvs tlvs;
|
struct nostr_tlvs tlvs;
|
||||||
struct nostr_tlv *tlv;
|
struct nostr_tlv *tlv;
|
||||||
@@ -173,13 +166,6 @@ static int parse_nostr_bech32_nevent(struct cursor *cur, struct bech32_nevent *n
|
|||||||
nevent->pubkey = NULL;
|
nevent->pubkey = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(find_tlv(&tlvs, TLV_KIND, &tlv)) {
|
|
||||||
nevent->kind = decode_tlv_u32(tlv->value);
|
|
||||||
nevent->has_kind = true;
|
|
||||||
} else {
|
|
||||||
nevent->has_kind = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return tlvs_to_relays(&tlvs, &nevent->relays);
|
return tlvs_to_relays(&tlvs, &nevent->relays);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,11 +187,6 @@ static int parse_nostr_bech32_naddr(struct cursor *cur, struct bech32_naddr *nad
|
|||||||
|
|
||||||
naddr->pubkey = tlv->value;
|
naddr->pubkey = tlv->value;
|
||||||
|
|
||||||
if(!find_tlv(&tlvs, TLV_KIND, &tlv)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
naddr->kind = decode_tlv_u32(tlv->value);
|
|
||||||
|
|
||||||
return tlvs_to_relays(&tlvs, &naddr->relays);
|
return tlvs_to_relays(&tlvs, &naddr->relays);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,6 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include "str_block.h"
|
#include "str_block.h"
|
||||||
#include "cursor.h"
|
#include "cursor.h"
|
||||||
#include <stdbool.h>
|
|
||||||
|
|
||||||
typedef unsigned char u8;
|
typedef unsigned char u8;
|
||||||
#define MAX_RELAYS 10
|
#define MAX_RELAYS 10
|
||||||
|
|
||||||
@@ -47,8 +45,6 @@ struct bech32_nevent {
|
|||||||
struct relays relays;
|
struct relays relays;
|
||||||
const u8 *event_id;
|
const u8 *event_id;
|
||||||
const u8 *pubkey; // optional
|
const u8 *pubkey; // optional
|
||||||
uint32_t kind;
|
|
||||||
bool has_kind;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct bech32_nprofile {
|
struct bech32_nprofile {
|
||||||
@@ -60,7 +56,6 @@ struct bech32_naddr {
|
|||||||
struct relays relays;
|
struct relays relays;
|
||||||
struct str_block identifier;
|
struct str_block identifier;
|
||||||
const u8 *pubkey;
|
const u8 *pubkey;
|
||||||
uint32_t kind;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct bech32_nrelay {
|
struct bech32_nrelay {
|
||||||
|
|||||||
@@ -1,24 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "babaf4d5748afecf49bbb702530d8e9576460692f478b0a50ee43195dd4440e2",
|
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
|
||||||
"identity" : "emojikit",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/tyiu/EmojiKit",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "05805f72d63a6d6a2d7dc7fe14abd37c1317b11a",
|
|
||||||
"version" : "0.1.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "emojipicker",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/tyiu/EmojiPicker.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "0c28b4a1a6b8840cf2580bda59517f6d0a733dc8",
|
|
||||||
"version" : "0.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"identity" : "gsplayer",
|
"identity" : "gsplayer",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -45,15 +26,6 @@
|
|||||||
"revision" : "40b4b38b3b1c83f7088c76189a742870e0ca06a9"
|
"revision" : "40b4b38b3b1c83f7088c76189a742870e0ca06a9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"identity" : "swift-collections",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/apple/swift-collections.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "ee97538f5b81ae89698fd95938896dec5217b148",
|
|
||||||
"version" : "1.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"identity" : "swift-markdown-ui",
|
"identity" : "swift-markdown-ui",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -61,43 +33,7 @@
|
|||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "76bb7971da7fbf429de1c84f1244adf657242fee"
|
"revision" : "76bb7971da7fbf429de1c84f1244adf657242fee"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-snapshot-testing",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "5b356adceabff6ca027f6574aac79e9fee145d26",
|
|
||||||
"version" : "1.14.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-syntax",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/apple/swift-syntax.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "74203046135342e4a4a627476dd6caf8b28fe11b",
|
|
||||||
"version" : "509.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-trie",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/tyiu/swift-trie",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "4c50bff6c168f74425f70476be62a072980d2da7",
|
|
||||||
"version" : "0.1.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swipeactions",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/aheze/SwipeActions",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "41e6f6dce02d8cfa164f8c5461a41340850ca3ab",
|
|
||||||
"version" : "1.1.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version" : 3
|
"version" : 2
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Scheme
|
|
||||||
LastUpgradeVersion = "1520"
|
|
||||||
wasCreatedForAppExtension = "YES"
|
|
||||||
version = "2.0">
|
|
||||||
<BuildAction
|
|
||||||
parallelizeBuildables = "YES"
|
|
||||||
buildImplicitDependencies = "YES">
|
|
||||||
<BuildActionEntries>
|
|
||||||
<BuildActionEntry
|
|
||||||
buildForTesting = "YES"
|
|
||||||
buildForRunning = "YES"
|
|
||||||
buildForProfiling = "YES"
|
|
||||||
buildForArchiving = "YES"
|
|
||||||
buildForAnalyzing = "YES">
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "D79C4C132AFEB061003A41B4"
|
|
||||||
BuildableName = "DamusNotificationService.appex"
|
|
||||||
BlueprintName = "DamusNotificationService"
|
|
||||||
ReferencedContainer = "container:damus.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</BuildActionEntry>
|
|
||||||
<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"
|
|
||||||
shouldAutocreateTestPlan = "YES">
|
|
||||||
</TestAction>
|
|
||||||
<LaunchAction
|
|
||||||
buildConfiguration = "Debug"
|
|
||||||
selectedDebuggerIdentifier = ""
|
|
||||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
|
||||||
launchStyle = "0"
|
|
||||||
askForAppToLaunch = "Yes"
|
|
||||||
useCustomWorkingDirectory = "NO"
|
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
|
||||||
debugDocumentVersioning = "YES"
|
|
||||||
debugServiceExtension = "internal"
|
|
||||||
allowLocationSimulation = "YES"
|
|
||||||
launchAutomaticallySubstyle = "2">
|
|
||||||
<RemoteRunnable
|
|
||||||
runnableDebuggingMode = "1"
|
|
||||||
BundleIdentifier = "com.jb55.damus2"
|
|
||||||
RemotePath = "/Users/danielnogueira/Library/Developer/CoreSimulator/Devices/99E60B35-CE5D-4B45-AC35-00818C0AF3CB/data/Containers/Bundle/Application/7D0A5302-D07E-4C7C-B509-A7C552BD5A65/damus.app">
|
|
||||||
</RemoteRunnable>
|
|
||||||
<MacroExpansion>
|
|
||||||
<BuildableReference
|
|
||||||
BuildableIdentifier = "primary"
|
|
||||||
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
|
|
||||||
BuildableName = "damus.app"
|
|
||||||
BlueprintName = "damus"
|
|
||||||
ReferencedContainer = "container:damus.xcodeproj">
|
|
||||||
</BuildableReference>
|
|
||||||
</MacroExpansion>
|
|
||||||
</LaunchAction>
|
|
||||||
<ProfileAction
|
|
||||||
buildConfiguration = "Release"
|
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
|
||||||
savedToolIdentifier = ""
|
|
||||||
useCustomWorkingDirectory = "NO"
|
|
||||||
debugDocumentVersioning = "YES"
|
|
||||||
askForAppToLaunch = "Yes"
|
|
||||||
launchAutomaticallySubstyle = "2">
|
|
||||||
<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>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1520"
|
LastUpgradeVersion = "1500"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1520"
|
LastUpgradeVersion = "1500"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
@@ -71,9 +71,6 @@
|
|||||||
ReferencedContainer = "container:damus.xcodeproj">
|
ReferencedContainer = "container:damus.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
<StoreKitConfigurationFileReference
|
|
||||||
identifier = "../../Purple.storekit">
|
|
||||||
</StoreKitConfigurationFileReference>
|
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Release"
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0x1A",
|
|
||||||
"green" : "0x93",
|
|
||||||
"red" : "0xF7"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0xD7",
|
|
||||||
"green" : "0xD1",
|
|
||||||
"red" : "0xD1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0x13",
|
|
||||||
"green" : "0x11",
|
|
||||||
"red" : "0x11"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0xF9",
|
|
||||||
"green" : "0xF3",
|
|
||||||
"red" : "0xF3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0x25",
|
|
||||||
"green" : "0x22",
|
|
||||||
"red" : "0x22"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "244",
|
|
||||||
"green" : "218",
|
|
||||||
"red" : "244"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "92",
|
|
||||||
"green" : "45",
|
|
||||||
"red" : "93"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "236",
|
|
||||||
"green" : "194",
|
|
||||||
"red" : "238"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "109",
|
|
||||||
"green" : "49",
|
|
||||||
"red" : "111"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "197",
|
|
||||||
"green" : "67",
|
|
||||||
"red" : "204"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "255",
|
|
||||||
"green" : "194",
|
|
||||||
"red" : "255"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0xFF",
|
|
||||||
"green" : "0xFF",
|
|
||||||
"red" : "0xFF"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0x00",
|
|
||||||
"green" : "0x00",
|
|
||||||
"red" : "0x00"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0xF2",
|
|
||||||
"green" : "0xD8",
|
|
||||||
"red" : "0xF4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances" : [
|
|
||||||
{
|
|
||||||
"appearance" : "luminosity",
|
|
||||||
"value" : "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"color" : {
|
|
||||||
"color-space" : "srgb",
|
|
||||||
"components" : {
|
|
||||||
"alpha" : "1.000",
|
|
||||||
"blue" : "0x45",
|
|
||||||
"green" : "0x17",
|
|
||||||
"red" : "0x47"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "Damus dark-gray.png",
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 122 KiB |
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "Damus dark.png",
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 66 KiB |
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "special-features.svg",
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"filename" : "special-features.svg",
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"filename" : "special-features.svg",
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 7.6 KiB |
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "stars-bg.png",
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 262 KiB |
@@ -1,328 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
width="500"
|
|
||||||
height="130"
|
|
||||||
viewBox="0 0 132.29166 34.395832"
|
|
||||||
version="1.1"
|
|
||||||
id="svg8"
|
|
||||||
inkscape:version="0.92.1 r15371"
|
|
||||||
sodipodi:docname="ActivityPub-logo.svg">
|
|
||||||
<title
|
|
||||||
id="title4590">ActivityPub logo</title>
|
|
||||||
<defs
|
|
||||||
id="defs2">
|
|
||||||
<linearGradient
|
|
||||||
id="AP-4-0"
|
|
||||||
osb:paint="solid">
|
|
||||||
<stop
|
|
||||||
style="stop-color:#5e5e5e;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop5660" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="linearGradient5640"
|
|
||||||
osb:paint="solid">
|
|
||||||
<stop
|
|
||||||
style="stop-color:#000000;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop5638" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="linearGradient5634"
|
|
||||||
osb:paint="solid">
|
|
||||||
<stop
|
|
||||||
style="stop-color:#000000;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop5632" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="linearGradient5628"
|
|
||||||
osb:paint="solid">
|
|
||||||
<stop
|
|
||||||
style="stop-color:#000000;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop5626" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="AP-3-7"
|
|
||||||
osb:paint="solid">
|
|
||||||
<stop
|
|
||||||
style="stop-color:#c678c5;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop5498" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="AP-2-3"
|
|
||||||
osb:paint="solid">
|
|
||||||
<stop
|
|
||||||
style="stop-color:#6d6d6d;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop5230" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="AP1-5"
|
|
||||||
osb:paint="solid">
|
|
||||||
<stop
|
|
||||||
style="stop-color:#f1007e;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop5212" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#AP-3-7"
|
|
||||||
id="linearGradient5749"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
x1="3319.292"
|
|
||||||
y1="-1291.2802"
|
|
||||||
x2="3344.3645"
|
|
||||||
y2="-1291.2802" />
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#AP1-5"
|
|
||||||
id="linearGradient7297-7"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
x1="3241.6836"
|
|
||||||
y1="-1355.4329"
|
|
||||||
x2="3254.9529"
|
|
||||||
y2="-1355.4329" />
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#AP-2-3"
|
|
||||||
id="linearGradient7303-7"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
x1="3225.7603"
|
|
||||||
y1="-1355.4329"
|
|
||||||
x2="3239.0295"
|
|
||||||
y2="-1355.4329" />
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#AP1-5"
|
|
||||||
id="linearGradient8308"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
x1="3241.6836"
|
|
||||||
y1="-1355.4329"
|
|
||||||
x2="3254.9529"
|
|
||||||
y2="-1355.4329" />
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#AP1-5"
|
|
||||||
id="linearGradient8310"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
x1="3241.6836"
|
|
||||||
y1="-1355.4329"
|
|
||||||
x2="3254.9529"
|
|
||||||
y2="-1355.4329" />
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#AP1-5"
|
|
||||||
id="linearGradient8312"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
x1="3241.6836"
|
|
||||||
y1="-1355.4329"
|
|
||||||
x2="3254.9529"
|
|
||||||
y2="-1355.4329" />
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#AP-2-3"
|
|
||||||
id="linearGradient8314"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
x1="3225.7603"
|
|
||||||
y1="-1355.4329"
|
|
||||||
x2="3239.0295"
|
|
||||||
y2="-1355.4329"
|
|
||||||
gradientTransform="matrix(3.7000834,0,0,3.7000834,-11935.582,4544.6634)" />
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#AP-2-3"
|
|
||||||
id="linearGradient5188"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
gradientTransform="matrix(0.42732603,0,0,0.42732603,-1363.3009,454.91899)"
|
|
||||||
x1="3269.126"
|
|
||||||
y1="-1354.6217"
|
|
||||||
x2="3322.1943"
|
|
||||||
y2="-1354.6217" />
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#AP-2-3"
|
|
||||||
id="linearGradient4523"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
gradientTransform="matrix(3.5811973,0,0,3.5811973,-11532.084,4918.1922)"
|
|
||||||
x1="3269.126"
|
|
||||||
y1="-1354.6217"
|
|
||||||
x2="3322.1943"
|
|
||||||
y2="-1354.6217" />
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#AP1-5"
|
|
||||||
id="linearGradient4526"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
gradientTransform="matrix(3.5811973,0,0,3.5811973,-11528.758,4918.1922)"
|
|
||||||
x1="3323.9951"
|
|
||||||
y1="-1356.5363"
|
|
||||||
x2="3349.0676"
|
|
||||||
y2="-1356.5363" />
|
|
||||||
</defs>
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="base"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="0.14509804"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:zoom="0.5"
|
|
||||||
inkscape:cx="395.506"
|
|
||||||
inkscape:cy="-201.19903"
|
|
||||||
inkscape:document-units="px"
|
|
||||||
inkscape:current-layer="layer1"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:snap-global="true"
|
|
||||||
showguides="false"
|
|
||||||
inkscape:guide-bbox="true"
|
|
||||||
showborder="true"
|
|
||||||
fit-margin-top="0"
|
|
||||||
fit-margin-left="0"
|
|
||||||
fit-margin-right="0"
|
|
||||||
fit-margin-bottom="0"
|
|
||||||
inkscape:showpageshadow="false"
|
|
||||||
borderlayer="false"
|
|
||||||
units="px">
|
|
||||||
<inkscape:grid
|
|
||||||
type="xygrid"
|
|
||||||
id="grid4572"
|
|
||||||
enabled="false"
|
|
||||||
originx="7.1437514"
|
|
||||||
originy="-404.28382" />
|
|
||||||
<inkscape:grid
|
|
||||||
type="axonomgrid"
|
|
||||||
id="grid4574"
|
|
||||||
units="mm"
|
|
||||||
empspacing="12"
|
|
||||||
originx="7.1437514"
|
|
||||||
originy="-404.28382"
|
|
||||||
enabled="false" />
|
|
||||||
<sodipodi:guide
|
|
||||||
position="3278.981,1256.5057"
|
|
||||||
orientation="0,1"
|
|
||||||
id="guide5059"
|
|
||||||
inkscape:locked="false" />
|
|
||||||
<sodipodi:guide
|
|
||||||
position="3278.981,1238.2495"
|
|
||||||
orientation="0,1"
|
|
||||||
id="guide5061"
|
|
||||||
inkscape:locked="false" />
|
|
||||||
</sodipodi:namedview>
|
|
||||||
<metadata
|
|
||||||
id="metadata5">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title>ActivityPub logo</dc:title>
|
|
||||||
<cc:license
|
|
||||||
rdf:resource="http://creativecommons.org/publicdomain/zero/1.0/" />
|
|
||||||
<dc:date>2017-04-15</dc:date>
|
|
||||||
<dc:creator>
|
|
||||||
<cc:Agent>
|
|
||||||
<dc:title>Robert Martinez</dc:title>
|
|
||||||
</cc:Agent>
|
|
||||||
</dc:creator>
|
|
||||||
<dc:subject>
|
|
||||||
<rdf:Bag>
|
|
||||||
<rdf:li>ActivityPub</rdf:li>
|
|
||||||
</rdf:Bag>
|
|
||||||
</dc:subject>
|
|
||||||
</cc:Work>
|
|
||||||
<cc:License
|
|
||||||
rdf:about="http://creativecommons.org/publicdomain/zero/1.0/">
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Reproduction" />
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#Distribution" />
|
|
||||||
<cc:permits
|
|
||||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
|
|
||||||
</cc:License>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<g
|
|
||||||
inkscape:label="Layer 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
style="opacity:1"
|
|
||||||
transform="translate(7.1437516,141.67967)">
|
|
||||||
<path
|
|
||||||
style="fill:#000000;stroke-width:0.26458335"
|
|
||||||
d=""
|
|
||||||
id="path5497"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
<g
|
|
||||||
id="g5197"
|
|
||||||
transform="translate(1.3229166)">
|
|
||||||
<g
|
|
||||||
id="g5132-90"
|
|
||||||
style="fill:url(#linearGradient7297-7);fill-opacity:1"
|
|
||||||
transform="matrix(0.9789804,0,0,0.9789804,-3157.9561,1202.4422)">
|
|
||||||
<g
|
|
||||||
transform="matrix(0.2553682,0,0,0.2553682,2615.9213,-1125.3113)"
|
|
||||||
id="g5080-78"
|
|
||||||
style="fill:url(#linearGradient8312);fill-opacity:1">
|
|
||||||
<path
|
|
||||||
inkscape:connector-curvature="0"
|
|
||||||
id="path5404-0-0"
|
|
||||||
d="m 2450.431,-937.13662 51.9615,30 v 12 l -51.9615,30 v -12 l 41.5693,-24 -41.5692,-24 z"
|
|
||||||
style="fill:url(#linearGradient8308);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
sodipodi:nodetypes="cccccccc" />
|
|
||||||
<path
|
|
||||||
sodipodi:nodetypes="cccc"
|
|
||||||
style="fill:url(#linearGradient8310);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="m 2450.431,-913.13662 20.7847,12 -20.7847,12 z"
|
|
||||||
id="path5406-6-3"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g5127-1"
|
|
||||||
style="fill:url(#linearGradient7303-7);fill-opacity:1"
|
|
||||||
transform="matrix(0.9789804,0,0,0.9789804,-3157.9561,1202.4422)">
|
|
||||||
<path
|
|
||||||
id="path5467-2-0"
|
|
||||||
transform="matrix(0.27026418,0,0,0.27026418,3225.7603,-1228.2597)"
|
|
||||||
d="M 49.097656,-504.56641 0,-476.2207 v 11.33789 l 39.277344,-22.67578 v 45.35351 l 9.820312,5.66992 z m -19.638672,34.01563 -19.6406246,11.33789 19.6406246,11.33789 z"
|
|
||||||
style="fill:url(#linearGradient8314);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.25000042px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g5203"
|
|
||||||
transform="matrix(2.2173353,0,0,2.2173353,-35.445741,150.88402)">
|
|
||||||
<g
|
|
||||||
id="g4523">
|
|
||||||
<path
|
|
||||||
sodipodi:nodetypes="scscscscsscscscscscccccccccccccccscsccccscscccccccccccscsccccscsccccccccccccscscccsccccscscsccccscccccccccccccccccccccccccccccscssccccccccc"
|
|
||||||
inkscape:connector-curvature="0"
|
|
||||||
id="text5037-6"
|
|
||||||
transform="matrix(0.1193249,0,0,0.1193249,12.763965,-131.94382)"
|
|
||||||
d="m 263.22656,34.349609 c -1.66644,0 -2.95278,0.477436 -3.85742,1.429688 -0.90464,0.904639 -1.35742,2.069669 -1.35742,3.498047 0,1.428378 0.45278,2.59536 1.35742,3.5 0.90464,0.857027 2.19098,1.285156 3.85742,1.285156 1.66644,0 2.99818,-0.428129 3.99805,-1.285156 0.99986,-0.857027 1.5,-2.024009 1.5,-3.5 0,-1.475991 -0.50014,-2.665673 -1.5,-3.570313 -0.99987,-0.904639 -2.33161,-1.357422 -3.99805,-1.357422 z m 43.95117,0 c -1.66644,0 -2.95082,0.477436 -3.85546,1.429688 -0.90464,0.904639 -1.35743,2.069669 -1.35743,3.498047 0,1.428378 0.45279,2.59536 1.35743,3.5 0.90464,0.857027 2.18902,1.285156 3.85546,1.285156 1.66645,0 3.00014,-0.428129 4,-1.285156 0.99987,-0.857027 1.5,-2.024009 1.5,-3.5 0,-1.475991 -0.50013,-2.665673 -1.5,-3.570313 -0.99986,-0.904639 -2.33355,-1.357422 -4,-1.357422 z m -118.46166,0.357422 -14.49805,50.351563 h 8.92774 l 2.92773,-11.285156 h 11.78516 l 3.07031,11.285156 h 9.42773 L 195.78638,34.707031 Z m 58.71166,5.285157 -8.49804,2.642578 v 6.71289 h -3.92774 v 7.570313 h 3.92774 v 18.71289 c 0,3.713784 0.66684,6.356519 2,7.927735 1.38076,1.571216 3.42866,2.355468 6.14258,2.355468 1.5236,0 2.9747,-0.189411 4.35546,-0.570312 1.38077,-0.333288 2.59511,-0.761418 3.64258,-1.285156 L 254,77.273438 c -0.66658,0.285675 -1.28607,0.49974 -1.85742,0.642578 -0.57135,0.142837 -1.2155,0.214843 -1.92969,0.214843 -1.04748,0 -1.78438,-0.430082 -2.21289,-1.287109 -0.3809,-0.857027 -0.57227,-2.308127 -0.57227,-4.355469 V 56.917969 h 6.92774 v -7.570313 h -6.92774 z m 80.23243,0 -8.49805,2.642578 v 6.71289 h -3.92969 v 7.570313 h 3.92969 v 18.71289 c 0,3.713784 0.66489,6.356519 1.99805,7.927735 1.38076,1.571216 3.42866,2.355468 6.14257,2.355468 1.52361,0 2.97666,-0.189411 4.35743,-0.570312 1.38076,-0.333288 2.5951,-0.761418 3.64257,-1.285156 l -1.07226,-6.785156 c -0.66658,0.285675 -1.28607,0.49974 -1.85742,0.642578 -0.57135,0.142837 -1.21355,0.214843 -1.92774,0.214843 -1.04747,0 -1.78437,-0.430082 -2.21289,-1.287109 -0.3809,-0.857027 -0.57226,-2.308127 -0.57226,-4.355469 V 56.917969 h 6.92773 v -7.570313 h -6.92773 z m -135.65894,6.855468 h 0.28516 l 1.14453,7.857422 2.85547,11.640625 h -8.2832 l 2.78515,-11.570312 z m 31.94605,1.572266 c -4.33275,0 -7.61963,1.570458 -9.85743,4.71289 -2.23779,3.142433 -3.35546,7.833061 -3.35546,14.070313 0,2.856756 0.21406,5.452139 0.64257,7.785156 0.47613,2.285405 1.23768,4.261293 2.28516,5.927735 1.04748,1.618828 2.38117,2.880516 4,3.785156 1.66644,0.857027 3.71238,1.285156 6.14062,1.285156 1.66645,0 3.33356,-0.238718 5,-0.714844 1.66645,-0.476126 3.09485,-1.190326 4.28516,-2.142578 l -1.78516,-6.570312 c -0.71418,0.476126 -1.50039,0.881555 -2.35742,1.214844 -0.80941,0.285675 -1.80968,0.427734 -3,0.427734 -2.23779,0 -3.88025,-1.022971 -4.92773,-3.070313 -0.99987,-2.047342 -1.5,-4.690077 -1.5,-7.927734 0,-3.856621 0.50013,-6.641415 1.5,-8.355469 0.99986,-1.761666 2.50027,-2.642578 4.5,-2.642578 1.09509,0 2.02335,0.117406 2.78515,0.355469 0.80942,0.19045 1.64298,0.501174 2.5,0.929687 l 2,-7.070312 c -1.04747,-0.571351 -2.26181,-1.048787 -3.64257,-1.429688 -1.33316,-0.3809 -3.07033,-0.570312 -5.21289,-0.570312 z m 35.06445,0.927734 v 35.710938 h 8.5 V 49.347656 Z m 11.05469,0 12.64257,36.066406 h 5.7129 l 11.99804,-36.066406 h -9.14062 l -4.42774,18.570313 -0.78711,5.71289 h -0.28515 l -0.85742,-5.642578 -4.92774,-18.640625 z m 32.89843,0 v 35.710938 h 8.49805 V 49.347656 Z m 33.53125,0 12.42774,35.710938 c -0.28568,1.571216 -0.64375,2.832904 -1.07227,3.785156 -0.42851,0.952252 -0.92865,1.641799 -1.5,2.070312 -0.52374,0.476127 -1.11858,0.714844 -1.78515,0.714844 -0.61897,0.04761 -1.23846,-0.04905 -1.85743,-0.287109 l -1.42773,7.285156 c 0.66658,0.380901 1.45278,0.642319 2.35742,0.785156 0.95225,0.190451 1.92787,0.28711 2.92774,0.28711 1.42837,0 2.64271,-0.430083 3.64257,-1.28711 1.04748,-0.809414 1.97575,-1.999096 2.78516,-3.570312 0.80941,-1.571216 1.57097,-3.475098 2.28516,-5.712891 0.71419,-2.237792 1.47574,-4.761168 2.28515,-7.570312 l 8.92774,-32.210938 h -8.71289 l -4.14258,19.998047 -0.64258,5.642578 h -0.35742 l -0.92774,-5.572265 -5,-20.06836 z"
|
|
||||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:8.52205467px;line-height:2.53632545px;font-family:'PT Sans Narrow';-inkscape-font-specification:'PT Sans Narrow Bold Condensed';letter-spacing:0.22319667px;word-spacing:2.60024095px;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:url(#linearGradient4523);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.27365798px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
|
|
||||||
<path
|
|
||||||
id="text5065-3"
|
|
||||||
transform="matrix(0.1193249,0,0,0.1193249,12.763965,-131.94382)"
|
|
||||||
d="m 386.9082,34.349609 c -2.04734,0 -4.09523,0.119359 -6.14258,0.357422 -2.04734,0.190451 -3.92657,0.476521 -5.64062,0.857422 v 49.494141 h 8.99805 V 67.845703 c 0.19045,0.04761 0.49922,0.09497 0.92773,0.142578 l 1.42969,0.142578 h 1.35742 0.92773 c 2.04735,0 4.02324,-0.30877 5.92774,-0.927734 1.95212,-0.618964 3.66659,-1.619234 5.14258,-3 1.47599,-1.380766 2.64102,-3.165289 3.49804,-5.355469 0.90464,-2.19018 1.35743,-4.857568 1.35743,-8 0,-3.47572 -0.52284,-6.285167 -1.57032,-8.427734 -1.04747,-2.19018 -2.40582,-3.879997 -4.07226,-5.070313 -1.66644,-1.190315 -3.57033,-1.976521 -5.71289,-2.357421 -2.09496,-0.428514 -4.23756,-0.642579 -6.42774,-0.642579 z m 51.72461,0.714844 v 48.564453 c 1.14271,0.571352 2.76052,1.09614 4.85547,1.572266 2.14257,0.476126 4.47653,0.71289 7,0.71289 4.61842,0 8.25948,-1.570458 10.92578,-4.71289 2.66631,-3.190045 4,-8.164791 4,-14.925781 0,-6.237252 -0.95292,-10.761169 -2.85742,-13.570313 -1.85689,-2.809144 -4.47497,-4.214844 -7.85547,-4.214844 -3.19004,0 -5.64141,1.047624 -7.35547,3.142578 h -0.21484 V 35.064453 Z m -50.86719,7.285156 c 0.99987,0 1.95279,0.142059 2.85743,0.427735 0.90464,0.285675 1.68889,0.761158 2.35547,1.427734 0.71418,0.618964 1.26167,1.477176 1.64257,2.572266 0.42852,1.09509 0.64258,2.428784 0.64258,4 0,1.856891 -0.21406,3.402697 -0.64258,4.640625 -0.42851,1.190315 -1.02335,2.143233 -1.78515,2.857422 -0.71419,0.666576 -1.54775,1.142058 -2.5,1.427734 -0.95225,0.285676 -1.95057,0.429687 -2.99805,0.429687 -0.28568,10e-7 -0.83316,-0.02465 -1.64258,-0.07227 -0.7618,-0.09522 -1.28659,-0.189931 -1.57226,-0.285156 v -17.06836 c 0.95225,-0.238063 2.16658,-0.357422 3.64257,-0.357422 z m 20.31836,6.998047 v 23.210938 c 0,2.666306 0.21407,4.880911 0.64258,6.642578 0.42852,1.714054 1.04606,3.070448 1.85547,4.070312 0.80942,0.999865 1.78699,1.691365 2.92969,2.072266 1.1427,0.428513 2.45174,0.642578 3.92773,0.642578 2.28541,0 4.18929,-0.547488 5.71289,-1.642578 1.57122,-1.09509 2.81021,-2.476137 3.71485,-4.142578 h 0.21289 l 1.5,4.857422 h 6.42773 c -0.3809,-1.523604 -0.64232,-3.215374 -0.78515,-5.072266 -0.14284,-1.904504 -0.21485,-3.833039 -0.21485,-5.785156 V 49.347656 h -8.49804 v 23.853516 c -0.38091,1.380765 -1.02505,2.572401 -1.92969,3.572266 -0.90464,0.952252 -2.02232,1.427734 -3.35547,1.427734 -1.38077,0 -2.33368,-0.547488 -2.85742,-1.642578 -0.52374,-1.09509 -0.78516,-3.046325 -0.78516,-5.855469 V 49.347656 Z m 43.83204,6.927735 c 1.61882,0 2.80851,0.858211 3.57031,2.572265 0.7618,1.666441 1.14258,4.307223 1.14258,7.925782 0,4.094684 -0.47549,7.023489 -1.42774,8.785156 -0.95225,1.714054 -2.3333,2.572265 -4.14258,2.572265 -1.61882,0 -2.92787,-0.26337 -3.92773,-0.787109 V 59.990234 c 0.80941,-2.475855 2.40453,-3.714843 4.78516,-3.714843 z"
|
|
||||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:8.52205467px;line-height:2.53632545px;font-family:'PT Sans Narrow';-inkscape-font-specification:'PT Sans Narrow Bold Condensed';letter-spacing:0.22319667px;word-spacing:2.60024095px;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:url(#linearGradient4526);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.24196777px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 18 KiB |
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "ActivityPub-logo.svg",
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "atproto.png",
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BIN
damus/Assets.xcassets/atproto.imageset/atproto.png
vendored
|
Before Width: | Height: | Size: 300 KiB |
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "damoose.jpeg",
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BIN
damus/Assets.xcassets/damoose.imageset/damoose.jpeg
vendored
|
Before Width: | Height: | Size: 122 KiB |
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "shadow-2.png",
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 1.0 MiB |
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "shadow.png",
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 511 KiB |
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "mutiny.png",
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BIN
damus/Assets.xcassets/mutiny.imageset/mutiny.png
vendored
|
Before Width: | Height: | Size: 1.6 KiB |
12
damus/Assets.xcassets/rss.imageset/Contents.json
vendored
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "rss.png",
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BIN
damus/Assets.xcassets/rss.imageset/rss.png
vendored
|
Before Width: | Height: | Size: 16 KiB |
12
damus/Assets.xcassets/tor.imageset/Contents.json
vendored
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "tor.svg.png",
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BIN
damus/Assets.xcassets/tor.imageset/tor.svg.png
vendored
|
Before Width: | Height: | Size: 4.2 KiB |
BIN
damus/Assets.xcassets/zeusln.imageset/zeus.png
vendored
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 109 KiB |
@@ -12,25 +12,31 @@ let RECTANGLE_GRADIENT = LinearGradient(gradient: Gradient(colors: [
|
|||||||
DamusColors.blue
|
DamusColors.blue
|
||||||
]), startPoint: .leading, endPoint: .trailing)
|
]), startPoint: .leading, endPoint: .trailing)
|
||||||
|
|
||||||
struct CustomPicker<SelectionValue: Hashable>: View {
|
struct CustomPicker<SelectionValue: Hashable, Content: View>: View {
|
||||||
|
|
||||||
let tabs: [(String, SelectionValue)]
|
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
@Namespace var picker
|
@Namespace var picker
|
||||||
@Binding var selection: SelectionValue
|
@Binding var selection: SelectionValue
|
||||||
|
@ViewBuilder let content: Content
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
|
let contentMirror = Mirror(reflecting: content)
|
||||||
|
let blocksCount = Mirror(reflecting: contentMirror.descendant("value")!).children.count
|
||||||
HStack {
|
HStack {
|
||||||
ForEach(tabs, id: \.1) { (text, tag) in
|
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 {
|
Button {
|
||||||
withAnimation(.spring()) {
|
withAnimation(.spring()) {
|
||||||
selection = tag
|
selection = tag
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Text(text).padding(EdgeInsets(top: 15, leading: 0, bottom: 10, trailing: 0))
|
text
|
||||||
|
.padding(EdgeInsets(top: 15, leading: 0, bottom: 10, trailing: 0))
|
||||||
.font(.system(size: 14, weight: .heavy))
|
.font(.system(size: 14, weight: .heavy))
|
||||||
.tag(tag)
|
|
||||||
}
|
}
|
||||||
.background(
|
.background(
|
||||||
Group {
|
Group {
|
||||||
|
|||||||
@@ -10,27 +10,18 @@ import SwiftUI
|
|||||||
|
|
||||||
class DamusColors {
|
class DamusColors {
|
||||||
static let adaptableGrey = Color("DamusAdaptableGrey")
|
static let adaptableGrey = Color("DamusAdaptableGrey")
|
||||||
static let adaptableGrey2 = Color("DamusAdaptableGrey 2")
|
|
||||||
static let adaptableLighterGrey = Color("DamusAdaptableLighterGrey")
|
|
||||||
static let adaptablePurpleBackground = Color("DamusAdaptablePurpleBackground 1")
|
|
||||||
static let adaptablePurpleBackground2 = Color("DamusAdaptablePurpleBackground 2")
|
|
||||||
static let adaptablePurpleForeground = Color("DamusAdaptablePurpleForeground")
|
|
||||||
static let adaptableBlack = Color("DamusAdaptableBlack")
|
static let adaptableBlack = Color("DamusAdaptableBlack")
|
||||||
static let adaptableWhite = Color("DamusAdaptableWhite")
|
|
||||||
static let white = Color("DamusWhite")
|
static let white = Color("DamusWhite")
|
||||||
static let black = Color("DamusBlack")
|
static let black = Color("DamusBlack")
|
||||||
static let brown = Color("DamusBrown")
|
static let brown = Color("DamusBrown")
|
||||||
static let yellow = Color("DamusYellow")
|
static let yellow = Color("DamusYellow")
|
||||||
static let gold = hex_col(r: 226, g: 168, b: 0)
|
|
||||||
static let lightGrey = Color("DamusLightGrey")
|
static let lightGrey = Color("DamusLightGrey")
|
||||||
static let mediumGrey = Color("DamusMediumGrey")
|
static let mediumGrey = Color("DamusMediumGrey")
|
||||||
static let darkGrey = Color("DamusDarkGrey")
|
static let darkGrey = Color("DamusDarkGrey")
|
||||||
static let green = Color("DamusGreen")
|
static let green = Color("DamusGreen")
|
||||||
static let purple = Color("DamusPurple")
|
static let purple = Color("DamusPurple")
|
||||||
static let deepPurple = Color("DamusDeepPurple")
|
static let deepPurple = Color("DamusDeepPurple")
|
||||||
static let highlight = Color("DamusHighlight")
|
|
||||||
static let blue = Color("DamusBlue")
|
static let blue = Color("DamusBlue")
|
||||||
static let bitcoin = Color("Bitcoin")
|
|
||||||
static let success = Color("DamusSuccessPrimary")
|
static let success = Color("DamusSuccessPrimary")
|
||||||
static let successSecondary = Color("DamusSuccessSecondary")
|
static let successSecondary = Color("DamusSuccessSecondary")
|
||||||
static let successTertiary = Color("DamusSuccessTertiary")
|
static let successTertiary = Color("DamusSuccessTertiary")
|
||||||
@@ -49,15 +40,5 @@ class DamusColors {
|
|||||||
static let neutral1 = Color("DamusNeutral1")
|
static let neutral1 = Color("DamusNeutral1")
|
||||||
static let neutral3 = Color("DamusNeutral3")
|
static let neutral3 = Color("DamusNeutral3")
|
||||||
static let neutral6 = Color("DamusNeutral6")
|
static let neutral6 = Color("DamusNeutral6")
|
||||||
static let pink = Color(red: 211/255.0, green: 76/255.0, blue: 217/255.0)
|
|
||||||
static let lighterPink = Color(red: 248/255.0, green: 105/255.0, blue: 182/255.0)
|
|
||||||
static let lightBackgroundPink = Color(red: 0xF8/255.0, green: 0xE7/255.0, blue: 0xF8/255.0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func hex_col(r: UInt8, g: UInt8, b: UInt8) -> Color {
|
|
||||||
return Color(.sRGB,
|
|
||||||
red: Double(r) / Double(0xff),
|
|
||||||
green: Double(g) / Double(0xff),
|
|
||||||
blue: Double(b) / Double(0xff),
|
|
||||||
opacity: 1.0)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
fileprivate let gold_grad_c1 = DamusColors.gold
|
fileprivate let gold_grad_c1 = hex_col(r: 226, g: 168, b: 0)
|
||||||
fileprivate let gold_grad_c2 = hex_col(r: 249, g: 243, b: 100)
|
fileprivate let gold_grad_c2 = hex_col(r: 249, g: 243, b: 100)
|
||||||
|
|
||||||
fileprivate let gold_grad = [gold_grad_c2, gold_grad_c1]
|
fileprivate let gold_grad = [gold_grad_c2, gold_grad_c1]
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
//
|
|
||||||
// MutinyGradient.swift
|
|
||||||
// damus
|
|
||||||
//
|
|
||||||
// Created by eric on 3/9/24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
fileprivate let mutiny_grad_c1 = hex_col(r: 39, g: 95, b: 161)
|
|
||||||
fileprivate let mutiny_grad_c2 = hex_col(r: 13, g: 33, b: 56)
|
|
||||||
fileprivate let mutiny_grad = [mutiny_grad_c2, mutiny_grad_c1]
|
|
||||||
|
|
||||||
let MutinyGradient: LinearGradient =
|
|
||||||
LinearGradient(colors: mutiny_grad, startPoint: .top, endPoint: .bottom)
|
|
||||||
@@ -31,49 +31,6 @@ struct ShareSheet: UIViewControllerRepresentable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom UIPageControl
|
|
||||||
struct PageControlView: UIViewRepresentable {
|
|
||||||
@Binding var currentPage: Int
|
|
||||||
var numberOfPages: Int
|
|
||||||
|
|
||||||
func makeCoordinator() -> Coordinator {
|
|
||||||
Coordinator(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeUIView(context: Context) -> UIPageControl {
|
|
||||||
let uiView = UIPageControl()
|
|
||||||
uiView.backgroundStyle = .minimal
|
|
||||||
uiView.currentPageIndicatorTintColor = UIColor(Color("DamusPurple"))
|
|
||||||
uiView.pageIndicatorTintColor = UIColor(Color("DamusLightGrey"))
|
|
||||||
uiView.currentPage = currentPage
|
|
||||||
uiView.numberOfPages = numberOfPages
|
|
||||||
uiView.addTarget(context.coordinator, action: #selector(Coordinator.valueChanged), for: .valueChanged)
|
|
||||||
return uiView
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUIView(_ uiView: UIPageControl, context: Context) {
|
|
||||||
uiView.currentPage = currentPage
|
|
||||||
uiView.numberOfPages = numberOfPages
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension PageControlView {
|
|
||||||
final class Coordinator: NSObject {
|
|
||||||
var parent: PageControlView
|
|
||||||
|
|
||||||
init(_ parent: PageControlView) {
|
|
||||||
self.parent = parent
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func valueChanged(sender: UIPageControl) {
|
|
||||||
let currentPage = sender.currentPage
|
|
||||||
withAnimation {
|
|
||||||
parent.currentPage = currentPage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
enum ImageShape {
|
enum ImageShape {
|
||||||
case square
|
case square
|
||||||
@@ -95,64 +52,42 @@ enum ImageShape {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CarouselModel: ObservableObject {
|
|
||||||
var current_url: URL?
|
|
||||||
var fillHeight: CGFloat
|
|
||||||
var maxHeight: CGFloat
|
|
||||||
var firstImageHeight: CGFloat?
|
|
||||||
|
|
||||||
@Published var open_sheet: Bool
|
|
||||||
@Published var selectedIndex: Int
|
|
||||||
@Published var video_size: CGSize?
|
|
||||||
@Published var image_fill: ImageFill?
|
|
||||||
|
|
||||||
init(image_fill: ImageFill?) {
|
|
||||||
self.current_url = nil
|
|
||||||
self.fillHeight = 350
|
|
||||||
self.maxHeight = UIScreen.main.bounds.height * 1.2 // 1.2
|
|
||||||
self.firstImageHeight = nil
|
|
||||||
self.open_sheet = false
|
|
||||||
self.selectedIndex = 0
|
|
||||||
self.video_size = nil
|
|
||||||
self.image_fill = image_fill
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Image Carousel
|
// MARK: - Image Carousel
|
||||||
@MainActor
|
@MainActor
|
||||||
struct ImageCarousel<Content: View>: View {
|
struct ImageCarousel: View {
|
||||||
var urls: [MediaUrl]
|
var urls: [MediaUrl]
|
||||||
|
|
||||||
let evid: NoteId
|
let evid: NoteId
|
||||||
|
|
||||||
let state: DamusState
|
let state: DamusState
|
||||||
@ObservedObject var model: CarouselModel
|
|
||||||
let content: ((_ dismiss: @escaping (() -> Void)) -> Content)?
|
@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 * 1.2 // 1.2
|
||||||
|
@State private var firstImageHeight: CGFloat? = nil
|
||||||
|
@State private var currentImageHeight: CGFloat?
|
||||||
|
@State private var selectedIndex = 0
|
||||||
|
@State private var video_size: CGSize? = nil
|
||||||
|
|
||||||
init(state: DamusState, evid: NoteId, urls: [MediaUrl]) {
|
init(state: DamusState, evid: NoteId, urls: [MediaUrl]) {
|
||||||
|
_open_sheet = State(initialValue: false)
|
||||||
|
_current_url = State(initialValue: nil)
|
||||||
|
let media_model = state.events.get_cache_data(evid).media_metadata_model
|
||||||
|
_image_fill = State(initialValue: media_model.fill)
|
||||||
self.urls = urls
|
self.urls = urls
|
||||||
self.evid = evid
|
self.evid = evid
|
||||||
self.state = state
|
self.state = state
|
||||||
let media_model = state.events.get_cache_data(evid).media_metadata_model
|
|
||||||
self._model = ObservedObject(initialValue: CarouselModel(image_fill: media_model.fill))
|
|
||||||
self.content = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
init(state: DamusState, evid: NoteId, urls: [MediaUrl], @ViewBuilder content: @escaping (_ dismiss: @escaping (() -> Void)) -> Content) {
|
|
||||||
self.urls = urls
|
|
||||||
self.evid = evid
|
|
||||||
self.state = state
|
|
||||||
let media_model = state.events.get_cache_data(evid).media_metadata_model
|
|
||||||
self._model = ObservedObject(initialValue: CarouselModel(image_fill: media_model.fill))
|
|
||||||
self.content = content
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var filling: Bool {
|
var filling: Bool {
|
||||||
model.image_fill?.filling == true
|
image_fill?.filling == true
|
||||||
}
|
}
|
||||||
|
|
||||||
var height: CGFloat {
|
var height: CGFloat {
|
||||||
model.firstImageHeight ?? model.image_fill?.height ?? model.fillHeight
|
firstImageHeight ?? image_fill?.height ?? fillHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
func Placeholder(url: URL, geo_size: CGSize, num_urls: Int) -> some View {
|
func Placeholder(url: URL, geo_size: CGSize, num_urls: Int) -> some View {
|
||||||
@@ -170,9 +105,9 @@ struct ImageCarousel<Content: View>: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if self.model.image_fill == nil, let size = state.video.size_for_url(url) {
|
if self.image_fill == nil, let size = state.video.size_for_url(url) {
|
||||||
let fill = ImageFill.calculate_image_fill(geo_size: geo_size, img_size: size, maxHeight: model.maxHeight, fillHeight: model.fillHeight)
|
let fill = ImageFill.calculate_image_fill(geo_size: geo_size, img_size: size, maxHeight: maxHeight, fillHeight: fillHeight)
|
||||||
self.model.image_fill = fill
|
self.image_fill = fill
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,23 +118,23 @@ struct ImageCarousel<Content: View>: View {
|
|||||||
case .image(let url):
|
case .image(let url):
|
||||||
Img(geo: geo, url: url, index: index)
|
Img(geo: geo, url: url, index: index)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
model.open_sheet = true
|
open_sheet = true
|
||||||
}
|
}
|
||||||
case .video(let url):
|
case .video(let url):
|
||||||
DamusVideoPlayer(url: url, video_size: $model.video_size, controller: state.video, style: .preview(on_tap: { model.open_sheet = true }))
|
DamusVideoPlayer(url: url, video_size: $video_size, controller: state.video)
|
||||||
.onChange(of: model.video_size) { size in
|
.onChange(of: video_size) { size in
|
||||||
guard let size else { return }
|
guard let size else { return }
|
||||||
|
|
||||||
let fill = ImageFill.calculate_image_fill(geo_size: geo.size, img_size: size, maxHeight: model.maxHeight, fillHeight: model.fillHeight)
|
let fill = ImageFill.calculate_image_fill(geo_size: geo.size, img_size: size, maxHeight: maxHeight, fillHeight: fillHeight)
|
||||||
|
|
||||||
print("video_size changed \(size)")
|
print("video_size changed \(size)")
|
||||||
if self.model.image_fill == nil {
|
if self.image_fill == nil {
|
||||||
print("video_size firstImageHeight \(fill.height)")
|
print("video_size firstImageHeight \(fill.height)")
|
||||||
self.model.firstImageHeight = fill.height
|
firstImageHeight = fill.height
|
||||||
state.events.get_cache_data(evid).media_metadata_model.fill = fill
|
state.events.get_cache_data(evid).media_metadata_model.fill = fill
|
||||||
}
|
}
|
||||||
|
|
||||||
self.model.image_fill = fill
|
self.image_fill = fill
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,7 +150,7 @@ struct ImageCarousel<Content: View>: View {
|
|||||||
.configure { view in
|
.configure { view in
|
||||||
view.framePreloadCount = 3
|
view.framePreloadCount = 3
|
||||||
}
|
}
|
||||||
.imageFill(for: geo.size, max: model.maxHeight, fill: model.fillHeight) { fill in
|
.imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in
|
||||||
state.events.get_cache_data(evid).media_metadata_model.fill = fill
|
state.events.get_cache_data(evid).media_metadata_model.fill = fill
|
||||||
// blur hash can be discarded when we have the url
|
// blur hash can be discarded when we have the url
|
||||||
// NOTE: this is the wrong place for this... we need to remove
|
// NOTE: this is the wrong place for this... we need to remove
|
||||||
@@ -224,9 +159,9 @@ struct ImageCarousel<Content: View>: View {
|
|||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||||
state.events.lookup_img_metadata(url: url)?.state = .not_needed
|
state.events.lookup_img_metadata(url: url)?.state = .not_needed
|
||||||
}
|
}
|
||||||
self.model.image_fill = fill
|
image_fill = fill
|
||||||
if index == 0 {
|
if index == 0 {
|
||||||
self.model.firstImageHeight = fill.height
|
firstImageHeight = fill.height
|
||||||
//maxHeight = firstImageHeight ?? maxHeight
|
//maxHeight = firstImageHeight ?? maxHeight
|
||||||
} else {
|
} else {
|
||||||
//maxHeight = firstImageHeight ?? fill.height
|
//maxHeight = firstImageHeight ?? fill.height
|
||||||
@@ -246,7 +181,7 @@ struct ImageCarousel<Content: View>: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var Medias: some View {
|
var Medias: some View {
|
||||||
TabView(selection: $model.selectedIndex) {
|
TabView(selection: $selectedIndex) {
|
||||||
ForEach(urls.indices, id: \.self) { index in
|
ForEach(urls.indices, id: \.self) { index in
|
||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
Media(geo: geo, url: urls[index], index: index)
|
Media(geo: geo, url: urls[index], index: index)
|
||||||
@@ -254,22 +189,14 @@ struct ImageCarousel<Content: View>: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
||||||
.fullScreenCover(isPresented: $model.open_sheet) {
|
.fullScreenCover(isPresented: $open_sheet) {
|
||||||
if let content {
|
ImageView(video_controller: state.video, urls: urls, disable_animation: state.settings.disable_animation)
|
||||||
FullScreenCarouselView<Content>(video_controller: state.video, urls: urls, settings: state.settings, selectedIndex: $model.selectedIndex) {
|
|
||||||
content({ // Dismiss closure
|
|
||||||
model.open_sheet = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
FullScreenCarouselView<AnyView>(video_controller: state.video, urls: urls, settings: state.settings, selectedIndex: $model.selectedIndex)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.frame(height: height)
|
.frame(height: height)
|
||||||
.onChange(of: model.selectedIndex) { value in
|
.onChange(of: selectedIndex) { value in
|
||||||
model.selectedIndex = value
|
selectedIndex = value
|
||||||
}
|
}
|
||||||
|
.tabViewStyle(PageTabViewStyle())
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -277,13 +204,33 @@ struct ImageCarousel<Content: View>: View {
|
|||||||
Medias
|
Medias
|
||||||
.onTapGesture { }
|
.onTapGesture { }
|
||||||
|
|
||||||
if urls.count > 1 {
|
// This is our custom carousel image indicator
|
||||||
PageControlView(currentPage: $model.selectedIndex, numberOfPages: urls.count)
|
CarouselDotsView(urls: urls, selectedIndex: $selectedIndex)
|
||||||
.frame(maxWidth: 0, maxHeight: 0)
|
|
||||||
.padding(.top, 5)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Custom Carousel
|
||||||
|
struct CarouselDotsView<T>: View {
|
||||||
|
let urls: [T]
|
||||||
|
@Binding var selectedIndex: Int
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if urls.count > 1 {
|
||||||
|
HStack {
|
||||||
|
ForEach(urls.indices, id: \.self) { index in
|
||||||
|
Circle()
|
||||||
|
.fill(index == selectedIndex ? Color("DamusPurple") : Color("DamusLightGrey"))
|
||||||
|
.frame(width: 10, height: 10)
|
||||||
|
.onTapGesture {
|
||||||
|
selectedIndex = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, CGFloat(8))
|
||||||
|
.id(UUID())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Image Modifier
|
// MARK: - Image Modifier
|
||||||
@@ -338,9 +285,7 @@ public struct ImageFill {
|
|||||||
struct ImageCarousel_Previews: PreviewProvider {
|
struct ImageCarousel_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!)
|
let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!)
|
||||||
let test_video_url: MediaUrl = .video(URL(string: "http://cdn.jb55.com/s/zaps-build.mp4")!)
|
ImageCarousel(state: test_damus_state, evid: test_note.id, urls: [url, url])
|
||||||
ImageCarousel<AnyView>(state: test_damus_state, evid: test_note.id, urls: [test_video_url, url])
|
|
||||||
.environmentObject(OrientationTracker())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,12 +39,7 @@ struct InvoiceView: View {
|
|||||||
if settings.show_wallet_selector {
|
if settings.show_wallet_selector {
|
||||||
present_sheet(.select_wallet(invoice: invoice.string))
|
present_sheet(.select_wallet(invoice: invoice.string))
|
||||||
} else {
|
} else {
|
||||||
do {
|
open_with_wallet(wallet: settings.default_wallet.model, invoice: invoice.string)
|
||||||
try open_with_wallet(wallet: settings.default_wallet.model, invoice: invoice.string)
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
present_sheet(.select_wallet(invoice: invoice.string))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
RoundedRectangle(cornerRadius: 20, style: .circular)
|
RoundedRectangle(cornerRadius: 20, style: .circular)
|
||||||
@@ -87,26 +82,21 @@ struct InvoiceView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum OpenWalletError: Error {
|
func open_with_wallet(wallet: Wallet.Model, invoice: String) {
|
||||||
case no_wallet_to_open
|
|
||||||
case store_link_invalid
|
|
||||||
case system_cannot_open_store_link
|
|
||||||
}
|
|
||||||
|
|
||||||
func open_with_wallet(wallet: Wallet.Model, invoice: String) throws {
|
|
||||||
if let url = URL(string: "\(wallet.link)\(invoice)"), UIApplication.shared.canOpenURL(url) {
|
if let url = URL(string: "\(wallet.link)\(invoice)"), UIApplication.shared.canOpenURL(url) {
|
||||||
UIApplication.shared.open(url)
|
UIApplication.shared.open(url)
|
||||||
} else {
|
} else {
|
||||||
guard let store_link = wallet.appStoreLink else {
|
guard let store_link = wallet.appStoreLink else {
|
||||||
throw OpenWalletError.no_wallet_to_open
|
// TODO: do something here if we don't have an appstore link
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let url = URL(string: store_link) else {
|
guard let url = URL(string: store_link) else {
|
||||||
throw OpenWalletError.store_link_invalid
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard UIApplication.shared.canOpenURL(url) else {
|
guard UIApplication.shared.canOpenURL(url) else {
|
||||||
throw OpenWalletError.system_cannot_open_store_link
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
UIApplication.shared.open(url)
|
UIApplication.shared.open(url)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ struct NIP05Badge: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var username_matches_nip05: Bool {
|
var username_matches_nip05: Bool {
|
||||||
guard let name = profiles.lookup(id: pubkey)?.map({ p in p?.name }).value
|
guard let name = profiles.lookup(id: pubkey).map({ p in p?.name }).value
|
||||||
else {
|
else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,49 +7,23 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
enum NeutralButtonShape {
|
|
||||||
case rounded, capsule, circle
|
|
||||||
|
|
||||||
var style: NeutralButtonStyle {
|
|
||||||
switch self {
|
|
||||||
case .rounded:
|
|
||||||
return NeutralButtonStyle(padding: EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10), cornerRadius: 12)
|
|
||||||
case .capsule:
|
|
||||||
return NeutralButtonStyle(padding: EdgeInsets(top: 5, leading: 15, bottom: 5, trailing: 15), cornerRadius: 20)
|
|
||||||
case .circle:
|
|
||||||
return NeutralButtonStyle(padding: EdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20), cornerRadius: 9999)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct NeutralButtonStyle: ButtonStyle {
|
struct NeutralButtonStyle: ButtonStyle {
|
||||||
let padding: EdgeInsets
|
func makeBody(configuration: Self.Configuration) -> some View {
|
||||||
let cornerRadius: CGFloat
|
return configuration.label
|
||||||
let scaleEffect: CGFloat
|
|
||||||
|
|
||||||
init(padding: EdgeInsets = EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0), cornerRadius: CGFloat = 15, scaleEffect: CGFloat = 0.95) {
|
|
||||||
self.padding = padding
|
|
||||||
self.cornerRadius = cornerRadius
|
|
||||||
self.scaleEffect = scaleEffect
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeBody(configuration: Configuration) -> some View {
|
|
||||||
configuration.label
|
|
||||||
.padding(padding)
|
|
||||||
.background(DamusColors.neutral1)
|
.background(DamusColors.neutral1)
|
||||||
.cornerRadius(cornerRadius)
|
.cornerRadius(12)
|
||||||
.overlay(
|
.overlay(
|
||||||
RoundedRectangle(cornerRadius: cornerRadius)
|
RoundedRectangle(cornerRadius: 12)
|
||||||
.stroke(DamusColors.neutral3, lineWidth: 1)
|
.stroke(DamusColors.neutral3, lineWidth: 1)
|
||||||
)
|
)
|
||||||
.scaleEffect(configuration.isPressed ? scaleEffect : 1)
|
.scaleEffect(configuration.isPressed ? 0.95 : 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
struct NeutralButtonStyle_Previews: PreviewProvider {
|
struct NeutralButtonStyle_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack {
|
VStack {
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
print("dynamic size")
|
print("dynamic size")
|
||||||
}) {
|
}) {
|
||||||
@@ -58,6 +32,7 @@ struct NeutralButtonStyle_Previews: PreviewProvider {
|
|||||||
}
|
}
|
||||||
.buttonStyle(NeutralButtonStyle())
|
.buttonStyle(NeutralButtonStyle())
|
||||||
|
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
print("infinite width")
|
print("infinite width")
|
||||||
}) {
|
}) {
|
||||||
@@ -69,17 +44,6 @@ struct NeutralButtonStyle_Previews: PreviewProvider {
|
|||||||
}
|
}
|
||||||
.buttonStyle(NeutralButtonStyle())
|
.buttonStyle(NeutralButtonStyle())
|
||||||
.padding()
|
.padding()
|
||||||
|
|
||||||
Button(String(stringLiteral: "Rounded Button"), action: {})
|
|
||||||
.buttonStyle(NeutralButtonShape.rounded.style)
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
Button(String(stringLiteral: "Capsule Button"), action: {})
|
|
||||||
.buttonStyle(NeutralButtonShape.capsule.style)
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
Button(action: {}, label: {Image("messages")})
|
|
||||||
.buttonStyle(NeutralButtonShape.circle.style)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,9 +83,9 @@ struct SingleCharacterAvatar: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NonImageAvatar {
|
NonImageAvatar {
|
||||||
Text(character)
|
Text(verbatim: character)
|
||||||
.font(.largeTitle.bold())
|
.font(.largeTitle.bold())
|
||||||
.mask(Text(character)
|
.mask(Text(verbatim: character)
|
||||||
.font(.largeTitle.bold()))
|
.font(.largeTitle.bold()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ struct NonImageAvatar<Content: View>: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
Circle()
|
Circle()
|
||||||
.fill(DamusColors.lightBackgroundPink)
|
.fill(Color(red: 0xF8/255.0, green: 0xE7/255.0, blue: 0xF8/255.0))
|
||||||
.frame(width: 54, height: 54)
|
.frame(width: 54, height: 54)
|
||||||
|
|
||||||
content
|
content
|
||||||
|
|||||||
@@ -9,26 +9,14 @@ import UIKit
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SelectableText: View {
|
struct SelectableText: View {
|
||||||
let damus_state: DamusState
|
|
||||||
let event: NostrEvent?
|
|
||||||
let attributedString: AttributedString
|
let attributedString: AttributedString
|
||||||
let textAlignment: NSTextAlignment
|
|
||||||
@State private var showHighlightPost = false
|
|
||||||
@State private var showMutePost = false
|
|
||||||
@State private var selectedText = ""
|
|
||||||
@State private var selectedTextHeight: CGFloat = .zero
|
@State private var selectedTextHeight: CGFloat = .zero
|
||||||
@State private var selectedTextWidth: CGFloat = .zero
|
@State private var selectedTextWidth: CGFloat = .zero
|
||||||
|
|
||||||
let size: EventViewKind
|
let size: EventViewKind
|
||||||
|
|
||||||
init(damus_state: DamusState, event: NostrEvent?, attributedString: AttributedString, textAlignment: NSTextAlignment? = nil, size: EventViewKind) {
|
|
||||||
self.damus_state = damus_state
|
|
||||||
self.event = event
|
|
||||||
self.attributedString = attributedString
|
|
||||||
self.textAlignment = textAlignment ?? NSTextAlignment.natural
|
|
||||||
self.size = size
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
TextViewRepresentable(
|
TextViewRepresentable(
|
||||||
@@ -36,11 +24,6 @@ struct SelectableText: View {
|
|||||||
textColor: UIColor.label,
|
textColor: UIColor.label,
|
||||||
font: eventviewsize_to_uifont(size),
|
font: eventviewsize_to_uifont(size),
|
||||||
fixedWidth: selectedTextWidth,
|
fixedWidth: selectedTextWidth,
|
||||||
textAlignment: self.textAlignment,
|
|
||||||
enableHighlighting: self.enableHighlighting(),
|
|
||||||
showHighlightPost: $showHighlightPost,
|
|
||||||
showMutePost: $showMutePost,
|
|
||||||
selectedText: $selectedText,
|
|
||||||
height: $selectedTextHeight
|
height: $selectedTextHeight
|
||||||
)
|
)
|
||||||
.padding([.leading, .trailing], -1.0)
|
.padding([.leading, .trailing], -1.0)
|
||||||
@@ -55,66 +38,8 @@ struct SelectableText: View {
|
|||||||
self.selectedTextWidth = newSize.width
|
self.selectedTextWidth = newSize.width
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showHighlightPost) {
|
|
||||||
if let event {
|
|
||||||
HighlightPostView(damus_state: damus_state, event: event, selectedText: $selectedText)
|
|
||||||
.presentationDragIndicator(.visible)
|
|
||||||
.presentationDetents([.height(selectedTextHeight + 150), .medium, .large])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $showMutePost) {
|
|
||||||
AddMuteItemView(state: damus_state, new_text: $selectedText)
|
|
||||||
.presentationDragIndicator(.visible)
|
|
||||||
.presentationDetents([.height(300), .medium, .large])
|
|
||||||
}
|
|
||||||
.frame(height: selectedTextHeight)
|
.frame(height: selectedTextHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
func enableHighlighting() -> Bool {
|
|
||||||
self.event != nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate class TextView: UITextView {
|
|
||||||
@Binding var showHighlightPost: Bool
|
|
||||||
@Binding var showMutePost: Bool
|
|
||||||
@Binding var selectedText: String
|
|
||||||
|
|
||||||
init(frame: CGRect, textContainer: NSTextContainer?, showHighlightPost: Binding<Bool>, showMutePost: Binding<Bool>, selectedText: Binding<String>) {
|
|
||||||
self._showHighlightPost = showHighlightPost
|
|
||||||
self._showMutePost = showMutePost
|
|
||||||
self._selectedText = selectedText
|
|
||||||
super.init(frame: frame, textContainer: textContainer)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
|
||||||
if action == #selector(highlightText(_:)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if action == #selector(muteText(_:)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.canPerformAction(action, withSender: sender)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc public func highlightText(_ sender: Any?) {
|
|
||||||
guard let selectedRange = self.selectedTextRange else { return }
|
|
||||||
selectedText = self.text(in: selectedRange) ?? ""
|
|
||||||
showHighlightPost.toggle()
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc public func muteText(_ sender: Any?) {
|
|
||||||
guard let selectedRange = self.selectedTextRange else { return }
|
|
||||||
selectedText = self.text(in: selectedRange) ?? ""
|
|
||||||
showMutePost.toggle()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate struct TextViewRepresentable: UIViewRepresentable {
|
fileprivate struct TextViewRepresentable: UIViewRepresentable {
|
||||||
@@ -123,15 +48,11 @@ fileprivate class TextView: UITextView {
|
|||||||
let textColor: UIColor
|
let textColor: UIColor
|
||||||
let font: UIFont
|
let font: UIFont
|
||||||
let fixedWidth: CGFloat
|
let fixedWidth: CGFloat
|
||||||
let textAlignment: NSTextAlignment
|
|
||||||
let enableHighlighting: Bool
|
|
||||||
@Binding var showHighlightPost: Bool
|
|
||||||
@Binding var showMutePost: Bool
|
|
||||||
@Binding var selectedText: String
|
|
||||||
@Binding var height: CGFloat
|
@Binding var height: CGFloat
|
||||||
|
|
||||||
func makeUIView(context: UIViewRepresentableContext<Self>) -> TextView {
|
func makeUIView(context: UIViewRepresentableContext<Self>) -> UITextView {
|
||||||
let view = TextView(frame: .zero, textContainer: nil, showHighlightPost: $showHighlightPost, showMutePost: $showMutePost, selectedText: $selectedText)
|
let view = UITextView()
|
||||||
view.isEditable = false
|
view.isEditable = false
|
||||||
view.dataDetectorTypes = .all
|
view.dataDetectorTypes = .all
|
||||||
view.isSelectable = true
|
view.isSelectable = true
|
||||||
@@ -140,20 +61,12 @@ fileprivate class TextView: UITextView {
|
|||||||
view.textContainerInset = .zero
|
view.textContainerInset = .zero
|
||||||
view.textContainerInset.left = 1.0
|
view.textContainerInset.left = 1.0
|
||||||
view.textContainerInset.right = 1.0
|
view.textContainerInset.right = 1.0
|
||||||
view.textAlignment = textAlignment
|
|
||||||
|
|
||||||
let menuController = UIMenuController.shared
|
|
||||||
let highlightItem = UIMenuItem(title: "Highlight", action: #selector(view.highlightText(_:)))
|
|
||||||
let muteItem = UIMenuItem(title: "Mute", action: #selector(view.muteText(_:)))
|
|
||||||
menuController.menuItems = self.enableHighlighting ? [highlightItem, muteItem] : []
|
|
||||||
|
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUIView(_ uiView: TextView, context: UIViewRepresentableContext<Self>) {
|
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<Self>) {
|
||||||
let mutableAttributedString = createNSAttributedString()
|
let mutableAttributedString = createNSAttributedString()
|
||||||
uiView.attributedText = mutableAttributedString
|
uiView.attributedText = mutableAttributedString
|
||||||
uiView.textAlignment = self.textAlignment
|
|
||||||
|
|
||||||
let newHeight = mutableAttributedString.height(containerWidth: fixedWidth)
|
let newHeight = mutableAttributedString.height(containerWidth: fixedWidth)
|
||||||
|
|
||||||
|
|||||||
@@ -52,21 +52,13 @@ enum StatusDuration: CustomStringConvertible, CaseIterable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Fields{
|
|
||||||
case status
|
|
||||||
case link
|
|
||||||
}
|
|
||||||
|
|
||||||
struct UserStatusSheet: View {
|
struct UserStatusSheet: View {
|
||||||
let damus_state: DamusState
|
|
||||||
let postbox: PostBox
|
let postbox: PostBox
|
||||||
let keypair: Keypair
|
let keypair: Keypair
|
||||||
|
|
||||||
@State var duration: StatusDuration = .never
|
@State var duration: StatusDuration = .never
|
||||||
@State var show_link: Bool = false
|
|
||||||
|
|
||||||
@ObservedObject var status: UserStatusModel
|
@ObservedObject var status: UserStatusModel
|
||||||
@Environment(\.colorScheme) var colorScheme
|
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
|
|
||||||
var status_binding: Binding<String> {
|
var status_binding: Binding<String> {
|
||||||
@@ -94,17 +86,45 @@ struct UserStatusSheet: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
// This is needed to prevent the view from being moved when the keyboard is shown
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
GeometryReader { geometry in
|
Text("Set Status", comment: "Title of view that allows the user to set their profile status (e.g. working, studying, coding)")
|
||||||
VStack {
|
.font(.largeTitle)
|
||||||
|
|
||||||
|
TextField(text: status_binding, label: {
|
||||||
|
Text("📋 Working", comment: "Placeholder as an example of what the user could set as their profile status.")
|
||||||
|
})
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
|
Image("link")
|
||||||
|
|
||||||
|
TextField(text: url_binding, label: {
|
||||||
|
Text("https://example.com", comment: "Placeholder as an example of what the user could set so that the link is opened when the status is tapped.")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("Clear status", comment: "Label to prompt user to select an expiration time for the profile status to clear.")
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Picker(NSLocalizedString("Duration", comment: "Label for profile status expiration duration picker."), selection: $duration) {
|
||||||
|
ForEach(StatusDuration.allCases, id: \.self) { d in
|
||||||
|
Text(verbatim: d.description)
|
||||||
|
.tag(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Toggle(isOn: $status.playing_enabled, label: {
|
||||||
|
Text("Broadcast music playing on Apple Music", comment: "Toggle to enable or disable broadcasting what music is being played on Apple Music in their profile status.")
|
||||||
|
})
|
||||||
|
|
||||||
|
HStack(alignment: .center) {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
dismiss()
|
dismiss()
|
||||||
}, label: {
|
}, label: {
|
||||||
Text("Cancel", comment: "Cancel button text for dismissing profile status settings view.")
|
Text("Cancel", comment: "Cancel button text for dismissing profile status settings view.")
|
||||||
.padding(10)
|
|
||||||
})
|
})
|
||||||
.buttonStyle(NeutralButtonStyle())
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
@@ -120,99 +140,21 @@ struct UserStatusSheet: View {
|
|||||||
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}, label: {
|
}, label: {
|
||||||
Text("Share", comment: "Save button text for saving profile status settings.")
|
Text("Save", comment: "Save button text for saving profile status settings.")
|
||||||
})
|
})
|
||||||
.buttonStyle(GradientButtonStyle(padding: 10))
|
.buttonStyle(GradientButtonStyle())
|
||||||
}
|
}
|
||||||
.padding(5)
|
.padding([.top], 30)
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
ZStack(alignment: .top) {
|
|
||||||
ProfilePicView(pubkey: keypair.pubkey, size: 120.0, highlight: .custom(DamusColors.white, 3.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
|
||||||
.padding(.top, 30)
|
|
||||||
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
HStack {
|
|
||||||
TextField(NSLocalizedString("Staying humble...", comment: "Placeholder as an example of what the user could set as their profile status."), text: status_binding, axis: .vertical)
|
|
||||||
.autocorrectionDisabled(true)
|
|
||||||
.textInputAutocapitalization(.never)
|
|
||||||
.lineLimit(3)
|
|
||||||
.frame(width: 175)
|
|
||||||
|
|
||||||
}
|
|
||||||
.padding(10)
|
|
||||||
.background(colorScheme == .light ? .white : DamusColors.neutral3)
|
|
||||||
.cornerRadius(15)
|
|
||||||
.shadow(color: colorScheme == .light ? DamusColors.neutral3 : .clear, radius: 15)
|
|
||||||
|
|
||||||
Circle()
|
|
||||||
.fill(colorScheme == .light ? .white : DamusColors.neutral3)
|
|
||||||
.frame(width: 12, height: 12)
|
|
||||||
.padding(.trailing, 140)
|
|
||||||
|
|
||||||
Circle()
|
|
||||||
.fill(colorScheme == .light ? .white : DamusColors.neutral3)
|
|
||||||
.frame(width: 7, height: 7)
|
|
||||||
.padding(.trailing, 120)
|
|
||||||
|
|
||||||
}
|
|
||||||
.padding(.leading, 60)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack {
|
|
||||||
HStack {
|
|
||||||
Image("link")
|
|
||||||
.foregroundColor(DamusColors.neutral3)
|
|
||||||
|
|
||||||
TextField(text: url_binding, label: {
|
|
||||||
Text("Add an external link", comment: "Placeholder as an example of what the user could set so that the link is opened when the status is tapped.")
|
|
||||||
})
|
|
||||||
.autocorrectionDisabled(true)
|
|
||||||
}
|
|
||||||
.padding(10)
|
|
||||||
.cornerRadius(12)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 12)
|
|
||||||
.stroke(DamusColors.neutral3, lineWidth: 1)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
Toggle(isOn: $status.playing_enabled, label: {
|
|
||||||
Text("Broadcast music playing on Apple Music", comment: "Toggle to enable or disable broadcasting what music is being played on Apple Music in their profile status.")
|
|
||||||
})
|
|
||||||
.tint(DamusColors.purple)
|
|
||||||
.padding(.horizontal)
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Text("Clear status", comment: "Label to prompt user to select an expiration time for the profile status to clear.")
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Picker(NSLocalizedString("Duration", comment: "Label for profile status expiration duration picker."), selection: $duration) {
|
|
||||||
ForEach(StatusDuration.allCases, id: \.self) { d in
|
|
||||||
Text(d.description)
|
|
||||||
.tag(d)
|
|
||||||
}
|
}
|
||||||
}
|
.padding(30)
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
}
|
|
||||||
.padding(.top)
|
|
||||||
.background(DamusColors.adaptableWhite.edgesIgnoringSafeArea(.all))
|
|
||||||
}
|
|
||||||
.dismissKeyboardOnTap()
|
|
||||||
.ignoresSafeArea(.keyboard, edges: .bottom)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
struct UserStatusSheet_Previews: PreviewProvider {
|
struct UserStatusSheet_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
UserStatusSheet(damus_state: test_damus_state, postbox: test_damus_state.postbox, keypair: test_keypair, status: .init())
|
UserStatusSheet(postbox: test_damus_state.postbox, keypair: test_keypair, status: .init())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,40 +8,17 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SupporterBadge: View {
|
struct SupporterBadge: View {
|
||||||
let percent: Int?
|
let percent: Int
|
||||||
let purple_account: DamusPurple.Account?
|
|
||||||
let style: Style
|
|
||||||
|
|
||||||
init(percent: Int?, purple_account: DamusPurple.Account? = nil, style: Style) {
|
|
||||||
self.percent = percent
|
|
||||||
self.purple_account = purple_account
|
|
||||||
self.style = style
|
|
||||||
}
|
|
||||||
|
|
||||||
let size: CGFloat = 17
|
let size: CGFloat = 17
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
if percent < 100 {
|
||||||
if let purple_account, purple_account.active == true {
|
|
||||||
HStack(spacing: 1) {
|
|
||||||
Image("star.fill")
|
|
||||||
.resizable()
|
|
||||||
.frame(width:size, height:size)
|
|
||||||
.foregroundStyle(GoldGradient)
|
|
||||||
if self.style == .full {
|
|
||||||
let date = format_date(date: purple_account.created_at, time_style: .none)
|
|
||||||
Text(date)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.font(.caption)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if let percent, percent < 100 {
|
|
||||||
Image("star.fill")
|
Image("star.fill")
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width:size, height:size)
|
.frame(width:size, height:size)
|
||||||
.foregroundColor(support_level_color(percent))
|
.foregroundColor(support_level_color(percent))
|
||||||
} else if let percent, percent == 100 {
|
} else {
|
||||||
Image("star.fill")
|
Image("star.fill")
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width:size, height:size)
|
.frame(width:size, height:size)
|
||||||
@@ -50,12 +27,6 @@ struct SupporterBadge: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Style {
|
|
||||||
case full // Shows the entire badge with a purple subscriber number if present
|
|
||||||
case compact // Does not show purple subscriber number. Only shows the star (if applicable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func support_level_color(_ percent: Int) -> Color {
|
func support_level_color(_ percent: Int) -> Color {
|
||||||
if percent == 0 {
|
if percent == 0 {
|
||||||
return .gray
|
return .gray
|
||||||
@@ -73,24 +44,13 @@ func support_level_color(_ percent: Int) -> Color {
|
|||||||
struct SupporterBadge_Previews: PreviewProvider {
|
struct SupporterBadge_Previews: PreviewProvider {
|
||||||
static func Level(_ p: Int) -> some View {
|
static func Level(_ p: Int) -> some View {
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
SupporterBadge(percent: p, style: .full)
|
SupporterBadge(percent: p)
|
||||||
.frame(width: 50)
|
.frame(width: 50)
|
||||||
Text(verbatim: p.formatted())
|
Text(verbatim: p.formatted())
|
||||||
.frame(width: 50)
|
.frame(width: 50)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func Purple(_ subscriber_number: Int) -> some View {
|
|
||||||
HStack(alignment: .center) {
|
|
||||||
SupporterBadge(
|
|
||||||
percent: nil,
|
|
||||||
purple_account: DamusPurple.Account(pubkey: test_pubkey, created_at: .now, expiry: .now.addingTimeInterval(10000), subscriber_number: subscriber_number, active: true),
|
|
||||||
style: .full
|
|
||||||
)
|
|
||||||
.frame(width: 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
@@ -106,12 +66,6 @@ struct SupporterBadge_Previews: PreviewProvider {
|
|||||||
Level(80)
|
Level(80)
|
||||||
Level(90)
|
Level(90)
|
||||||
Level(100)
|
Level(100)
|
||||||
Purple(1)
|
|
||||||
Purple(2)
|
|
||||||
Purple(3)
|
|
||||||
Purple(99)
|
|
||||||
Purple(100)
|
|
||||||
Purple(1971)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ enum TranslateStatus: Equatable {
|
|||||||
case not_needed
|
case not_needed
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate let MIN_UNIQUE_CHARS = 2
|
|
||||||
|
|
||||||
struct TranslateView: View {
|
struct TranslateView: View {
|
||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
let event: NostrEvent
|
let event: NostrEvent
|
||||||
@@ -53,7 +51,7 @@ struct TranslateView: View {
|
|||||||
.padding([.top, .bottom], 10)
|
.padding([.top, .bottom], 10)
|
||||||
|
|
||||||
if self.size == .selected {
|
if self.size == .selected {
|
||||||
SelectableText(damus_state: damus_state, event: event, attributedString: artifacts.content.attributed, size: self.size)
|
SelectableText(attributedString: artifacts.content.attributed, size: self.size)
|
||||||
} else {
|
} else {
|
||||||
artifacts.content.text
|
artifacts.content.text
|
||||||
.font(eventviewsize_to_font(self.size, font_size: font_size))
|
.font(eventviewsize_to_font(self.size, font_size: font_size))
|
||||||
@@ -66,13 +64,21 @@ struct TranslateView: View {
|
|||||||
guard let note_language = translations_model.note_language else {
|
guard let note_language = translations_model.note_language else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let res = await translate_note(profiles: damus_state.profiles, keypair: damus_state.keypair, event: event, settings: damus_state.settings, note_lang: note_language, purple: damus_state.purple)
|
let res = await translate_note(profiles: damus_state.profiles, keypair: damus_state.keypair, event: event, settings: damus_state.settings, note_lang: note_language)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.translations_model.state = res
|
self.translations_model.state = res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func attempt_translation() {
|
||||||
|
guard should_translate(event: event, our_keypair: damus_state.keypair, settings: damus_state.settings, note_lang: self.translations_model.note_language), damus_state.settings.auto_translate else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
translate()
|
||||||
|
}
|
||||||
|
|
||||||
func should_transl(_ note_lang: String) -> Bool {
|
func should_transl(_ note_lang: String) -> Bool {
|
||||||
should_translate(event: event, our_keypair: damus_state.keypair, settings: damus_state.settings, note_lang: note_lang)
|
should_translate(event: event, our_keypair: damus_state.keypair, settings: damus_state.settings, note_lang: note_lang)
|
||||||
}
|
}
|
||||||
@@ -97,10 +103,9 @@ struct TranslateView: View {
|
|||||||
Text("")
|
Text("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.task {
|
||||||
|
attempt_translation()
|
||||||
}
|
}
|
||||||
|
|
||||||
func translationMeetsStringDistanceRequirements(original: String, translated: String) -> Bool {
|
|
||||||
return levenshteinDistanceIsGreaterThanOrEqualTo(from: original, to: translated, threshold: MIN_UNIQUE_CHARS)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,10 +125,10 @@ struct TranslateView_Previews: PreviewProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func translate_note(profiles: Profiles, keypair: Keypair, event: NostrEvent, settings: UserSettingsStore, note_lang: String, purple: DamusPurple) async -> TranslateStatus {
|
func translate_note(profiles: Profiles, keypair: Keypair, event: NostrEvent, settings: UserSettingsStore, note_lang: String) async -> TranslateStatus {
|
||||||
|
|
||||||
// If the note language is different from our preferred languages, send a translation request.
|
// If the note language is different from our preferred languages, send a translation request.
|
||||||
let translator = Translator(settings, purple: purple)
|
let translator = Translator(settings)
|
||||||
let originalContent = event.get_content(keypair)
|
let originalContent = event.get_content(keypair)
|
||||||
let translated_note = try? await translator.translate(originalContent, from: note_lang, to: current_language())
|
let translated_note = try? await translator.translate(originalContent, from: note_lang, to: current_language())
|
||||||
|
|
||||||
@@ -137,10 +142,6 @@ func translate_note(profiles: Profiles, keypair: Keypair, event: NostrEvent, set
|
|||||||
return .not_needed
|
return .not_needed
|
||||||
}
|
}
|
||||||
|
|
||||||
guard translationMeetsStringDistanceRequirements(original: originalContent, translated: translated_note) else {
|
|
||||||
return .not_needed
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render translated note
|
// Render translated note
|
||||||
let translated_blocks = parse_note_content(content: .content(translated_note, event.tags))
|
let translated_blocks = parse_note_content(content: .content(translated_note, event.tags))
|
||||||
let artifacts = render_blocks(blocks: translated_blocks, profiles: profiles)
|
let artifacts = render_blocks(blocks: translated_blocks, profiles: profiles)
|
||||||
@@ -157,50 +158,3 @@ func current_language() -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func levenshteinDistanceIsGreaterThanOrEqualTo(from source: String, to target: String, threshold: Int) -> Bool {
|
|
||||||
let sourceCount = source.count
|
|
||||||
let targetCount = target.count
|
|
||||||
|
|
||||||
// Early return if the difference in lengths is already greater than or equal to the threshold,
|
|
||||||
// indicating the edit distance meets the condition without further calculation.
|
|
||||||
if abs(sourceCount - targetCount) >= threshold {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
var matrix = [[Int]](repeating: [Int](repeating: 0, count: targetCount + 1), count: sourceCount + 1)
|
|
||||||
|
|
||||||
for i in 0...sourceCount {
|
|
||||||
matrix[i][0] = i
|
|
||||||
}
|
|
||||||
|
|
||||||
for j in 0...targetCount {
|
|
||||||
matrix[0][j] = j
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in 1...sourceCount {
|
|
||||||
var rowMin = Int.max
|
|
||||||
for j in 1...targetCount {
|
|
||||||
let sourceIndex = source.index(source.startIndex, offsetBy: i - 1)
|
|
||||||
let targetIndex = target.index(target.startIndex, offsetBy: j - 1)
|
|
||||||
|
|
||||||
let cost = source[sourceIndex] == target[targetIndex] ? 0 : 1
|
|
||||||
matrix[i][j] = min(
|
|
||||||
matrix[i - 1][j] + 1, // Deletion
|
|
||||||
matrix[i][j - 1] + 1, // Insertion
|
|
||||||
matrix[i - 1][j - 1] + cost // Substitution
|
|
||||||
)
|
|
||||||
rowMin = min(rowMin, matrix[i][j])
|
|
||||||
}
|
|
||||||
// If the minimum edit distance found in any row is already greater than or equal to the threshold,
|
|
||||||
// you can conclude the edit distance meets the criteria.
|
|
||||||
if rowMin >= threshold {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return matrix[sourceCount][targetCount] >= threshold
|
|
||||||
}
|
|
||||||
|
|
||||||
func translationMeetsStringDistanceRequirements(original: String, translated: String) -> Bool {
|
|
||||||
return levenshteinDistanceIsGreaterThanOrEqualTo(from: original, to: translated, threshold: MIN_UNIQUE_CHARS)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,14 +9,7 @@ import SwiftUI
|
|||||||
|
|
||||||
struct TruncatedText: View {
|
struct TruncatedText: View {
|
||||||
let text: CompatibleText
|
let text: CompatibleText
|
||||||
let maxChars: Int
|
let maxChars: Int = 280
|
||||||
let show_show_more_button: Bool
|
|
||||||
|
|
||||||
init(text: CompatibleText, maxChars: Int = 280, show_show_more_button: Bool) {
|
|
||||||
self.text = text
|
|
||||||
self.maxChars = maxChars
|
|
||||||
self.show_show_more_button = show_show_more_button
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let truncatedAttributedString: AttributedString? = text.attributed.truncateOrNil(maxLength: maxChars)
|
let truncatedAttributedString: AttributedString? = text.attributed.truncateOrNil(maxLength: maxChars)
|
||||||
@@ -31,21 +24,19 @@ struct TruncatedText: View {
|
|||||||
|
|
||||||
if truncatedAttributedString != nil {
|
if truncatedAttributedString != nil {
|
||||||
Spacer()
|
Spacer()
|
||||||
if self.show_show_more_button {
|
|
||||||
Button(NSLocalizedString("Show more", comment: "Button to show entire note.")) { }
|
Button(NSLocalizedString("Show more", comment: "Button to show entire note.")) { }
|
||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
struct TruncatedText_Previews: PreviewProvider {
|
struct TruncatedText_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack(spacing: 100) {
|
VStack(spacing: 100) {
|
||||||
TruncatedText(text: CompatibleText(stringLiteral: "hello\nthere\none\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven"), show_show_more_button: true)
|
TruncatedText(text: CompatibleText(stringLiteral: "hello\nthere\none\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven"))
|
||||||
.frame(width: 200, height: 200)
|
.frame(width: 200, height: 200)
|
||||||
|
|
||||||
TruncatedText(text: CompatibleText(stringLiteral: "hello\nthere\none\ntwo\nthree\nfour"), show_show_more_button: true)
|
TruncatedText(text: CompatibleText(stringLiteral: "hello\nthere\none\ntwo\nthree\nfour"))
|
||||||
.frame(width: 200, height: 200)
|
.frame(width: 200, height: 200)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,57 +9,33 @@ import SwiftUI
|
|||||||
|
|
||||||
struct WebsiteLink: View {
|
struct WebsiteLink: View {
|
||||||
let url: URL
|
let url: URL
|
||||||
let style: StyleVariant
|
|
||||||
@Environment(\.openURL) var openURL
|
@Environment(\.openURL) var openURL
|
||||||
|
|
||||||
init(url: URL, style: StyleVariant? = nil) {
|
|
||||||
self.url = url
|
|
||||||
self.style = style ?? .normal
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
Image("link")
|
Image("link")
|
||||||
.resizable()
|
.foregroundColor(.gray)
|
||||||
.frame(width: 16, height: 16)
|
.font(.footnote)
|
||||||
.foregroundColor(self.style == .accent ? .white : .gray)
|
|
||||||
.padding(.vertical, 5)
|
|
||||||
.padding([.leading], 10)
|
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
openURL(url)
|
openURL(url)
|
||||||
}, label: {
|
}, label: {
|
||||||
Text(link_text)
|
Text(link_text)
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundColor(self.style == .accent ? .white : .accentColor)
|
.foregroundColor(.accentColor)
|
||||||
.truncationMode(.tail)
|
.truncationMode(.tail)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
})
|
})
|
||||||
.padding(.vertical, 5)
|
|
||||||
.padding([.trailing], 10)
|
|
||||||
}
|
}
|
||||||
.background(
|
|
||||||
self.style == .accent ?
|
|
||||||
AnyView(RoundedRectangle(cornerRadius: 50).fill(PinkGradient))
|
|
||||||
: AnyView(Color.clear)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var link_text: String {
|
var link_text: String {
|
||||||
url.host ?? url.absoluteString
|
url.host ?? url.absoluteString
|
||||||
}
|
}
|
||||||
|
|
||||||
enum StyleVariant {
|
|
||||||
case normal
|
|
||||||
case accent
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WebsiteLink_Previews: PreviewProvider {
|
struct WebsiteLink_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
WebsiteLink(url: URL(string: "https://jb55.com")!)
|
WebsiteLink(url: URL(string: "https://jb55.com")!)
|
||||||
.previewDisplayName("Normal")
|
|
||||||
WebsiteLink(url: URL(string: "https://jb55.com")!, style: .accent)
|
|
||||||
.previewDisplayName("Accent")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// NoteZapButton.swift
|
// ZapButton.swift
|
||||||
// damus
|
// damus
|
||||||
//
|
//
|
||||||
// Created by William Casarin on 2023-01-17.
|
// Created by William Casarin on 2023-01-17.
|
||||||
@@ -18,19 +18,6 @@ enum ZappingError {
|
|||||||
case bad_lnurl
|
case bad_lnurl
|
||||||
case canceled
|
case canceled
|
||||||
case send_failed
|
case send_failed
|
||||||
|
|
||||||
func humanReadableMessage() -> String {
|
|
||||||
switch self {
|
|
||||||
case .fetching_invoice:
|
|
||||||
return NSLocalizedString("Error fetching lightning invoice", comment: "Message to display when there was an error fetching a lightning invoice while attempting to zap.")
|
|
||||||
case .bad_lnurl:
|
|
||||||
return NSLocalizedString("Invalid lightning address", comment: "Message to display when there was an error attempting to zap due to an invalid lightning address.")
|
|
||||||
case .canceled:
|
|
||||||
return NSLocalizedString("Zap attempt from connected wallet was canceled.", comment: "Message to display when a zap from the user's connected wallet was canceled.")
|
|
||||||
case .send_failed:
|
|
||||||
return NSLocalizedString("Zap attempt from connected wallet failed.", comment: "Message to display when sending a zap from the user's connected wallet failed.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ZappingEvent {
|
struct ZappingEvent {
|
||||||
@@ -39,7 +26,7 @@ struct ZappingEvent {
|
|||||||
let target: ZapTarget
|
let target: ZapTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NoteZapButton: View {
|
struct ZapButton: View {
|
||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
let target: ZapTarget
|
let target: ZapTarget
|
||||||
let lnurl: String
|
let lnurl: String
|
||||||
@@ -157,7 +144,7 @@ struct ZapButton_Previews: PreviewProvider {
|
|||||||
let pending_zap = PendingZap(amount_msat: 1000, target: ZapTarget.note(id: test_note.id, author: test_note.pubkey), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice)))
|
let pending_zap = PendingZap(amount_msat: 1000, target: ZapTarget.note(id: test_note.id, author: test_note.pubkey), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice)))
|
||||||
let zaps = ZapsDataModel([.pending(pending_zap)])
|
let zaps = ZapsDataModel([.pending(pending_zap)])
|
||||||
|
|
||||||
NoteZapButton(damus_state: test_damus_state, target: ZapTarget.note(id: test_note.id, author: test_note.pubkey), lnurl: "lnurl", zaps: zaps)
|
ZapButton(damus_state: test_damus_state, target: ZapTarget.note(id: test_note.id, author: test_note.pubkey), lnurl: "lnurl", zaps: zaps)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,57 +59,66 @@ func parse_note_content(content: NoteContent) -> Blocks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func interpret_event_refs(tags: TagsSequence) -> ThreadReply? {
|
func interpret_event_refs_ndb(blocks: [Block], tags: TagsSequence) -> [EventRef] {
|
||||||
// migration is long over, lets just do this to fix tests
|
|
||||||
return interpret_event_refs_ndb(tags: tags)
|
|
||||||
}
|
|
||||||
|
|
||||||
func interpret_event_refs_ndb(tags: TagsSequence) -> ThreadReply? {
|
|
||||||
if tags.count == 0 {
|
if tags.count == 0 {
|
||||||
return nil
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
return interp_event_refs_without_mentions_ndb(References<NoteRef>(tags: tags))
|
/// build a set of indices for each event mention
|
||||||
|
let mention_indices = build_mention_indices(blocks, type: .e)
|
||||||
|
|
||||||
|
/// simpler case with no mentions
|
||||||
|
if mention_indices.count == 0 {
|
||||||
|
return interp_event_refs_without_mentions_ndb(tags.note.referenced_noterefs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func interp_event_refs_without_mentions_ndb(_ ev_tags: References<NoteRef>) -> ThreadReply? {
|
return interp_event_refs_with_mentions_ndb(tags: tags, mention_indices: mention_indices)
|
||||||
|
}
|
||||||
|
|
||||||
|
func interp_event_refs_without_mentions_ndb(_ ev_tags: References<NoteRef>) -> [EventRef] {
|
||||||
|
|
||||||
|
var count = 0
|
||||||
|
var evrefs: [EventRef] = []
|
||||||
var first: Bool = true
|
var first: Bool = true
|
||||||
var root_id: NoteRef? = nil
|
var first_ref: NoteRef? = nil
|
||||||
var reply_id: NoteRef? = nil
|
|
||||||
var mention: NoteRef? = nil
|
|
||||||
var any_marker: Bool = false
|
|
||||||
|
|
||||||
for ref in ev_tags {
|
for ref in ev_tags {
|
||||||
if let marker = ref.marker {
|
|
||||||
any_marker = true
|
|
||||||
switch marker {
|
|
||||||
case .root: root_id = ref
|
|
||||||
case .reply: reply_id = ref
|
|
||||||
case .mention: mention = ref
|
|
||||||
}
|
|
||||||
// deprecated form, only activate if we don't have any markers set
|
|
||||||
} else if !any_marker {
|
|
||||||
if first {
|
if first {
|
||||||
root_id = ref
|
first_ref = ref
|
||||||
|
evrefs.append(.thread_id(ref))
|
||||||
first = false
|
first = false
|
||||||
} else {
|
} else {
|
||||||
reply_id = ref
|
|
||||||
}
|
evrefs.append(.reply(ref))
|
||||||
}
|
}
|
||||||
|
count += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// If either reply or root_id is blank while the other is not, then this is
|
if let first_ref, count == 1 {
|
||||||
// considered reply-to-root. We should always have a root and reply tag, if they
|
let r = first_ref
|
||||||
// are equal this is reply-to-root
|
return [.reply_to_root(r)]
|
||||||
if reply_id == nil && root_id != nil {
|
|
||||||
reply_id = root_id
|
|
||||||
} else if root_id == nil && reply_id != nil {
|
|
||||||
root_id = reply_id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let reply_id, let root_id else {
|
return evrefs
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ThreadReply(root: root_id, reply: reply_id, mention: mention.map { m in .noteref(m) })
|
func interp_event_refs_with_mentions_ndb(tags: TagsSequence, mention_indices: Set<Int>) -> [EventRef] {
|
||||||
|
var mentions: [EventRef] = []
|
||||||
|
var ev_refs: [NoteRef] = []
|
||||||
|
var i: Int = 0
|
||||||
|
|
||||||
|
for tag in tags {
|
||||||
|
if let note_id = NoteRef.from_tag(tag: tag) {
|
||||||
|
if mention_indices.contains(i) {
|
||||||
|
mentions.append(.mention(.noteref(note_id, index: i)))
|
||||||
|
} else {
|
||||||
|
ev_refs.append(note_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var replies = interp_event_refs_without_mentions(ev_refs)
|
||||||
|
replies.append(contentsOf: mentions)
|
||||||
|
return replies
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import AVKit
|
import AVKit
|
||||||
import MediaPlayer
|
import MediaPlayer
|
||||||
import EmojiPicker
|
|
||||||
|
|
||||||
struct ZapSheet {
|
struct ZapSheet {
|
||||||
let target: ZapTarget
|
let target: ZapTarget
|
||||||
@@ -23,14 +22,11 @@ enum Sheets: Identifiable {
|
|||||||
case post(PostAction)
|
case post(PostAction)
|
||||||
case report(ReportTarget)
|
case report(ReportTarget)
|
||||||
case event(NostrEvent)
|
case event(NostrEvent)
|
||||||
case profile_action(Pubkey)
|
|
||||||
case zap(ZapSheet)
|
case zap(ZapSheet)
|
||||||
case select_wallet(SelectWallet)
|
case select_wallet(SelectWallet)
|
||||||
case filter
|
case filter
|
||||||
case user_status
|
case user_status
|
||||||
case onboardingSuggestions
|
case suggestedUsers
|
||||||
case purple(DamusPurpleURL)
|
|
||||||
case purple_onboarding
|
|
||||||
|
|
||||||
static func zap(target: ZapTarget, lnurl: String) -> Sheets {
|
static func zap(target: ZapTarget, lnurl: String) -> Sheets {
|
||||||
return .zap(ZapSheet(target: target, lnurl: lnurl))
|
return .zap(ZapSheet(target: target, lnurl: lnurl))
|
||||||
@@ -46,20 +42,16 @@ enum Sheets: Identifiable {
|
|||||||
case .user_status: return "user_status"
|
case .user_status: return "user_status"
|
||||||
case .post(let action): return "post-" + (action.ev?.id.hex() ?? "")
|
case .post(let action): return "post-" + (action.ev?.id.hex() ?? "")
|
||||||
case .event(let ev): return "event-" + ev.id.hex()
|
case .event(let ev): return "event-" + ev.id.hex()
|
||||||
case .profile_action(let pubkey): return "profile-action-" + pubkey.npub
|
|
||||||
case .zap(let sheet): return "zap-" + hex_encode(sheet.target.id)
|
case .zap(let sheet): return "zap-" + hex_encode(sheet.target.id)
|
||||||
case .select_wallet: return "select-wallet"
|
case .select_wallet: return "select-wallet"
|
||||||
case .filter: return "filter"
|
case .filter: return "filter"
|
||||||
case .onboardingSuggestions: return "onboarding-suggestions"
|
case .suggestedUsers: return "suggested-users"
|
||||||
case .purple(let purple_url): return "purple" + purple_url.url_string()
|
|
||||||
case .purple_onboarding: return "purple_onboarding"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
let keypair: Keypair
|
let keypair: Keypair
|
||||||
let appDelegate: AppDelegate?
|
|
||||||
|
|
||||||
var pubkey: Pubkey {
|
var pubkey: Pubkey {
|
||||||
return keypair.pubkey
|
return keypair.pubkey
|
||||||
@@ -72,24 +64,77 @@ struct ContentView: View {
|
|||||||
@Environment(\.scenePhase) var scenePhase
|
@Environment(\.scenePhase) var scenePhase
|
||||||
|
|
||||||
@State var active_sheet: Sheets? = nil
|
@State var active_sheet: Sheets? = nil
|
||||||
@State var damus_state: DamusState!
|
@State var damus_state: DamusState? = nil
|
||||||
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home
|
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home
|
||||||
@State var muting: MuteItem? = nil
|
@State var muting: Pubkey? = nil
|
||||||
@State var confirm_mute: Bool = false
|
@State var confirm_mute: Bool = false
|
||||||
@State var hide_bar: Bool = false
|
|
||||||
@State var user_muted_confirm: Bool = false
|
@State var user_muted_confirm: Bool = false
|
||||||
@State var confirm_overwrite_mutelist: Bool = false
|
@State var confirm_overwrite_mutelist: Bool = false
|
||||||
|
@SceneStorage("ContentView.filter_state") var filter_state : FilterState = .posts_and_replies
|
||||||
@State private var isSideBarOpened = false
|
@State private var isSideBarOpened = false
|
||||||
var home: HomeModel = HomeModel()
|
var home: HomeModel = HomeModel()
|
||||||
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
|
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
|
||||||
@AppStorage("has_seen_suggested_users") private var hasSeenOnboardingSuggestions = false
|
@AppStorage("has_seen_suggested_users") private var hasSeenSuggestedUsers = false
|
||||||
let sub_id = UUID().description
|
let sub_id = UUID().description
|
||||||
|
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
// connect retry timer
|
// connect retry timer
|
||||||
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||||
|
|
||||||
func navIsAtRoot() -> Bool {
|
var mystery: some View {
|
||||||
return navigationCoordinator.isAtRoot()
|
Text("Are you lost?", comment: "Text asking the user if they are lost in the app.")
|
||||||
|
.id("what")
|
||||||
|
}
|
||||||
|
|
||||||
|
func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
|
||||||
|
var filters = ContentFilters.defaults(damus_state!.settings)
|
||||||
|
filters.append(fstate.filter)
|
||||||
|
return ContentFilters(filters: filters).filter
|
||||||
|
}
|
||||||
|
|
||||||
|
var PostingTimelineView: some View {
|
||||||
|
VStack {
|
||||||
|
ZStack {
|
||||||
|
TabView(selection: $filter_state) {
|
||||||
|
// This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why.
|
||||||
|
mystery
|
||||||
|
|
||||||
|
contentTimelineView(filter: content_filter(.posts))
|
||||||
|
.tag(FilterState.posts)
|
||||||
|
.id(FilterState.posts)
|
||||||
|
contentTimelineView(filter: content_filter(.posts_and_replies))
|
||||||
|
.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(.posting(.none))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.safeAreaInset(edge: .top, spacing: 0) {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
CustomPicker(selection: $filter_state, content: {
|
||||||
|
Text("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies).").tag(FilterState.posts)
|
||||||
|
Text("Notes & Replies", comment: "Label for filter for seeing notes and replies (instead of only notes).").tag(FilterState.posts_and_replies)
|
||||||
|
})
|
||||||
|
Divider()
|
||||||
|
.frame(height: 1)
|
||||||
|
}
|
||||||
|
.background(colorScheme == .dark ? Color.black : Color.white)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
|
||||||
|
ZStack {
|
||||||
|
if let damus = self.damus_state {
|
||||||
|
TimelineView<AnyView>(events: home.events, loading: .constant(false), damus: damus, show_friend_icon: false, filter: filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func popToRoot() {
|
func popToRoot() {
|
||||||
@@ -115,7 +160,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case .home:
|
case .home:
|
||||||
PostingTimelineView(damus_state: damus_state!, home: home, active_sheet: $active_sheet)
|
PostingTimelineView
|
||||||
|
|
||||||
case .notifications:
|
case .notifications:
|
||||||
NotificationsView(state: damus, notifications: home.notifications)
|
NotificationsView(state: damus, notifications: home.notifications)
|
||||||
@@ -135,9 +180,6 @@ struct ContentView: View {
|
|||||||
.shadow(color: DamusColors.purple, radius: 2)
|
.shadow(color: DamusColors.purple, radius: 2)
|
||||||
.opacity(isSideBarOpened ? 0 : 1)
|
.opacity(isSideBarOpened ? 0 : 1)
|
||||||
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
|
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
|
||||||
.onTapGesture {
|
|
||||||
isSideBarOpened.toggle()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
timelineNavItem
|
timelineNavItem
|
||||||
.opacity(isSideBarOpened ? 0 : 1)
|
.opacity(isSideBarOpened ? 0 : 1)
|
||||||
@@ -150,11 +192,15 @@ struct ContentView: View {
|
|||||||
|
|
||||||
func MaybeReportView(target: ReportTarget) -> some View {
|
func MaybeReportView(target: ReportTarget) -> some View {
|
||||||
Group {
|
Group {
|
||||||
|
if let damus_state {
|
||||||
if let keypair = damus_state.keypair.to_full() {
|
if let keypair = damus_state.keypair.to_full() {
|
||||||
ReportView(postbox: damus_state.postbox, target: target, keypair: keypair)
|
ReportView(postbox: damus_state.postbox, target: target, keypair: keypair)
|
||||||
} else {
|
} else {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,13 +255,16 @@ struct ContentView: View {
|
|||||||
|
|
||||||
// maybe expand this to other timelines in the future
|
// maybe expand this to other timelines in the future
|
||||||
if selected_timeline == .search {
|
if selected_timeline == .search {
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
|
//isFilterVisible.toggle()
|
||||||
present_sheet(.filter)
|
present_sheet(.filter)
|
||||||
}, label: {
|
}) {
|
||||||
Image("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."), image: "filter")
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
})
|
//.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,7 +272,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||||
.overlay(
|
.overlay(
|
||||||
SideMenuView(damus_state: damus_state!, isSidebarVisible: $isSideBarOpened.animation())
|
SideMenuView(damus_state: damus, isSidebarVisible: $isSideBarOpened.animation())
|
||||||
)
|
)
|
||||||
.navigationDestination(for: Route.self) { route in
|
.navigationDestination(for: Route.self) { route in
|
||||||
route.view(navigationCoordinator: navigationCoordinator, damusState: damus_state!)
|
route.view(navigationCoordinator: navigationCoordinator, damusState: damus_state!)
|
||||||
@@ -234,26 +283,20 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
.navigationViewStyle(.stack)
|
.navigationViewStyle(.stack)
|
||||||
|
|
||||||
if !hide_bar {
|
|
||||||
TabBar(nstatus: home.notification_status, selected: $selected_timeline, settings: damus.settings, action: switch_timeline)
|
TabBar(nstatus: home.notification_status, selected: $selected_timeline, settings: damus.settings, action: switch_timeline)
|
||||||
.padding([.bottom], 8)
|
.padding([.bottom], 8)
|
||||||
.background(Color(uiColor: .systemBackground).ignoresSafeArea())
|
.background(Color(uiColor: .systemBackground).ignoresSafeArea())
|
||||||
} else {
|
|
||||||
Text("")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ignoresSafeArea(.keyboard)
|
.ignoresSafeArea(.keyboard)
|
||||||
.edgesIgnoringSafeArea(hide_bar ? [.bottom] : [])
|
|
||||||
.onAppear() {
|
.onAppear() {
|
||||||
self.connect()
|
self.connect()
|
||||||
try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers)
|
try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers)
|
||||||
setup_notifications()
|
setup_notifications()
|
||||||
if !hasSeenOnboardingSuggestions || damus_state!.settings.always_show_onboarding_suggestions {
|
if !hasSeenSuggestedUsers {
|
||||||
active_sheet = .onboardingSuggestions
|
active_sheet = .suggestedUsers
|
||||||
hasSeenOnboardingSuggestions = true
|
hasSeenSuggestedUsers = true
|
||||||
}
|
}
|
||||||
self.appDelegate?.state = damus_state
|
|
||||||
}
|
}
|
||||||
.sheet(item: $active_sheet) { item in
|
.sheet(item: $active_sheet) { item in
|
||||||
switch item {
|
switch item {
|
||||||
@@ -262,27 +305,24 @@ struct ContentView: View {
|
|||||||
case .post(let action):
|
case .post(let action):
|
||||||
PostView(action: action, damus_state: damus_state!)
|
PostView(action: action, damus_state: damus_state!)
|
||||||
case .user_status:
|
case .user_status:
|
||||||
UserStatusSheet(damus_state: damus_state!, postbox: damus_state!.postbox, keypair: damus_state!.keypair, status: damus_state!.profiles.profile_data(damus_state!.pubkey).status)
|
UserStatusSheet(postbox: damus_state!.postbox, keypair: damus_state!.keypair, status: damus_state!.profiles.profile_data(damus_state!.pubkey).status)
|
||||||
.presentationDragIndicator(.visible)
|
|
||||||
case .event:
|
case .event:
|
||||||
EventDetailView()
|
EventDetailView()
|
||||||
case .profile_action(let pubkey):
|
|
||||||
ProfileActionSheetView(damus_state: damus_state!, pubkey: pubkey)
|
|
||||||
case .zap(let zapsheet):
|
case .zap(let zapsheet):
|
||||||
CustomizeZapView(state: damus_state!, target: zapsheet.target, lnurl: zapsheet.lnurl)
|
CustomizeZapView(state: damus_state!, target: zapsheet.target, lnurl: zapsheet.lnurl)
|
||||||
case .select_wallet(let select):
|
case .select_wallet(let select):
|
||||||
SelectWalletView(default_wallet: damus_state!.settings.default_wallet, active_sheet: $active_sheet, our_pubkey: damus_state!.pubkey, invoice: select.invoice)
|
SelectWalletView(default_wallet: damus_state!.settings.default_wallet, active_sheet: $active_sheet, our_pubkey: damus_state!.pubkey, invoice: select.invoice)
|
||||||
case .filter:
|
case .filter:
|
||||||
let timeline = selected_timeline
|
let timeline = selected_timeline
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
RelayFilterView(state: damus_state!, timeline: timeline)
|
RelayFilterView(state: damus_state!, timeline: timeline)
|
||||||
.presentationDetents([.height(550)])
|
.presentationDetents([.height(550)])
|
||||||
.presentationDragIndicator(.visible)
|
.presentationDragIndicator(.visible)
|
||||||
case .onboardingSuggestions:
|
} else {
|
||||||
OnboardingSuggestionsView(model: SuggestedUsersViewModel(damus_state: damus_state!))
|
RelayFilterView(state: damus_state!, timeline: timeline)
|
||||||
case .purple(let purple_url):
|
}
|
||||||
DamusPurpleURLSheetView(damus_state: damus_state!, purple_url: purple_url)
|
case .suggestedUsers:
|
||||||
case .purple_onboarding:
|
SuggestedUsersView(model: SuggestedUsersViewModel(damus_state: damus_state!))
|
||||||
DamusPurpleNewUserOnboardingView(damus_state: damus_state)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onOpenURL { url in
|
.onOpenURL { url in
|
||||||
@@ -297,31 +337,12 @@ struct ContentView: View {
|
|||||||
case .event(let ev): self.open_event(ev: ev)
|
case .event(let ev): self.open_event(ev: ev)
|
||||||
case .wallet_connect(let nwc): self.open_wallet(nwc: nwc)
|
case .wallet_connect(let nwc): self.open_wallet(nwc: nwc)
|
||||||
case .script(let data): self.open_script(data)
|
case .script(let data): self.open_script(data)
|
||||||
case .purple(let purple_url):
|
|
||||||
if case let .welcome(checkout_id) = purple_url.variant {
|
|
||||||
// If this is a welcome link, do the following before showing the onboarding screen:
|
|
||||||
// 1. Check if this is legitimate and good to go.
|
|
||||||
// 2. Mark as complete if this is good to go.
|
|
||||||
Task {
|
|
||||||
let is_good_to_go = try? await damus_state.purple.check_and_mark_ln_checkout_is_good_to_go(checkout_id: checkout_id)
|
|
||||||
if is_good_to_go == true {
|
|
||||||
self.active_sheet = .purple(purple_url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
self.active_sheet = .purple(purple_url)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.compose)) { action in
|
.onReceive(handle_notify(.compose)) { action in
|
||||||
self.active_sheet = .post(action)
|
self.active_sheet = .post(action)
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.display_tabbar)) { display in
|
|
||||||
let show = display
|
|
||||||
self.hide_bar = !show
|
|
||||||
}
|
|
||||||
.onReceive(timer) { n in
|
.onReceive(timer) { n in
|
||||||
self.damus_state?.postbox.try_flushing_events()
|
self.damus_state?.postbox.try_flushing_events()
|
||||||
self.damus_state!.profiles.profile_data(self.damus_state!.pubkey).status.try_expire()
|
self.damus_state!.profiles.profile_data(self.damus_state!.pubkey).status.try_expire()
|
||||||
@@ -329,8 +350,8 @@ struct ContentView: View {
|
|||||||
.onReceive(handle_notify(.report)) { target in
|
.onReceive(handle_notify(.report)) { target in
|
||||||
self.active_sheet = .report(target)
|
self.active_sheet = .report(target)
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.mute)) { mute_item in
|
.onReceive(handle_notify(.mute)) { pubkey in
|
||||||
self.muting = mute_item
|
self.muting = pubkey
|
||||||
self.confirm_mute = true
|
self.confirm_mute = true
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.attached_wallet)) { nwc in
|
.onReceive(handle_notify(.attached_wallet)) { nwc in
|
||||||
@@ -338,9 +359,14 @@ struct ContentView: View {
|
|||||||
// wallet with an associated
|
// wallet with an associated
|
||||||
guard let ds = self.damus_state,
|
guard let ds = self.damus_state,
|
||||||
let lud16 = nwc.lud16,
|
let lud16 = nwc.lud16,
|
||||||
let keypair = ds.keypair.to_full(),
|
let keypair = ds.keypair.to_full()
|
||||||
let profile_txn = ds.profiles.lookup(id: ds.pubkey),
|
else {
|
||||||
let profile = profile_txn.unsafeUnownedValue,
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let profile_txn = ds.profiles.lookup(id: ds.pubkey)
|
||||||
|
|
||||||
|
guard let profile = profile_txn.unsafeUnownedValue,
|
||||||
lud16 != profile.lud16 else {
|
lud16 != profile.lud16 else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -410,65 +436,24 @@ struct ContentView: View {
|
|||||||
present_sheet(.select_wallet(invoice: inv))
|
present_sheet(.select_wallet(invoice: inv))
|
||||||
} else {
|
} else {
|
||||||
let wallet = damus_state!.settings.default_wallet.model
|
let wallet = damus_state!.settings.default_wallet.model
|
||||||
do {
|
open_with_wallet(wallet: wallet, invoice: inv)
|
||||||
try open_with_wallet(wallet: wallet, invoice: inv)
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
present_sheet(.select_wallet(invoice: inv))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case .sent_from_nwc:
|
case .sent_from_nwc:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.disconnect_relays)) { () in
|
|
||||||
damus_state.pool.disconnect()
|
|
||||||
}
|
|
||||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { obj in
|
|
||||||
print("txn: 📙 DAMUS ACTIVE NOTIFY")
|
|
||||||
if damus_state.ndb.reopen() {
|
|
||||||
print("txn: NOSTRDB REOPENED")
|
|
||||||
} else {
|
|
||||||
print("txn: NOSTRDB FAILED TO REOPEN closed:\(damus_state.ndb.is_closed)")
|
|
||||||
}
|
|
||||||
if damus_state.purple.checkout_ids_in_progress.count > 0 {
|
|
||||||
// For extra assurance, run this after one second, to avoid race conditions if the app is also handling a damus purple welcome url.
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
|
||||||
Task {
|
|
||||||
let freshly_completed_checkout_ids = try? await damus_state.purple.check_status_of_checkouts_in_progress()
|
|
||||||
let there_is_a_completed_checkout: Bool = (freshly_completed_checkout_ids?.count ?? 0) > 0
|
|
||||||
let account_info = try await damus_state.purple.fetch_account(pubkey: self.keypair.pubkey)
|
|
||||||
if there_is_a_completed_checkout == true && account_info?.active == true {
|
|
||||||
if damus_state.purple.onboarding_status.user_has_never_seen_the_onboarding_before() {
|
|
||||||
// Show welcome sheet
|
|
||||||
self.active_sheet = .purple_onboarding
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
self.active_sheet = .purple(DamusPurpleURL.init(is_staging: damus_state.purple.environment == .staging, variant: .landing))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Task {
|
|
||||||
await damus_state.purple.check_and_send_app_notifications_if_needed(handler: home.handle_damus_app_notification)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onChange(of: scenePhase) { (phase: ScenePhase) in
|
.onChange(of: scenePhase) { (phase: ScenePhase) in
|
||||||
guard let damus_state else { return }
|
|
||||||
switch phase {
|
switch phase {
|
||||||
case .background:
|
case .background:
|
||||||
print("txn: 📙 DAMUS BACKGROUNDED")
|
print("📙 DAMUS BACKGROUNDED")
|
||||||
Task { @MainActor in
|
|
||||||
damus_state.ndb.close()
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
case .inactive:
|
case .inactive:
|
||||||
print("txn: 📙 DAMUS INACTIVE")
|
print("📙 DAMUS INACTIVE")
|
||||||
break
|
break
|
||||||
case .active:
|
case .active:
|
||||||
print("txn: 📙 DAMUS ACTIVE")
|
print("📙 DAMUS ACTIVE")
|
||||||
damus_state.pool.ping()
|
guard let ds = damus_state else { return }
|
||||||
|
ds.pool.ping()
|
||||||
@unknown default:
|
@unknown default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -481,25 +466,32 @@ struct ContentView: View {
|
|||||||
open_profile(pubkey: pubkey)
|
open_profile(pubkey: pubkey)
|
||||||
|
|
||||||
case .note(let noteId):
|
case .note(let noteId):
|
||||||
openEvent(noteId: noteId, notificationType: local.type)
|
guard let target = damus_state.events.lookup(noteId) else {
|
||||||
case .nevent(let nevent):
|
return
|
||||||
openEvent(noteId: nevent.noteid, notificationType: local.type)
|
}
|
||||||
case .nprofile(let nprofile):
|
|
||||||
open_profile(pubkey: nprofile.author)
|
switch local.type {
|
||||||
case .nrelay(_):
|
case .dm:
|
||||||
break
|
selected_timeline = .dms
|
||||||
case .naddr(let naddr):
|
damus_state.dms.set_active_dm(target.pubkey)
|
||||||
|
navigationCoordinator.push(route: Route.DMChat(dms: damus_state.dms.active_model))
|
||||||
|
case .like, .zap, .mention, .repost:
|
||||||
|
open_event(ev: target)
|
||||||
|
case .profile_zap:
|
||||||
|
// Handled separately above.
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.onlyzaps_mode)) { hide in
|
.onReceive(handle_notify(.onlyzaps_mode)) { hide in
|
||||||
home.filter_events()
|
home.filter_events()
|
||||||
|
|
||||||
guard let ds = damus_state,
|
guard let ds = damus_state else { return }
|
||||||
let profile_txn = ds.profiles.lookup(id: ds.pubkey),
|
let profile_txn = ds.profiles.lookup(id: ds.pubkey)
|
||||||
let profile = profile_txn.unsafeUnownedValue,
|
|
||||||
|
guard let profile = profile_txn.unsafeUnownedValue,
|
||||||
let keypair = ds.keypair.to_full()
|
let keypair = ds.keypair.to_full()
|
||||||
else {
|
else {
|
||||||
return
|
return
|
||||||
@@ -515,10 +507,10 @@ struct ContentView: View {
|
|||||||
user_muted_confirm = false
|
user_muted_confirm = false
|
||||||
}
|
}
|
||||||
}, message: {
|
}, message: {
|
||||||
if case let .user(pubkey, _) = self.muting {
|
if let pubkey = self.muting {
|
||||||
let profile_txn = damus_state!.profiles.lookup(id: pubkey)
|
let name = damus_state!.profiles.lookup(id: pubkey).map { profile in
|
||||||
let profile = profile_txn?.unsafeUnownedValue
|
Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
|
||||||
let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
|
}.value
|
||||||
Text("\(name) has been muted", comment: "Alert message that informs a user was muted.")
|
Text("\(name) has been muted", comment: "Alert message that informs a user was muted.")
|
||||||
} else {
|
} else {
|
||||||
Text("User has been muted", comment: "Alert message that informs a user was muted.")
|
Text("User has been muted", comment: "Alert message that informs a user was muted.")
|
||||||
@@ -533,13 +525,13 @@ struct ContentView: View {
|
|||||||
Button(NSLocalizedString("Yes, Overwrite", comment: "Text of button that confirms to overwrite the existing mutelist.")) {
|
Button(NSLocalizedString("Yes, Overwrite", comment: "Text of button that confirms to overwrite the existing mutelist.")) {
|
||||||
guard let ds = damus_state,
|
guard let ds = damus_state,
|
||||||
let keypair = ds.keypair.to_full(),
|
let keypair = ds.keypair.to_full(),
|
||||||
let muting,
|
let pubkey = muting,
|
||||||
let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: muting)
|
let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .pubkey(pubkey))
|
||||||
else {
|
else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ds.mutelist_manager.set_mutelist(mutelist)
|
damus_state?.contacts.set_mutelist(mutelist)
|
||||||
ds.postbox.send(mutelist)
|
ds.postbox.send(mutelist)
|
||||||
|
|
||||||
confirm_overwrite_mutelist = false
|
confirm_overwrite_mutelist = false
|
||||||
@@ -558,28 +550,28 @@ struct ContentView: View {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ds.mutelist_manager.event == nil {
|
if ds.contacts.mutelist == nil {
|
||||||
confirm_overwrite_mutelist = true
|
confirm_overwrite_mutelist = true
|
||||||
} else {
|
} else {
|
||||||
guard let keypair = ds.keypair.to_full(),
|
guard let keypair = ds.keypair.to_full(),
|
||||||
let muting
|
let pubkey = muting
|
||||||
else {
|
else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.mutelist_manager.event, to_add: muting) else {
|
guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.contacts.mutelist, to_add: .pubkey(pubkey)) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ds.mutelist_manager.set_mutelist(ev)
|
damus_state?.contacts.set_mutelist(ev)
|
||||||
ds.postbox.send(ev)
|
ds.postbox.send(ev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, message: {
|
}, message: {
|
||||||
if case let .user(pubkey, _) = muting {
|
if let pubkey = muting {
|
||||||
let profile_txn = damus_state?.profiles.lookup(id: pubkey)
|
let name = damus_state?.profiles.lookup(id: pubkey).map({ profile in
|
||||||
let profile = profile_txn?.unsafeUnownedValue
|
Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
|
||||||
let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
|
}).value ?? "unknown"
|
||||||
Text("Mute \(name)?", comment: "Alert message prompt to ask if a user should be muted.")
|
Text("Mute \(name)?", comment: "Alert message prompt to ask if a user should be muted.")
|
||||||
} else {
|
} else {
|
||||||
Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.")
|
Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.")
|
||||||
@@ -589,12 +581,11 @@ struct ContentView: View {
|
|||||||
|
|
||||||
func switch_timeline(_ timeline: Timeline) {
|
func switch_timeline(_ timeline: Timeline) {
|
||||||
self.isSideBarOpened = false
|
self.isSideBarOpened = false
|
||||||
let navWasAtRoot = self.navIsAtRoot()
|
|
||||||
self.popToRoot()
|
self.popToRoot()
|
||||||
|
|
||||||
notify(.switched_timeline(timeline))
|
notify(.switched_timeline(timeline))
|
||||||
|
|
||||||
if timeline == self.selected_timeline && navWasAtRoot {
|
if timeline == self.selected_timeline {
|
||||||
notify(.scroll_to_top)
|
notify(.scroll_to_top)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -604,33 +595,25 @@ struct ContentView: View {
|
|||||||
|
|
||||||
func connect() {
|
func connect() {
|
||||||
// nostrdb
|
// nostrdb
|
||||||
var mndb = Ndb()
|
let ndb = Ndb()!
|
||||||
if mndb == nil {
|
|
||||||
// try recovery
|
|
||||||
print("DB ISSUE! RECOVERING")
|
|
||||||
mndb = Ndb.safemode()
|
|
||||||
|
|
||||||
// out of space or something?? maybe we need a in-memory fallback
|
let pool = RelayPool(ndb: ndb)
|
||||||
if mndb == nil {
|
|
||||||
logout(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let ndb = mndb else { return }
|
|
||||||
|
|
||||||
let pool = RelayPool(ndb: ndb, keypair: keypair)
|
|
||||||
let model_cache = RelayModelCache()
|
let model_cache = RelayModelCache()
|
||||||
let relay_filters = RelayFilters(our_pubkey: pubkey)
|
let relay_filters = RelayFilters(our_pubkey: pubkey)
|
||||||
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
|
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
|
||||||
|
|
||||||
let settings = UserSettingsStore.globally_load_for(pubkey: pubkey)
|
// dumb stuff needed for property wrappers
|
||||||
|
UserSettingsStore.pubkey = pubkey
|
||||||
|
let settings = UserSettingsStore()
|
||||||
|
UserSettingsStore.shared = settings
|
||||||
|
|
||||||
let new_relay_filters = load_relay_filters(pubkey) == nil
|
let new_relay_filters = load_relay_filters(pubkey) == nil
|
||||||
for relay in bootstrap_relays {
|
for relay in bootstrap_relays {
|
||||||
let descriptor = RelayDescriptor(url: relay, info: .rw)
|
if let url = RelayURL(relay) {
|
||||||
|
let descriptor = RelayDescriptor(url: url, info: .rw)
|
||||||
add_new_relay(model_cache: model_cache, relay_filters: relay_filters, pool: pool, descriptor: descriptor, new_relay_filters: new_relay_filters, logging_enabled: settings.developer_mode)
|
add_new_relay(model_cache: model_cache, relay_filters: relay_filters, pool: pool, descriptor: descriptor, new_relay_filters: new_relay_filters, logging_enabled: settings.developer_mode)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
|
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
|
||||||
|
|
||||||
@@ -639,12 +622,13 @@ struct ContentView: View {
|
|||||||
try? pool.add_relay(.nwc(url: nwc.relay))
|
try? pool.add_relay(.nwc(url: nwc.relay))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let user_search_cache = UserSearchCache()
|
||||||
self.damus_state = DamusState(pool: pool,
|
self.damus_state = DamusState(pool: pool,
|
||||||
keypair: keypair,
|
keypair: keypair,
|
||||||
likes: EventCounter(our_pubkey: pubkey),
|
likes: EventCounter(our_pubkey: pubkey),
|
||||||
boosts: EventCounter(our_pubkey: pubkey),
|
boosts: EventCounter(our_pubkey: pubkey),
|
||||||
contacts: Contacts(our_pubkey: pubkey),
|
contacts: Contacts(our_pubkey: pubkey),
|
||||||
mutelist_manager: MutelistManager(user_keypair: keypair),
|
|
||||||
profiles: Profiles(ndb: ndb),
|
profiles: Profiles(ndb: ndb),
|
||||||
dms: home.dms,
|
dms: home.dms,
|
||||||
previews: PreviewCache(),
|
previews: PreviewCache(),
|
||||||
@@ -659,28 +643,15 @@ struct ContentView: View {
|
|||||||
postbox: PostBox(pool: pool),
|
postbox: PostBox(pool: pool),
|
||||||
bootstrap_relays: bootstrap_relays,
|
bootstrap_relays: bootstrap_relays,
|
||||||
replies: ReplyCounter(our_pubkey: pubkey),
|
replies: ReplyCounter(our_pubkey: pubkey),
|
||||||
|
muted_threads: MutedThreadsManager(keypair: keypair),
|
||||||
wallet: WalletModel(settings: settings),
|
wallet: WalletModel(settings: settings),
|
||||||
nav: self.navigationCoordinator,
|
nav: self.navigationCoordinator,
|
||||||
music: MusicController(onChange: music_changed),
|
music: MusicController(onChange: music_changed),
|
||||||
video: VideoController(),
|
video: VideoController(),
|
||||||
ndb: ndb,
|
ndb: ndb
|
||||||
quote_reposts: .init(our_pubkey: pubkey),
|
|
||||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
home.damus_state = self.damus_state!
|
home.damus_state = self.damus_state!
|
||||||
|
|
||||||
if let damus_state, damus_state.purple.enable_purple {
|
|
||||||
// Assign delegate so that we can send receipts to the Purple API server as soon as we get updates from user's purchases
|
|
||||||
StoreObserver.standard.delegate = damus_state.purple
|
|
||||||
Task {
|
|
||||||
await damus_state.purple.check_and_send_app_notifications_if_needed(handler: home.handle_damus_app_notification)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Purple API is an experimental feature. If not enabled, do not connect `StoreObserver` with Purple API to avoid leaking receipts
|
|
||||||
}
|
|
||||||
|
|
||||||
pool.connect()
|
pool.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -708,27 +679,11 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func openEvent(noteId: NoteId, notificationType: LocalNotificationType) {
|
|
||||||
guard let target = damus_state.events.lookup(noteId) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch notificationType {
|
|
||||||
case .dm:
|
|
||||||
selected_timeline = .dms
|
|
||||||
damus_state.dms.set_active_dm(target.pubkey)
|
|
||||||
navigationCoordinator.push(route: Route.DMChat(dms: damus_state.dms.active_model))
|
|
||||||
case .like, .zap, .mention, .repost:
|
|
||||||
open_event(ev: target)
|
|
||||||
case .profile_zap:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ContentView_Previews: PreviewProvider {
|
struct ContentView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ContentView(keypair: Keypair(pubkey: test_pubkey, privkey: nil), appDelegate: nil)
|
ContentView(keypair: Keypair(pubkey: test_pubkey, privkey: nil))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -778,12 +733,6 @@ func save_last_event(_ ev: NostrEvent, timeline: Timeline) {
|
|||||||
UserDefaults.standard.set(String(ev.created_at), forKey: "last_\(str)_time")
|
UserDefaults.standard.set(String(ev.created_at), forKey: "last_\(str)_time")
|
||||||
}
|
}
|
||||||
|
|
||||||
func save_last_event(_ ev_id: NoteId, created_at: UInt32, timeline: Timeline) {
|
|
||||||
let str = timeline.rawValue
|
|
||||||
UserDefaults.standard.set(ev_id.hex(), forKey: "last_\(str)")
|
|
||||||
UserDefaults.standard.set(String(created_at), forKey: "last_\(str)_time")
|
|
||||||
}
|
|
||||||
|
|
||||||
func update_filters_with_since(last_of_kind: [UInt32: NostrEvent], filters: [NostrFilter]) -> [NostrFilter] {
|
func update_filters_with_since(last_of_kind: [UInt32: NostrEvent], filters: [NostrFilter]) -> [NostrFilter] {
|
||||||
|
|
||||||
return filters.map { filter in
|
return filters.map { filter in
|
||||||
@@ -835,13 +784,13 @@ func setup_notifications() {
|
|||||||
|
|
||||||
struct FindEvent {
|
struct FindEvent {
|
||||||
let type: FindEventType
|
let type: FindEventType
|
||||||
let find_from: [RelayURL]?
|
let find_from: [String]?
|
||||||
|
|
||||||
static func profile(pubkey: Pubkey, find_from: [RelayURL]? = nil) -> FindEvent {
|
static func profile(pubkey: Pubkey, find_from: [String]? = nil) -> FindEvent {
|
||||||
return FindEvent(type: .profile(pubkey), find_from: find_from)
|
return FindEvent(type: .profile(pubkey), find_from: find_from)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func event(evid: NoteId, find_from: [RelayURL]? = nil) -> FindEvent {
|
static func event(evid: NoteId, find_from: [String]? = nil) -> FindEvent {
|
||||||
return FindEvent(type: .event(evid), find_from: find_from)
|
return FindEvent(type: .event(evid), find_from: find_from)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -869,8 +818,7 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
|
|||||||
|
|
||||||
switch query {
|
switch query {
|
||||||
case .profile(let pubkey):
|
case .profile(let pubkey):
|
||||||
if let profile_txn = state.ndb.lookup_profile(pubkey),
|
if let record = state.ndb.lookup_profile(pubkey).unsafeUnownedValue,
|
||||||
let record = profile_txn.unsafeUnownedValue,
|
|
||||||
record.profile != nil
|
record.profile != nil
|
||||||
{
|
{
|
||||||
callback(.profile(pubkey))
|
callback(.profile(pubkey))
|
||||||
@@ -929,41 +877,11 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
|
|||||||
}
|
}
|
||||||
case .notice:
|
case .notice:
|
||||||
break
|
break
|
||||||
case .auth:
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func naddrLookup(damus_state: DamusState, naddr: NAddr, callback: @escaping (NostrEvent?) -> ()) {
|
|
||||||
var nostrKinds: [NostrKind]? = NostrKind(rawValue: naddr.kind).map { [$0] }
|
|
||||||
|
|
||||||
let filter = NostrFilter(kinds: nostrKinds, authors: [naddr.author])
|
|
||||||
|
|
||||||
let subid = UUID().description
|
|
||||||
|
|
||||||
damus_state.pool.subscribe_to(sub_id: subid, filters: [filter], to: nil) { relay_id, res in
|
|
||||||
guard case .nostr_event(let ev) = res else {
|
|
||||||
damus_state.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if case .event(_, let ev) = ev {
|
|
||||||
for tag in ev.tags {
|
|
||||||
if(tag.count >= 2 && tag[0].string() == "d"){
|
|
||||||
if (tag[1].string() == naddr.identifier){
|
|
||||||
damus_state.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
|
||||||
callback(ev)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
damus_state.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func timeline_name(_ timeline: Timeline?) -> String {
|
func timeline_name(_ timeline: Timeline?) -> String {
|
||||||
guard let timeline else {
|
guard let timeline else {
|
||||||
return ""
|
return ""
|
||||||
@@ -1061,12 +979,6 @@ func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: Ev
|
|||||||
postbox.send(ev)
|
postbox.send(ev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for qref in new_ev.referenced_quote_ids.prefix(3) {
|
|
||||||
// also broadcast at most 3 referenced quoted events
|
|
||||||
if let ev = events.lookup(qref.note_id) {
|
|
||||||
postbox.send(ev)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
case .cancel:
|
case .cancel:
|
||||||
print("post cancelled")
|
print("post cancelled")
|
||||||
@@ -1081,15 +993,9 @@ enum OpenResult {
|
|||||||
case event(NostrEvent)
|
case event(NostrEvent)
|
||||||
case wallet_connect(WalletConnectURL)
|
case wallet_connect(WalletConnectURL)
|
||||||
case script([UInt8])
|
case script([UInt8])
|
||||||
case purple(DamusPurpleURL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) -> Void) {
|
func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) -> Void) {
|
||||||
if let purple_url = DamusPurpleURL(url: url) {
|
|
||||||
result(.purple(purple_url))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if let nwc = WalletConnectURL(str: url.absoluteString) {
|
if let nwc = WalletConnectURL(str: url.absoluteString) {
|
||||||
result(.wallet_connect(nwc))
|
result(.wallet_connect(nwc))
|
||||||
return
|
return
|
||||||
@@ -1111,15 +1017,10 @@ func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) ->
|
|||||||
result(.event(ev))
|
result(.event(ev))
|
||||||
}
|
}
|
||||||
case .hashtag(let ht):
|
case .hashtag(let ht):
|
||||||
result(.filter(.filter_hashtag([ht.hashtag])))
|
result(.filter(.filter_hashtag([ht.string()])))
|
||||||
case .param, .quote:
|
case .param, .quote:
|
||||||
// doesn't really make sense here
|
// doesn't really make sense here
|
||||||
break
|
break
|
||||||
case .naddr(let naddr):
|
|
||||||
naddrLookup(damus_state: state, naddr: naddr) { res in
|
|
||||||
guard let res = res else { return }
|
|
||||||
result(.event(res))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case .filter(let filt):
|
case .filter(let filt):
|
||||||
result(.filter(filt))
|
result(.filter(filt))
|
||||||
@@ -1131,10 +1032,3 @@ func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func logout(_ state: DamusState?)
|
|
||||||
{
|
|
||||||
state?.close()
|
|
||||||
notify(.logout)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -67,10 +67,10 @@
|
|||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>Damus needs access to your camera in order to upload photos and scan QR codes.</string>
|
<string>Damus needs access to your camera if you want to upload photos from it</string>
|
||||||
<key>NSAppleMusicUsageDescription</key>
|
<key>NSAppleMusicUsageDescription</key>
|
||||||
<string>Damus needs access to your media library for playback statuses</string>
|
<string>Damus needs access to your media library for playback statuses</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>Damus needs access to your microphone for creating video recording posts</string>
|
<string>Damus needs access to your microphone if you want to upload recorded videos from it</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -16,12 +16,10 @@ enum Zapped {
|
|||||||
class ActionBarModel: ObservableObject {
|
class ActionBarModel: ObservableObject {
|
||||||
@Published var our_like: NostrEvent?
|
@Published var our_like: NostrEvent?
|
||||||
@Published var our_boost: NostrEvent?
|
@Published var our_boost: NostrEvent?
|
||||||
@Published var our_quote_repost: NostrEvent?
|
|
||||||
@Published var our_reply: NostrEvent?
|
@Published var our_reply: NostrEvent?
|
||||||
@Published var our_zap: Zapping?
|
@Published var our_zap: Zapping?
|
||||||
@Published var likes: Int
|
@Published var likes: Int
|
||||||
@Published var boosts: Int
|
@Published var boosts: Int
|
||||||
@Published var quote_reposts: Int
|
|
||||||
@Published private(set) var zaps: Int
|
@Published private(set) var zaps: Int
|
||||||
@Published var zap_total: Int64
|
@Published var zap_total: Int64
|
||||||
@Published var replies: Int
|
@Published var replies: Int
|
||||||
@@ -30,7 +28,7 @@ class ActionBarModel: ObservableObject {
|
|||||||
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
|
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(likes: Int = 0, boosts: Int = 0, zaps: Int = 0, zap_total: Int64 = 0, replies: Int = 0, our_like: NostrEvent? = nil, our_boost: NostrEvent? = nil, our_zap: Zapping? = nil, our_reply: NostrEvent? = nil, our_quote_repost: NostrEvent? = nil, quote_reposts: Int = 0) {
|
init(likes: Int = 0, boosts: Int = 0, zaps: Int = 0, zap_total: Int64 = 0, replies: Int = 0, our_like: NostrEvent? = nil, our_boost: NostrEvent? = nil, our_zap: Zapping? = nil, our_reply: NostrEvent? = nil) {
|
||||||
self.likes = likes
|
self.likes = likes
|
||||||
self.boosts = boosts
|
self.boosts = boosts
|
||||||
self.zaps = zaps
|
self.zaps = zaps
|
||||||
@@ -40,8 +38,6 @@ class ActionBarModel: ObservableObject {
|
|||||||
self.our_boost = our_boost
|
self.our_boost = our_boost
|
||||||
self.our_zap = our_zap
|
self.our_zap = our_zap
|
||||||
self.our_reply = our_reply
|
self.our_reply = our_reply
|
||||||
self.our_quote_repost = our_quote_repost
|
|
||||||
self.quote_reposts = quote_reposts
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(damus: DamusState, evid: NoteId) {
|
func update(damus: DamusState, evid: NoteId) {
|
||||||
@@ -49,13 +45,11 @@ class ActionBarModel: ObservableObject {
|
|||||||
self.boosts = damus.boosts.counts[evid] ?? 0
|
self.boosts = damus.boosts.counts[evid] ?? 0
|
||||||
self.zaps = damus.zaps.event_counts[evid] ?? 0
|
self.zaps = damus.zaps.event_counts[evid] ?? 0
|
||||||
self.replies = damus.replies.get_replies(evid)
|
self.replies = damus.replies.get_replies(evid)
|
||||||
self.quote_reposts = damus.quote_reposts.counts[evid] ?? 0
|
|
||||||
self.zap_total = damus.zaps.event_totals[evid] ?? 0
|
self.zap_total = damus.zaps.event_totals[evid] ?? 0
|
||||||
self.our_like = damus.likes.our_events[evid]
|
self.our_like = damus.likes.our_events[evid]
|
||||||
self.our_boost = damus.boosts.our_events[evid]
|
self.our_boost = damus.boosts.our_events[evid]
|
||||||
self.our_zap = damus.zaps.our_zaps[evid]?.first
|
self.our_zap = damus.zaps.our_zaps[evid]?.first
|
||||||
self.our_reply = damus.replies.our_reply(evid)
|
self.our_reply = damus.replies.our_reply(evid)
|
||||||
self.our_quote_repost = damus.quote_reposts.our_events[evid]
|
|
||||||
self.objectWillChange.send()
|
self.objectWillChange.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,8 +68,4 @@ class ActionBarModel: ObservableObject {
|
|||||||
var boosted: Bool {
|
var boosted: Bool {
|
||||||
return our_boost != nil
|
return our_boost != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var quoted: Bool {
|
|
||||||
return our_quote_repost != nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
//
|
|
||||||
// CameraModel.swift
|
|
||||||
// damus
|
|
||||||
//
|
|
||||||
// Created by Suhail Saqan on 8/5/23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import AVFoundation
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
final class CameraModel: ObservableObject {
|
|
||||||
private let service = CameraService()
|
|
||||||
|
|
||||||
@Published var showAlertError = false
|
|
||||||
|
|
||||||
@Published var isFlashOn = false
|
|
||||||
|
|
||||||
@Published var willCapturePhoto = false
|
|
||||||
|
|
||||||
@Published var isCameraButtonDisabled = false
|
|
||||||
|
|
||||||
@Published var isPhotoProcessing = false
|
|
||||||
|
|
||||||
@Published var isRecording = false
|
|
||||||
|
|
||||||
@Published var captureMode: CameraMediaType = .image
|
|
||||||
|
|
||||||
@Published public var mediaItems: [MediaItem] = []
|
|
||||||
|
|
||||||
@Published var thumbnail: Thumbnail!
|
|
||||||
|
|
||||||
var alertError: AlertError!
|
|
||||||
|
|
||||||
var session: AVCaptureSession
|
|
||||||
|
|
||||||
private var subscriptions = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
init() {
|
|
||||||
self.session = service.session
|
|
||||||
|
|
||||||
service.$shouldShowAlertView.sink { [weak self] (val) in
|
|
||||||
self?.alertError = self?.service.alertError
|
|
||||||
self?.showAlertError = val
|
|
||||||
}
|
|
||||||
.store(in: &self.subscriptions)
|
|
||||||
|
|
||||||
service.$flashMode.sink { [weak self] (mode) in
|
|
||||||
self?.isFlashOn = mode == .on
|
|
||||||
}
|
|
||||||
.store(in: &self.subscriptions)
|
|
||||||
|
|
||||||
service.$willCapturePhoto.sink { [weak self] (val) in
|
|
||||||
self?.willCapturePhoto = val
|
|
||||||
}
|
|
||||||
.store(in: &self.subscriptions)
|
|
||||||
|
|
||||||
service.$isCameraButtonDisabled.sink { [weak self] (val) in
|
|
||||||
self?.isCameraButtonDisabled = val
|
|
||||||
}
|
|
||||||
.store(in: &self.subscriptions)
|
|
||||||
|
|
||||||
service.$isPhotoProcessing.sink { [weak self] (val) in
|
|
||||||
self?.isPhotoProcessing = val
|
|
||||||
}
|
|
||||||
.store(in: &self.subscriptions)
|
|
||||||
|
|
||||||
service.$isRecording.sink { [weak self] (val) in
|
|
||||||
self?.isRecording = val
|
|
||||||
}
|
|
||||||
.store(in: &self.subscriptions)
|
|
||||||
|
|
||||||
service.$captureMode.sink { [weak self] (mode) in
|
|
||||||
self?.captureMode = mode
|
|
||||||
}
|
|
||||||
.store(in: &self.subscriptions)
|
|
||||||
|
|
||||||
service.$mediaItems.sink { [weak self] (mode) in
|
|
||||||
self?.mediaItems = mode
|
|
||||||
}
|
|
||||||
.store(in: &self.subscriptions)
|
|
||||||
|
|
||||||
service.$thumbnail.sink { [weak self] (thumbnail) in
|
|
||||||
guard let pic = thumbnail else { return }
|
|
||||||
self?.thumbnail = pic
|
|
||||||
}
|
|
||||||
.store(in: &self.subscriptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
func configure() {
|
|
||||||
service.checkForPermissions()
|
|
||||||
service.configure()
|
|
||||||
}
|
|
||||||
|
|
||||||
func stop() {
|
|
||||||
service.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
func capturePhoto() {
|
|
||||||
service.capturePhoto()
|
|
||||||
}
|
|
||||||
|
|
||||||
func startRecording() {
|
|
||||||
service.startRecording()
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopRecording() {
|
|
||||||
service.stopRecording()
|
|
||||||
}
|
|
||||||
|
|
||||||
func flipCamera() {
|
|
||||||
service.changeCamera()
|
|
||||||
}
|
|
||||||
|
|
||||||
func zoom(with factor: CGFloat) {
|
|
||||||
service.set(zoom: factor)
|
|
||||||
}
|
|
||||||
|
|
||||||
func switchFlash() {
|
|
||||||
service.flashMode = service.flashMode == .on ? .off : .on
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
//
|
|
||||||
// CameraService+Extensions.swift
|
|
||||||
// damus
|
|
||||||
//
|
|
||||||
// Created by Suhail Saqan on 8/5/23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
import AVFoundation
|
|
||||||
|
|
||||||
extension AVCaptureVideoOrientation {
|
|
||||||
init?(deviceOrientation: UIDeviceOrientation) {
|
|
||||||
switch deviceOrientation {
|
|
||||||
case .portrait: self = .portrait
|
|
||||||
case .portraitUpsideDown: self = .portraitUpsideDown
|
|
||||||
case .landscapeLeft: self = .landscapeRight
|
|
||||||
case .landscapeRight: self = .landscapeLeft
|
|
||||||
default: return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init?(interfaceOrientation: UIInterfaceOrientation) {
|
|
||||||
switch interfaceOrientation {
|
|
||||||
case .portrait: self = .portrait
|
|
||||||
case .portraitUpsideDown: self = .portraitUpsideDown
|
|
||||||
case .landscapeLeft: self = .landscapeLeft
|
|
||||||
case .landscapeRight: self = .landscapeRight
|
|
||||||
default: return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,693 +0,0 @@
|
|||||||
//
|
|
||||||
// CameraService.swift
|
|
||||||
// Campus
|
|
||||||
//
|
|
||||||
// Created by Suhail Saqan on 8/5/23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Combine
|
|
||||||
import AVFoundation
|
|
||||||
import Photos
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
public struct Thumbnail: Identifiable, Equatable {
|
|
||||||
public var id: String
|
|
||||||
public var type: CameraMediaType
|
|
||||||
public var url: URL
|
|
||||||
|
|
||||||
public init(id: String = UUID().uuidString, type: CameraMediaType, url: URL) {
|
|
||||||
self.id = id
|
|
||||||
self.type = type
|
|
||||||
self.url = url
|
|
||||||
}
|
|
||||||
|
|
||||||
public var thumbnailImage: UIImage? {
|
|
||||||
switch type {
|
|
||||||
case .image:
|
|
||||||
return ImageResizer(targetWidth: 100).resize(at: url)
|
|
||||||
case .video:
|
|
||||||
return generateVideoThumbnail(for: url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct AlertError {
|
|
||||||
public var title: String = ""
|
|
||||||
public var message: String = ""
|
|
||||||
public var primaryButtonTitle = "Accept"
|
|
||||||
public var secondaryButtonTitle: String?
|
|
||||||
public var primaryAction: (() -> ())?
|
|
||||||
public var secondaryAction: (() -> ())?
|
|
||||||
|
|
||||||
public init(title: String = "", message: String = "", primaryButtonTitle: String = "Accept", secondaryButtonTitle: String? = nil, primaryAction: (() -> ())? = nil, secondaryAction: (() -> ())? = nil) {
|
|
||||||
self.title = title
|
|
||||||
self.message = message
|
|
||||||
self.primaryAction = primaryAction
|
|
||||||
self.primaryButtonTitle = primaryButtonTitle
|
|
||||||
self.secondaryAction = secondaryAction
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateVideoThumbnail(for videoURL: URL) -> UIImage? {
|
|
||||||
let asset = AVAsset(url: videoURL)
|
|
||||||
let imageGenerator = AVAssetImageGenerator(asset: asset)
|
|
||||||
imageGenerator.appliesPreferredTrackTransform = true
|
|
||||||
|
|
||||||
do {
|
|
||||||
let cgImage = try imageGenerator.copyCGImage(at: .zero, actualTime: nil)
|
|
||||||
return UIImage(cgImage: cgImage)
|
|
||||||
} catch {
|
|
||||||
print("Error generating thumbnail: \(error)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum CameraMediaType {
|
|
||||||
case image
|
|
||||||
case video
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct MediaItem {
|
|
||||||
let url: URL
|
|
||||||
let type: CameraMediaType
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CameraService: NSObject, Identifiable {
|
|
||||||
public let session = AVCaptureSession()
|
|
||||||
|
|
||||||
public var isSessionRunning = false
|
|
||||||
public var isConfigured = false
|
|
||||||
var setupResult: SessionSetupResult = .success
|
|
||||||
|
|
||||||
public var alertError: AlertError = AlertError()
|
|
||||||
|
|
||||||
@Published public var flashMode: AVCaptureDevice.FlashMode = .off
|
|
||||||
@Published public var shouldShowAlertView = false
|
|
||||||
@Published public var isPhotoProcessing = false
|
|
||||||
@Published public var captureMode: CameraMediaType = .image
|
|
||||||
@Published public var isRecording: Bool = false
|
|
||||||
|
|
||||||
@Published public var willCapturePhoto = false
|
|
||||||
@Published public var isCameraButtonDisabled = false
|
|
||||||
@Published public var isCameraUnavailable = false
|
|
||||||
@Published public var thumbnail: Thumbnail?
|
|
||||||
@Published public var mediaItems: [MediaItem] = []
|
|
||||||
|
|
||||||
public let sessionQueue = DispatchQueue(label: "io.damus.camera")
|
|
||||||
|
|
||||||
@objc dynamic public var videoDeviceInput: AVCaptureDeviceInput!
|
|
||||||
@objc dynamic public var audioDeviceInput: AVCaptureDeviceInput!
|
|
||||||
|
|
||||||
public let videoDeviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera, .builtInDualCamera, .builtInTrueDepthCamera], mediaType: .video, position: .unspecified)
|
|
||||||
|
|
||||||
public let photoOutput = AVCapturePhotoOutput()
|
|
||||||
|
|
||||||
public let movieOutput = AVCaptureMovieFileOutput()
|
|
||||||
|
|
||||||
var videoCaptureProcessor: VideoCaptureProcessor?
|
|
||||||
var photoCaptureProcessor: PhotoCaptureProcessor?
|
|
||||||
|
|
||||||
public var keyValueObservations = [NSKeyValueObservation]()
|
|
||||||
|
|
||||||
override public init() {
|
|
||||||
super.init()
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.isCameraButtonDisabled = true
|
|
||||||
self.isCameraUnavailable = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum SessionSetupResult {
|
|
||||||
case success
|
|
||||||
case notAuthorized
|
|
||||||
case configurationFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
public func configure() {
|
|
||||||
if !self.isSessionRunning && !self.isConfigured {
|
|
||||||
sessionQueue.async {
|
|
||||||
self.configureSession()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func checkForPermissions() {
|
|
||||||
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
|
||||||
case .authorized:
|
|
||||||
break
|
|
||||||
case .notDetermined:
|
|
||||||
sessionQueue.suspend()
|
|
||||||
AVCaptureDevice.requestAccess(for: .video, completionHandler: { granted in
|
|
||||||
if !granted {
|
|
||||||
self.setupResult = .notAuthorized
|
|
||||||
}
|
|
||||||
self.sessionQueue.resume()
|
|
||||||
})
|
|
||||||
|
|
||||||
default:
|
|
||||||
setupResult = .notAuthorized
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.alertError = AlertError(title: "Camera Access", message: "Damus needs camera and microphone access. Enable in settings.", primaryButtonTitle: "Go to settings", secondaryButtonTitle: nil, primaryAction: {
|
|
||||||
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!,
|
|
||||||
options: [:], completionHandler: nil)
|
|
||||||
|
|
||||||
}, secondaryAction: nil)
|
|
||||||
self.shouldShowAlertView = true
|
|
||||||
self.isCameraUnavailable = true
|
|
||||||
self.isCameraButtonDisabled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func configureSession() {
|
|
||||||
if setupResult != .success {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
session.beginConfiguration()
|
|
||||||
|
|
||||||
session.sessionPreset = .high
|
|
||||||
|
|
||||||
// Add video input.
|
|
||||||
do {
|
|
||||||
var defaultVideoDevice: AVCaptureDevice?
|
|
||||||
|
|
||||||
if let backCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) {
|
|
||||||
// If a rear dual camera is not available, default to the rear wide angle camera.
|
|
||||||
defaultVideoDevice = backCameraDevice
|
|
||||||
} else if let frontCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) {
|
|
||||||
// If the rear wide angle camera isn't available, default to the front wide angle camera.
|
|
||||||
defaultVideoDevice = frontCameraDevice
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let videoDevice = defaultVideoDevice else {
|
|
||||||
print("Default video device is unavailable.")
|
|
||||||
setupResult = .configurationFailed
|
|
||||||
session.commitConfiguration()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let videoDeviceInput = try AVCaptureDeviceInput(device: videoDevice)
|
|
||||||
|
|
||||||
if session.canAddInput(videoDeviceInput) {
|
|
||||||
session.addInput(videoDeviceInput)
|
|
||||||
self.videoDeviceInput = videoDeviceInput
|
|
||||||
} else {
|
|
||||||
print("Couldn't add video device input to the session.")
|
|
||||||
setupResult = .configurationFailed
|
|
||||||
session.commitConfiguration()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let audioDevice = AVCaptureDevice.default(for: .audio)
|
|
||||||
let audioDeviceInput = try AVCaptureDeviceInput(device: audioDevice!)
|
|
||||||
|
|
||||||
if session.canAddInput(audioDeviceInput) {
|
|
||||||
session.addInput(audioDeviceInput)
|
|
||||||
self.audioDeviceInput = audioDeviceInput
|
|
||||||
} else {
|
|
||||||
print("Couldn't add audio device input to the session.")
|
|
||||||
setupResult = .configurationFailed
|
|
||||||
session.commitConfiguration()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add video output
|
|
||||||
if session.canAddOutput(movieOutput) {
|
|
||||||
session.addOutput(movieOutput)
|
|
||||||
} else {
|
|
||||||
print("Could not add movie output to the session")
|
|
||||||
setupResult = .configurationFailed
|
|
||||||
session.commitConfiguration()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
print("Couldn't create video device input: \(error)")
|
|
||||||
setupResult = .configurationFailed
|
|
||||||
session.commitConfiguration()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the photo output.
|
|
||||||
if session.canAddOutput(photoOutput) {
|
|
||||||
session.addOutput(photoOutput)
|
|
||||||
|
|
||||||
photoOutput.maxPhotoQualityPrioritization = .quality
|
|
||||||
|
|
||||||
} else {
|
|
||||||
print("Could not add photo output to the session")
|
|
||||||
setupResult = .configurationFailed
|
|
||||||
session.commitConfiguration()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
session.commitConfiguration()
|
|
||||||
self.isConfigured = true
|
|
||||||
|
|
||||||
self.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func resumeInterruptedSession() {
|
|
||||||
sessionQueue.async {
|
|
||||||
self.session.startRunning()
|
|
||||||
self.isSessionRunning = self.session.isRunning
|
|
||||||
if !self.session.isRunning {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.alertError = AlertError(title: "Camera Error", message: "Unable to resume camera", primaryButtonTitle: "Accept", secondaryButtonTitle: nil, primaryAction: nil, secondaryAction: nil)
|
|
||||||
self.shouldShowAlertView = true
|
|
||||||
self.isCameraUnavailable = true
|
|
||||||
self.isCameraButtonDisabled = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.isCameraUnavailable = false
|
|
||||||
self.isCameraButtonDisabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func changeCamera() {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.isCameraButtonDisabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionQueue.async {
|
|
||||||
let currentVideoDevice = self.videoDeviceInput.device
|
|
||||||
let currentPosition = currentVideoDevice.position
|
|
||||||
|
|
||||||
let preferredPosition: AVCaptureDevice.Position
|
|
||||||
let preferredDeviceType: AVCaptureDevice.DeviceType
|
|
||||||
|
|
||||||
switch currentPosition {
|
|
||||||
case .unspecified, .front:
|
|
||||||
preferredPosition = .back
|
|
||||||
preferredDeviceType = .builtInWideAngleCamera
|
|
||||||
|
|
||||||
case .back:
|
|
||||||
preferredPosition = .front
|
|
||||||
preferredDeviceType = .builtInWideAngleCamera
|
|
||||||
|
|
||||||
@unknown default:
|
|
||||||
print("Unknown capture position. Defaulting to back, dual-camera.")
|
|
||||||
preferredPosition = .back
|
|
||||||
preferredDeviceType = .builtInWideAngleCamera
|
|
||||||
}
|
|
||||||
let devices = self.videoDeviceDiscoverySession.devices
|
|
||||||
var newVideoDevice: AVCaptureDevice? = nil
|
|
||||||
|
|
||||||
if let device = devices.first(where: { $0.position == preferredPosition && $0.deviceType == preferredDeviceType }) {
|
|
||||||
newVideoDevice = device
|
|
||||||
} else if let device = devices.first(where: { $0.position == preferredPosition }) {
|
|
||||||
newVideoDevice = device
|
|
||||||
}
|
|
||||||
|
|
||||||
if let videoDevice = newVideoDevice {
|
|
||||||
do {
|
|
||||||
let videoDeviceInput = try AVCaptureDeviceInput(device: videoDevice)
|
|
||||||
|
|
||||||
self.session.beginConfiguration()
|
|
||||||
|
|
||||||
self.session.removeInput(self.videoDeviceInput)
|
|
||||||
|
|
||||||
if self.session.canAddInput(videoDeviceInput) {
|
|
||||||
NotificationCenter.default.removeObserver(self, name: .AVCaptureDeviceSubjectAreaDidChange, object: currentVideoDevice)
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(self.subjectAreaDidChange), name: .AVCaptureDeviceSubjectAreaDidChange, object: videoDeviceInput.device)
|
|
||||||
|
|
||||||
self.session.addInput(videoDeviceInput)
|
|
||||||
self.videoDeviceInput = videoDeviceInput
|
|
||||||
} else {
|
|
||||||
self.session.addInput(self.videoDeviceInput)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let connection = self.photoOutput.connection(with: .video) {
|
|
||||||
if connection.isVideoStabilizationSupported {
|
|
||||||
connection.preferredVideoStabilizationMode = .auto
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.photoOutput.maxPhotoQualityPrioritization = .quality
|
|
||||||
|
|
||||||
self.session.commitConfiguration()
|
|
||||||
} catch {
|
|
||||||
print("Error occurred while creating video device input: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.isCameraButtonDisabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func focus(with focusMode: AVCaptureDevice.FocusMode, exposureMode: AVCaptureDevice.ExposureMode, at devicePoint: CGPoint, monitorSubjectAreaChange: Bool) {
|
|
||||||
sessionQueue.async {
|
|
||||||
guard let device = self.videoDeviceInput?.device else { return }
|
|
||||||
do {
|
|
||||||
try device.lockForConfiguration()
|
|
||||||
|
|
||||||
if device.isFocusPointOfInterestSupported && device.isFocusModeSupported(focusMode) {
|
|
||||||
device.focusPointOfInterest = devicePoint
|
|
||||||
device.focusMode = focusMode
|
|
||||||
}
|
|
||||||
|
|
||||||
if device.isExposurePointOfInterestSupported && device.isExposureModeSupported(exposureMode) {
|
|
||||||
device.exposurePointOfInterest = devicePoint
|
|
||||||
device.exposureMode = exposureMode
|
|
||||||
}
|
|
||||||
|
|
||||||
device.isSubjectAreaChangeMonitoringEnabled = monitorSubjectAreaChange
|
|
||||||
device.unlockForConfiguration()
|
|
||||||
} catch {
|
|
||||||
print("Could not lock device for configuration: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public func focus(at focusPoint: CGPoint) {
|
|
||||||
let device = self.videoDeviceInput.device
|
|
||||||
do {
|
|
||||||
try device.lockForConfiguration()
|
|
||||||
if device.isFocusPointOfInterestSupported {
|
|
||||||
device.focusPointOfInterest = focusPoint
|
|
||||||
device.exposurePointOfInterest = focusPoint
|
|
||||||
device.exposureMode = .continuousAutoExposure
|
|
||||||
device.focusMode = .continuousAutoFocus
|
|
||||||
device.unlockForConfiguration()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
print(error.localizedDescription)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc public func stop(completion: (() -> ())? = nil) {
|
|
||||||
sessionQueue.async {
|
|
||||||
if self.isSessionRunning {
|
|
||||||
if self.setupResult == .success {
|
|
||||||
self.session.stopRunning()
|
|
||||||
self.isSessionRunning = self.session.isRunning
|
|
||||||
print("CAMERA STOPPED")
|
|
||||||
self.removeObservers()
|
|
||||||
|
|
||||||
if !self.session.isRunning {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.isCameraButtonDisabled = true
|
|
||||||
self.isCameraUnavailable = true
|
|
||||||
completion?()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc public func start() {
|
|
||||||
sessionQueue.async {
|
|
||||||
if !self.isSessionRunning && self.isConfigured {
|
|
||||||
switch self.setupResult {
|
|
||||||
case .success:
|
|
||||||
self.addObservers()
|
|
||||||
self.session.startRunning()
|
|
||||||
print("CAMERA RUNNING")
|
|
||||||
self.isSessionRunning = self.session.isRunning
|
|
||||||
|
|
||||||
if self.session.isRunning {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.isCameraButtonDisabled = false
|
|
||||||
self.isCameraUnavailable = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case .notAuthorized:
|
|
||||||
print("Application not authorized to use camera")
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.isCameraButtonDisabled = true
|
|
||||||
self.isCameraUnavailable = true
|
|
||||||
}
|
|
||||||
|
|
||||||
case .configurationFailed:
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.alertError = AlertError(title: "Camera Error", message: "Camera configuration failed. Either your device camera is not available or other application is using it", primaryButtonTitle: "Accept", secondaryButtonTitle: nil, primaryAction: nil, secondaryAction: nil)
|
|
||||||
self.shouldShowAlertView = true
|
|
||||||
self.isCameraButtonDisabled = true
|
|
||||||
self.isCameraUnavailable = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func set(zoom: CGFloat) {
|
|
||||||
let factor = zoom < 1 ? 1 : zoom
|
|
||||||
let device = self.videoDeviceInput.device
|
|
||||||
|
|
||||||
do {
|
|
||||||
try device.lockForConfiguration()
|
|
||||||
device.videoZoomFactor = factor
|
|
||||||
device.unlockForConfiguration()
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
print(error.localizedDescription)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func capturePhoto() {
|
|
||||||
if self.setupResult != .configurationFailed {
|
|
||||||
let videoPreviewLayerOrientation: AVCaptureVideoOrientation = .portrait
|
|
||||||
self.isCameraButtonDisabled = true
|
|
||||||
|
|
||||||
sessionQueue.async {
|
|
||||||
if let photoOutputConnection = self.photoOutput.connection(with: .video) {
|
|
||||||
photoOutputConnection.videoOrientation = videoPreviewLayerOrientation
|
|
||||||
}
|
|
||||||
var photoSettings = AVCapturePhotoSettings()
|
|
||||||
|
|
||||||
// Capture HEIF photos when supported. Enable according to user settings and high-resolution photos.
|
|
||||||
if (self.photoOutput.availablePhotoCodecTypes.contains(.hevc)) {
|
|
||||||
photoSettings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.hevc])
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.videoDeviceInput.device.isFlashAvailable {
|
|
||||||
photoSettings.flashMode = self.flashMode
|
|
||||||
}
|
|
||||||
|
|
||||||
if !photoSettings.__availablePreviewPhotoPixelFormatTypes.isEmpty {
|
|
||||||
photoSettings.previewPhotoFormat = [kCVPixelBufferPixelFormatTypeKey as String: photoSettings.__availablePreviewPhotoPixelFormatTypes.first!]
|
|
||||||
}
|
|
||||||
|
|
||||||
photoSettings.photoQualityPrioritization = .speed
|
|
||||||
|
|
||||||
if self.photoCaptureProcessor == nil {
|
|
||||||
self.photoCaptureProcessor = PhotoCaptureProcessor(with: photoSettings, photoOutput: self.photoOutput, willCapturePhotoAnimation: {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.willCapturePhoto.toggle()
|
|
||||||
self.willCapturePhoto.toggle()
|
|
||||||
}
|
|
||||||
}, completionHandler: { (photoCaptureProcessor) in
|
|
||||||
if let data = photoCaptureProcessor.photoData {
|
|
||||||
let url = self.savePhoto(data: data)
|
|
||||||
if let unwrappedURL = url {
|
|
||||||
self.thumbnail = Thumbnail(type: .image, url: unwrappedURL)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
print("Data for photo not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
self.isCameraButtonDisabled = false
|
|
||||||
}, photoProcessingHandler: { animate in
|
|
||||||
self.isPhotoProcessing = animate
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
self.photoCaptureProcessor?.capturePhoto(settings: photoSettings)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func startRecording() {
|
|
||||||
if self.setupResult != .configurationFailed {
|
|
||||||
let videoPreviewLayerOrientation: AVCaptureVideoOrientation = .portrait
|
|
||||||
self.isCameraButtonDisabled = true
|
|
||||||
|
|
||||||
sessionQueue.async {
|
|
||||||
if let videoOutputConnection = self.movieOutput.connection(with: .video) {
|
|
||||||
videoOutputConnection.videoOrientation = videoPreviewLayerOrientation
|
|
||||||
|
|
||||||
var videoSettings = [String: Any]()
|
|
||||||
|
|
||||||
if self.movieOutput.availableVideoCodecTypes.contains(.hevc) == true {
|
|
||||||
videoSettings[AVVideoCodecKey] = AVVideoCodecType.hevc
|
|
||||||
self.movieOutput.setOutputSettings(videoSettings, for: videoOutputConnection)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.videoCaptureProcessor == nil {
|
|
||||||
self.videoCaptureProcessor = VideoCaptureProcessor(movieOutput: self.movieOutput, beginHandler: {
|
|
||||||
self.isRecording = true
|
|
||||||
}, completionHandler: { (videoCaptureProcessor, outputFileURL) in
|
|
||||||
self.isCameraButtonDisabled = false
|
|
||||||
self.captureMode = .image
|
|
||||||
|
|
||||||
self.mediaItems.append(MediaItem(url: outputFileURL, type: .video))
|
|
||||||
self.thumbnail = Thumbnail(type: .video, url: outputFileURL)
|
|
||||||
}, videoProcessingHandler: { animate in
|
|
||||||
self.isPhotoProcessing = animate
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
self.videoCaptureProcessor?.startCapture(session: self.session)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopRecording() {
|
|
||||||
if let videoCaptureProcessor = self.videoCaptureProcessor {
|
|
||||||
isRecording = false
|
|
||||||
videoCaptureProcessor.stopCapture()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func savePhoto(imageType: String = "jpeg", data: Data) -> URL? {
|
|
||||||
guard let uiImage = UIImage(data: data) else {
|
|
||||||
print("Error converting media data to UIImage")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let compressedData = uiImage.jpegData(compressionQuality: 0.8) else {
|
|
||||||
print("Error converting UIImage to JPEG data")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let temporaryDirectory = NSTemporaryDirectory()
|
|
||||||
let tempFileName = "\(UUID().uuidString).\(imageType)"
|
|
||||||
let tempFileURL = URL(fileURLWithPath: temporaryDirectory).appendingPathComponent(tempFileName)
|
|
||||||
|
|
||||||
do {
|
|
||||||
try compressedData.write(to: tempFileURL)
|
|
||||||
self.mediaItems.append(MediaItem(url: tempFileURL, type: .image))
|
|
||||||
return tempFileURL
|
|
||||||
} catch {
|
|
||||||
print("Error saving image data to temporary URL: \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func addObservers() {
|
|
||||||
let systemPressureStateObservation = observe(\.videoDeviceInput.device.systemPressureState, options: .new) { _, change in
|
|
||||||
guard let systemPressureState = change.newValue else { return }
|
|
||||||
self.setRecommendedFrameRateRangeForPressureState(systemPressureState: systemPressureState)
|
|
||||||
}
|
|
||||||
keyValueObservations.append(systemPressureStateObservation)
|
|
||||||
|
|
||||||
// NotificationCenter.default.addObserver(self, selector: #selector(self.onOrientationChange), name: UIDevice.orientationDidChangeNotification, object: nil)
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self,
|
|
||||||
selector: #selector(subjectAreaDidChange),
|
|
||||||
name: .AVCaptureDeviceSubjectAreaDidChange,
|
|
||||||
object: videoDeviceInput.device)
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(uiRequestedNewFocusArea), name: .init(rawValue: "UserDidRequestNewFocusPoint"), object: nil)
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self,
|
|
||||||
selector: #selector(sessionRuntimeError),
|
|
||||||
name: .AVCaptureSessionRuntimeError,
|
|
||||||
object: session)
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self,
|
|
||||||
selector: #selector(sessionWasInterrupted),
|
|
||||||
name: .AVCaptureSessionWasInterrupted,
|
|
||||||
object: session)
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self,
|
|
||||||
selector: #selector(sessionInterruptionEnded),
|
|
||||||
name: .AVCaptureSessionInterruptionEnded,
|
|
||||||
object: session)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func removeObservers() {
|
|
||||||
NotificationCenter.default.removeObserver(self)
|
|
||||||
|
|
||||||
for keyValueObservation in keyValueObservations {
|
|
||||||
keyValueObservation.invalidate()
|
|
||||||
}
|
|
||||||
keyValueObservations.removeAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func uiRequestedNewFocusArea(notification: NSNotification) {
|
|
||||||
guard let userInfo = notification.userInfo as? [String: Any], let devicePoint = userInfo["devicePoint"] as? CGPoint else { return }
|
|
||||||
self.focus(at: devicePoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc
|
|
||||||
private func subjectAreaDidChange(notification: NSNotification) {
|
|
||||||
let devicePoint = CGPoint(x: 0.5, y: 0.5)
|
|
||||||
focus(with: .continuousAutoFocus, exposureMode: .continuousAutoExposure, at: devicePoint, monitorSubjectAreaChange: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc
|
|
||||||
private func sessionRuntimeError(notification: NSNotification) {
|
|
||||||
guard let error = notification.userInfo?[AVCaptureSessionErrorKey] as? AVError else { return }
|
|
||||||
|
|
||||||
print("Capture session runtime error: \(error)")
|
|
||||||
|
|
||||||
if error.code == .mediaServicesWereReset {
|
|
||||||
sessionQueue.async {
|
|
||||||
if self.isSessionRunning {
|
|
||||||
self.session.startRunning()
|
|
||||||
self.isSessionRunning = self.session.isRunning
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setRecommendedFrameRateRangeForPressureState(systemPressureState: AVCaptureDevice.SystemPressureState) {
|
|
||||||
let pressureLevel = systemPressureState.level
|
|
||||||
if pressureLevel == .serious || pressureLevel == .critical {
|
|
||||||
do {
|
|
||||||
try self.videoDeviceInput.device.lockForConfiguration()
|
|
||||||
print("WARNING: Reached elevated system pressure level: \(pressureLevel). Throttling frame rate.")
|
|
||||||
self.videoDeviceInput.device.activeVideoMinFrameDuration = CMTime(value: 1, timescale: 20)
|
|
||||||
self.videoDeviceInput.device.activeVideoMaxFrameDuration = CMTime(value: 1, timescale: 15)
|
|
||||||
self.videoDeviceInput.device.unlockForConfiguration()
|
|
||||||
} catch {
|
|
||||||
print("Could not lock device for configuration: \(error)")
|
|
||||||
}
|
|
||||||
} else if pressureLevel == .shutdown {
|
|
||||||
print("Session stopped running due to shutdown system pressure level.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc
|
|
||||||
private func sessionWasInterrupted(notification: NSNotification) {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.isCameraUnavailable = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if let userInfoValue = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as AnyObject?,
|
|
||||||
let reasonIntegerValue = userInfoValue.integerValue,
|
|
||||||
let reason = AVCaptureSession.InterruptionReason(rawValue: reasonIntegerValue) {
|
|
||||||
print("Capture session was interrupted with reason \(reason)")
|
|
||||||
|
|
||||||
if reason == .audioDeviceInUseByAnotherClient || reason == .videoDeviceInUseByAnotherClient {
|
|
||||||
print("Session stopped running due to video devies in use by another client.")
|
|
||||||
} else if reason == .videoDeviceNotAvailableWithMultipleForegroundApps {
|
|
||||||
print("Session stopped running due to video devies is not available with multiple foreground apps.")
|
|
||||||
} else if reason == .videoDeviceNotAvailableDueToSystemPressure {
|
|
||||||
print("Session stopped running due to shutdown system pressure level.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc
|
|
||||||
private func sessionInterruptionEnded(notification: NSNotification) {
|
|
||||||
print("Capture session interruption ended")
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.isCameraUnavailable = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
//
|
|
||||||
// ImageResizer.swift
|
|
||||||
// damus
|
|
||||||
//
|
|
||||||
// Created by Suhail Saqan on 8/5/23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
public enum ImageResizingError: Error {
|
|
||||||
case cannotRetrieveFromURL
|
|
||||||
case cannotRetrieveFromData
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct ImageResizer {
|
|
||||||
public var targetWidth: CGFloat
|
|
||||||
|
|
||||||
public init(targetWidth: CGFloat) {
|
|
||||||
self.targetWidth = targetWidth
|
|
||||||
}
|
|
||||||
|
|
||||||
public func resize(at url: URL) -> UIImage? {
|
|
||||||
guard let image = UIImage(contentsOfFile: url.path) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.resize(image: image)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func resize(image: UIImage) -> UIImage {
|
|
||||||
let originalSize = image.size
|
|
||||||
let targetSize = CGSize(width: targetWidth, height: targetWidth*originalSize.height/originalSize.width)
|
|
||||||
let renderer = UIGraphicsImageRenderer(size: targetSize)
|
|
||||||
return renderer.image { (context) in
|
|
||||||
image.draw(in: CGRect(origin: .zero, size: targetSize))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
//
|
|
||||||
// PhotoCaptureProcessor.swift
|
|
||||||
// damus
|
|
||||||
//
|
|
||||||
// Created by Suhail Saqan on 8/5/23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Photos
|
|
||||||
|
|
||||||
class PhotoCaptureProcessor: NSObject {
|
|
||||||
private(set) var requestedPhotoSettings: AVCapturePhotoSettings
|
|
||||||
private(set) var photoOutput: AVCapturePhotoOutput?
|
|
||||||
|
|
||||||
lazy var context = CIContext()
|
|
||||||
var photoData: Data?
|
|
||||||
private var maxPhotoProcessingTime: CMTime?
|
|
||||||
|
|
||||||
private let willCapturePhotoAnimation: () -> Void
|
|
||||||
private let completionHandler: (PhotoCaptureProcessor) -> Void
|
|
||||||
private let photoProcessingHandler: (Bool) -> Void
|
|
||||||
|
|
||||||
init(with requestedPhotoSettings: AVCapturePhotoSettings,
|
|
||||||
photoOutput: AVCapturePhotoOutput?,
|
|
||||||
willCapturePhotoAnimation: @escaping () -> Void,
|
|
||||||
completionHandler: @escaping (PhotoCaptureProcessor) -> Void,
|
|
||||||
photoProcessingHandler: @escaping (Bool) -> Void) {
|
|
||||||
self.requestedPhotoSettings = requestedPhotoSettings
|
|
||||||
self.willCapturePhotoAnimation = willCapturePhotoAnimation
|
|
||||||
self.completionHandler = completionHandler
|
|
||||||
self.photoProcessingHandler = photoProcessingHandler
|
|
||||||
self.photoOutput = photoOutput
|
|
||||||
}
|
|
||||||
|
|
||||||
func capturePhoto(settings: AVCapturePhotoSettings) {
|
|
||||||
if let photoOutput = self.photoOutput {
|
|
||||||
photoOutput.capturePhoto(with: settings, delegate: self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension PhotoCaptureProcessor: AVCapturePhotoCaptureDelegate {
|
|
||||||
func photoOutput(_ output: AVCapturePhotoOutput, willBeginCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings) {
|
|
||||||
maxPhotoProcessingTime = resolvedSettings.photoProcessingTimeRange.start + resolvedSettings.photoProcessingTimeRange.duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func photoOutput(_ output: AVCapturePhotoOutput, willCapturePhotoFor resolvedSettings: AVCaptureResolvedPhotoSettings) {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.willCapturePhotoAnimation()
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let maxPhotoProcessingTime = maxPhotoProcessingTime else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.photoProcessingHandler(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
let oneSecond = CMTime(seconds: 2, preferredTimescale: 1)
|
|
||||||
if maxPhotoProcessingTime > oneSecond {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.photoProcessingHandler(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.photoProcessingHandler(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let error = error {
|
|
||||||
print("Error capturing photo: \(error)")
|
|
||||||
} else {
|
|
||||||
photoData = photo.fileDataRepresentation()
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func photoOutput(_ output: AVCapturePhotoOutput, didFinishCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings, error: Error?) {
|
|
||||||
if let error = error {
|
|
||||||
print("Error capturing photo: \(error)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.completionHandler(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
//
|
|
||||||
// VideoCaptureProcessor.swift
|
|
||||||
// damus
|
|
||||||
//
|
|
||||||
// Created by Suhail Saqan on 8/5/23.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import AVFoundation
|
|
||||||
import Photos
|
|
||||||
|
|
||||||
class VideoCaptureProcessor: NSObject {
|
|
||||||
private(set) var movieOutput: AVCaptureMovieFileOutput?
|
|
||||||
|
|
||||||
private let beginHandler: () -> Void
|
|
||||||
private let completionHandler: (VideoCaptureProcessor, URL) -> Void
|
|
||||||
private let videoProcessingHandler: (Bool) -> Void
|
|
||||||
private var session: AVCaptureSession?
|
|
||||||
|
|
||||||
init(movieOutput: AVCaptureMovieFileOutput?,
|
|
||||||
beginHandler: @escaping () -> Void,
|
|
||||||
completionHandler: @escaping (VideoCaptureProcessor, URL) -> Void,
|
|
||||||
videoProcessingHandler: @escaping (Bool) -> Void) {
|
|
||||||
self.beginHandler = beginHandler
|
|
||||||
self.completionHandler = completionHandler
|
|
||||||
self.videoProcessingHandler = videoProcessingHandler
|
|
||||||
self.movieOutput = movieOutput
|
|
||||||
}
|
|
||||||
|
|
||||||
func startCapture(session: AVCaptureSession) {
|
|
||||||
if let movieOutput = self.movieOutput, session.isRunning {
|
|
||||||
let outputFileURL = uniqueOutputFileURL()
|
|
||||||
movieOutput.startRecording(to: outputFileURL, recordingDelegate: self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopCapture() {
|
|
||||||
if let movieOutput = self.movieOutput {
|
|
||||||
if movieOutput.isRecording {
|
|
||||||
movieOutput.stopRecording()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func uniqueOutputFileURL() -> URL {
|
|
||||||
let tempDirectory = FileManager.default.temporaryDirectory
|
|
||||||
let fileName = UUID().uuidString + ".mov"
|
|
||||||
return tempDirectory.appendingPathComponent(fileName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension VideoCaptureProcessor: AVCaptureFileOutputRecordingDelegate {
|
|
||||||
|
|
||||||
func fileOutput(_ output: AVCaptureFileOutput, didStartRecordingTo fileURL: URL, from connections: [AVCaptureConnection]) {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.beginHandler()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fileOutput(_ output: AVCaptureFileOutput, willFinishRecordingTo fileURL: URL, from connections: [AVCaptureConnection]) {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.videoProcessingHandler(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
|
|
||||||
if let error = error {
|
|
||||||
print("Error capturing video: \(error)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.completionHandler(self, outputFileURL)
|
|
||||||
self.videoProcessingHandler(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
//
|
|
||||||
// Contacts+.swift
|
|
||||||
// damus
|
|
||||||
//
|
|
||||||
// Extra functionality and utilities for `Contacts.swift`
|
|
||||||
//
|
|
||||||
// Created by Daniel D’Aquino on 2023-11-24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
func follow_reference(box: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, follow: FollowRef) -> NostrEvent? {
|
|
||||||
guard let ev = follow_user_event(our_contacts: our_contacts, keypair: keypair, follow: follow) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
box.send(ev)
|
|
||||||
|
|
||||||
return ev
|
|
||||||
}
|
|
||||||
|
|
||||||
func unfollow_reference(postbox: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, unfollow: FollowRef) -> NostrEvent? {
|
|
||||||
guard let cs = our_contacts else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let ev = unfollow_reference_event(our_contacts: cs, keypair: keypair, unfollow: unfollow) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
postbox.send(ev)
|
|
||||||
|
|
||||||
return ev
|
|
||||||
}
|
|
||||||
|
|
||||||
func unfollow_reference_event(our_contacts: NostrEvent, keypair: FullKeypair, unfollow: FollowRef) -> NostrEvent? {
|
|
||||||
let tags = our_contacts.tags.reduce(into: [[String]]()) { ts, tag in
|
|
||||||
if let tag = FollowRef.from_tag(tag: tag), tag == unfollow {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ts.append(tag.strings())
|
|
||||||
}
|
|
||||||
|
|
||||||
let kind = NostrKind.contacts.rawValue
|
|
||||||
|
|
||||||
return NostrEvent(content: our_contacts.content, keypair: keypair.to_keypair(), kind: kind, tags: Array(tags))
|
|
||||||
}
|
|
||||||
|
|
||||||
func follow_user_event(our_contacts: NostrEvent?, keypair: FullKeypair, follow: FollowRef) -> NostrEvent? {
|
|
||||||
guard let cs = our_contacts else {
|
|
||||||
// don't create contacts for now so we don't nuke our contact list due to connectivity issues
|
|
||||||
// we should only create contacts during profile creation
|
|
||||||
//return create_contacts(relays: relays, our_pubkey: our_pubkey, follow: follow)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let ev = follow_with_existing_contacts(keypair: keypair, our_contacts: cs, follow: follow) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return ev
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func decode_json_relays(_ content: String) -> [RelayURL: RelayInfo]? {
|
|
||||||
return decode_json(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
func remove_relay(ev: NostrEvent, current_relays: [RelayDescriptor], keypair: FullKeypair, relay: RelayURL) -> NostrEvent?{
|
|
||||||
var relays = ensure_relay_info(relays: current_relays, content: ev.content)
|
|
||||||
|
|
||||||
relays.removeValue(forKey: relay)
|
|
||||||
|
|
||||||
guard let content = encode_json(relays) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags.strings())
|
|
||||||
}
|
|
||||||
|
|
||||||
func add_relay(ev: NostrEvent, keypair: FullKeypair, current_relays: [RelayDescriptor], relay: RelayURL, info: RelayInfo) -> NostrEvent? {
|
|
||||||
var relays = ensure_relay_info(relays: current_relays, content: ev.content)
|
|
||||||
|
|
||||||
// If kind:3 content is empty, or if the relay doesn't exist in the list,
|
|
||||||
// we want to create a kind:3 event with the new relay
|
|
||||||
guard ev.content.isEmpty || relays.index(forKey: relay) == nil else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
relays[relay] = info
|
|
||||||
|
|
||||||
guard let content = encode_json(relays) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags.strings())
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensure_relay_info(relays: [RelayDescriptor], content: String) -> [RelayURL: RelayInfo] {
|
|
||||||
return decode_json_relays(content) ?? make_contact_relays(relays)
|
|
||||||
}
|
|
||||||
|
|
||||||
func is_already_following(contacts: NostrEvent, follow: FollowRef) -> Bool {
|
|
||||||
return contacts.references.contains { ref in
|
|
||||||
switch (ref, follow) {
|
|
||||||
case let (.hashtag(ht), .hashtag(follow_ht)):
|
|
||||||
return ht.hashtag == follow_ht
|
|
||||||
case let (.pubkey(pk), .pubkey(follow_pk)):
|
|
||||||
return pk == follow_pk
|
|
||||||
case (.hashtag, .pubkey), (.pubkey, .hashtag),
|
|
||||||
(.event, _), (.quote, _), (.param, _), (.naddr, _):
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func follow_with_existing_contacts(keypair: FullKeypair, our_contacts: NostrEvent, follow: FollowRef) -> NostrEvent? {
|
|
||||||
// don't update if we're already following
|
|
||||||
if is_already_following(contacts: our_contacts, follow: follow) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let kind = NostrKind.contacts.rawValue
|
|
||||||
|
|
||||||
var tags = our_contacts.tags.strings()
|
|
||||||
tags.append(follow.tag)
|
|
||||||
|
|
||||||
return NostrEvent(content: our_contacts.content, keypair: keypair.to_keypair(), kind: kind, tags: tags)
|
|
||||||
}
|
|
||||||
|
|
||||||
func make_contact_relays(_ relays: [RelayDescriptor]) -> [RelayURL: RelayInfo] {
|
|
||||||
return relays.reduce(into: [:]) { acc, relay in
|
|
||||||
acc[relay.url] = relay.info
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func make_relay_metadata(relays: [RelayDescriptor], keypair: FullKeypair) -> NostrEvent? {
|
|
||||||
let tags = relays.compactMap { r -> [String]? in
|
|
||||||
var tag = ["r", r.url.absoluteString]
|
|
||||||
if (r.info.read ?? true) != (r.info.write ?? true) {
|
|
||||||
tag += r.info.read == true ? ["read"] : ["write"]
|
|
||||||
}
|
|
||||||
if ((r.info.read ?? true) || (r.info.write ?? true)) && r.variant == .regular {
|
|
||||||
return tag;
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return NostrEvent(content: "", keypair: keypair.to_keypair(), kind: 10_002, tags: tags)
|
|
||||||
}
|
|
||||||
@@ -7,25 +7,57 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
class Contacts {
|
class Contacts {
|
||||||
private var friends: Set<Pubkey> = Set()
|
private var friends: Set<Pubkey> = Set()
|
||||||
private var friend_of_friends: Set<Pubkey> = Set()
|
private var friend_of_friends: Set<Pubkey> = Set()
|
||||||
/// Tracks which friends are friends of a given pubkey.
|
/// Tracks which friends are friends of a given pubkey.
|
||||||
private var pubkey_to_our_friends = [Pubkey : Set<Pubkey>]()
|
private var pubkey_to_our_friends = [Pubkey : Set<Pubkey>]()
|
||||||
|
private var muted: Set<Pubkey> = Set()
|
||||||
|
|
||||||
let our_pubkey: Pubkey
|
let our_pubkey: Pubkey
|
||||||
var delegate: ContactsDelegate? = nil
|
var event: NostrEvent?
|
||||||
var event: NostrEvent? {
|
var mutelist: NostrEvent?
|
||||||
didSet {
|
|
||||||
guard let event else { return }
|
|
||||||
self.delegate?.latest_contact_event_changed(new_event: event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init(our_pubkey: Pubkey) {
|
init(our_pubkey: Pubkey) {
|
||||||
self.our_pubkey = our_pubkey
|
self.our_pubkey = our_pubkey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func is_muted(_ pk: Pubkey) -> Bool {
|
||||||
|
return muted.contains(pk)
|
||||||
|
}
|
||||||
|
|
||||||
|
func set_mutelist(_ ev: NostrEvent) {
|
||||||
|
let oldlist = self.mutelist
|
||||||
|
self.mutelist = ev
|
||||||
|
|
||||||
|
let old = oldlist.map({ ev in Set(ev.referenced_pubkeys) }) ?? Set<Pubkey>()
|
||||||
|
let new = Set(ev.referenced_pubkeys)
|
||||||
|
let diff = old.symmetricDifference(new)
|
||||||
|
|
||||||
|
var new_mutes = Set<Pubkey>()
|
||||||
|
var new_unmutes = Set<Pubkey>()
|
||||||
|
|
||||||
|
for d in diff {
|
||||||
|
if new.contains(d) {
|
||||||
|
new_mutes.insert(d)
|
||||||
|
} else {
|
||||||
|
new_unmutes.insert(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: set local mutelist here
|
||||||
|
self.muted = Set(ev.referenced_pubkeys)
|
||||||
|
|
||||||
|
if new_mutes.count > 0 {
|
||||||
|
notify(.new_mutes(new_mutes))
|
||||||
|
}
|
||||||
|
|
||||||
|
if new_unmutes.count > 0 {
|
||||||
|
notify(.new_unmutes(new_unmutes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func remove_friend(_ pubkey: Pubkey) {
|
func remove_friend(_ pubkey: Pubkey) {
|
||||||
friends.remove(pubkey)
|
friends.remove(pubkey)
|
||||||
|
|
||||||
@@ -94,7 +126,127 @@ class Contacts {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delegate protocol for `Contacts`. Use this to listen to significant updates from a `Contacts` instance
|
func follow_reference(box: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, follow: FollowRef) -> NostrEvent? {
|
||||||
protocol ContactsDelegate {
|
guard let ev = follow_user_event(our_contacts: our_contacts, keypair: keypair, follow: follow) else {
|
||||||
func latest_contact_event_changed(new_event: NostrEvent)
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
box.send(ev)
|
||||||
|
|
||||||
|
return ev
|
||||||
|
}
|
||||||
|
|
||||||
|
func unfollow_reference(postbox: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, unfollow: FollowRef) -> NostrEvent? {
|
||||||
|
guard let cs = our_contacts else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let ev = unfollow_reference_event(our_contacts: cs, keypair: keypair, unfollow: unfollow) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
postbox.send(ev)
|
||||||
|
|
||||||
|
return ev
|
||||||
|
}
|
||||||
|
|
||||||
|
func unfollow_reference_event(our_contacts: NostrEvent, keypair: FullKeypair, unfollow: FollowRef) -> NostrEvent? {
|
||||||
|
let tags = our_contacts.tags.reduce(into: [[String]]()) { ts, tag in
|
||||||
|
if let tag = FollowRef.from_tag(tag: tag), tag == unfollow {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ts.append(tag.strings())
|
||||||
|
}
|
||||||
|
|
||||||
|
let kind = NostrKind.contacts.rawValue
|
||||||
|
|
||||||
|
return NostrEvent(content: our_contacts.content, keypair: keypair.to_keypair(), kind: kind, tags: Array(tags))
|
||||||
|
}
|
||||||
|
|
||||||
|
func follow_user_event(our_contacts: NostrEvent?, keypair: FullKeypair, follow: FollowRef) -> NostrEvent? {
|
||||||
|
guard let cs = our_contacts else {
|
||||||
|
// don't create contacts for now so we don't nuke our contact list due to connectivity issues
|
||||||
|
// we should only create contacts during profile creation
|
||||||
|
//return create_contacts(relays: relays, our_pubkey: our_pubkey, follow: follow)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let ev = follow_with_existing_contacts(keypair: keypair, our_contacts: cs, follow: follow) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ev
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func decode_json_relays(_ content: String) -> [String: RelayInfo]? {
|
||||||
|
return decode_json(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove_relay(ev: NostrEvent, current_relays: [RelayDescriptor], keypair: FullKeypair, relay: String) -> NostrEvent?{
|
||||||
|
var relays = ensure_relay_info(relays: current_relays, content: ev.content)
|
||||||
|
relays.removeValue(forKey: relay)
|
||||||
|
|
||||||
|
guard let content = encode_json(relays) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags.strings())
|
||||||
|
}
|
||||||
|
|
||||||
|
func add_relay(ev: NostrEvent, keypair: FullKeypair, current_relays: [RelayDescriptor], relay: String, info: RelayInfo) -> NostrEvent? {
|
||||||
|
var relays = ensure_relay_info(relays: current_relays, content: ev.content)
|
||||||
|
|
||||||
|
guard relays.index(forKey: relay) == nil else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
relays[relay] = info
|
||||||
|
|
||||||
|
guard let content = encode_json(relays) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags.strings())
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensure_relay_info(relays: [RelayDescriptor], content: String) -> [String: RelayInfo] {
|
||||||
|
guard let relay_info = decode_json_relays(content) else {
|
||||||
|
return make_contact_relays(relays)
|
||||||
|
}
|
||||||
|
return relay_info
|
||||||
|
}
|
||||||
|
|
||||||
|
func is_already_following(contacts: NostrEvent, follow: FollowRef) -> Bool {
|
||||||
|
return contacts.references.contains { ref in
|
||||||
|
switch (ref, follow) {
|
||||||
|
case let (.hashtag(ht), .hashtag(follow_ht)):
|
||||||
|
return ht.string() == follow_ht
|
||||||
|
case let (.pubkey(pk), .pubkey(follow_pk)):
|
||||||
|
return pk == follow_pk
|
||||||
|
case (.hashtag, .pubkey), (.pubkey, .hashtag),
|
||||||
|
(.event, _), (.quote, _), (.param, _):
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func follow_with_existing_contacts(keypair: FullKeypair, our_contacts: NostrEvent, follow: FollowRef) -> NostrEvent? {
|
||||||
|
// don't update if we're already following
|
||||||
|
if is_already_following(contacts: our_contacts, follow: follow) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let kind = NostrKind.contacts.rawValue
|
||||||
|
|
||||||
|
var tags = our_contacts.tags.strings()
|
||||||
|
tags.append(follow.tag)
|
||||||
|
|
||||||
|
return NostrEvent(content: our_contacts.content, keypair: keypair.to_keypair(), kind: kind, tags: tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func make_contact_relays(_ relays: [RelayDescriptor]) -> [String: RelayInfo] {
|
||||||
|
return relays.reduce(into: [:]) { acc, relay in
|
||||||
|
acc[relay.url.url.absoluteString] = relay.info
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ enum FilterState : Int {
|
|||||||
func filter(ev: NostrEvent) -> Bool {
|
func filter(ev: NostrEvent) -> Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .posts:
|
case .posts:
|
||||||
return ev.known_kind == .boost || ev.known_kind == .highlight || !ev.is_reply()
|
return ev.known_kind == .boost || !ev.is_reply(.empty)
|
||||||
case .posts_and_replies:
|
case .posts_and_replies:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -28,15 +28,6 @@ func nsfw_tag_filter(ev: NostrEvent) -> Bool {
|
|||||||
return ev.referenced_hashtags.first(where: { t in t.hashtag == "nsfw" }) == nil
|
return ev.referenced_hashtags.first(where: { t in t.hashtag == "nsfw" }) == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func get_repost_of_muted_user_filter(damus_state: DamusState) -> ((_ ev: NostrEvent) -> Bool) {
|
|
||||||
return { ev in
|
|
||||||
guard ev.known_kind == .boost else { return true }
|
|
||||||
// This needs to use cached because it can be way too slow otherwise
|
|
||||||
guard let inner_ev = ev.get_cached_inner_event(cache: damus_state.events) else { return true }
|
|
||||||
return should_show_event(state: damus_state, ev: inner_ev)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generic filter with various tweakable settings
|
/// Generic filter with various tweakable settings
|
||||||
struct ContentFilters {
|
struct ContentFilters {
|
||||||
var filters: [(NostrEvent) -> Bool]
|
var filters: [(NostrEvent) -> Bool]
|
||||||
@@ -53,16 +44,11 @@ struct ContentFilters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension ContentFilters {
|
extension ContentFilters {
|
||||||
static func default_filters(damus_state: DamusState) -> ContentFilters {
|
static func defaults(_ settings: UserSettingsStore) -> [(NostrEvent) -> Bool] {
|
||||||
return ContentFilters(filters: ContentFilters.defaults(damus_state: damus_state))
|
|
||||||
}
|
|
||||||
|
|
||||||
static func defaults(damus_state: DamusState) -> [(NostrEvent) -> Bool] {
|
|
||||||
var filters = Array<(NostrEvent) -> Bool>()
|
var filters = Array<(NostrEvent) -> Bool>()
|
||||||
if damus_state.settings.hide_nsfw_tagged_content {
|
if settings.hide_nsfw_tagged_content {
|
||||||
filters.append(nsfw_tag_filter)
|
filters.append(nsfw_tag_filter)
|
||||||
}
|
}
|
||||||
filters.append(get_repost_of_muted_user_filter(damus_state: damus_state))
|
|
||||||
return filters
|
return filters
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,31 +9,31 @@ import Foundation
|
|||||||
|
|
||||||
|
|
||||||
class CreateAccountModel: ObservableObject {
|
class CreateAccountModel: ObservableObject {
|
||||||
@Published var display_name: String = ""
|
@Published var real_name: String = ""
|
||||||
@Published var name: String = ""
|
@Published var nick_name: String = ""
|
||||||
@Published var about: String = ""
|
@Published var about: String = ""
|
||||||
@Published var pubkey: Pubkey = .empty
|
@Published var pubkey: Pubkey = .empty
|
||||||
@Published var privkey: Privkey = .empty
|
@Published var privkey: Privkey = .empty
|
||||||
@Published var profile_image: URL? = nil
|
@Published var profile_image: URL? = nil
|
||||||
|
|
||||||
var rendered_name: String {
|
var rendered_name: String {
|
||||||
if display_name.isEmpty {
|
if real_name.isEmpty {
|
||||||
return name
|
return nick_name
|
||||||
}
|
}
|
||||||
return display_name
|
return real_name
|
||||||
}
|
}
|
||||||
|
|
||||||
var keypair: Keypair {
|
var keypair: Keypair {
|
||||||
return Keypair(pubkey: self.pubkey, privkey: self.privkey)
|
return Keypair(pubkey: self.pubkey, privkey: self.privkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(display_name: String = "", name: String = "", about: String = "") {
|
init(real: String = "", nick: String = "", about: String = "") {
|
||||||
let keypair = generate_new_keypair()
|
let keypair = generate_new_keypair()
|
||||||
self.pubkey = keypair.pubkey
|
self.pubkey = keypair.pubkey
|
||||||
self.privkey = keypair.privkey
|
self.privkey = keypair.privkey
|
||||||
|
|
||||||
self.display_name = display_name
|
self.real_name = real
|
||||||
self.name = name
|
self.nick_name = nick
|
||||||
self.about = about
|
self.about = about
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
//
|
|
||||||
// DamusCacheManager.swift
|
|
||||||
// damus
|
|
||||||
//
|
|
||||||
// Created by Daniel D’Aquino on 2023-10-04.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Kingfisher
|
|
||||||
|
|
||||||
struct DamusCacheManager {
|
|
||||||
static var shared: DamusCacheManager = DamusCacheManager()
|
|
||||||
|
|
||||||
func clear_cache(damus_state: DamusState, completion: (() -> Void)? = nil) {
|
|
||||||
Log.info("Clearing all caches", for: .storage)
|
|
||||||
clear_kingfisher_cache(completion: {
|
|
||||||
clear_cache_folder(completion: {
|
|
||||||
Log.info("All caches cleared", for: .storage)
|
|
||||||
completion?()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func clear_kingfisher_cache(completion: (() -> Void)? = nil) {
|
|
||||||
Log.info("Clearing Kingfisher cache", for: .storage)
|
|
||||||
KingfisherManager.shared.cache.clearMemoryCache()
|
|
||||||
KingfisherManager.shared.cache.clearDiskCache {
|
|
||||||
Log.info("Kingfisher cache cleared", for: .storage)
|
|
||||||
completion?()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func clear_cache_folder(completion: (() -> Void)? = nil) {
|
|
||||||
Log.info("Clearing entire cache folder", for: .storage)
|
|
||||||
let cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
|
||||||
|
|
||||||
do {
|
|
||||||
let fileNames = try FileManager.default.contentsOfDirectory(atPath: cacheURL.path)
|
|
||||||
|
|
||||||
for fileName in fileNames {
|
|
||||||
let filePath = cacheURL.appendingPathComponent(fileName)
|
|
||||||
|
|
||||||
// Prevent issues by double-checking if files are in use, and do not delete them if they are.
|
|
||||||
// This is not perfect. There is still a small chance for a race condition if a file is opened between this check and the file removal.
|
|
||||||
let isBusy = (!(access(filePath.path, F_OK) == -1 && errno == ETXTBSY))
|
|
||||||
if isBusy {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
try FileManager.default.removeItem(at: filePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.info("Cache folder cleared successfully.", for: .storage)
|
|
||||||
completion?()
|
|
||||||
} catch {
|
|
||||||
Log.error("Could not clear cache folder", for: .storage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,16 +7,13 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import LinkPresentation
|
import LinkPresentation
|
||||||
import EmojiPicker
|
|
||||||
|
|
||||||
class DamusState: HeadlessDamusState {
|
struct DamusState {
|
||||||
let pool: RelayPool
|
let pool: RelayPool
|
||||||
let keypair: Keypair
|
let keypair: Keypair
|
||||||
let likes: EventCounter
|
let likes: EventCounter
|
||||||
let boosts: EventCounter
|
let boosts: EventCounter
|
||||||
let quote_reposts: EventCounter
|
|
||||||
let contacts: Contacts
|
let contacts: Contacts
|
||||||
let mutelist_manager: MutelistManager
|
|
||||||
let profiles: Profiles
|
let profiles: Profiles
|
||||||
let dms: DirectMessagesModel
|
let dms: DirectMessagesModel
|
||||||
let previews: PreviewCache
|
let previews: PreviewCache
|
||||||
@@ -29,51 +26,14 @@ class DamusState: HeadlessDamusState {
|
|||||||
let events: EventCache
|
let events: EventCache
|
||||||
let bookmarks: BookmarksManager
|
let bookmarks: BookmarksManager
|
||||||
let postbox: PostBox
|
let postbox: PostBox
|
||||||
let bootstrap_relays: [RelayURL]
|
let bootstrap_relays: [String]
|
||||||
let replies: ReplyCounter
|
let replies: ReplyCounter
|
||||||
|
let muted_threads: MutedThreadsManager
|
||||||
let wallet: WalletModel
|
let wallet: WalletModel
|
||||||
let nav: NavigationCoordinator
|
let nav: NavigationCoordinator
|
||||||
let music: MusicController?
|
let music: MusicController?
|
||||||
let video: VideoController
|
let video: VideoController
|
||||||
let ndb: Ndb
|
let ndb: Ndb
|
||||||
var purple: DamusPurple
|
|
||||||
var push_notification_client: PushNotificationClient
|
|
||||||
let emoji_provider: EmojiProvider
|
|
||||||
|
|
||||||
init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [RelayURL], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider) {
|
|
||||||
self.pool = pool
|
|
||||||
self.keypair = keypair
|
|
||||||
self.likes = likes
|
|
||||||
self.boosts = boosts
|
|
||||||
self.contacts = contacts
|
|
||||||
self.mutelist_manager = mutelist_manager
|
|
||||||
self.profiles = profiles
|
|
||||||
self.dms = dms
|
|
||||||
self.previews = previews
|
|
||||||
self.zaps = zaps
|
|
||||||
self.lnurls = lnurls
|
|
||||||
self.settings = settings
|
|
||||||
self.relay_filters = relay_filters
|
|
||||||
self.relay_model_cache = relay_model_cache
|
|
||||||
self.drafts = drafts
|
|
||||||
self.events = events
|
|
||||||
self.bookmarks = bookmarks
|
|
||||||
self.postbox = postbox
|
|
||||||
self.bootstrap_relays = bootstrap_relays
|
|
||||||
self.replies = replies
|
|
||||||
self.wallet = wallet
|
|
||||||
self.nav = nav
|
|
||||||
self.music = music
|
|
||||||
self.video = video
|
|
||||||
self.ndb = ndb
|
|
||||||
self.purple = purple ?? DamusPurple(
|
|
||||||
settings: settings,
|
|
||||||
keypair: keypair
|
|
||||||
)
|
|
||||||
self.quote_reposts = quote_reposts
|
|
||||||
self.push_notification_client = PushNotificationClient(keypair: keypair, settings: settings)
|
|
||||||
self.emoji_provider = emoji_provider
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func add_zap(zap: Zapping) -> Bool {
|
func add_zap(zap: Zapping) -> Bool {
|
||||||
@@ -100,13 +60,6 @@ class DamusState: HeadlessDamusState {
|
|||||||
keypair.privkey != nil
|
keypair.privkey != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func close() {
|
|
||||||
print("txn: damus close")
|
|
||||||
wallet.disconnect()
|
|
||||||
pool.close()
|
|
||||||
ndb.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
static var empty: DamusState {
|
static var empty: DamusState {
|
||||||
let empty_pub: Pubkey = .empty
|
let empty_pub: Pubkey = .empty
|
||||||
let empty_sec: Privkey = .empty
|
let empty_sec: Privkey = .empty
|
||||||
@@ -118,7 +71,6 @@ class DamusState: HeadlessDamusState {
|
|||||||
likes: EventCounter(our_pubkey: empty_pub),
|
likes: EventCounter(our_pubkey: empty_pub),
|
||||||
boosts: EventCounter(our_pubkey: empty_pub),
|
boosts: EventCounter(our_pubkey: empty_pub),
|
||||||
contacts: Contacts(our_pubkey: empty_pub),
|
contacts: Contacts(our_pubkey: empty_pub),
|
||||||
mutelist_manager: MutelistManager(user_keypair: kp),
|
|
||||||
profiles: Profiles(ndb: .empty),
|
profiles: Profiles(ndb: .empty),
|
||||||
dms: DirectMessagesModel(our_pubkey: empty_pub),
|
dms: DirectMessagesModel(our_pubkey: empty_pub),
|
||||||
previews: PreviewCache(),
|
previews: PreviewCache(),
|
||||||
@@ -133,13 +85,12 @@ class DamusState: HeadlessDamusState {
|
|||||||
postbox: PostBox(pool: RelayPool(ndb: .empty)),
|
postbox: PostBox(pool: RelayPool(ndb: .empty)),
|
||||||
bootstrap_relays: [],
|
bootstrap_relays: [],
|
||||||
replies: ReplyCounter(our_pubkey: empty_pub),
|
replies: ReplyCounter(our_pubkey: empty_pub),
|
||||||
|
muted_threads: MutedThreadsManager(keypair: kp),
|
||||||
wallet: WalletModel(settings: UserSettingsStore()),
|
wallet: WalletModel(settings: UserSettingsStore()),
|
||||||
nav: NavigationCoordinator(),
|
nav: NavigationCoordinator(),
|
||||||
music: nil,
|
music: nil,
|
||||||
video: VideoController(),
|
video: VideoController(),
|
||||||
ndb: .empty,
|
ndb: .empty
|
||||||
quote_reposts: .init(our_pubkey: empty_pub),
|
|
||||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
//
|
|
||||||
// DamusUserDefaults.swift
|
|
||||||
// damus
|
|
||||||
//
|
|
||||||
// Created by Daniel D’Aquino on 2023-11-25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
/// # DamusUserDefaults
|
|
||||||
///
|
|
||||||
/// This struct acts like a UserDefaults object, but is also capable of automatically mirroring values to a separate store.
|
|
||||||
///
|
|
||||||
/// It works by using a specific store container as the main source of truth, and by optionally mirroring values to a different container if needed.
|
|
||||||
///
|
|
||||||
/// This is useful when the data of a UserDefaults object needs to be accessible from another store container,
|
|
||||||
/// as it offers ways to automatically mirror information over a different container (e.g. When using app extensions)
|
|
||||||
///
|
|
||||||
/// Since it mirrors items instead of migrating them, this object can be used in a backwards compatible manner.
|
|
||||||
///
|
|
||||||
/// The easiest way to use this is to use `DamusUserDefaults.standard` as a drop-in replacement for `UserDefaults.standard`
|
|
||||||
/// Or, you can initialize a custom object with customizable stores.
|
|
||||||
struct DamusUserDefaults {
|
|
||||||
|
|
||||||
// MARK: - Helper data structures
|
|
||||||
|
|
||||||
enum Store: Equatable {
|
|
||||||
case standard
|
|
||||||
case shared
|
|
||||||
case custom(UserDefaults)
|
|
||||||
|
|
||||||
func get_user_defaults() -> UserDefaults? {
|
|
||||||
switch self {
|
|
||||||
case .standard:
|
|
||||||
return UserDefaults.standard
|
|
||||||
case .shared:
|
|
||||||
return UserDefaults(suiteName: Constants.DAMUS_APP_GROUP_IDENTIFIER)
|
|
||||||
case .custom(let user_defaults):
|
|
||||||
return user_defaults
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum DamusUserDefaultsError: Error {
|
|
||||||
case cannot_initialize_user_defaults
|
|
||||||
case cannot_mirror_main_user_defaults
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Stored properties
|
|
||||||
|
|
||||||
private let main: UserDefaults
|
|
||||||
private let mirrors: [UserDefaults]
|
|
||||||
|
|
||||||
// MARK: - Initializers
|
|
||||||
|
|
||||||
init?(main: Store, mirror mirrors: [Store] = []) throws {
|
|
||||||
guard let main_user_defaults = main.get_user_defaults() else { throw DamusUserDefaultsError.cannot_initialize_user_defaults }
|
|
||||||
let mirror_user_defaults: [UserDefaults] = try mirrors.compactMap({ mirror_store in
|
|
||||||
guard let mirror_user_default = mirror_store.get_user_defaults() else {
|
|
||||||
throw DamusUserDefaultsError.cannot_initialize_user_defaults
|
|
||||||
}
|
|
||||||
guard mirror_store != main else {
|
|
||||||
throw DamusUserDefaultsError.cannot_mirror_main_user_defaults
|
|
||||||
}
|
|
||||||
return mirror_user_default
|
|
||||||
})
|
|
||||||
|
|
||||||
self.main = main_user_defaults
|
|
||||||
self.mirrors = mirror_user_defaults
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Functions for feature parity with UserDefaults
|
|
||||||
|
|
||||||
func string(forKey defaultName: String) -> String? {
|
|
||||||
let value = self.main.string(forKey: defaultName)
|
|
||||||
self.mirror(value, forKey: defaultName)
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func set(_ value: Any?, forKey defaultName: String) {
|
|
||||||
self.main.set(value, forKey: defaultName)
|
|
||||||
self.mirror(value, forKey: defaultName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeObject(forKey defaultName: String) {
|
|
||||||
self.main.removeObject(forKey: defaultName)
|
|
||||||
self.mirror_object_removal(forKey: defaultName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func object(forKey defaultName: String) -> Any? {
|
|
||||||
let value = self.main.object(forKey: defaultName)
|
|
||||||
self.mirror(value, forKey: defaultName)
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Mirroring utilities
|
|
||||||
|
|
||||||
private func mirror(_ value: Any?, forKey defaultName: String) {
|
|
||||||
for mirror in self.mirrors {
|
|
||||||
mirror.set(value, forKey: defaultName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func mirror_object_removal(forKey defaultName: String) {
|
|
||||||
for mirror in self.mirrors {
|
|
||||||
mirror.removeObject(forKey: defaultName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Default convenience objects
|
|
||||||
|
|
||||||
/// # Convenience objects
|
|
||||||
///
|
|
||||||
/// - `DamusUserDefaults.standard`: will detect the bundle identifier and pick an appropriate object. You should generally use this one.
|
|
||||||
/// - `DamusUserDefaults.app`: stores things on its own container, and mirrors them to the shared container.
|
|
||||||
/// - `DamusUserDefaults.shared`: stores things on the shared container and does no mirroring
|
|
||||||
extension DamusUserDefaults {
|
|
||||||
static let app: DamusUserDefaults = try! DamusUserDefaults(main: .standard, mirror: [.shared])! // Since the underlying behavior is very static, the risk of crashing on force unwrap is low
|
|
||||||
static let shared: DamusUserDefaults = try! DamusUserDefaults(main: .shared)! // Since the underlying behavior is very static, the risk of crashing on force unwrap is low
|
|
||||||
static var standard: DamusUserDefaults {
|
|
||||||
get {
|
|
||||||
switch Bundle.main.bundleIdentifier {
|
|
||||||
case Constants.MAIN_APP_BUNDLE_IDENTIFIER:
|
|
||||||
return Self.app
|
|
||||||
case Constants.NOTIFICATION_EXTENSION_BUNDLE_IDENTIFIER:
|
|
||||||
return Self.shared
|
|
||||||
default:
|
|
||||||
return Self.shared
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class DraftArtifacts: Equatable {
|
class DraftArtifacts {
|
||||||
var content: NSMutableAttributedString
|
var content: NSMutableAttributedString
|
||||||
var media: [UploadedMedia]
|
var media: [UploadedMedia]
|
||||||
|
|
||||||
@@ -15,13 +15,6 @@ class DraftArtifacts: Equatable {
|
|||||||
self.content = content
|
self.content = content
|
||||||
self.media = media
|
self.media = media
|
||||||
}
|
}
|
||||||
|
|
||||||
static func == (lhs: DraftArtifacts, rhs: DraftArtifacts) -> Bool {
|
|
||||||
return (
|
|
||||||
lhs.media == rhs.media &&
|
|
||||||
lhs.content.string == rhs.content.string // Comparing the text content is not perfect but acceptable in this case because attributes for our post editor are determined purely from text content
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Drafts: ObservableObject {
|
class Drafts: ObservableObject {
|
||||||
|
|||||||
148
damus/Models/EventRef.swift
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
//
|
||||||
|
// EventRef.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2022-05-08.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum EventRef: Equatable {
|
||||||
|
case mention(Mention<NoteRef>)
|
||||||
|
case thread_id(NoteRef)
|
||||||
|
case reply(NoteRef)
|
||||||
|
case reply_to_root(NoteRef)
|
||||||
|
|
||||||
|
var is_mention: NoteRef? {
|
||||||
|
if case .mention(let m) = self { return m.ref }
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var is_direct_reply: NoteRef? {
|
||||||
|
switch self {
|
||||||
|
case .mention:
|
||||||
|
return nil
|
||||||
|
case .thread_id:
|
||||||
|
return nil
|
||||||
|
case .reply(let refid):
|
||||||
|
return refid
|
||||||
|
case .reply_to_root(let refid):
|
||||||
|
return refid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var is_thread_id: NoteRef? {
|
||||||
|
switch self {
|
||||||
|
case .mention:
|
||||||
|
return nil
|
||||||
|
case .thread_id(let referencedId):
|
||||||
|
return referencedId
|
||||||
|
case .reply:
|
||||||
|
return nil
|
||||||
|
case .reply_to_root(let referencedId):
|
||||||
|
return referencedId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var is_reply: NoteRef? {
|
||||||
|
switch self {
|
||||||
|
case .mention:
|
||||||
|
return nil
|
||||||
|
case .thread_id:
|
||||||
|
return nil
|
||||||
|
case .reply(let refid):
|
||||||
|
return refid
|
||||||
|
case .reply_to_root(let refid):
|
||||||
|
return refid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func build_mention_indices(_ blocks: [Block], type: MentionType) -> Set<Int> {
|
||||||
|
return blocks.reduce(into: []) { acc, block in
|
||||||
|
switch block {
|
||||||
|
case .mention(let m):
|
||||||
|
if m.ref.key == type, let idx = m.index {
|
||||||
|
acc.insert(idx)
|
||||||
|
}
|
||||||
|
case .relay:
|
||||||
|
return
|
||||||
|
case .text:
|
||||||
|
return
|
||||||
|
case .hashtag:
|
||||||
|
return
|
||||||
|
case .url:
|
||||||
|
return
|
||||||
|
case .invoice:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func interp_event_refs_without_mentions(_ refs: [NoteRef]) -> [EventRef] {
|
||||||
|
if refs.count == 0 {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if refs.count == 1 {
|
||||||
|
return [.reply_to_root(refs[0])]
|
||||||
|
}
|
||||||
|
|
||||||
|
var evrefs: [EventRef] = []
|
||||||
|
var first: Bool = true
|
||||||
|
for ref in refs {
|
||||||
|
if first {
|
||||||
|
evrefs.append(.thread_id(ref))
|
||||||
|
first = false
|
||||||
|
} else {
|
||||||
|
evrefs.append(.reply(ref))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return evrefs
|
||||||
|
}
|
||||||
|
|
||||||
|
func interp_event_refs_with_mentions(tags: Tags, mention_indices: Set<Int>) -> [EventRef] {
|
||||||
|
var mentions: [EventRef] = []
|
||||||
|
var ev_refs: [NoteRef] = []
|
||||||
|
var i: Int = 0
|
||||||
|
|
||||||
|
for tag in tags {
|
||||||
|
if let ref = NoteRef.from_tag(tag: tag) {
|
||||||
|
if mention_indices.contains(i) {
|
||||||
|
let mention = Mention<NoteRef>(index: i, ref: ref)
|
||||||
|
mentions.append(.mention(mention))
|
||||||
|
} else {
|
||||||
|
ev_refs.append(ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var replies = interp_event_refs_without_mentions(ev_refs)
|
||||||
|
replies.append(contentsOf: mentions)
|
||||||
|
return replies
|
||||||
|
}
|
||||||
|
|
||||||
|
func interpret_event_refs(blocks: [Block], tags: Tags) -> [EventRef] {
|
||||||
|
if tags.count == 0 {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
/// build a set of indices for each event mention
|
||||||
|
let mention_indices = build_mention_indices(blocks, type: .e)
|
||||||
|
|
||||||
|
/// simpler case with no mentions
|
||||||
|
if mention_indices.count == 0 {
|
||||||
|
return interp_event_refs_without_mentions_ndb(References<NoteRef>(tags: tags))
|
||||||
|
}
|
||||||
|
|
||||||
|
return interp_event_refs_with_mentions(tags: tags, mention_indices: mention_indices)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func event_is_reply(_ refs: [EventRef]) -> Bool {
|
||||||
|
return refs.contains { evref in
|
||||||
|
return evref.is_reply != nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -7,62 +7,25 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
class EventsModel: ObservableObject {
|
class EventsModel: ObservableObject {
|
||||||
let state: DamusState
|
let state: DamusState
|
||||||
let target: NoteId
|
let target: NoteId
|
||||||
let kind: QueryKind
|
let kind: NostrKind
|
||||||
let sub_id = UUID().uuidString
|
let sub_id = UUID().uuidString
|
||||||
let profiles_id = UUID().uuidString
|
let profiles_id = UUID().uuidString
|
||||||
var events: EventHolder
|
|
||||||
@Published var loading: Bool
|
|
||||||
|
|
||||||
enum QueryKind {
|
@Published var events: [NostrEvent] = []
|
||||||
case kind(NostrKind)
|
|
||||||
case quotes
|
|
||||||
}
|
|
||||||
|
|
||||||
init(state: DamusState, target: NoteId, kind: NostrKind) {
|
init(state: DamusState, target: NoteId, kind: NostrKind) {
|
||||||
self.state = state
|
self.state = state
|
||||||
self.target = target
|
self.target = target
|
||||||
self.kind = .kind(kind)
|
self.kind = kind
|
||||||
self.loading = true
|
|
||||||
self.events = EventHolder(on_queue: { ev in
|
|
||||||
preload_events(state: state, events: [ev])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
init(state: DamusState, target: NoteId, query: EventsModel.QueryKind) {
|
|
||||||
self.state = state
|
|
||||||
self.target = target
|
|
||||||
self.kind = query
|
|
||||||
self.loading = true
|
|
||||||
self.events = EventHolder(on_queue: { ev in
|
|
||||||
preload_events(state: state, events: [ev])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func quotes(state: DamusState, target: NoteId) -> EventsModel {
|
|
||||||
EventsModel(state: state, target: target, query: .quotes)
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func reposts(state: DamusState, target: NoteId) -> EventsModel {
|
|
||||||
EventsModel(state: state, target: target, kind: .boost)
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func likes(state: DamusState, target: NoteId) -> EventsModel {
|
|
||||||
EventsModel(state: state, target: target, kind: .like)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func get_filter() -> NostrFilter {
|
private func get_filter() -> NostrFilter {
|
||||||
var filter: NostrFilter
|
var filter = NostrFilter(kinds: [kind])
|
||||||
switch kind {
|
|
||||||
case .kind(let k):
|
|
||||||
filter = NostrFilter(kinds: [k])
|
|
||||||
filter.referenced_ids = [target]
|
filter.referenced_ids = [target]
|
||||||
case .quotes:
|
|
||||||
filter = NostrFilter(kinds: [.text])
|
|
||||||
filter.quotes = [target]
|
|
||||||
}
|
|
||||||
filter.limit = 500
|
filter.limit = 500
|
||||||
return filter
|
return filter
|
||||||
}
|
}
|
||||||
@@ -77,15 +40,19 @@ class EventsModel: ObservableObject {
|
|||||||
state.pool.unsubscribe(sub_id: sub_id)
|
state.pool.unsubscribe(sub_id: sub_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handle_event(relay_id: RelayURL, ev: NostrEvent) {
|
private func handle_event(relay_id: String, ev: NostrEvent) {
|
||||||
if events.insert(ev) {
|
guard ev.kind == kind.rawValue,
|
||||||
|
ev.referenced_ids.last == 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()
|
objectWillChange.send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_nostr_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
func handle_nostr_event(relay_id: String, ev: NostrConnectionEvent) {
|
||||||
guard case .nostr_event(let nev) = ev, nev.subid == self.sub_id
|
guard case .nostr_event(let nev) = ev else {
|
||||||
else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,14 +63,8 @@ class EventsModel: ObservableObject {
|
|||||||
break
|
break
|
||||||
case .ok:
|
case .ok:
|
||||||
break
|
break
|
||||||
case .auth:
|
|
||||||
break
|
|
||||||
case .eose:
|
case .eose:
|
||||||
self.loading = false
|
load_profiles(profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state)
|
||||||
guard let txn = NdbTxn(ndb: self.state.ndb) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
load_profiles(context: "events_model", profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events.all_events), damus_state: state, txn: txn)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
//
|
|
||||||
// FollowState.swift
|
|
||||||
// damus
|
|
||||||
//
|
|
||||||
// Created by Daniel D’Aquino on 2023-11-24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
enum FollowState {
|
|
||||||
case follows
|
|
||||||
case following
|
|
||||||
case unfollowing
|
|
||||||
case unfollows
|
|
||||||
}
|
|
||||||
@@ -53,8 +53,8 @@ class FollowersModel: ObservableObject {
|
|||||||
has_contact.insert(ev.pubkey)
|
has_contact.insert(ev.pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func load_profiles<Y>(relay_id: RelayURL, txn: NdbTxn<Y>) {
|
func load_profiles(relay_id: String) {
|
||||||
let authors = find_profiles_to_fetch_from_keys(profiles: damus_state.profiles, pks: contacts ?? [], txn: txn)
|
let authors = find_profiles_to_fetch_from_keys(profiles: damus_state.profiles, pks: contacts ?? [])
|
||||||
if authors.isEmpty {
|
if authors.isEmpty {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ class FollowersModel: ObservableObject {
|
|||||||
damus_state.pool.subscribe_to(sub_id: profiles_id, filters: [filter], to: [relay_id], handler: handle_event)
|
damus_state.pool.subscribe_to(sub_id: profiles_id, filters: [filter], to: [relay_id], handler: handle_event)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
func handle_event(relay_id: String, ev: NostrConnectionEvent) {
|
||||||
guard case .nostr_event(let nev) = ev else {
|
guard case .nostr_event(let nev) = ev else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -83,16 +83,13 @@ class FollowersModel: ObservableObject {
|
|||||||
|
|
||||||
case .eose(let sub_id):
|
case .eose(let sub_id):
|
||||||
if sub_id == self.sub_id {
|
if sub_id == self.sub_id {
|
||||||
guard let txn = NdbTxn(ndb: self.damus_state.ndb) else { return }
|
load_profiles(relay_id: relay_id)
|
||||||
load_profiles(relay_id: relay_id, txn: txn)
|
|
||||||
} else if sub_id == self.profiles_id {
|
} else if sub_id == self.profiles_id {
|
||||||
damus_state.pool.unsubscribe(sub_id: profiles_id, to: [relay_id])
|
damus_state.pool.unsubscribe(sub_id: profiles_id, to: [relay_id])
|
||||||
}
|
}
|
||||||
|
|
||||||
case .ok:
|
case .ok:
|
||||||
break
|
break
|
||||||
case .auth:
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ class FollowingModel {
|
|||||||
self.hashtags = hashtags
|
self.hashtags = hashtags
|
||||||
}
|
}
|
||||||
|
|
||||||
func get_filter<Y>(txn: NdbTxn<Y>) -> NostrFilter {
|
func get_filter() -> NostrFilter {
|
||||||
var f = NostrFilter(kinds: [.metadata])
|
var f = NostrFilter(kinds: [.metadata])
|
||||||
f.authors = self.contacts.reduce(into: Array<Pubkey>()) { acc, pk in
|
f.authors = self.contacts.reduce(into: Array<Pubkey>()) { acc, pk in
|
||||||
// don't fetch profiles we already have
|
// don't fetch profiles we already have
|
||||||
if damus_state.profiles.has_fresh_profile(id: pk, txn: txn) {
|
if damus_state.profiles.has_fresh_profile(id: pk) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
acc.append(pk)
|
acc.append(pk)
|
||||||
@@ -34,8 +34,8 @@ class FollowingModel {
|
|||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
func subscribe<Y>(txn: NdbTxn<Y>) {
|
func subscribe() {
|
||||||
let filter = get_filter(txn: txn)
|
let filter = get_filter()
|
||||||
if (filter.authors?.count ?? 0) == 0 {
|
if (filter.authors?.count ?? 0) == 0 {
|
||||||
needs_sub = false
|
needs_sub = false
|
||||||
return
|
return
|
||||||
@@ -53,7 +53,7 @@ class FollowingModel {
|
|||||||
self.damus_state.pool.unsubscribe(sub_id: sub_id)
|
self.damus_state.pool.unsubscribe(sub_id: sub_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
func handle_event(relay_id: String, ev: NostrConnectionEvent) {
|
||||||
// don't need to do anything here really
|
// don't need to do anything here really
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
//
|
|
||||||
// FriendFilter.swift
|
|
||||||
// damus
|
|
||||||
//
|
|
||||||
// Created by Daniel D’Aquino on 2023-11-24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
enum FriendFilter: String, StringCodable {
|
|
||||||
case all
|
|
||||||
case friends
|
|
||||||
|
|
||||||
init?(from string: String) {
|
|
||||||
guard let ff = FriendFilter(rawValue: string) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
self = ff
|
|
||||||
}
|
|
||||||
|
|
||||||
func to_string() -> String {
|
|
||||||
self.rawValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func filter(contacts: Contacts, pubkey: Pubkey) -> Bool {
|
|
||||||
switch self {
|
|
||||||
case .all:
|
|
||||||
return true
|
|
||||||
case .friends:
|
|
||||||
return contacts.is_in_friendosphere(pubkey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
//
|
|
||||||
// HeadlessDamusState.swift
|
|
||||||
// damus
|
|
||||||
//
|
|
||||||
// Created by Daniel D’Aquino on 2023-11-27.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
/// HeadlessDamusState
|
|
||||||
///
|
|
||||||
/// A protocl for a lighter headless alternative to DamusState that does not have dependencies on View objects or UI logic.
|
|
||||||
/// This is useful in limited environments (e.g. Notification Service Extension) where we do not want View/UI dependencies
|
|
||||||
protocol HeadlessDamusState {
|
|
||||||
var ndb: Ndb { get }
|
|
||||||
var settings: UserSettingsStore { get }
|
|
||||||
var contacts: Contacts { get }
|
|
||||||
var mutelist_manager: MutelistManager { get }
|
|
||||||
var keypair: Keypair { get }
|
|
||||||
var profiles: Profiles { get }
|
|
||||||
var zaps: Zaps { get }
|
|
||||||
var lnurls: LNUrls { get }
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func add_zap(zap: Zapping) -> Bool
|
|
||||||
}
|
|
||||||