Compare commits

..

3 Commits

Author SHA1 Message Date
tyiu d946dbe50d WIP Fix mentions when writing notes 2023-03-25 22:52:20 -06:00
tyiu 9590166367 Modify a string 2023-03-25 08:15:24 -06:00
tyiu efdecaf118 WIP translations CI 2023-03-25 08:15:23 -06:00
195 changed files with 1535 additions and 5522 deletions
@@ -0,0 +1,49 @@
name: Export Source Translations
on:
push:
branches:
- master
jobs:
export-source-translations:
name: Update translations branch
runs-on: macos-12
strategy:
matrix:
include:
- xcode: "14.2"
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Run export script
run: |
sh devtools/export-source-translation.sh
- name: Push source translations to Transifex
uses: transifex/cli-action@v2
with:
token: ${{ secrets.TX_TOKEN }}
args: push --branch ''
- name: Remove extraneous /tmp/tx file from running transifex cli that breaks the next pull step
run: |
rm -rf /tmp/tx
- name: Pull translations from Transifex
uses: transifex/cli-action@v2
with:
token: ${{ secrets.TX_TOKEN }}
args: pull --branch ''
- name: Commit translation changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: Update Translations 🤖
branch: translations
create_branch: true
push_options: '--force'
- name: Create Pull Request
uses: repo-sync/pull-request@v2
with:
source_branch: "translations"
destination_branch: "master"
pr_title: "Update Translations 🤖"
if: steps.auto-commit-action.outputs.changes_detected == 'true'
-2
View File
@@ -1,2 +0,0 @@
translations/
*.lproj/
Executable
+24
View File
@@ -0,0 +1,24 @@
[main]
host = https://www.transifex.com
lang_map = aa_DJ: aa-DJ, af_ZA: af-ZA, am_ET: am-ET, ar_AA: ar-AA, ar_AE: ar-AE, ar_DZ: ar-DZ, ar_EG: ar-EG, ar_IQ: ar-IQ, ar_JO: ar-JO, ar_LB: ar-LB, ar_SA: ar-SA, ar_SD: ar-SD, ar_SY: ar-SY, as_IN: as-IN, ast_ES: ast-ES, az_AZ: az-AZ, az_IR: az-IR, be_BY: be-BY, bem_ZM: bem-ZM, bg_BG: bg-BG, bg_US: bg-US, bn_BD: bn-BD, bn_IN: bn-IN, bo_CN: bo-CN, bqi_IR: bqi-IR, br_FR: br-FR, bs_BA: bs-BA, bs_BA-SRP: bs-BA-SRP, ca_ES: ca-ES, cs_CZ: cs-CZ, cy_GB: cy-GB, da_DK: da-DK, de_AT: de-AT, de_CH: de-CH, de_DE: de-DE, dz_BT: dz-BT, el_CY: el-CY, el_DE: el-DE, el_GR: el-GR, en_AE: en-AE, en_AL: en-AL, en_AT: en-AT, en_AU: en-AU, en_BA: en-BA, en_BA-SRP: en-BA-SRP, en_BD: en-BD, en_BE: en-BE, en_BG: en-BG, en_BH: en-BH, en_BR: en-BR, en_CA: en-CA, en_CH: en-CH, en_CL: en-CL, en_CO: en-CO, en_CY: en-CY, en_CZ: en-CZ, en_DE: en-DE, en_DK: en-DK, en_EC: en-EC, en_EG: en-EG, en_ES: en-ES, en_FI: en-FI, en_FJ: en-FJ, en_FR: en-FR, en_GB: en-GB, en_GH: en-GH, en_GR: en-GR, en_HK: en-HK, en_HR: en-HR, en_HU: en-HU, en_IE: en-IE, en_IN: en-IN, en_IT: en-IT, en_JP: en-JP, en_KR: en-KR, en_KW: en-KW, en_LK: en-LK, en_MX: en-MX, en_MY: en-MY, en_NG: en-NG, en_NL: en-NL, en_NO: en-NO, en_NZ: en-NZ, en_PE: en-PE, en_PG: en-PG, en_PH: en-PH, en_PK: en-PK, en_PL: en-PL, en_PR: en-PR, en_PT: en-PT, en_QA: en-QA, en_RO: en-RO, en_RS: en-RS, en_SA: en-SA, en_SE: en-SE, en_SG: en-SG, en_SI: en-SI, en_SK: en-SK, en_TT: en-TT, en_UG: en-UG, en_ZA: en-ZA, en_ZM: en-ZM, en_ee: en-ee, en_lt: en-lt, en_lv: en-lv, es_419: es-419, es_AR: es-AR, es_BO: es-BO, es_CL: es-CL, es_CO: es-CO, es_CR: es-CR, es_CU: es-CU, es_DO: es-DO, es_EC: es-EC, es_ES: es-ES, es_GT: es-GT, es_HN: es-HN, es_MX: es-MX, es_NI: es-NI, es_PA: es-PA, es_PE: es-PE, es_PR: es-PR, es_PY: es-PY, es_SA: es-SA, es_SV: es-SV, es_US: es-US, es_UY: es-UY, es_VE: es-VE, et_EE: et-EE, eu_ES: eu-ES, fa_AF: fa-AF, fa_IR: fa-IR, ff_SN: ff-SN, fi_FI: fi-FI, fil_PH: fil-PH, fo_FO: fo-FO, fr_BE: fr-BE, fr_CA: fr-CA, fr_CH: fr-CH, fr_CI: fr-CI, fr_CM: fr-CM, fr_FR: fr-FR, fr_GA: fr-GA, fr_LU: fr-LU, fy_NL: fy-NL, ga_IE: ga-IE, gl_ES: gl-ES, gu_IN: gu-IN, gug_PY: gug-PY, he_IL: he-IL, hi_IN: hi-IN, hr_BA: hr-BA, hr_BA-SRP: hr-BA-SRP, hr_HR: hr-HR, ht_HT: ht-HT, hu_HU: hu-HU, hu_RO: hu-RO, hu_SK: hu-SK, hy_AM: hy-AM, hy_RU: hy-RU, hye_RU: hye-RU, id_ID: id-ID, is_IS: is-IS, it_CH: it-CH, it_IT: it-IT, ja_JP: ja-JP, ka_GE: ka-GE, kk_KZ: kk-KZ, km_KH: km-KH, kn_IN: kn-IN, ko_KR: ko-KR, ks_IN: ks-IN, ku_IQ: ku-IQ, lg_UG: lg-UG, lo_LA: lo-LA, loz_ZM: loz-ZM, lt_LT: lt-LT, lv_LV: lv-LV, mhr_RU: mhr-RU, mk_MK: mk-MK, ml_IN: ml-IN, mn_MN: mn-MN, mr_IN: mr-IN, ms_BN: ms-BN, ms_MY: ms-MY, mt_MT: mt-MT, my_MM: my-MM, nb_NO: nb-NO, ne_NP: ne-NP, nl_BE: nl-BE, nl_NL: nl-NL, nn_NO: nn-NO, no_NO: no-NO, or_IN: or-IN, pa_IN: pa-IN, pa_PK: pa-PK, pl_PL: pl-PL, ps_AF: ps-AF, pt_AO: pt-AO, pt_BR: pt-BR, pt_MZ: pt-MZ, pt_PT: pt-PT, qu_EC: qu-EC, ro_MD: ro-MD, ro_RO: ro-RO, ru_RU: ru-RU, ru_UA: ru-UA, ru_ee: ru-ee, ru_lt: ru-lt, ru_lv: ru-lv, si_LK: si-LK, sk_SK: sk-SK, sl_SI: sl-SI, sq_AL: sq-AL, sr_BA-SRP: sr-BA-SRP, sr_ME: sr-ME, sr_RS: sr-RS, st_ZA: st-ZA, sv_FI: sv-FI, sv_SE: sv-SE, sw_CD: sw-CD, sw_KE: sw-KE, sw_TZ: sw-TZ, sw_UG: sw-UG, ta_IN: ta-IN, ta_LK: ta-LK, te_IN: te-IN, tg_TJ: tg-TJ, th_TH: th-TH, tk_TM: tk-TM, tl_PH: tl-PH, tr_CY: tr-CY, tr_DE: tr-DE, tr_TR: tr-TR, uk_UA: uk-UA, ur_PK: ur-PK, uz_UZ: uz-UZ, vi_VN: vi-VN, wo_SN: wo-SN, yue_CN: yue-CN, zh_CN: zh-CN, zh_HK: zh-HK, zh_SG: zh-SG, zh_TW: zh-TW, zu_ZA: zu-ZA
[o:damus:p:damus-ios-staging:r:infopliststrings]
file_filter = damus/<lang>.lproj/InfoPlist.strings
source_file = damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings
type = STRINGS_UTF8
minimum_perc = 0
resource_name = damus..en-US.lproj/InfoPlist.strings (translations)
[o:damus:p:damus-ios-staging:r:localizablestrings]
file_filter = damus/<lang>.lproj/Localizable.strings
source_file = damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings
type = STRINGS_UTF8
minimum_perc = 0
resource_name = damus..en-US.lproj/Localizable.strings (translations)
[o:damus:p:damus-ios-staging:r:localizablestringsdict]
file_filter = damus/<lang>.lproj/Localizable.stringsdict
source_file = damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict
type = STRINGSDICT
minimum_perc = 0
resource_name = damus..en-US.lproj/Localizable.stringsdict (translations)
+4 -120
View File
@@ -1,99 +1,3 @@
## [1.4.1-3] - 2023-04-05
### Added
- Added text truncation settings (William Casarin)
### Changed
- Rename block to mute (William Casarin)
### Fixed
- Reduce chopping of images (mainvolume)
- Fix some notification settings not saving (William Casarin)
- Fix broken camera uploads (again) (Joel Klabo)
[1.4.1-3]: https://github.com/damus-io/damus/releases/tag/v1.4.1-3
## [1.4.1-2] - 2023-04-04
### Added
- Reply counts (William Casarin)
- Add option to only show notification from people you follow (Swift)
- Added local notifications for other events (Swift)
- Show a custom view when tagged user isn't found (ericholguin)
- Show referenced notes in DMs (William Casarin)
### Changed
- Show full bleed images on selected events in threads (William Casarin)
- Improvement to square image displaying (mainvolume)
### Fixed
- Fix broken website links that have missing https:// prefixes (William Casarin)
- Get around CCP bootstrap relay banning by caching user's relays as their bootstrap relays (William Casarin)
[1.4.1-2]: https://github.com/damus-io/damus/releases/tag/v1.4.1-2
## [1.4.1] - 2023-04-03
### Added
- Profile Picture Upload (Joel Klabo)
- Enable offline posting (William Casarin)
- Add auto-translation caching to ruduce api usage (Terry Yiu)
- Added support for gif uploads (Swift)
- Add a Divider in the Follows List for Large Screens (Joel Klabo)
- Upload Photos and Videos from Camera (Joel Klabo)
- Added ability to lookup users by nip05 identifiers (William Casarin)
### Changed
- Only truncate timeline text if enabled in settings (William Casarin)
- Make mentions wide in notifications like in timeline (William Casarin)
- Broadcast events you are replying to (William Casarin)
- Broadcast now also broadcasts event user's profile (William Casarin)
- Improved look of reply view (ericholguin)
- Remove gradient in some places for visibility (ericholguin)
### Fixed
- Fix cropped images (mainvolume)
- Truncate long text in notification items (William Casarin)
- Restore missing reply description on selected events (William Casarin)
- Show sent DMs immediately (William Casarin)
- Fixed size of translated text (William Casarin)
- Fix crash when reposting (William Casarin)
- Fix unclickable image dismiss button (OlegAba)
[1.4.1]: https://github.com/damus-io/damus/releases/tag/v1.4.1
## [1.4.0] - 2023-03-27
### Added
- Local zap notifications (Swift)
- Add support for video uploads (Swift)
- Auto Translation (Terry Yiu)
- Portuguese (Brazil) translations (Andressa Munturo)
- Spanish (Spain) translations (Max Pleb)
- Vietnamese translations (ShiryoRyo)
### Fixed
- Fixed small notification hit boxes (Terry Yiu)
[1.4.0]: https://github.com/damus-io/damus/releases/tag/v1.4.0
## [1.3.0-7] - 2023-03-24
- New experimental timeline view
@@ -153,10 +57,6 @@
- Add image uploader (Swift)
- Add option to always show images (never blur) (William Casarin)
- Canadian French (Pierre - synoptic_okubo)
- Hungarian translations (Zoltan)
- Korean translations (sogoagain)
- Swedish translations (Pextar)
### Changed
@@ -179,9 +79,6 @@
- Extend user tagging search to all local profiles (William Casarin)
- Vibrate when a zap is received (Swift)
- New and Improved Share sheet (ericholguin)
- Bulgarian translations (elsat)
- Persian translations (Mahdi Taghizadeh)
- Ukrainian translations (Valeriia Khudiakova, Tony B)
### Changed
@@ -280,8 +177,6 @@
- Customized zaps (William Casarin)
- Add new Notifications View (William Casarin)
- Bookmarking (Joel Klabo)
- Chinese, Traditional (Hong Kong) translations (rasputin)
- Chinese, Traditional (Taiwan) translations (rasputin)
### Changed
@@ -307,9 +202,6 @@
- Added the ability to select text on posts (OlegAba)
- Added Posts or Post & Replies selector to Profile (ericholguin)
- Improved profile navbar (OlegAba)
- Czech translations (Martin Gabrhel)
- Indonesian translations (johnybergzy)
- Russian translations (Tony B)
### Changed
@@ -358,6 +250,7 @@
### Added
- Relay Filtering (William Casarin)
- Japanese translations (Terry Yiu)
- Add password autofill on account login and creation (Terry Yiu)
- Show if relay is paid (William Casarin)
- Add "Follows You" indicator on profile (William Casarin)
@@ -370,10 +263,6 @@
- Copy invoice button (Joel Klabo)
- Receive Lightning Zaps (William Casarin)
- Allow text selection in bio (Suhail Saqan)
- Chinese, Simplified (China mainland) translations (haolong, rasputin)
- Dutch translations (Heimen Stoffels - Vistaus)
- Greek translations (milicode)
- Japanese translations (akiomik, foxytanuki, Guetsu Ren - Nighthaven, h3y6e, middlingphys)
### Changed
@@ -408,7 +297,6 @@
- LibreTranslate note translations (Terry Yiu)
- Added support for account deletion (William Casarin)
- User tagging and autocompletion in posts (Swift)
- Polish translations (pysiak)
### Changed
@@ -431,8 +319,7 @@
### Added
- Arabic translations (Barodane)
- Portuguese translations (Antonio Chagas)
- Added Arabic and Portuguese translations (Barodane, Antonio Chagas)
- Add QRCode view for sharing your pubkey (ericholguin)
- Added nostr: uri handling (William Casarin)
@@ -459,8 +346,7 @@
### Added
- Reposts view (Terry Yiu)
- Italian translations (Nicolò Carcagnì)
- Latvian translations (SYX)
- Translations for it_IT, it_CH, fr_FR, de_DE, de_AT and lv_LV (Nicolò Carcagnì, Solobalbo, Gregor, Peter Gerstbach, SYX)
- Added ability to block users (William Casarin)
- Added a way to report content (William Casarin)
- Stretchable profile cover header (Swift)
@@ -487,9 +373,7 @@
- Show website on profiles (William Casarin)
- Add the ability to choose participants when replying (Joel Klabo)
- German translations (Gregor, Peter Gerstbach)
- Turkish translations (Taylan Benli)
- French (France) translations (Solobalbo)
- Translations for de_AT, de_DE, tr_TR, fr_FR (Gregor, Peter Gerstbach, Taylan Benli, Solobalbo)
- Add DM Message Requests (William Casarin)
+29 -134
View File
@@ -17,7 +17,6 @@
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */; };
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */; };
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; };
3A48E23B29D518F000BA313D /* Translations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E23A29D518F000BA313D /* Translations.swift */; };
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
@@ -38,13 +37,6 @@
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F8E280F640A000448DE /* ThreadModel.swift */; };
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F90280F6528000448DE /* ChatView.swift */; };
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.swift */; };
4C1A9A1A29DCA17E00516EAC /* ReplyCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */; };
4C1A9A1D29DDCF9B00516EAC /* NotificationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A1C29DDCF9B00516EAC /* NotificationSettingsView.swift */; };
4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A1E29DDD24B00516EAC /* AppearanceSettingsView.swift */; };
4C1A9A2129DDD3E100516EAC /* KeySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2029DDD3E100516EAC /* KeySettingsView.swift */; };
4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2229DDDB8100516EAC /* IconLabel.swift */; };
4C1A9A2529DDDF2600516EAC /* ZapSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */; };
4C1A9A2729DDE31900516EAC /* TranslationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */; };
4C216F32286E388800040376 /* DMChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F31286E388800040376 /* DMChatView.swift */; };
4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; };
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */; };
@@ -137,7 +129,6 @@
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFBA2804A34C0006080F /* ProofOfWork.swift */; };
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FF7D42823313F009601DB /* Mentions.swift */; };
4C8682872814DE470026224F /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8682862814DE470026224F /* ProfileView.swift */; };
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */; };
4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD152839DB54008EE7EF /* NostrMetadata.swift */; };
4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD17283A9EE5008EE7EF /* LoginView.swift */; };
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD19283AA67F008EE7EF /* Bech32.swift */; };
@@ -172,7 +163,6 @@
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */; };
4CB9D4A92992D2F400A9A7E4 /* FollowsYou.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */; };
4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */; };
4CC6193A29DC777C006A86D1 /* RelayBootstrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC6193929DC777C006A86D1 /* RelayBootstrap.swift */; };
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAE6297EFA7B00430951 /* Zap.swift */; };
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */; };
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEC297F0B9E00430951 /* Highlight.swift */; };
@@ -190,9 +180,6 @@
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */; };
4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */; };
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */; };
4CE4F0F229D4FCFA005914DB /* DebouncedOnChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F0F129D4FCFA005914DB /* DebouncedOnChange.swift */; };
4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F0F329D779B5005914DB /* PostBox.swift */; };
4CE4F0F829DB7399005914DB /* ThiccDivider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F0F729DB7399005914DB /* ThiccDivider.swift */; };
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F8CC281352B30009DFBB /* Notifications.swift */; };
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */; };
4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */; };
@@ -238,15 +225,12 @@
4CFF8F6329CC9AD7008DB934 /* ImageContextMenuModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */; };
4CFF8F6729CC9E3A008DB934 /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6629CC9E3A008DB934 /* ImageView.swift */; };
4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */; };
4CFF8F6B29CD0079008DB934 /* RepostedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */; };
4CFF8F6D29CD022E008DB934 /* WideEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6C29CD022E008DB934 /* WideEventView.swift */; };
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; };
5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; };
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
5CF72FC229B9142F00124A13 /* ShareAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF72FC129B9142F00124A13 /* ShareAction.swift */; };
6439E014296790CF0020672B /* ProfilePicImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfilePicImageView.swift */; };
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfileZoomView.swift */; };
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643EA5C7296B764E005081BB /* RelayFilterView.swift */; };
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; };
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FBD06E296255C400D9D3B2 /* Theme.swift */; };
@@ -264,12 +248,10 @@
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; };
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; };
E9E4ED0B295867B900DD7078 /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadView.swift */; };
F757933A29D7AECD007DEAC1 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F757933929D7AECD007DEAC1 /* ImagePicker.swift */; };
F75BA12D29A1855400E10810 /* BookmarksManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75BA12C29A1855400E10810 /* BookmarksManager.swift */; };
F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75BA12E29A18EF500E10810 /* BookmarksView.swift */; };
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E91298B0F0700AB113A /* RelayDetailView.swift */; };
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */; };
F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */; };
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */; };
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParicipantsView.swift */; };
/* End PBXBuildFile section */
@@ -315,16 +297,9 @@
3A3040FE29A91F31008A0F29 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-TW"; path = "zh-TW.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A3040FF29AB02D1008A0F29 /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-US"; path = "en-US.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGroupViewTests.swift; sourceTree = "<group>"; };
3A325AC429C9E0B8002BE7ED /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = "<group>"; };
3A325AC529C9E0B8002BE7ED /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A325AC629C9E0B8002BE7ED /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = vi; path = vi.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A325AC729C9E0CF002BE7ED /* es-ES */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-ES"; path = "es-ES.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A325AC829C9E0CF002BE7ED /* es-ES */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-ES"; path = "es-ES.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A325AC929C9E0CF002BE7ED /* es-ES */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-ES"; path = "es-ES.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A41E559299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A48E23A29D518F000BA313D /* Translations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Translations.swift; sourceTree = "<group>"; };
3A4F3320297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A4F3321297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A4F3322297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-FR"; path = "fr-FR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -369,9 +344,6 @@
3AC524EE298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3AC524EF298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
3AC524F0298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ar; path = ar.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3AC59CA729CDDB78007E04A6 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3AC59CA829CDDB78007E04A6 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AC59CA929CDDB78007E04A6 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-BR"; path = "pt-BR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3ACB685B297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3ACB685E297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = "<group>"; };
3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeAgoTests.swift; sourceTree = "<group>"; };
@@ -408,13 +380,6 @@
4C0A3F8E280F640A000448DE /* ThreadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadModel.swift; sourceTree = "<group>"; };
4C0A3F90280F6528000448DE /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; };
4C0A3F92280F66F5000448DE /* ReplyMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyMap.swift; sourceTree = "<group>"; };
4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyCounter.swift; sourceTree = "<group>"; };
4C1A9A1C29DDCF9B00516EAC /* NotificationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsView.swift; sourceTree = "<group>"; };
4C1A9A1E29DDD24B00516EAC /* AppearanceSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceSettingsView.swift; sourceTree = "<group>"; };
4C1A9A2029DDD3E100516EAC /* KeySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeySettingsView.swift; sourceTree = "<group>"; };
4C1A9A2229DDDB8100516EAC /* IconLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconLabel.swift; sourceTree = "<group>"; };
4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapSettingsView.swift; sourceTree = "<group>"; };
4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationSettingsView.swift; sourceTree = "<group>"; };
4C216F31286E388800040376 /* DMChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMChatView.swift; sourceTree = "<group>"; };
4C216F33286F5ACD00040376 /* DMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMView.swift; sourceTree = "<group>"; };
4C216F352870A9A700040376 /* InputDismissKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDismissKeyboard.swift; sourceTree = "<group>"; };
@@ -537,7 +502,6 @@
4C75EFBA2804A34C0006080F /* ProofOfWork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProofOfWork.swift; sourceTree = "<group>"; };
4C7FF7D42823313F009601DB /* Mentions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mentions.swift; sourceTree = "<group>"; };
4C8682862814DE470026224F /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusColors.swift; sourceTree = "<group>"; };
4C90BD152839DB54008EE7EF /* NostrMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrMetadata.swift; sourceTree = "<group>"; };
4C90BD17283A9EE5008EE7EF /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
4C90BD19283AA67F008EE7EF /* Bech32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32.swift; sourceTree = "<group>"; };
@@ -572,7 +536,6 @@
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNameView.swift; sourceTree = "<group>"; };
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowsYou.swift; sourceTree = "<group>"; };
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteLink.swift; sourceTree = "<group>"; };
4CC6193929DC777C006A86D1 /* RelayBootstrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayBootstrap.swift; sourceTree = "<group>"; };
4CC7AAE6297EFA7B00430951 /* Zap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Zap.swift; sourceTree = "<group>"; };
4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuilderEventView.swift; sourceTree = "<group>"; };
4CC7AAEC297F0B9E00430951 /* Highlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Highlight.swift; sourceTree = "<group>"; };
@@ -590,9 +553,6 @@
4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventHolder.swift; sourceTree = "<group>"; };
4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreButton.swift; sourceTree = "<group>"; };
4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InnerTimelineView.swift; sourceTree = "<group>"; };
4CE4F0F129D4FCFA005914DB /* DebouncedOnChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebouncedOnChange.swift; sourceTree = "<group>"; };
4CE4F0F329D779B5005914DB /* PostBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostBox.swift; sourceTree = "<group>"; };
4CE4F0F729DB7399005914DB /* ThiccDivider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThiccDivider.swift; sourceTree = "<group>"; };
4CE4F8CC281352B30009DFBB /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigView.swift; sourceTree = "<group>"; };
4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldAlert.swift; sourceTree = "<group>"; };
@@ -641,15 +601,12 @@
4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContextMenuModifier.swift; sourceTree = "<group>"; };
4CFF8F6629CC9E3A008DB934 /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = "<group>"; };
4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContainerView.swift; sourceTree = "<group>"; };
4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostedEvent.swift; sourceTree = "<group>"; };
4CFF8F6C29CD022E008DB934 /* WideEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WideEventView.swift; sourceTree = "<group>"; };
4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; };
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; };
5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.swift; sourceTree = "<group>"; };
5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; };
5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
5CF72FC129B9142F00124A13 /* ShareAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAction.swift; sourceTree = "<group>"; };
6439E013296790CF0020672B /* ProfilePicImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePicImageView.swift; sourceTree = "<group>"; };
6439E013296790CF0020672B /* ProfileZoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZoomView.swift; sourceTree = "<group>"; };
643EA5C7296B764E005081BB /* RelayFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterView.swift; sourceTree = "<group>"; };
647D9A8C2968520300A295DE /* SideMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = "<group>"; };
64FBD06E296255C400D9D3B2 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
@@ -666,12 +623,10 @@
DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownTests.swift; sourceTree = "<group>"; };
E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = "<group>"; };
E9E4ED0A295867B900DD7078 /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = "<group>"; };
F757933929D7AECD007DEAC1 /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; };
F75BA12C29A1855400E10810 /* BookmarksManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksManager.swift; sourceTree = "<group>"; };
F75BA12E29A18EF500E10810 /* BookmarksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksView.swift; sourceTree = "<group>"; };
F7908E91298B0F0700AB113A /* RelayDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayDetailView.swift; sourceTree = "<group>"; };
F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIPURLBuilder.swift; sourceTree = "<group>"; };
F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfilePictureControl.swift; sourceTree = "<group>"; };
F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToDismiss.swift; sourceTree = "<group>"; };
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParicipantsView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -709,7 +664,6 @@
isa = PBXGroup;
children = (
3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */,
5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */,
);
path = "Empty Views";
sourceTree = "<group>";
@@ -718,7 +672,6 @@
isa = PBXGroup;
children = (
3AA24801297E3DC20090C62D /* RepostView.swift */,
4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */,
);
path = Reposts;
sourceTree = "<group>";
@@ -823,23 +776,10 @@
3AA59D1C2999B0400061C48E /* DraftsModel.swift */,
4C54AA0629A540BA003E4487 /* NotificationsModel.swift */,
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */,
3A48E23A29D518F000BA313D /* Translations.swift */,
);
path = Models;
sourceTree = "<group>";
};
4C1A9A1B29DDCF8B00516EAC /* Settings */ = {
isa = PBXGroup;
children = (
4C1A9A1C29DDCF9B00516EAC /* NotificationSettingsView.swift */,
4C1A9A1E29DDD24B00516EAC /* AppearanceSettingsView.swift */,
4C1A9A2029DDD3E100516EAC /* KeySettingsView.swift */,
4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */,
4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */,
);
path = Settings;
sourceTree = "<group>";
};
4C30AC7029A5676F00E2BD5A /* Notifications */ = {
isa = PBXGroup;
children = (
@@ -863,7 +803,6 @@
4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup;
children = (
4C1A9A1B29DDCF8B00516EAC /* Settings */,
4CFF8F6129CC9A80008DB934 /* Images */,
4CCEB7AC29B53D180078AA28 /* Search */,
4C30AC7029A5676F00E2BD5A /* Notifications */,
@@ -900,7 +839,6 @@
4C75EFAC28049CFB0006080F /* PostButton.swift */,
4C75EFA327FA577B0006080F /* PostView.swift */,
9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */,
F757933929D7AECD007DEAC1 /* ImagePicker.swift */,
9C83F89229A937B900136C08 /* TextViewWrapper.swift */,
4CEE2AF8280B2EAC00AB5EEF /* PowView.swift */,
4C3AC7A42836987600E1F516 /* MainTabView.swift */,
@@ -919,6 +857,7 @@
647D9A8C2968520300A295DE /* SideMenuView.swift */,
9609F057296E220800069BF3 /* BannerImageView.swift */,
4CB8838E296F781C00DC99E7 /* ReactionsView.swift */,
6439E013296790CF0020672B /* ProfileZoomView.swift */,
4CF0ABD529817F5B00D66079 /* ReportView.swift */,
4CF0ABE42981EE0C00D66079 /* EULAView.swift */,
3AA247FE297E3D900090C62D /* RepostsView.swift */,
@@ -951,7 +890,6 @@
4C7FF7D628233637009601DB /* Util */ = {
isa = PBXGroup;
children = (
4CE4F0F329D779B5005914DB /* PostBox.swift */,
7C0F392D29B57C8F0039859C /* Extensions */,
4CE879492995B58700F758CC /* Relays */,
4CF0ABEA29844B2F00D66079 /* AnyCodable */,
@@ -984,8 +922,6 @@
3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */,
4C30AC7729A577AB00E2BD5A /* EventCache.swift */,
4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */,
4CE4F0F129D4FCFA005914DB /* DebouncedOnChange.swift */,
4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -1028,7 +964,6 @@
children = (
4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */,
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */,
F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */,
4CEE2AF2280B25C500AB5EEF /* ProfilePicView.swift */,
4C8682862814DE470026224F /* ProfileView.swift */,
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */,
@@ -1052,7 +987,6 @@
4CF0ABE6298444FC00D66079 /* MutedEventView.swift */,
4C3D52B5298DB4E6001C5831 /* ZapEvent.swift */,
4C3D52B7298DB5C6001C5831 /* TextEvent.swift */,
4CFF8F6C29CD022E008DB934 /* WideEventView.swift */,
);
path = Events;
sourceTree = "<group>";
@@ -1102,9 +1036,6 @@
4CB883AF297705DD00DC99E7 /* ZapButton.swift */,
4C42812B298C848200DBF26F /* TranslateView.swift */,
7CFF6316299FEFE5005D382A /* SelectableText.swift */,
4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */,
4CE4F0F729DB7399005914DB /* ThiccDivider.swift */,
4C1A9A2229DDDB8100516EAC /* IconLabel.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -1198,7 +1129,6 @@
children = (
4CE8794729941DA700F758CC /* RelayFilters.swift */,
4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */,
4CC6193929DC777C006A86D1 /* RelayBootstrap.swift */,
);
path = Relays;
sourceTree = "<group>";
@@ -1259,7 +1189,6 @@
children = (
4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */,
4CFF8F6629CC9E3A008DB934 /* ImageView.swift */,
6439E013296790CF0020672B /* ProfilePicImageView.swift */,
4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */,
);
path = Images;
@@ -1374,35 +1303,32 @@
hasScannedForEncodings = 0;
knownRegions = (
Base,
ar,
bg,
cs,
de,
"el-GR",
"en-US",
"es-419",
"es-ES",
fa,
"fr-CA",
"fr-FR",
"hu-HU",
id,
"it-IT",
ja,
ko,
"lv-LV",
nl,
"pl-PL",
"pt-BR",
"pt-PT",
ru,
"sv-SE",
"en-US",
"tr-TR",
uk,
vi,
"fr-FR",
"lv-LV",
"it-IT",
de,
"pt-PT",
"pl-PL",
ar,
nl,
"zh-CN",
"el-GR",
ja,
id,
cs,
ru,
"zh-HK",
"zh-TW",
uk,
bg,
fa,
ko,
"hu-HU",
"sv-SE",
"fr-CA",
);
mainGroup = 4CE6DEDA27F7A08100C66700;
packageReferences = (
@@ -1475,7 +1401,6 @@
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */,
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
F757933A29D7AECD007DEAC1 /* ImagePicker.swift in Sources */,
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */,
4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */,
4CCEB7A929B29DD50078AA28 /* SearchResultsModel.swift in Sources */,
@@ -1486,7 +1411,6 @@
4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */,
4C363AA228296A7E006E126D /* SearchView.swift in Sources */,
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */,
4CC6193A29DC777C006A86D1 /* RelayBootstrap.swift in Sources */,
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */,
4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */,
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
@@ -1565,7 +1489,6 @@
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */,
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */,
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */,
4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */,
4C3EA64928FF597700C48A62 /* bech32.c in Sources */,
4CE879522996B68900F758CC /* RelayType.swift in Sources */,
4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */,
@@ -1579,7 +1502,6 @@
4CE879582996C45300F758CC /* ZapsView.swift in Sources */,
4C30AC7429A5680900E2BD5A /* EventGroupView.swift in Sources */,
4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */,
4CFF8F6D29CD022E008DB934 /* WideEventView.swift in Sources */,
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */,
4C363A94282704FA006E126D /* Post.swift in Sources */,
4C216F32286E388800040376 /* DMChatView.swift in Sources */,
@@ -1589,11 +1511,9 @@
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */,
4CE4F0F829DB7399005914DB /* ThiccDivider.swift in Sources */,
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */,
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */,
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
@@ -1609,20 +1529,16 @@
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */,
4C3EA64F28FF59F200C48A62 /* tal.c in Sources */,
4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */,
5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */,
4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */,
4CFF8F6B29CD0079008DB934 /* RepostedEvent.swift in Sources */,
4C8682872814DE470026224F /* ProfileView.swift in Sources */,
4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */,
4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */,
4C1A9A2729DDE31900516EAC /* TranslationSettingsView.swift in Sources */,
4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */,
4CF0ABD82981980C00D66079 /* Lists.swift in Sources */,
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */,
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */,
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
6439E014296790CF0020672B /* ProfilePicImageView.swift in Sources */,
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */,
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */,
4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */,
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */,
@@ -1653,10 +1569,8 @@
4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */,
4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */,
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */,
4CE4F0F229D4FCFA005914DB /* DebouncedOnChange.swift in Sources */,
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
4C1A9A1A29DCA17E00516EAC /* ReplyCounter.swift in Sources */,
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */,
4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */,
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
@@ -1666,11 +1580,9 @@
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */,
3A48E23B29D518F000BA313D /* Translations.swift in Sources */,
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
4C06670B28FDE64700038D2A /* damus.c in Sources */,
4C1A9A2529DDDF2600516EAC /* ZapSettingsView.swift in Sources */,
4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */,
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */,
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
@@ -1681,19 +1593,15 @@
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */,
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */,
4C1A9A2129DDD3E100516EAC /* KeySettingsView.swift in Sources */,
4C1A9A1D29DDCF9B00516EAC /* NotificationSettingsView.swift in Sources */,
4C75EFB528049D790006080F /* Relay.swift in Sources */,
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */,
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
7C0F392F29B57CAF0039859C /* Binding+.swift in Sources */,
4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */,
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */,
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1774,9 +1682,6 @@
3AD14EB529C40F38009D2D9C /* hu-HU */,
3AD14EB829C40F3F009D2D9C /* sv-SE */,
3AD14EBC29C40F47009D2D9C /* fr-CA */,
3A325AC629C9E0B8002BE7ED /* vi */,
3A325AC929C9E0CF002BE7ED /* es-ES */,
3AC59CA929CDDB78007E04A6 /* pt-BR */,
);
name = Localizable.stringsdict;
sourceTree = "<group>";
@@ -1809,9 +1714,6 @@
3AD14EB629C40F38009D2D9C /* hu-HU */,
3AD14EB929C40F3F009D2D9C /* sv-SE */,
3AD14EBB29C40F47009D2D9C /* fr-CA */,
3A325AC529C9E0B8002BE7ED /* vi */,
3A325AC829C9E0CF002BE7ED /* es-ES */,
3AC59CA829CDDB78007E04A6 /* pt-BR */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
@@ -1845,9 +1747,6 @@
3AD14EB729C40F38009D2D9C /* hu-HU */,
3AD14EBA29C40F3F009D2D9C /* sv-SE */,
3AD14EBD29C40F47009D2D9C /* fr-CA */,
3A325AC429C9E0B8002BE7ED /* vi */,
3A325AC729C9E0CF002BE7ED /* es-ES */,
3AC59CA729CDDB78007E04A6 /* pt-BR */,
);
name = Localizable.strings;
sourceTree = "<group>";
@@ -1983,7 +1882,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -1991,9 +1890,7 @@
INFOPLIST_FILE = damus/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Damus;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
INFOPLIST_KEY_NSCameraUsageDescription = "Damus needs access to your camera if you want to upload photos from it";
INFOPLIST_KEY_NSFaceIDUsageDescription = "Local authentication to access private key";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Damus needs access to your microphone if you want to upload recorded videos from it";
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Granting Damus access to your photos allows you to save images.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
@@ -2008,7 +1905,7 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.4.1;
MARKETING_VERSION = 1.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -2027,7 +1924,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -2035,9 +1932,7 @@
INFOPLIST_FILE = damus/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Damus;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
INFOPLIST_KEY_NSCameraUsageDescription = "Damus needs access to your camera if you want to upload photos from it";
INFOPLIST_KEY_NSFaceIDUsageDescription = "Local authentication to access private key";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Damus needs access to your microphone if you want to upload recorded videos from it";
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Granting Damus access to your photos allows you to save images.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
@@ -2052,7 +1947,7 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.4.1;
MARKETING_VERSION = 1.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -1,38 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xF4",
"green" : "0xEE",
"red" : "0xEE"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x1E",
"green" : "0x1C",
"red" : "0x1C"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "bitcoin-logo.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20px" height="20px" viewBox="0 0 20 20" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(96.862745%,57.647059%,10.196078%);fill-opacity:1;" d="M 19.699219 12.417969 C 18.363281 17.777344 12.9375 21.035156 7.582031 19.699219 C 2.226562 18.363281 -1.035156 12.9375 0.300781 7.582031 C 1.636719 2.222656 7.0625 -1.035156 12.417969 0.300781 C 17.773438 1.632812 21.035156 7.0625 19.699219 12.417969 Z M 19.699219 12.417969 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 14.410156 8.574219 C 14.609375 7.246094 13.59375 6.53125 12.210938 6.050781 L 12.660156 4.25 L 11.5625 3.976562 L 11.125 5.730469 C 10.835938 5.660156 10.539062 5.589844 10.246094 5.523438 L 10.6875 3.757812 L 9.589844 3.484375 L 9.140625 5.285156 C 8.902344 5.230469 8.667969 5.179688 8.4375 5.121094 L 8.441406 5.117188 L 6.925781 4.738281 L 6.636719 5.910156 C 6.636719 5.910156 7.449219 6.097656 7.433594 6.109375 C 7.875 6.21875 7.957031 6.511719 7.941406 6.746094 L 7.429688 8.800781 C 7.460938 8.808594 7.5 8.820312 7.546875 8.835938 L 7.429688 8.808594 L 6.710938 11.683594 C 6.65625 11.820312 6.519531 12.023438 6.210938 11.945312 C 6.21875 11.960938 5.410156 11.746094 5.410156 11.746094 L 4.867188 13 L 6.296875 13.359375 C 6.5625 13.425781 6.820312 13.492188 7.078125 13.558594 L 6.621094 15.382812 L 7.71875 15.65625 L 8.167969 13.851562 C 8.46875 13.933594 8.757812 14.007812 9.042969 14.078125 L 8.59375 15.875 L 9.691406 16.148438 L 10.144531 14.328125 C 12.015625 14.683594 13.425781 14.539062 14.015625 12.847656 C 14.492188 11.484375 13.992188 10.699219 13.007812 10.1875 C 13.726562 10.019531 14.265625 9.550781 14.410156 8.574219 Z M 11.902344 12.089844 C 11.5625 13.453125 9.269531 12.71875 8.523438 12.53125 L 9.128906 10.117188 C 9.871094 10.300781 12.253906 10.667969 11.902344 12.089844 Z M 12.242188 8.554688 C 11.933594 9.796875 10.023438 9.164062 9.402344 9.011719 L 9.949219 6.820312 C 10.570312 6.976562 12.5625 7.261719 12.242188 8.554688 Z M 12.242188 8.554688 "/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

+3 -3
View File
@@ -8,8 +8,8 @@
import SwiftUI
let RECTANGLE_GRADIENT = LinearGradient(gradient: Gradient(colors: [
DamusColors.purple,
DamusColors.blue
Color("DamusPurple"),
Color("DamusBlue")
]), startPoint: .leading, endPoint: .trailing)
struct CustomPicker<SelectionValue: Hashable, Content: View>: View {
@@ -56,6 +56,6 @@ struct CustomPicker<SelectionValue: Hashable, Content: View>: View {
}
func textColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white
colorScheme == .light ? Color("DamusBlack") : Color("DamusWhite")
}
}
-22
View File
@@ -1,22 +0,0 @@
//
// DamusColors.swift
// damus
//
// Created by William Casarin on 2023-03-27.
//
import Foundation
import SwiftUI
class DamusColors {
static let adaptableGrey = Color("DamusAdaptableGrey")
static let white = Color("DamusWhite")
static let black = Color("DamusBlack")
static let lightGrey = Color("DamusLightGrey")
static let mediumGrey = Color("DamusMediumGrey")
static let darkGrey = Color("DamusDarkGrey")
static let green = Color("DamusGreen")
static let purple = Color("DamusPurple")
static let blue = Color("DamusBlue")
}
-44
View File
@@ -1,44 +0,0 @@
//
// IconLabel.swift
// damus
//
// Created by William Casarin on 2023-04-05.
//
import SwiftUI
import UIKit
struct IconLabel: View {
let text: String
let img_name: String
let img_color: Color
init(_ text: String, img_name: String, color: Color) {
self.text = text
self.img_name = img_name
self.img_color = color
}
var body: some View {
HStack(spacing: 0) {
Image(systemName: img_name)
.foregroundColor(img_color)
.frame(width: 20)
.padding([.trailing], 20)
Text(text)
}
}}
struct IconLabel_Previews: PreviewProvider {
static var previews: some View {
Form {
Section {
IconLabel(NSLocalizedString("Keys", comment: "Settings section for managing keys"), img_name: "key.fill", color: .orange)
IconLabel(NSLocalizedString("Local Notifications", comment: "Section header for damus local notifications user configuration"), img_name: "bell.fill", color: .blue)
IconLabel(NSLocalizedString("Appearance", comment: "Section header for text and appearance settings"), img_name: "textformat", color: .red)
}
}
}
}
+23 -115
View File
@@ -32,42 +32,14 @@ struct ShareSheet: UIViewControllerRepresentable {
}
enum ImageShape {
case square
case landscape
case portrait
case unknown
}
struct ImageCarousel: View {
var urls: [URL]
let evid: String
let previews: PreviewCache
@State private var open_sheet: Bool = false
@State private var current_url: URL? = nil
@State private var image_fill: ImageFill? = nil
@State private var fillHeight: CGFloat = 350
@State private var maxHeight: CGFloat = UIScreen.main.bounds.height * 0.85
init(previews: PreviewCache, evid: String, urls: [URL]) {
_open_sheet = State(initialValue: false)
_current_url = State(initialValue: nil)
_image_fill = State(initialValue: previews.lookup_image_meta(evid))
self.urls = urls
self.evid = evid
self.previews = previews
}
var filling: Bool {
image_fill?.filling == true
}
var height: CGFloat {
image_fill?.height ?? 0
}
@State var open_sheet: Bool = false
@State var current_url: URL? = nil
var body: some View {
TabView {
@@ -75,32 +47,31 @@ struct ImageCarousel: View {
Rectangle()
.foregroundColor(Color.clear)
.overlay {
GeometryReader { geo in
KFAnimatedImage(url)
.callbackQueue(.dispatch(.global(qos:.background)))
.backgroundDecode(true)
.imageContext(.note)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
}
.imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in
previews.cache_image_meta(evid: evid, image_fill: fill)
image_fill = fill
}
.aspectRatio(contentMode: filling ? .fill : .fit)
.tabItem {
Text(url.absoluteString)
}
.id(url.absoluteString)
}
KFAnimatedImage(url)
.imageContext(.note)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
}
.aspectRatio(contentMode: .fit)
.cornerRadius(10)
.tabItem {
Text(url.absoluteString)
}
.id(url.absoluteString)
// .contextMenu {
// Button(NSLocalizedString("Copy Image", comment: "Context menu option to copy an image to clipboard.")) {
// UIPasteboard.general.string = url.absoluteString
// }
// }
}
}
}
.fullScreenCover(isPresented: $open_sheet) {
ImageView(urls: urls)
}
.frame(height: height)
.frame(height: 350)
.clipped()
.onTapGesture {
open_sheet = true
}
@@ -108,71 +79,8 @@ struct ImageCarousel: View {
}
}
// MARK: - Image Modifier
extension KFOptionSetter {
/// Sets a block to get image size
///
/// - Parameter block: The block which is used to read the image object.
/// - Returns: `Self` value after read size
public func imageFill(for size: CGSize, max: CGFloat, fill: CGFloat, block: @escaping (ImageFill) throws -> Void) -> Self {
let modifier = AnyImageModifier { image -> KFCrossPlatformImage in
let img_size = image.size
let geo_size = size
let fill = ImageFill.calculate_image_fill(geo_size: geo_size,
img_size: img_size,
maxHeight: max,
fillHeight: fill)
DispatchQueue.main.async { [block, fill] in
try? block(fill)
}
return image
}
options.imageModifier = modifier
return self
}
}
public struct ImageFill {
let filling: Bool?
let height: CGFloat
static func determine_image_shape(_ size: CGSize) -> ImageShape {
guard size.height > 0 else {
return .unknown
}
let imageRatio = size.width / size.height
switch imageRatio {
case 1.0: return .square
case ..<1.0: return .portrait
case 1.0...: return .landscape
default: return .unknown
}
}
static func calculate_image_fill(geo_size: CGSize, img_size: CGSize, maxHeight: CGFloat, fillHeight: CGFloat) -> ImageFill {
let shape = determine_image_shape(img_size)
let xfactor = geo_size.width / img_size.width
let scaled = img_size.height * xfactor
// calculate scaled image height
// set scale factor and constrain images to minimum 150
// and animations to scaled factor for dynamic size adjustment
switch shape {
case .portrait, .landscape:
let filling = scaled > maxHeight
let height = filling ? fillHeight : scaled
return ImageFill(filling: filling, height: height)
case .square, .unknown:
return ImageFill(filling: nil, height: scaled)
}
}
}
struct ImageCarousel_Previews: PreviewProvider {
static var previews: some View {
ImageCarousel(previews: test_damus_state().previews, evid: "evid", urls: [URL(string: "https://jb55.com/red-me.jpg")!,URL(string: "https://jb55.com/red-me.jpg")!])
ImageCarousel(urls: [URL(string: "https://jb55.com/red-me.jpg")!,URL(string: "https://jb55.com/red-me.jpg")!])
}
}
+1 -1
View File
@@ -29,7 +29,7 @@ struct InvoiceView: View {
.foregroundColor(.gray)
} else {
Image(systemName: "checkmark.circle")
.foregroundColor(DamusColors.green)
.foregroundColor(Color("DamusGreen"))
}
}
}
+9 -34
View File
@@ -24,33 +24,19 @@ struct NIP05Badge: View {
self.clickable = clickable
}
var nip05_color: Bool {
return use_nip05_color(pubkey: pubkey, contacts: contacts)
}
var Seal: some View {
Group {
if nip05_color {
LINEAR_GRADIENT
.mask(Image(systemName: "checkmark.seal.fill")
.resizable()
).frame(width: 14, height: 14)
} else {
Image(systemName: "checkmark.seal.fill")
.font(.footnote)
.foregroundColor(.gray)
}
}
var nip05_color: Color {
return get_nip05_color(pubkey: pubkey, contacts: contacts)
}
var body: some View {
HStack(spacing: 2) {
Seal
Image(systemName: "checkmark.seal.fill")
.font(.footnote)
.foregroundColor(nip05_color)
if show_domain {
if clickable {
Text(nip05.host)
.nip05_colorized(gradient: nip05_color)
.foregroundColor(nip05_color)
.onTapGesture {
if let nip5url = nip05.siteUrl {
openURL(nip5url)
@@ -58,7 +44,7 @@ struct NIP05Badge: View {
}
} else {
Text(nip05.host)
.foregroundColor(.gray)
.foregroundColor(nip05_color)
}
}
}
@@ -66,19 +52,8 @@ struct NIP05Badge: View {
}
}
extension View {
func nip05_colorized(gradient: Bool) -> some View {
if gradient {
return AnyView(self.foregroundStyle(LINEAR_GRADIENT))
} else {
return AnyView(self.foregroundColor(.gray))
}
}
}
func use_nip05_color(pubkey: String, contacts: Contacts) -> Bool {
return contacts.is_friend_or_self(pubkey) ? true : false
func get_nip05_color(pubkey: String, contacts: Contacts) -> Color {
return contacts.is_friend_or_self(pubkey) ? .accentColor : .gray
}
struct NIP05Badge_Previews: PreviewProvider {
+2
View File
@@ -15,10 +15,12 @@ struct Reposted: View {
var body: some View {
HStack(alignment: .center) {
Image(systemName: "arrow.2.squarepath")
.font(.system(size: 13, weight: .heavy))
.foregroundColor(Color.gray)
ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true, show_nip5_domain: false)
.foregroundColor(Color.gray)
Text("Reposted", comment: "Text indicating that the post was reposted (i.e. re-shared).")
.font(.system(size: 14, weight: .heavy))
.foregroundColor(Color.gray)
}
}
+1 -3
View File
@@ -15,14 +15,12 @@ struct SelectableText: View {
@State private var selectedTextHeight: CGFloat = .zero
@State private var selectedTextWidth: CGFloat = .zero
let size: EventViewKind
var body: some View {
GeometryReader { geo in
TextViewRepresentable(
attributedString: attributedString,
textColor: UIColor.label,
font: eventviewsize_to_uifont(size),
font: UIFont.preferredFont(forTextStyle: .title2),
fixedWidth: selectedTextWidth,
height: $selectedTextHeight
)
-22
View File
@@ -1,22 +0,0 @@
//
// ThiccDivider.swift
// damus
//
// Created by William Casarin on 2023-04-03.
//
import SwiftUI
struct ThiccDivider: View {
var body: some View {
Rectangle()
.frame(height: 4)
.foregroundColor(DamusColors.adaptableGrey)
}
}
struct ThiccDivider_Previews: PreviewProvider {
static var previews: some View {
ThiccDivider()
}
}
+90 -86
View File
@@ -11,131 +11,135 @@ import NaturalLanguage
struct TranslateView: View {
let damus_state: DamusState
let event: NostrEvent
let size: EventViewKind
@State var checkingTranslationStatus: Bool = false
@State var translatable: Bool = true
@State var noteLanguage: String?
@State var show_translated_note: Bool
@State var translated_artifacts: NoteArtifacts?
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind) {
self.damus_state = damus_state
self.event = event
self.size = size
self._noteLanguage = State(initialValue: damus_state.translations.detectLanguage(event, state: damus_state))
if let translationWithLanguage = damus_state.translations.cachedTranslation(event) {
self._noteLanguage = State(initialValue: translationWithLanguage.language)
let translatedBlocks = event.get_blocks(content: translationWithLanguage.translation)
self._translated_artifacts = State.init(initialValue: render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey))
} else {
self._translated_artifacts = State(initialValue: nil)
}
self._show_translated_note = State(initialValue: damus_state.settings.auto_translate)
}
@State var currentLanguage: String = "en"
@State var noteLanguage: String? = nil
@State var translated_note: String? = nil
@State var show_translated_note: Bool = false
@State var translated_artifacts: NoteArtifacts? = nil
var TranslateButton: some View {
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
Button(NSLocalizedString("Translate Note into your language", comment: "Button to translate note from different language.")) {
show_translated_note = true
processTranslation()
}
.translate_button_style()
}
func processTranslation() {
guard noteLanguage != nil && !checkingTranslationStatus && translatable else {
return
}
checkingTranslationStatus = true
show_translated_note = true
Task {
let translationWithLanguage = await damus_state.translations.translate(event, state: damus_state)
DispatchQueue.main.async {
guard translationWithLanguage != nil else {
noteLanguage = damus_state.translations.targetLanguage
checkingTranslationStatus = false
show_translated_note = false
translatable = false
return
}
noteLanguage = translationWithLanguage!.language
// Render translated note.
let translatedBlocks = event.get_blocks(content: translationWithLanguage!.translation)
translated_artifacts = render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
translatable = true
checkingTranslationStatus = false
}
}
}
func Translated(lang: String, artifacts: NoteArtifacts) -> some View {
return Group {
Button(String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang)) {
Button(NSLocalizedString("Translated from \(lang)", comment: "Button to indicate that the note has been translated from a different language.")) {
show_translated_note = false
}
.translate_button_style()
SelectableText(attributedString: artifacts.content, size: self.size)
SelectableText(attributedString: artifacts.content)
}
}
func CheckingStatus(lang: String) -> some View {
return Button(NSLocalizedString("Translating from \(lang)...", comment: "Button to indicate that the note is in the process of being translated from a different language.")) {
show_translated_note = false
}
.translate_button_style()
}
func MainContent(note_lang: String) -> some View {
return Group {
if translatable {
let languageName = Locale.current.localizedString(forLanguageCode: note_lang)
if let languageName, let translated_artifacts, show_translated_note {
Translated(lang: languageName, artifacts: translated_artifacts)
} else if !damus_state.settings.auto_translate {
TranslateButton
} else {
EmptyView()
let languageName = Locale.current.localizedString(forLanguageCode: note_lang)
if let lang = languageName, show_translated_note {
if checkingTranslationStatus {
CheckingStatus(lang: lang)
} else if let artifacts = translated_artifacts {
Translated(lang: lang, artifacts: artifacts)
}
} else {
EmptyView()
TranslateButton
}
}
}
var body: some View {
Group {
if let note_lang = noteLanguage, note_lang != damus_state.translations.targetLanguage {
if let note_lang = noteLanguage, noteLanguage != currentLanguage {
MainContent(note_lang: note_lang)
.task {
if show_translated_note {
processTranslation()
}
}
} else {
Text("")
}
}
}
}
.task {
guard noteLanguage == nil && !checkingTranslationStatus && damus_state.settings.can_translate(damus_state.pubkey) else {
return
}
checkingTranslationStatus = true
extension View {
func translate_button_style() -> some View {
return self
.font(.footnote)
.contentShape(Rectangle())
.padding([.top, .bottom], 10)
if #available(iOS 16, *) {
currentLanguage = Locale.current.language.languageCode?.identifier ?? "en"
} else {
currentLanguage = Locale.current.languageCode ?? "en"
}
// Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in
// and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer.
let originalBlocks = event.blocks(damus_state.keypair.privkey)
let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ")
// Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate.
let languageRecognizer = NLLanguageRecognizer()
languageRecognizer.processString(originalOnlyText)
noteLanguage = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= 0.5 })?.key.rawValue ?? currentLanguage
if let lang = noteLanguage, noteLanguage != currentLanguage {
// If the detected dominant language is a variant, remove the variant component and just take the language part as translation services typically only supports the variant-less language.
if #available(iOS 16, *) {
noteLanguage = Locale.LanguageCode(stringLiteral: lang).identifier(.alpha2)
} else {
noteLanguage = NSLocale(localeIdentifier: lang).languageCode
}
}
guard let note_lang = noteLanguage else {
noteLanguage = currentLanguage
translated_note = nil
checkingTranslationStatus = false
return
}
if note_lang != currentLanguage {
do {
// If the note language is different from our language, send a translation request.
let translator = Translator(damus_state.settings)
let originalContent = event.get_content(damus_state.keypair.privkey)
translated_note = try await translator.translate(originalContent, from: note_lang, to: currentLanguage)
if originalContent == translated_note {
// If the translation is the same as the original, don't bother showing it.
noteLanguage = currentLanguage
translated_note = nil
}
} catch {
// If for whatever reason we're not able to figure out the language of the note, or translate the note, fail gracefully and do not retry. It's not the end of the world. Don't want to take down someone's translation server with an accidental denial of service attack.
noteLanguage = currentLanguage
translated_note = nil
}
}
if let translated = translated_note {
// Render translated note.
let translatedBlocks = event.get_blocks(content: translated)
translated_artifacts = render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
}
checkingTranslationStatus = false
}
}
}
struct TranslateView_Previews: PreviewProvider {
static var previews: some View {
let ds = test_damus_state()
TranslateView(damus_state: ds, event: test_event, size: .normal)
TranslateView(damus_state: ds, event: test_event)
}
}
-1
View File
@@ -22,7 +22,6 @@ struct WebsiteLink: View {
}, label: {
Text(link_text)
.font(.footnote)
.foregroundColor(.accentColor)
})
}
}
+1 -1
View File
@@ -134,7 +134,7 @@ struct ZapButton: View {
struct ZapButton_Previews: PreviewProvider {
static var previews: some View {
let bar = ActionBarModel(likes: 0, boosts: 0, zaps: 10, zap_total: 15623414, replies: 2, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
let bar = ActionBarModel(likes: 0, boosts: 0, zaps: 10, zap_total: 15623414, our_like: nil, our_boost: nil, our_zap: nil)
ZapButton(damus_state: test_damus_state(), event: test_event, lnurl: "lnurl", bar: bar)
}
}
+68 -73
View File
@@ -8,10 +8,16 @@
import SwiftUI
import Starscream
var BOOTSTRAP_RELAYS = [
"wss://relay.damus.io",
"wss://eden.nostr.land",
"wss://nostr.wine",
"wss://nos.lol",
]
struct TimestampedProfile {
let profile: Profile
let timestamp: Int64
let event: NostrEvent
}
enum Sheets: Identifiable {
@@ -76,9 +82,9 @@ struct ContentView: View {
@State var profile_open: Bool = false
@State var thread_open: Bool = false
@State var search_open: Bool = false
@State var muting: String? = nil
@State var confirm_mute: Bool = false
@State var user_muted_confirm: Bool = false
@State var blocking: String? = nil
@State var confirm_block: Bool = false
@State var user_blocked_confirm: Bool = false
@State var confirm_overwrite_mutelist: Bool = false
@State var current_boost: NostrEvent? = nil
@State var filter_state : FilterState = .posts_and_replies
@@ -141,8 +147,22 @@ struct ContentView: View {
}
var timelineNavItem: Text {
return Text(timeline_name(selected_timeline))
.bold()
switch selected_timeline {
case .home:
return Text("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.")
.bold()
case .dms:
return Text("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
.bold()
case .notifications:
return Text("Notifications", comment: "Toolbar label for Notifications view.")
.bold()
case .search:
return Text("Universe 🛸", comment: "Toolbar label for the universal view where posts from all connected relay servers appear.")
.bold()
case .none:
return Text(verbatim: "")
}
}
func MainContent(damus: DamusState) -> some View {
@@ -191,7 +211,7 @@ struct ContentView: View {
Image("damus-home")
.resizable()
.frame(width:30,height:30)
.shadow(color: DamusColors.purple, radius: 2)
.shadow(color: Color("DamusPurple"), radius: 2)
.opacity(isSideBarOpened ? 0 : 1)
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
} else {
@@ -228,9 +248,9 @@ struct ContentView: View {
func MaybeReportView(target: ReportTarget) -> some View {
Group {
if let damus_state {
if let sec = damus_state.keypair.privkey {
ReportView(postbox: damus_state.postbox, target: target, privkey: sec)
if let ds = damus_state {
if let sec = ds.keypair.privkey {
ReportView(pool: ds.pool, target: target, privkey: sec)
} else {
EmptyView()
}
@@ -306,9 +326,9 @@ struct ContentView: View {
case .report(let target):
MaybeReportView(target: target)
case .post:
PostView(replying_to: nil, damus_state: damus_state!)
PostView(replying_to: nil, references: [], damus_state: damus_state!)
case .reply(let event):
PostView(replying_to: event, damus_state: damus_state!)
ReplyView(replying_to: event, damus: damus_state!)
case .event:
EventDetailView()
case .filter:
@@ -369,20 +389,14 @@ struct ContentView: View {
let target = notif.object as! ReportTarget
self.active_sheet = .report(target)
}
.onReceive(handle_notify(.mute)) { notif in
.onReceive(handle_notify(.block)) { notif in
let pubkey = notif.object as! String
self.muting = pubkey
self.confirm_mute = true
self.blocking = pubkey
self.confirm_block = true
}
.onReceive(handle_notify(.broadcast_event)) { obj in
let ev = obj.object as! NostrEvent
guard let ds = self.damus_state else {
return
}
ds.postbox.send(ev)
if let profile = ds.profiles.profiles[ev.pubkey] {
ds.postbox.send(profile.event)
}
self.damus_state?.pool.send(.event(ev))
}
.onReceive(handle_notify(.unfollow)) { notif in
guard let privkey = self.privkey else {
@@ -396,7 +410,7 @@ struct ContentView: View {
let target = notif.object as! FollowTarget
let pk = target.pubkey
if let ev = unfollow_user(postbox: damus.postbox,
if let ev = unfollow_user(pool: damus.pool,
our_contacts: damus.contacts.event,
pubkey: damus.pubkey,
privkey: privkey,
@@ -447,16 +461,7 @@ struct ContentView: View {
//let to_relays = tup.1
print("post \(post.content)")
let new_ev = post_to_event(post: post, privkey: privkey, pubkey: pubkey)
guard let ds = self.damus_state else {
return
}
ds.postbox.send(new_ev)
for eref in new_ev.referenced_ids.prefix(3) {
// also broadcast at most 3 referenced events
if let ev = ds.events.lookup(eref.ref_id) {
ds.postbox.send(ev)
}
}
self.damus_state?.pool.send(.event(new_ev))
case .cancel:
active_sheet = nil
print("post cancelled")
@@ -474,23 +479,23 @@ struct ContentView: View {
notify(.logout, ())
}
}
.alert(NSLocalizedString("User muted", comment: "Alert message to indicate the user has been muted"), isPresented: $user_muted_confirm, actions: {
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to muted a user was successful.")) {
user_muted_confirm = false
.alert(NSLocalizedString("User blocked", comment: "Alert message to indicate the user has been blocked"), isPresented: $user_blocked_confirm, actions: {
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to block a user was successful.")) {
user_blocked_confirm = false
}
}, message: {
if let pubkey = self.muting {
if let pubkey = self.blocking {
let profile = damus_state!.profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: profile, pubkey: pubkey).username
Text("\(name) has been muted", comment: "Alert message that informs a user was muted.")
Text("\(name) has been blocked", comment: "Alert message that informs a user was blocked.")
} else {
Text("User has been muted", comment: "Alert message that informs a user was d.")
Text("User has been blocked", comment: "Alert message that informs a user was blocked.")
}
})
.alert(NSLocalizedString("Create new mutelist", comment: "Title of alert prompting the user to create a new mutelist."), isPresented: $confirm_overwrite_mutelist, actions: {
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of alert that creates a new mutelist.")) {
confirm_overwrite_mutelist = false
confirm_mute = false
confirm_block = false
}
Button(NSLocalizedString("Yes, Overwrite", comment: "Text of button that confirms to overwrite the existing mutelist.")) {
@@ -502,7 +507,7 @@ struct ContentView: View {
return
}
guard let pubkey = muting else {
guard let pubkey = blocking else {
return
}
@@ -511,20 +516,20 @@ struct ContentView: View {
}
damus_state?.contacts.set_mutelist(mutelist)
ds.postbox.send(mutelist)
ds.pool.send(.event(mutelist))
confirm_overwrite_mutelist = false
confirm_mute = false
user_muted_confirm = true
confirm_block = false
user_blocked_confirm = true
}
}, message: {
Text("No mute list found, create a new one? This will overwrite any previous mute lists.", comment: "Alert message prompt that asks if the user wants to create a new mute list, overwriting previous mute lists.")
Text("No block list found, create a new one? This will overwrite any previous block lists.", comment: "Alert message prompt that asks if the user wants to create a new block list, overwriting previous block lists.")
})
.alert(NSLocalizedString("Mute User", comment: "Title of alert for muting a user."), isPresented: $confirm_mute, actions: {
Button(NSLocalizedString("Cancel", comment: "Alert button to cancel out of alert for muting a user."), role: .cancel) {
confirm_mute = false
.alert(NSLocalizedString("Block User", comment: "Title of alert for blocking a user."), isPresented: $confirm_block, actions: {
Button(NSLocalizedString("Cancel", comment: "Alert button to cancel out of alert for blocking a user."), role: .cancel) {
confirm_block = false
}
Button(NSLocalizedString("Mute", comment: "Alert button to mute a user."), role: .destructive) {
Button(NSLocalizedString("Block", comment: "Alert button to block a user."), role: .destructive) {
guard let ds = damus_state else {
return
}
@@ -535,7 +540,7 @@ struct ContentView: View {
guard let keypair = ds.keypair.to_full() else {
return
}
guard let pubkey = muting else {
guard let pubkey = blocking else {
return
}
@@ -543,16 +548,16 @@ struct ContentView: View {
return
}
damus_state?.contacts.set_mutelist(ev)
ds.postbox.send(ev)
ds.pool.send(.event(ev))
}
}
}, message: {
if let pubkey = muting {
if let pubkey = blocking {
let profile = damus_state?.profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: profile, pubkey: pubkey).username
Text("Mute \(name)?", comment: "Alert message prompt to ask if a user should be muted.")
Text("Block \(name)?", comment: "Alert message prompt to ask if a user should be blocked.")
} 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 block...", comment: "Alert message to indicate that the blocked user could not be found.")
}
})
.alert(NSLocalizedString("Repost", comment: "Title of alert for confirming to repost a post."), isPresented: $current_boost.mappedToBool()) {
@@ -560,9 +565,7 @@ struct ContentView: View {
current_boost = nil
}
Button(NSLocalizedString("Repost", comment: "Button to confirm reposting a post.")) {
if let current_boost {
self.damus_state?.pool.send(.event(current_boost))
}
self.damus_state?.pool.send(.event(current_boost!))
}
} message: {
Text("Are you sure you want to repost this?", comment: "Alert message to ask if user wants to repost a post.")
@@ -598,10 +601,9 @@ struct ContentView: View {
let pool = RelayPool()
let metadatas = RelayMetadatas()
let relay_filters = RelayFilters(our_pubkey: pubkey)
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
let new_relay_filters = load_relay_filters(pubkey) == nil
for relay in bootstrap_relays {
for relay in BOOTSTRAP_RELAYS {
if let url = URL(string: relay) {
add_new_relay(relay_filters: relay_filters, metadatas: metadatas, pool: pool, url: url, info: .rw, new_relay_filters: new_relay_filters)
}
@@ -609,8 +611,6 @@ struct ContentView: View {
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
let settings = UserSettingsStore()
self.damus_state = DamusState(pool: pool,
keypair: keypair,
likes: EventCounter(our_pubkey: pubkey),
@@ -622,16 +622,12 @@ struct ContentView: View {
previews: PreviewCache(),
zaps: Zaps(our_pubkey: pubkey),
lnurls: LNUrls(),
settings: settings,
settings: UserSettingsStore(),
relay_filters: relay_filters,
relay_metadata: metadatas,
drafts: Drafts(),
events: EventCache(),
bookmarks: BookmarksManager(pubkey: pubkey),
postbox: PostBox(pool: pool),
bootstrap_relays: bootstrap_relays,
replies: ReplyCounter(our_pubkey: pubkey),
translations: Translations(settings)
bookmarks: BookmarksManager(pubkey: pubkey)
)
home.damus_state = self.damus_state!
@@ -780,6 +776,7 @@ func setup_notifications() {
}
}
func find_event(state: DamusState, evid: String, search_type: SearchType, find_from: [String]?, callback: @escaping (NostrEvent?) -> ()) {
if let ev = state.events.lookup(evid) {
callback(ev)
@@ -809,8 +806,6 @@ func find_event(state: DamusState, evid: String, search_type: SearchType, find_f
}
switch ev {
case .ok:
break
case .event(_, let ev):
has_event = true
callback(ev)
@@ -837,12 +832,12 @@ func timeline_name(_ timeline: Timeline?) -> String {
}
switch timeline {
case .home:
return NSLocalizedString("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.")
return "Home"
case .notifications:
return NSLocalizedString("Notifications", comment: "Toolbar label for Notifications view.")
return "Notifications"
case .search:
return NSLocalizedString("Universe 🛸", comment: "Toolbar label for the universal view where posts from all connected relay servers appear.")
return "Universe 🛸"
case .dms:
return NSLocalizedString("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
return "DMs"
}
}
-4
View File
@@ -46,9 +46,5 @@
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSCameraUsageDescription</key>
<string>Damus needs access to your camera if you want to upload photos from it</string>
<key>NSMicrophoneUsageDescription</key>
<string>Damus needs access to your microphone if you want to upload recorded videos from it</string>
</dict>
</plist>
+2 -12
View File
@@ -11,40 +11,34 @@ import Foundation
class ActionBarModel: ObservableObject {
@Published var our_like: NostrEvent?
@Published var our_boost: NostrEvent?
@Published var our_reply: NostrEvent?
@Published var our_zap: Zap?
@Published var likes: Int
@Published var boosts: Int
@Published var zaps: Int
@Published var zap_total: Int64
@Published var replies: Int
static func empty() -> ActionBarModel {
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, our_like: nil, our_boost: nil, our_zap: nil)
}
init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, replies: Int, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zap?, our_reply: NostrEvent?) {
init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zap?) {
self.likes = likes
self.boosts = boosts
self.zaps = zaps
self.replies = replies
self.zap_total = zap_total
self.our_like = our_like
self.our_boost = our_boost
self.our_zap = our_zap
self.our_reply = our_reply
}
func update(damus: DamusState, evid: String) {
self.likes = damus.likes.counts[evid] ?? 0
self.boosts = damus.boosts.counts[evid] ?? 0
self.zaps = damus.zaps.event_counts[evid] ?? 0
self.replies = damus.replies.get_replies(evid)
self.zap_total = damus.zaps.event_totals[evid] ?? 0
self.our_like = damus.likes.our_events[evid]
self.our_boost = damus.boosts.our_events[evid]
self.our_zap = damus.zaps.our_zaps[evid]?.first
self.our_reply = damus.replies.our_reply(evid)
self.objectWillChange.send()
}
@@ -60,10 +54,6 @@ class ActionBarModel: ObservableObject {
return our_like != nil
}
var replied: Bool {
return our_reply != nil
}
var boosted: Bool {
return our_boost != nil
}
+2 -2
View File
@@ -140,7 +140,7 @@ func follow_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, pri
return ev
}
func unfollow_user(postbox: PostBox, our_contacts: NostrEvent?, pubkey: String, privkey: String, unfollow: String) -> NostrEvent? {
func unfollow_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, privkey: String, unfollow: String) -> NostrEvent? {
guard let cs = our_contacts else {
return nil
}
@@ -149,7 +149,7 @@ func unfollow_user(postbox: PostBox, our_contacts: NostrEvent?, pubkey: String,
ev.calculate_id()
ev.sign(privkey: privkey)
postbox.send(ev)
pool.send(.event(ev))
return ev
}
-1
View File
@@ -14,7 +14,6 @@ class CreateAccountModel: ObservableObject {
@Published var about: String = ""
@Published var pubkey: String = ""
@Published var privkey: String = ""
@Published var profile_image: String? = nil
var pubkey_bech32: String {
return bech32_pubkey(self.pubkey) ?? ""
+2 -6
View File
@@ -26,10 +26,7 @@ struct DamusState {
let drafts: Drafts
let events: EventCache
let bookmarks: BookmarksManager
let postbox: PostBox
let bootstrap_relays: [String]
let replies: ReplyCounter
let translations: Translations
var pubkey: String {
return keypair.pubkey
}
@@ -39,7 +36,6 @@ struct DamusState {
}
static var empty: DamusState {
let settings = UserSettingsStore()
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), translations: Translations(settings))
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""))
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
//
// DraftModel.swift
// DraftsModel.swift
// damus
//
// Created by Terry Yiu on 2/12/23.
-2
View File
@@ -64,8 +64,6 @@ class EventsModel: ObservableObject {
handle_event(relay_id: relay_id, ev: ev)
case .notice(_):
break
case .ok:
break
case .eose(_):
load_profiles(profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state)
}
-3
View File
@@ -94,9 +94,6 @@ class FollowersModel: ObservableObject {
} else if sub_id == self.profiles_id {
damus_state.pool.unsubscribe(sub_id: profiles_id, to: [relay_id])
}
case .ok:
break
}
}
}
-2
View File
@@ -58,8 +58,6 @@ class FollowingModel {
break
case .nostr_event(let nev):
switch nev {
case .ok:
break
case .event(_, let ev):
if ev.kind == 0 {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
+11 -151
View File
@@ -130,7 +130,7 @@ class HomeModel: ObservableObject {
}
}
func handle_zap_event_with_zapper(profiles: Profiles, ev: NostrEvent, our_keypair: Keypair, zapper: String) {
func handle_zap_event_with_zapper(_ ev: NostrEvent, our_keypair: Keypair, zapper: String) {
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
return
}
@@ -145,15 +145,9 @@ class HomeModel: ObservableObject {
return
}
if handle_last_event(ev: ev, timeline: .notifications) {
if damus_state.settings.zap_vibration {
// Generate zap vibration
zap_vibrate(zap_amount: zap.invoice.amount)
}
if damus_state.settings.zap_notification {
// Create in-app local notification for zap received.
create_in_app_zap_notification(profiles: profiles, zap: zap)
}
if handle_last_event(ev: ev, timeline: .notifications) && damus_state.settings.zap_vibration {
// Generate zap vibration
zap_vibrate(zap_amount: zap.invoice.amount)
}
return
@@ -167,7 +161,7 @@ class HomeModel: ObservableObject {
let our_keypair = damus_state.keypair
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) {
handle_zap_event_with_zapper(profiles: self.damus_state.profiles, ev: ev, our_keypair: our_keypair, zapper: local_zapper)
handle_zap_event_with_zapper(ev, our_keypair: our_keypair, zapper: local_zapper)
return
}
@@ -186,7 +180,7 @@ class HomeModel: ObservableObject {
DispatchQueue.main.async {
self.damus_state.profiles.zappers[ptag] = zapper
self.handle_zap_event_with_zapper(profiles: self.damus_state.profiles, ev: ev, our_keypair: our_keypair, zapper: zapper)
self.handle_zap_event_with_zapper(ev, our_keypair: our_keypair, zapper: zapper)
}
}
@@ -335,11 +329,7 @@ class HomeModel: ObservableObject {
self.loading = false
break
case .ok:
break
}
}
}
@@ -465,7 +455,7 @@ class HomeModel: ObservableObject {
return m[kind]
}
func handle_notification(ev: NostrEvent) {
// don't show notifications from ourselves
guard ev.pubkey != damus_state.pubkey else {
@@ -489,10 +479,7 @@ class HomeModel: ObservableObject {
return
}
if handle_last_event(ev: ev, timeline: .notifications) {
process_local_notification(damus_state: damus_state, event: ev)
}
handle_last_event(ev: ev, timeline: .notifications)
}
@discardableResult
@@ -511,13 +498,11 @@ class HomeModel: ObservableObject {
}
}
func handle_text_event(sub_id: String, _ ev: NostrEvent) {
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
return
}
damus_state.replies.count_replies(ev)
damus_state.events.insert(ev)
if sub_id == home_subid {
@@ -543,13 +528,9 @@ class HomeModel: ObservableObject {
incoming_dms.append(ev)
dm_debouncer.debounce { [self] in
dm_debouncer.debounce {
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
self.new_events = notifs
if damus_state.settings.dm_notification,
let displayName = damus_state.profiles.lookup(id: self.incoming_dms.last!.pubkey)?.display_name {
create_local_notification(displayName: displayName, conversation: "You have received a direct message", type: .dm)
}
}
self.incoming_dms = []
}
@@ -678,7 +659,7 @@ func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEve
}
}
let tprof = TimestampedProfile(profile: profile, timestamp: ev.created_at, event: ev)
let tprof = TimestampedProfile(profile: profile, timestamp: ev.created_at)
profiles.add(id: ev.pubkey, profile: tprof)
if let nip05 = profile.nip05, old_nip05 != profile.nip05 {
@@ -690,7 +671,6 @@ func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEve
DispatchQueue.main.async {
profiles.validated[ev.pubkey] = validated
profiles.nip05_pubkey[nip05] = ev.pubkey
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
}
}
@@ -744,7 +724,7 @@ func process_contact_event(state: DamusState, ev: NostrEvent) {
func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
let bootstrap_dict: [String: RelayInfo] = [:]
let old_decoded = m_old_ev.flatMap { decode_json_relays($0.content) } ?? state.bootstrap_relays.reduce(into: bootstrap_dict) { (d, r) in
let old_decoded = m_old_ev.flatMap { decode_json_relays($0.content) } ?? BOOTSTRAP_RELAYS.reduce(into: bootstrap_dict) { (d, r) in
d[r] = .rw
}
@@ -779,7 +759,6 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
}
if changed {
save_bootstrap_relays(pubkey: state.pubkey, relays: Array(new))
notify(.relays_changed, ())
}
}
@@ -949,122 +928,3 @@ func zap_vibrate(zap_amount: Int64) {
vibration_generator.impactOccurred()
}
func zap_notification_title(_ zap: Zap) -> String {
if zap.private_request != nil {
return NSLocalizedString("Private Zap", comment: "Title of notification when a private zap is received.")
} else {
return NSLocalizedString("Zap", comment: "Title of notification when a non-private zap is received.")
}
}
func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) -> String {
let src = zap.private_request ?? zap.request.ev
let anon = event_is_anonymous(ev: src)
let pk = anon ? "anon" : src.pubkey
let profile = profiles.lookup(id: pk)
let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0))
let formattedSats = format_msats_abbrev(zap.invoice.amount)
let name = Profile.displayName(profile: profile, pubkey: pk).display_name
if src.content.isEmpty {
let format = localizedStringFormat(key: "zap_notification_no_message", locale: locale)
return String(format: format, locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats, name)
} else {
let format = localizedStringFormat(key: "zap_notification_with_message", locale: locale)
return String(format: format, locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats, name, src.content)
}
}
func create_in_app_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) {
let content = UNMutableNotificationContent()
content.title = zap_notification_title(zap)
content.body = zap_notification_body(profiles: profiles, zap: zap, locale: locale)
content.sound = UNNotificationSound.default
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let request = UNNotificationRequest(identifier: "myZapNotification", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("Error: \(error)")
} else {
print("Local notification scheduled")
}
}
}
func process_local_notification(damus_state: DamusState, event ev: NostrEvent) {
guard let type = ev.known_kind else {
return
}
if damus_state.settings.notification_only_from_following,
damus_state.contacts.follow_state(ev.pubkey) != .follows
{
return
}
if type == .text && damus_state.settings.mention_notification {
for block in ev.blocks(damus_state.keypair.privkey) {
if case .mention(let mention) = block, mention.ref.ref_id == damus_state.keypair.pubkey,
let displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name {
let justContent = NSAttributedString(render_note_content(ev: ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey).content).string
create_local_notification(displayName: displayName, conversation: justContent, type: type)
}
}
} else if type == .boost && damus_state.settings.repost_notification,
let displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name {
if let inner_ev = ev.inner_event {
create_local_notification(displayName: displayName, conversation: inner_ev.content, type: type)
}
} else if type == .like && damus_state.settings.like_notification,
let displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name,
let e_ref = ev.referenced_ids.first?.ref_id,
let content = damus_state.events.lookup(e_ref)?.content {
create_local_notification(displayName: displayName, conversation: content, type: type)
}
}
func create_local_notification(displayName: String, conversation: String, type: NostrKind) {
let content = UNMutableNotificationContent()
var title = ""
var identifier = ""
switch type {
case .text:
title = String(format: NSLocalizedString("Mentioned by %@", comment: "Mentioned by heading in local notification"), displayName)
identifier = "myMentionNotification"
case .boost:
title = String(format: NSLocalizedString("Reposted by %@", comment: "Reposted by heading in local notification"), displayName)
identifier = "myBoostNotification"
case .like:
title = String(format: NSLocalizedString("Liked by %@", comment: "Liked by heading in local notification"), displayName)
identifier = "myLikeNotification"
case .dm:
title = String(format: NSLocalizedString("DM by %@", comment: "DM by heading in local notification"), displayName)
identifier = "myDMNotification"
default:
break
}
content.title = title
content.body = conversation
content.sound = UNNotificationSound.default
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("Error: \(error)")
} else {
print("Local notification scheduled")
}
}
}
+2 -28
View File
@@ -9,37 +9,11 @@ import Foundation
import UIKit
enum MediaUpload {
case image(URL)
case video(URL)
var genericFileName: String {
"damus_generic_filename.\(file_extension)"
}
var file_extension: String {
switch self {
case .image(let url):
return url.pathExtension
case .video(let url):
return url.pathExtension
}
}
var is_image: Bool {
if case .image = self {
return true
}
return false
}
}
class ImageUploadModel: NSObject, URLSessionTaskDelegate, ObservableObject {
@Published var progress: Double? = nil
func start(media: MediaUpload, uploader: MediaUploader) async -> ImageUploadResult {
let res = await create_upload_request(mediaToUpload: media, mediaUploader: uploader, progress: self)
func start(img: UIImage, uploader: ImageUploader) async -> ImageUploadResult {
let res = await create_image_upload_request(imageToUpload: img, imageUploader: uploader, progress: self)
DispatchQueue.main.async {
self.progress = nil
}
-2
View File
@@ -133,8 +133,6 @@ class ProfileModel: ObservableObject, Equatable {
return
}
switch resp {
case .ok:
break
case .event(_, let ev):
add_event(ev)
case .notice(let notice):
-2
View File
@@ -68,8 +68,6 @@ class SearchHomeModel: ObservableObject {
}
case .notice(let msg):
print("search home notice: \(msg)")
case .ok:
break
case .eose(let sub_id):
loading = false
-3
View File
@@ -131,9 +131,6 @@ func handle_subid_event(pool: RelayPool, relay_id: String, ev: NostrConnectionEv
case .event(let ev_subid, let ev):
handle(ev_subid, ev)
return (ev_subid, false)
case .ok:
return (nil, false)
case .notice(let note):
if note.contains("Too many subscription filters") {
-1
View File
@@ -114,7 +114,6 @@ class ThreadModel: ObservableObject {
}
let the_ev = damus_state.events.upsert(ev)
damus_state.replies.count_replies(the_ev)
damus_state.events.add_replies(ev: the_ev)
event_map.insert(ev)
-150
View File
@@ -1,150 +0,0 @@
//
// Translations.swift
// damus
//
// Created by Terry Yiu on 3/29/23.
//
import Foundation
import NaturalLanguage
class Translations: ObservableObject {
private static let languageDetectionMinConfidence = 0.5
@Published var translations: [NostrEvent: String] = [:]
@Published var languages: [NostrEvent: String] = [:]
let settings: UserSettingsStore
let translator: Translator
let targetLanguage = currentLanguage()
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
init(_ settings: UserSettingsStore) {
self.settings = settings
self.translator = Translator(settings)
}
/**
Attempts to detect the language of the content of a given nostr event using Apple's offline NaturalLanguage API.
The detected language will be returned only if it has a 50% or more confidence.
This is a best effort guess and could be incorrect.
*/
func detectLanguage(_ event: NostrEvent, state: DamusState) -> String? {
if let cachedLanguage = languages[event] {
return cachedLanguage
}
// Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in
// and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer.
let originalBlocks = event.blocks(state.keypair.privkey)
let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ")
// Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate.
let languageRecognizer = NLLanguageRecognizer()
languageRecognizer.processString(originalOnlyText)
guard let locale = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= Translations.languageDetectionMinConfidence })?.key.rawValue else {
return nil
}
// Remove the variant component and just take the language part as translation services typically only supports the variant-less language.
// Moreover, speakers of one variant can generally understand other variants.
let language = localeToLanguage(locale)
languages[event] = language
return language
}
/**
Returns true if the given translation is effectively the same as the original note, ignoring whitespaces and new lines.
*/
private func translationSameAsOriginal(_ translation: String, event: NostrEvent, state: DamusState) -> Bool {
return translation.trimmingCharacters(in: .whitespacesAndNewlines) == event.get_content(state.keypair.privkey).trimmingCharacters(in: .whitespacesAndNewlines)
}
func hasCachedTranslation(_ event: NostrEvent) -> Bool {
return languages[event] != nil
}
func cachedTranslation(_ event: NostrEvent) -> TranslationWithLanguage? {
if let cachedLanguage = languages[event] {
if let cachedTranslation = translations[event] {
return TranslationWithLanguage(translation: cachedTranslation, language: cachedLanguage)
} else {
return nil
}
} else {
return nil
}
}
func translate(_ event: NostrEvent, state: DamusState) async -> TranslationWithLanguage? {
guard shouldTranslate(event, state: state) else {
return nil
}
guard let noteLanguage = detectLanguage(event, state: state) else {
return nil
}
if languages[event] != nil {
return cachedTranslation(event)
}
do {
guard let translationWithLanguage = try await translator.translate(event.get_content(state.keypair.privkey), from: noteLanguage, to: targetLanguage) else {
return nil
}
// If the translated content is identical to the original content, don't return the translation.
if translationSameAsOriginal(translationWithLanguage.translation, event: event, state: state) {
// Nil out the translation as it's the same as the original.
translations[event] = nil
// Leave an entry so that we don't attempt to translate it again in the future.
languages[event] = targetLanguage
return nil
} else {
translations[event] = translationWithLanguage.translation
languages[event] = translationWithLanguage.language
return translationWithLanguage
}
} catch {
return nil
}
}
func shouldTranslate(_ event: NostrEvent, state: DamusState) -> Bool {
// Do not translate self-authored content because if the language recognizer guesses the wrong language for your own note,
// it's annoying and unexpected for the translation to show up.
if event.pubkey == state.pubkey && state.is_privkey_user {
return false
}
// Avoid translating if no translation service is configured.
switch settings.translation_service {
case .none:
return false
case .libretranslate:
if URLComponents(string: settings.libretranslate_url) == nil {
return false
}
case .deepl:
if settings.deepl_api_key == "" {
return false
}
}
// If translation was attempted before, use the results of the cached translation to determine if it should be shown.
if languages[event] != nil {
return translations[event] != nil
}
// Avoid translating notes if language cannot be detected or if it is in one of the user's preferred languages.
guard let noteLanguage = detectLanguage(event, state: state), !preferredLanguages.contains(noteLanguage) else {
return false
}
return true
}
}
+18 -77
View File
@@ -50,10 +50,10 @@ func get_default_wallet(_ pubkey: String) -> Wallet {
}
}
func get_media_uploader(_ pubkey: String) -> MediaUploader {
if let defaultMediaUploader = UserDefaults.standard.string(forKey: "default_media_uploader"),
let defaultMediaUploader = MediaUploader(rawValue: defaultMediaUploader) {
return defaultMediaUploader
func get_image_uploader(_ pubkey: String) -> ImageUploader {
if let defaultImageUploader = UserDefaults.standard.string(forKey: "default_image_uploader"),
let defaultImageUploader = ImageUploader(rawValue: defaultImageUploader) {
return defaultImageUploader
} else {
return .nostrBuild
}
@@ -98,9 +98,9 @@ class UserSettingsStore: ObservableObject {
}
}
@Published var default_media_uploader: MediaUploader {
@Published var default_image_uploader: ImageUploader {
didSet {
UserDefaults.standard.set(default_media_uploader.rawValue, forKey: "default_media_uploader")
UserDefaults.standard.set(default_image_uploader.rawValue, forKey: "default_image_uploader")
}
}
@@ -128,66 +128,6 @@ class UserSettingsStore: ObservableObject {
}
}
@Published var zap_notification: Bool {
didSet {
UserDefaults.standard.set(zap_notification, forKey: "zap_notification")
}
}
@Published var mention_notification: Bool {
didSet {
UserDefaults.standard.set(mention_notification, forKey: "mention_notification")
}
}
@Published var repost_notification: Bool {
didSet {
UserDefaults.standard.set(repost_notification, forKey: "repost_notification")
}
}
@Published var dm_notification: Bool {
didSet {
UserDefaults.standard.set(dm_notification, forKey: "dm_notification")
}
}
@Published var like_notification: Bool {
didSet {
UserDefaults.standard.set(like_notification, forKey: "like_notification")
}
}
@Published var notification_only_from_following: Bool {
didSet {
UserDefaults.standard.set(notification_only_from_following, forKey: "notification_only_from_following")
}
}
@Published var truncate_timeline_text: Bool {
didSet {
UserDefaults.standard.set(truncate_timeline_text, forKey: "truncate_timeline_text")
}
}
@Published var truncate_mention_text: Bool {
didSet {
UserDefaults.standard.set(truncate_mention_text, forKey: "truncate_mention_text")
}
}
@Published var auto_translate: Bool {
didSet {
UserDefaults.standard.set(auto_translate, forKey: "auto_translate")
}
}
@Published var show_only_preferred_languages: Bool {
didSet {
UserDefaults.standard.set(show_only_preferred_languages, forKey: "show_only_preferred_languages")
}
}
@Published var translation_service: TranslationService {
didSet {
UserDefaults.standard.set(translation_service.rawValue, forKey: "translation_service")
@@ -265,21 +205,11 @@ class UserSettingsStore: ObservableObject {
show_wallet_selector = should_show_wallet_selector(pubkey)
always_show_images = UserDefaults.standard.object(forKey: "always_show_images") as? Bool ?? false
default_media_uploader = get_media_uploader(pubkey)
default_image_uploader = get_image_uploader(pubkey)
left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
zap_vibration = UserDefaults.standard.object(forKey: "zap_vibration") as? Bool ?? false
zap_notification = UserDefaults.standard.object(forKey: "zap_notification") as? Bool ?? true
mention_notification = UserDefaults.standard.object(forKey: "mention_notification") as? Bool ?? true
repost_notification = UserDefaults.standard.object(forKey: "repost_notification") as? Bool ?? true
like_notification = UserDefaults.standard.object(forKey: "like_notification") as? Bool ?? true
dm_notification = UserDefaults.standard.object(forKey: "dm_notification") as? Bool ?? true
notification_only_from_following = UserDefaults.standard.object(forKey: "notification_only_from_following") as? Bool ?? false
truncate_timeline_text = UserDefaults.standard.object(forKey: "truncate_timeline_text") as? Bool ?? false
truncate_mention_text = UserDefaults.standard.object(forKey: "truncate_mention_text") as? Bool ?? false
disable_animation = should_disable_image_animation()
auto_translate = UserDefaults.standard.object(forKey: "auto_translate") as? Bool ?? true
show_only_preferred_languages = UserDefaults.standard.object(forKey: "show_only_preferred_languages") as? Bool ?? false
// Note from @tyiu:
// Default translation service is disabled by default for now until we gain some confidence that it is working well in production.
@@ -336,6 +266,17 @@ class UserSettingsStore: ObservableObject {
private func clearDeepLApiKey() throws {
try Vault.deletePrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration())
}
func can_translate(_ pubkey: String) -> Bool {
switch translation_service {
case .none:
return false
case .libretranslate:
return URLComponents(string: libretranslate_url) != nil
case .deepl:
return deepl_api_key != ""
}
}
}
struct DamusLibreTranslateKeychainConfiguration: KeychainConfiguration {
+13 -13
View File
@@ -42,42 +42,42 @@ enum Wallet: String, CaseIterable, Identifiable {
return .init(index: -1, tag: "systemdefaultwallet", displayName: NSLocalizedString("Local default", comment: "Dropdown option label for system default for Lightning wallet."),
link: "lightning:", appStoreLink: "lightning:", image: "")
case .strike:
return .init(index: 0, tag: "strike", displayName: "Strike", link: "strike:",
return .init(index: 0, tag: "strike", displayName: NSLocalizedString("Strike", comment: "Dropdown option label for Lightning wallet, Strike."), link: "strike:",
appStoreLink: "https://apps.apple.com/us/app/strike-bitcoin-payments/id1488724463", image: "strike")
case .cashapp:
return .init(index: 1, tag: "cashapp", displayName: "Cash App", link: "https://cash.app/launch/lightning/",
return .init(index: 1, tag: "cashapp", displayName: NSLocalizedString("Cash App", comment: "Dropdown option label for Lightning wallet, Cash App."), link: "https://cash.app/launch/lightning/",
appStoreLink: "https://apps.apple.com/us/app/cash-app/id711923939", image: "cashapp")
case .muun:
return .init(index: 2, tag: "muun", displayName: "Muun", link: "muun:", appStoreLink: "https://apps.apple.com/us/app/muun-wallet/id1482037683", image: "muun")
return .init(index: 2, tag: "muun", displayName: NSLocalizedString("Muun", comment: "Dropdown option label for Lightning wallet, Muun."), link: "muun:", appStoreLink: "https://apps.apple.com/us/app/muun-wallet/id1482037683", image: "muun")
case .bluewallet:
return .init(index: 3, tag: "bluewallet", displayName: "Blue Wallet", link: "bluewallet:lightning:",
return .init(index: 3, tag: "bluewallet", displayName: NSLocalizedString("Blue Wallet", comment: "Dropdown option label for Lightning wallet, Blue Wallet."), link: "bluewallet:lightning:",
appStoreLink: "https://apps.apple.com/us/app/bluewallet-bitcoin-wallet/id1376878040", image: "bluewallet")
case .walletofsatoshi:
return .init(index: 4, tag: "walletofsatoshi", displayName: "Wallet of Satoshi", link: "walletofsatoshi:lightning:",
return .init(index: 4, tag: "walletofsatoshi", displayName: NSLocalizedString("Wallet of Satoshi", comment: "Dropdown option label for Lightning wallet, Wallet of Satoshi."), link: "walletofsatoshi:lightning:",
appStoreLink: "https://apps.apple.com/us/app/wallet-of-satoshi/id1438599608", image: "walletofsatoshi")
case .zebedee:
return .init(index: 5, tag: "zebedee", displayName: "Zebedee", link: "zebedee:lightning:",
return .init(index: 5, tag: "zebedee", displayName: NSLocalizedString("Zebedee", comment: "Dropdown option label for Lightning wallet, Zebedee."), link: "zebedee:lightning:",
appStoreLink: "https://apps.apple.com/us/app/zebedee-wallet/id1484394401", image: "zebedee")
case .zeusln:
return .init(index: 6, tag: "zeusln", displayName: "Zeus LN", link: "zeusln:lightning:",
return .init(index: 6, tag: "zeusln", displayName: NSLocalizedString("Zeus LN", comment: "Dropdown option label for Lightning wallet, Zeus LN."), link: "zeusln:lightning:",
appStoreLink: "https://apps.apple.com/us/app/zeus-ln/id1456038895", image: "zeusln")
case .lnlink:
return .init(index: 7, tag: "lnlink", displayName: "LNLink", link: "lnlink:lightning:",
return .init(index: 7, tag: "lnlink", displayName: NSLocalizedString("LNLink", comment: "Dropdown option label for Lightning wallet, LNLink."), link: "lnlink:lightning:",
appStoreLink: "https://testflight.apple.com/join/aNY4yuuZ", image: "lnlink")
case .phoenix:
return .init(index: 8, tag: "phoenix", displayName: "Phoenix", link: "phoenix://",
return .init(index: 8, tag: "phoenix", displayName: NSLocalizedString("Phoenix", comment: "Dropdown option label for Lightning wallet, Phoenix."), link: "phoenix://",
appStoreLink: "https://apps.apple.com/us/app/phoenix-wallet/id1544097028", image: "phoenix")
case .breez:
return .init(index: 9, tag: "breez", displayName: "Breez", link: "breez:",
return .init(index: 9, tag: "breez", displayName: NSLocalizedString("Breez", comment: "Dropdown option label for Lightning wallet, Breez."), link: "breez:",
appStoreLink: "https://apps.apple.com/us/app/breez-lightning-client-pos/id1463604142", image: "breez")
case .bitcoinbeach:
return .init(index: 10, tag: "bitcoinbeach", displayName: "Bitcoin Beach", link: "bitcoinbeach://",
return .init(index: 10, tag: "bitcoinbeach", displayName: NSLocalizedString("Bitcoin Beach", comment: "Dropdown option label for Lightning wallet, Bitcoin Beach."), link: "bitcoinbeach://",
appStoreLink: "https://apps.apple.com/sv/app/bitcoin-beach-wallet/id1531383905", image: "bbw")
case .blixtwallet:
return .init(index: 11, tag: "blixtwallet", displayName: "Blixt Wallet", link: "blixtwallet:lightning:",
return .init(index: 11, tag: "blixtwallet", displayName: NSLocalizedString("Blixt Wallet", comment: "Dropdown option label for Lightning wallet, Blixt Wallet"), link: "blixtwallet:lightning:",
appStoreLink: "https://testflight.apple.com/join/EXvGhRzS", image: "blixt-wallet")
case .river:
return .init(index: 12, tag: "river", displayName: "River", link: "river://",
return .init(index: 12, tag: "river", displayName: NSLocalizedString("River", comment: "Dropdown option label for Lightning wallet, River"), link: "river://",
appStoreLink: "https://apps.apple.com/us/app/river-buy-mine-bitcoin/id1536176542", image: "river")
}
-2
View File
@@ -46,8 +46,6 @@ class ZapsModel: ObservableObject {
}
switch resp {
case .ok:
break
case .notice:
break
case .eose:
+1 -10
View File
@@ -98,16 +98,7 @@ struct Profile: Codable {
}
var website_url: URL? {
if self.website?.trimmingCharacters(in: .whitespacesAndNewlines) == "" {
return nil
}
return self.website.flatMap { url in
let trim = url.trimmingCharacters(in: .whitespacesAndNewlines)
if !(trim.hasPrefix("http://") || trim.hasPrefix("https://")) {
return URL(string: "https://" + trim)
}
return URL(string: trim)
}
return self.website.flatMap { URL(string: $0) }
}
var lnurl: String? {
+2 -8
View File
@@ -10,7 +10,6 @@ import CommonCrypto
import secp256k1
import secp256k1_implementation
import CryptoKit
import NaturalLanguage
@@ -519,21 +518,16 @@ func make_first_contact_event(keypair: Keypair) -> NostrEvent? {
guard let privkey = keypair.privkey else {
return nil
}
let bootstrap_relays = load_bootstrap_relays(pubkey: keypair.pubkey)
let rw_relay_info = RelayInfo(read: true, write: true)
var relays: [String: RelayInfo] = [:]
for relay in bootstrap_relays {
for relay in BOOTSTRAP_RELAYS {
relays[relay] = rw_relay_info
}
let relay_json = encode_json(relays)!
let damus_pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
let jb55_pubkey = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245" // lol
let tags = [
["p", damus_pubkey],
["p", jb55_pubkey],
["p", keypair.pubkey] // you're a friend of yourself!
]
let ev = NostrEvent(content: relay_json,
+1 -1
View File
@@ -21,5 +21,5 @@ struct NostrMetadata: Codable {
}
func create_account_to_metadata(_ model: CreateAccountModel) -> NostrMetadata {
return NostrMetadata(display_name: model.real_name, name: model.nick_name, about: model.about, website: nil, nip05: nil, picture: model.profile_image, banner: nil, lud06: nil, lud16: nil)
return NostrMetadata(display_name: model.real_name, name: model.nick_name, about: model.about, website: nil, nip05: nil, picture: nil, banner: nil, lud06: nil, lud16: nil)
}
+1 -24
View File
@@ -7,22 +7,13 @@
import Foundation
struct CommandResult {
let event_id: String
let ok: Bool
let msg: String
}
enum NostrResponse: Decodable {
case event(String, NostrEvent)
case notice(String)
case eose(String)
case ok(CommandResult)
var subid: String? {
switch self {
case .ok(_):
return nil
case .event(let sub_id, _):
return sub_id
case .eose(let sub_id):
@@ -57,23 +48,9 @@ enum NostrResponse: Decodable {
let sub_id = try container.decode(String.self)
self = .eose(sub_id)
return
} else if typ == "OK" {
var cr: CommandResult
do {
let event_id = try container.decode(String.self)
let ok = try container.decode(Bool.self)
let msg = try container.decode(String.self)
cr = CommandResult(event_id: event_id, ok: ok, msg: msg)
} catch {
print(error)
throw error
}
self = .ok(cr)
return
//ev.pow = count_hash_leading_zero_bits(ev.id)
}
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "expected EVENT, NOTICE or OK, got \(typ)"))
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "expected EVENT or NOTICE, got \(typ)"))
}
}
-1
View File
@@ -12,7 +12,6 @@ import UIKit
class Profiles {
var profiles: [String: TimestampedProfile] = [:]
var validated: [String: NIP05] = [:]
var nip05_pubkey: [String: String] = [:]
var zappers: [String: String] = [:]
func is_validated(_ pk: String) -> NIP05? {
+1
View File
@@ -256,6 +256,7 @@ class RelayPool {
}
}
// handle reconnect logic, etc?
for handler in handlers {
handler.callback(relay_id, event)
}
-69
View File
@@ -1,69 +0,0 @@
// https://github.com/Tunous/DebouncedOnChange/blob/5670ea13e8ad33e9cc3197f6d13ce492dc0e46ab/Sources/DebouncedOnChange/DebouncedChangeViewModifier.swift
import SwiftUI
import Foundation
extension View {
/// Adds a modifier for this view that fires an action only when a time interval in seconds represented by
/// `debounceTime` elapses between value changes.
///
/// Each time the value changes before `debounceTime` passes, the previous action will be cancelled and the next
/// action /// will be scheduled to run after that time passes again. This mean that the action will only execute
/// after changes to the value /// stay unmodified for the specified `debounceTime` in seconds.
///
/// - Parameters:
/// - value: The value to check against when determining whether to run the closure.
/// - debounceTime: The time in seconds to wait after each value change before running `action` closure.
/// - action: A closure to run when the value changes.
/// - Returns: A view that fires an action after debounced time when the specified value changes.
public func onChange<Value>(
of value: Value,
debounceTime: TimeInterval,
perform action: @escaping (_ newValue: Value) -> Void
) -> some View where Value: Equatable {
self.modifier(DebouncedChangeViewModifier(trigger: value, debounceTime: debounceTime, action: action))
}
}
private struct DebouncedChangeViewModifier<Value>: ViewModifier where Value: Equatable {
let trigger: Value
let debounceTime: TimeInterval
let action: (Value) -> Void
@State private var debouncedTask: Task<Void, Never>?
func body(content: Content) -> some View {
content.onChange(of: trigger) { value in
debouncedTask?.cancel()
debouncedTask = Task.delayed(seconds: debounceTime) { @MainActor in
action(value)
}
}
}
}
extension Task {
/// Asynchronously runs the given `operation` in its own task after the specified number of `seconds`.
///
/// The operation will be executed after specified number of `seconds` passes. You can cancel the task earlier
/// for the operation to be skipped.
///
/// - Parameters:
/// - time: Delay time in seconds.
/// - operation: The operation to execute.
/// - Returns: Handle to the task which can be cancelled.
@discardableResult
public static func delayed(
seconds: TimeInterval,
operation: @escaping @Sendable () async -> Void
) -> Self where Success == Void, Failure == Never {
Self {
do {
try await Task<Never, Never>.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
await operation()
} catch {}
}
}
}
+1 -1
View File
@@ -39,7 +39,7 @@ enum DisplayName {
func parse_display_name(profile: Profile?, pubkey: String) -> DisplayName {
if pubkey == "anon" {
return .one(NSLocalizedString("Anonymous", comment: "Placeholder display name of anonymous user."))
return .one("Anonymous")
}
guard let profile else {
-19
View File
@@ -21,22 +21,3 @@ func localizedStringFormat(key: String, locale: Locale?) -> String {
let fallback = bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: key, value: nil, table: nil)
return bundle.localizedString(forKey: key, value: fallback, table: nil)
}
func currentLanguage() -> String {
if #available(iOS 16, *) {
return Locale.current.language.languageCode?.identifier ?? "en"
} else {
return Locale.current.languageCode ?? "en"
}
}
/**
Removes the variant part of a locale code so that it contains only the language code.
*/
func localeToLanguage(_ locale: String) -> String? {
if #available(iOS 16, *) {
return Locale.LanguageCode(stringLiteral: locale).identifier(.alpha2)
} else {
return NSLocale(localeIdentifier: locale).languageCode
}
}
+1 -22
View File
@@ -39,20 +39,11 @@ enum NIP05Validation {
case valid
}
struct FetchedNIP05 {
let response: NIP05Response
let nip05: NIP05Response
}
func fetch_nip05_str(nip05_str: String) async -> NIP05Response? {
func validate_nip05(pubkey: String, nip05_str: String) async -> NIP05? {
guard let nip05 = NIP05.parse(nip05_str) else {
return nil
}
return await fetch_nip05(nip05: nip05)
}
func fetch_nip05(nip05: NIP05) async -> NIP05Response? {
guard let url = nip05.url else {
return nil
}
@@ -66,18 +57,6 @@ func fetch_nip05(nip05: NIP05) async -> NIP05Response? {
return nil
}
return decoded
}
func validate_nip05(pubkey: String, nip05_str: String) async -> NIP05? {
guard let nip05 = NIP05.parse(nip05_str) else {
return nil
}
guard let decoded = await fetch_nip05(nip05: nip05) else {
return nil
}
guard let stored_pk = decoded.names[nip05.username] else {
return nil
}
+2 -2
View File
@@ -86,8 +86,8 @@ extension Notification.Name {
static var report: Notification.Name {
return Notification.Name("report")
}
static var mute: Notification.Name {
return Notification.Name("mute")
static var block: Notification.Name {
return Notification.Name("block")
}
static var new_mutes: Notification.Name {
return Notification.Name("new_mutes")
-113
View File
@@ -6,116 +6,3 @@
//
import Foundation
class Relayer {
let relay: String
var attempts: Int
var retry_after: Double
var last_attempt: Int64?
init(relay: String, attempts: Int, retry_after: Double) {
self.relay = relay
self.attempts = attempts
self.retry_after = retry_after
self.last_attempt = nil
}
}
class PostedEvent {
let event: NostrEvent
var remaining: [Relayer]
init(event: NostrEvent, remaining: [String]) {
self.event = event
self.remaining = remaining.map {
Relayer(relay: $0, attempts: 0, retry_after: 2.0)
}
}
}
class PostBox {
let pool: RelayPool
var events: [String: PostedEvent]
init(pool: RelayPool) {
self.pool = pool
self.events = [:]
pool.register_handler(sub_id: "postbox", handler: handle_event)
}
func try_flushing_events() {
let now = Int64(Date().timeIntervalSince1970)
for kv in events {
let event = kv.value
for relayer in event.remaining {
if relayer.last_attempt == nil || (now >= (relayer.last_attempt! + Int64(relayer.retry_after))) {
print("attempt #\(relayer.attempts) to flush event '\(event.event.content)' to \(relayer.relay) after \(relayer.retry_after) seconds")
flush_event(event, to_relay: relayer)
}
}
}
}
func handle_event(relay_id: String, _ ev: NostrConnectionEvent) {
try_flushing_events()
guard case .nostr_event(let resp) = ev else {
return
}
guard case .ok(let cr) = resp else {
return
}
remove_relayer(relay_id: relay_id, event_id: cr.event_id)
}
func remove_relayer(relay_id: String, event_id: String) {
guard let ev = self.events[event_id] else {
return
}
ev.remaining = ev.remaining.filter {
$0.relay != relay_id
}
if ev.remaining.count == 0 {
self.events.removeValue(forKey: event_id)
}
}
private func flush_event(_ event: PostedEvent, to_relay: Relayer? = nil) {
var relayers = event.remaining
if let to_relay {
relayers = [to_relay]
}
for relayer in relayers {
relayer.attempts += 1
relayer.last_attempt = Int64(Date().timeIntervalSince1970)
relayer.retry_after *= 1.5
pool.send(.event(event.event), to: [relayer.relay])
}
}
func flush() {
for event in events {
flush_event(event.value)
}
}
func send(_ event: NostrEvent) {
// Don't add event if we already have it
if events[event.id] != nil {
return
}
let remaining = pool.descriptors.map {
$0.url.absoluteString
}
let posted_ev = PostedEvent(event: event, remaining: remaining)
events[event.id] = posted_ev
flush_event(posted_ev)
}
}
+1 -11
View File
@@ -24,21 +24,12 @@ enum Preview {
}
class PreviewCache {
private var previews: [String: Preview]
private var image_meta: [String: ImageFill]
var previews: [String: Preview]
func lookup(_ evid: String) -> Preview? {
return previews[evid]
}
func lookup_image_meta(_ evid: String) -> ImageFill? {
return image_meta[evid]
}
func cache_image_meta(evid: String, image_fill: ImageFill) {
self.image_meta[evid] = image_fill
}
func store(evid: String, preview: LPLinkMetadata?) {
switch preview {
case .none:
@@ -50,6 +41,5 @@ class PreviewCache {
init() {
self.previews = [:]
self.image_meta = [:]
}
}
-44
View File
@@ -1,44 +0,0 @@
//
// RelayBootstrap.swift
// damus
//
// Created by William Casarin on 2023-04-04.
//
import Foundation
let BOOTSTRAP_RELAYS = [
"wss://relay.damus.io",
"wss://eden.nostr.land",
"wss://nostr.wine",
"wss://nos.lol",
]
func bootstrap_relays_setting_key(pubkey: String) -> String {
return pk_setting_key(pubkey, key: "bootstrap_relays")
}
func save_bootstrap_relays(pubkey: String, relays: [String]) {
let key = bootstrap_relays_setting_key(pubkey: pubkey)
UserDefaults.standard.set(relays, forKey: key)
}
func load_bootstrap_relays(pubkey: String) -> [String] {
let key = bootstrap_relays_setting_key(pubkey: pubkey)
guard let relays = UserDefaults.standard.stringArray(forKey: key) else {
print("loading default bootstrap relays")
return BOOTSTRAP_RELAYS.map { $0 }
}
if relays.count == 0 {
print("loading default bootstrap relays")
return BOOTSTRAP_RELAYS.map { $0 }
}
let loaded_relays = Array(Set(relays + BOOTSTRAP_RELAYS))
print("Loading custom bootstrap relays: \(loaded_relays)")
return loaded_relays
}
-54
View File
@@ -1,54 +0,0 @@
//
// ReplyCounter.swift
// damus
//
// Created by William Casarin on 2023-04-04.
//
import Foundation
class ReplyCounter {
private var replies: [String: Int]
private var counted: Set<String>
private var our_replies: [String: NostrEvent]
private let our_pubkey: String
init(our_pubkey: String) {
self.our_pubkey = our_pubkey
replies = [:]
counted = Set()
our_replies = [:]
}
func our_reply(_ evid: String) -> NostrEvent? {
return our_replies[evid]
}
func get_replies(_ evid: String) -> Int {
return replies[evid] ?? 0
}
func count_replies(_ event: NostrEvent) {
guard event.is_textlike else {
return
}
if counted.contains(event.id) {
return
}
counted.insert(event.id)
for reply in event.direct_replies(nil) {
if event.pubkey == our_pubkey {
self.our_replies[reply.ref_id] = event
}
if replies[reply.ref_id] != nil {
replies[reply.ref_id] = replies[reply.ref_id]! + 1
} else {
replies[reply.ref_id] = 1
}
}
}
}
+9 -34
View File
@@ -20,28 +20,18 @@ public struct Translator {
self.userSettingsStore = userSettingsStore
}
/**
Translates a string from source language to target language.
If the translation provider supports its own language detection, it may determine the source language by itself that could be
different from what is passed in as the sourceLanguage argument.
The source language that is actually used in the translation will be returned as part of the TranslationWithLanguage object.
If the translation was unable to be fetched for whatever reason, nil is returned.
*/
public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> TranslationWithLanguage? {
public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
switch userSettingsStore.translation_service {
case .libretranslate:
return try await translateWithLibreTranslate(text, from: sourceLanguage, to: targetLanguage)
case .deepl:
return try await translateWithDeepL(text, to: targetLanguage)
return try await translateWithDeepL(text, from: sourceLanguage, to: targetLanguage)
case .none:
return nil
return text
}
}
/**
Translates a string from sourceLanguage to targetLanguage using LibreTranslate. We do not rely on LibreTranslate's language detection API as it requires a separate API call. Instead, we rely on the passed in sourceLanguage argument.
*/
private func translateWithLibreTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> TranslationWithLanguage? {
private func translateWithLibreTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
let url = try makeURL(userSettingsStore.libretranslate_url, path: "/translate")
var request = URLRequest(url: url)
@@ -61,15 +51,10 @@ public struct Translator {
let translatedText: String
}
let response: Response = try await decodedData(for: request)
let translation = response.translatedText
return TranslationWithLanguage(translation: translation, language: targetLanguage)
return response.translatedText
}
/**
Translates a string to targetLanguage using DeepL. We do not accept a sourceLanguage as an argument as DeepL performs language detection within the translate API, its models are generally fairly accurate, and does not require a separate API call like LibreTranslate.
*/
private func translateWithDeepL(_ text: String, to targetLanguage: String) async throws -> TranslationWithLanguage? {
private func translateWithDeepL(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
if userSettingsStore.deepl_api_key == "" {
return nil
}
@@ -83,9 +68,10 @@ public struct Translator {
struct RequestBody: Encodable {
let text: [String]
let source_lang: String
let target_lang: String
}
let body = RequestBody(text: [text], target_lang: targetLanguage.uppercased())
let body = RequestBody(text: [text], source_lang: sourceLanguage.uppercased(), target_lang: targetLanguage.uppercased())
request.httpBody = try encoder.encode(body)
struct Response: Decodable {
@@ -97,13 +83,7 @@ public struct Translator {
}
let response: Response = try await decodedData(for: request)
if response.translations.isEmpty {
return nil
}
let translation = response.translations.map { $0.text }.joined(separator: " ")
return TranslationWithLanguage(translation: translation, language: response.translations.first!.detected_source_language)
return response.translations.map { $0.text }.joined(separator: " ")
}
private func makeURL(_ baseUrl: String, path: String) throws -> URL {
@@ -124,11 +104,6 @@ public struct Translator {
}
}
public struct TranslationWithLanguage {
let translation: String
let language: String
}
private extension URLSession {
func data(for request: URLRequest) async throws -> Data {
var task: URLSessionDataTask?
+14 -26
View File
@@ -47,15 +47,10 @@ struct EventActionBar: View {
var body: some View {
HStack {
if damus_state.keypair.privkey != nil {
HStack(spacing: 4) {
EventActionButton(img: "bubble.left", col: bar.replied ? Color.blue : Color.gray) {
notify(.reply, event)
}
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
Text(verbatim: "\(bar.replies > 0 ? "\(bar.replies)" : "")")
.font(.footnote.weight(.medium))
.foregroundColor(bar.replied ? Color.blue : Color.gray)
EventActionButton(img: "bubble.left", col: nil) {
notify(.reply, event)
}
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
}
Spacer()
HStack(spacing: 4) {
@@ -82,10 +77,10 @@ struct EventActionBar: View {
send_like()
}
}
Text(verbatim: "\(bar.likes > 0 ? "\(bar.likes)" : "")")
.font(.footnote.weight(.medium))
.nip05_colorized(gradient: bar.liked)
.foregroundColor(bar.liked ? Color.accentColor : Color.gray)
}
if let lnurl = self.lnurl {
@@ -159,7 +154,7 @@ struct EventActionBar: View {
generator.impactOccurred()
damus_state.postbox.send(like_ev)
damus_state.pool.send(.event(like_ev))
}
}
@@ -193,15 +188,8 @@ struct LikeButton: View {
amountOfAngleIncrease = 20.0
}
}) {
if liked {
LINEAR_GRADIENT
.mask(Image("shaka-full")
.resizable()
).frame(width: 14, height: 14)
} else {
Image("shaka-line")
.foregroundColor(.gray)
}
Image(liked ? "shaka-full" : "shaka-line")
.foregroundColor(liked ? .accentColor : .gray)
}
.accessibilityLabel(NSLocalizedString("Like", comment: "Accessibility Label for Like button"))
.rotationEffect(Angle(degrees: shouldAnimate ? rotationAngle : 0))
@@ -230,12 +218,12 @@ struct EventActionBar_Previews: PreviewProvider {
let ev = NostrEvent(content: "hi", pubkey: pk)
let bar = ActionBarModel.empty()
let likedbar = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
let likedbar_ours = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: test_event, our_boost: nil, our_zap: nil, our_reply: nil)
let maxed_bar = ActionBarModel(likes: 999, boosts: 999, zaps: 999, zap_total: 99999999, replies: 999, our_like: test_event, our_boost: test_event, our_zap: nil, our_reply: nil)
let extra_max_bar = ActionBarModel(likes: 9999, boosts: 9999, zaps: 9999, zap_total: 99999999, replies: 9999, our_like: test_event, our_boost: test_event, our_zap: nil, our_reply: test_event)
let mega_max_bar = ActionBarModel(likes: 9999999, boosts: 99999, zaps: 9999, zap_total: 99999999, replies: 9999999, our_like: test_event, our_boost: test_event, our_zap: test_zap, our_reply: test_event)
let zapbar = ActionBarModel(likes: 0, boosts: 0, zaps: 5, zap_total: 10000000, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
let likedbar = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, our_like: nil, our_boost: nil, our_zap: nil)
let likedbar_ours = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: nil, our_zap: nil)
let maxed_bar = ActionBarModel(likes: 999, boosts: 999, zaps: 999, zap_total: 99999999, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_zap: nil)
let extra_max_bar = ActionBarModel(likes: 9999, boosts: 9999, zaps: 9999, zap_total: 99999999, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_zap: nil)
let mega_max_bar = ActionBarModel(likes: 9999999, boosts: 99999, zaps: 9999, zap_total: 99999999, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_zap: nil)
let zapbar = ActionBarModel(likes: 0, boosts: 0, zaps: 5, zap_total: 10000000, our_like: nil, our_boost: nil, our_zap: nil)
VStack(spacing: 50) {
EventActionBar(damus_state: ds, event: ev, bar: bar)
+6 -6
View File
@@ -29,7 +29,7 @@ struct ShareAction: View {
var body: some View {
let col = colorScheme == .light ? DamusColors.mediumGrey : DamusColors.white
let col = colorScheme == .light ? Color("DamusMediumGrey") : Color("DamusWhite")
VStack {
Text("Share Note", comment: "Title text to indicate that the buttons below are meant to be used to share a note with others.")
@@ -40,15 +40,15 @@ struct ShareAction: View {
HStack(alignment: .top, spacing: 25) {
ShareActionButton(img: "link", text: NSLocalizedString("Copy Link", comment: "Button to copy link to note"), col: col) {
ShareActionButton(img: "link", text: NSLocalizedString("Copy Link TempChange", comment: "Button to copy link to note"), col: col) {
show_share_action = false
UIPasteboard.general.string = "https://damus.io/" + (bech32_note_id(event.id) ?? event.id)
}
let bookmarkImg = isBookmarked ? "bookmark.slash" : "bookmark"
let bookmarkTxt = isBookmarked ? NSLocalizedString("Remove Bookmark", comment: "Button text to remove bookmark from a note.") : NSLocalizedString("Add Bookmark", comment: "Button text to add bookmark to a note.")
let bookmarkTxt = isBookmarked ? "Remove\nBookmark" : "Bookmark"
let boomarkCol = isBookmarked ? Color(.red) : col
ShareActionButton(img: bookmarkImg, text: bookmarkTxt, col: boomarkCol) {
ShareActionButton(img: bookmarkImg, text: NSLocalizedString(bookmarkTxt, comment: "Button to bookmark to note"), col: boomarkCol) {
show_share_action = false
self.bookmarks.updateBookmark(event)
isBookmarked = self.bookmarks.isBookmarked(event)
@@ -75,10 +75,10 @@ struct ShareAction: View {
}) {
Text(NSLocalizedString("Cancel", comment: "Button to cancel a repost."))
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .center)
.foregroundColor(colorScheme == .light ? DamusColors.black : DamusColors.white)
.foregroundColor(colorScheme == .light ? Color("DamusBlack") : Color("DamusWhite"))
.overlay {
RoundedRectangle(cornerRadius: 24)
.stroke(colorScheme == .light ? DamusColors.mediumGrey : DamusColors.white, lineWidth: 1)
.stroke(colorScheme == .light ? Color("DamusMediumGrey") : Color("DamusWhite"), lineWidth: 1)
}
.padding(EdgeInsets(top: 10, leading: 50, bottom: 25, trailing: 50))
}
+51 -20
View File
@@ -8,41 +8,72 @@
import SwiftUI
struct AddRelayView: View {
@Binding var show_add_relay: Bool
@Binding var relay: String
let action: (String?) -> Void
var body: some View {
ZStack(alignment: .leading) {
HStack{
TextField(NSLocalizedString("wss://some.relay.com", comment: "Placeholder example for relay server address."), text: $relay)
.padding(2)
.padding(.leading, 25)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
Label("", systemImage: "xmark.circle.fill")
.foregroundColor(.accentColor)
.padding(.trailing, -25.0)
.opacity((relay == "") ? 0.0 : 1.0)
.onTapGesture {
self.relay = ""
VStack(alignment: .leading) {
Form {
Section(NSLocalizedString("Add Relay", comment: "Label for section for adding a relay server.")) {
ZStack(alignment: .leading) {
HStack{
TextField(NSLocalizedString("wss://some.relay.com", comment: "Placeholder example for relay server address."), text: $relay)
.padding(2)
.padding(.leading, 25)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
Label("", systemImage: "xmark.circle.fill")
.foregroundColor(.blue)
.padding(.trailing, -25.0)
.opacity((relay == "") ? 0.0 : 1.0)
.onTapGesture {
self.relay = ""
}
}
Label("", systemImage: "doc.on.clipboard")
.padding(.leading, -10)
.onTapGesture {
if let pastedrelay = UIPasteboard.general.string {
self.relay = pastedrelay
}
}
}
}
}
Label("", systemImage: "doc.on.clipboard")
.padding(.leading, -10)
.onTapGesture {
if let pastedrelay = UIPasteboard.general.string {
self.relay = pastedrelay
VStack {
HStack {
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of view adding user inputted relay.")) {
show_add_relay = false
action(nil)
}
.contentShape(Rectangle())
Spacer()
Button(NSLocalizedString("Add", comment: "Button to confirm adding user inputted relay.")) {
show_add_relay = false
action(relay)
relay = ""
}
.buttonStyle(.borderedProminent)
.contentShape(Rectangle())
}
.padding()
}
}
}
}
struct AddRelayView_Previews: PreviewProvider {
@State static var show: Bool = true
@State static var relay: String = ""
static var previews: some View {
AddRelayView(relay: $relay)
AddRelayView(show_add_relay: $show, relay: $relay, action: {_ in })
}
}
+82 -33
View File
@@ -15,22 +15,23 @@ enum ImageUploadResult {
case failed(Error?)
}
fileprivate func create_upload_body(mediaData: Data, boundary: String, mediaUploader: MediaUploader, mediaToUpload: MediaUpload) -> Data {
fileprivate func create_upload_body(imageDataKey: Data, boundary: String, imageUploader: ImageUploader) -> Data {
let body = NSMutableData();
let contentType = mediaToUpload.is_image ? "image/jpg" : "video/mp4"
let contentType = "image/jpg"
body.appendString(string: "Content-Type: multipart/form-data; boundary=\(boundary)\r\n\r\n")
body.appendString(string: "--\(boundary)\r\n")
body.appendString(string: "Content-Disposition: form-data; name=\(mediaUploader.nameParam); filename=\(mediaToUpload.genericFileName)\r\n")
body.appendString(string: "Content-Disposition: form-data; name=\(imageUploader.nameParam); filename=\"damus_generic_filename.jpg\"\r\n")
body.appendString(string: "Content-Type: \(contentType)\r\n\r\n")
body.append(mediaData as Data)
body.append(imageDataKey as Data)
body.appendString(string: "\r\n")
body.appendString(string: "--\(boundary)--\r\n")
return body as Data
}
func create_upload_request(mediaToUpload: MediaUpload, mediaUploader: MediaUploader, progress: URLSessionTaskDelegate) async -> ImageUploadResult {
var mediaData: Data?
guard let url = URL(string: mediaUploader.postAPI) else {
func create_image_upload_request(imageToUpload: UIImage, imageUploader: ImageUploader, progress: URLSessionTaskDelegate) async -> ImageUploadResult {
guard let url = URL(string: imageUploader.postAPI) else {
return .failed(nil)
}
@@ -39,26 +40,13 @@ func create_upload_request(mediaToUpload: MediaUpload, mediaUploader: MediaUploa
let boundary = "Boundary-\(UUID().description)"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
switch mediaToUpload {
case .image(let url):
do {
mediaData = try Data(contentsOf: url)
} catch {
return .failed(error)
}
case .video(let url):
do {
mediaData = try Data(contentsOf: url)
} catch {
return .failed(error)
}
}
guard let mediaData else {
// otherwise convert to jpg
guard let jpegData = imageToUpload.jpegData(compressionQuality: 0.8) else {
// somehow failed, just return original
return .failed(nil)
}
request.httpBody = create_upload_body(mediaData: mediaData, boundary: boundary, mediaUploader: mediaUploader, mediaToUpload: mediaToUpload)
request.httpBody = create_upload_body(imageDataKey: jpegData, boundary: boundary, imageUploader: imageUploader)
do {
let (data, _) = try await URLSession.shared.data(for: request, delegate: progress)
@@ -68,8 +56,8 @@ func create_upload_request(mediaToUpload: MediaUpload, mediaUploader: MediaUploa
return .failed(nil)
}
guard let url = mediaUploader.getMediaURL(from: responseString, mediaIsImage: mediaToUpload.is_image) else {
print("Upload failed getting media url")
guard let url = imageUploader.getImageURL(from: responseString) else {
print("Upload failed getting image url")
return .failed(nil)
}
@@ -78,6 +66,67 @@ func create_upload_request(mediaToUpload: MediaUpload, mediaUploader: MediaUploa
} catch {
return .failed(error)
}
}
extension PostView {
struct ImagePicker: UIViewControllerRepresentable {
@Environment(\.presentationMode)
private var presentationMode
let sourceType: UIImagePickerController.SourceType
let onImagePicked: (UIImage) -> Void
final class Coordinator: NSObject,
UINavigationControllerDelegate,
UIImagePickerControllerDelegate {
@Binding
private var presentationMode: PresentationMode
private let sourceType: UIImagePickerController.SourceType
private let onImagePicked: (UIImage) -> Void
init(presentationMode: Binding<PresentationMode>,
sourceType: UIImagePickerController.SourceType,
onImagePicked: @escaping (UIImage) -> Void) {
_presentationMode = presentationMode
self.sourceType = sourceType
self.onImagePicked = onImagePicked
}
func imagePickerController(_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let uiImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage
onImagePicked(uiImage)
presentationMode.dismiss()
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
presentationMode.dismiss()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(presentationMode: presentationMode,
sourceType: sourceType,
onImagePicked: onImagePicked)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = sourceType
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController,
context: UIViewControllerRepresentableContext<ImagePicker>) {
}
}
}
extension NSMutableData {
@@ -89,7 +138,7 @@ extension NSMutableData {
}
}
enum MediaUploader: String, CaseIterable, Identifiable {
enum ImageUploader: String, CaseIterable, Identifiable {
var id: String { self.rawValue }
case nostrBuild
case nostrImg
@@ -103,12 +152,12 @@ enum MediaUploader: String, CaseIterable, Identifiable {
}
}
var supportsVideo: Bool {
var displayImageUploaderName: String {
switch self {
case .nostrBuild:
return true
return "NostrBuild"
case .nostrImg:
return false
return "NostrImg"
}
}
@@ -138,7 +187,7 @@ enum MediaUploader: String, CaseIterable, Identifiable {
}
}
func getMediaURL(from responseString: String, mediaIsImage: Bool) -> String? {
func getImageURL(from responseString: String) -> String? {
switch self {
case .nostrBuild:
guard let startIndex = responseString.range(of: "nostr.build_")?.lowerBound else {
@@ -150,7 +199,7 @@ enum MediaUploader: String, CaseIterable, Identifiable {
return nil
}
let nostrBuildImageName = responseString[startIndex..<endIndex]
let nostrBuildURL = mediaIsImage ? "https://nostr.build/i/\(nostrBuildImageName)" : "https://nostr.build/av/\(nostrBuildImageName)"
let nostrBuildURL = "https://nostr.build/i/\(nostrBuildImageName)"
return nostrBuildURL
case .nostrImg:
+1 -1
View File
@@ -114,7 +114,7 @@ struct ChatView: View {
show_images: show_images,
size: .normal,
artifacts: .just_content(event.content),
options: [])
truncate: false)
if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey {
let bar = make_actionbar_model(ev: event.id, damus: damus_state)
+267 -23
View File
@@ -17,47 +17,217 @@ struct ConfigView: View {
@State var confirm_logout: Bool = false
@State var delete_account_warning: Bool = false
@State var confirm_delete_account: Bool = false
@State var show_privkey: Bool = false
@State var has_authenticated_locally: Bool = false
@State var show_api_key: Bool = false
@State var privkey: String
@State var privkey_copied: Bool = false
@State var pubkey_copied: Bool = false
@State var delete_text: String = ""
@State var default_zap_amount: String
@ObservedObject var settings: UserSettingsStore
private let DELETE_KEYWORD = "DELETE"
let generator = UIImpactFeedbackGenerator(style: .light)
init(state: DamusState) {
self.state = state
let zap_amt = get_default_zap_amount(pubkey: state.pubkey).map({ "\($0)" }) ?? "1000"
_default_zap_amount = State(initialValue: zap_amt)
_privkey = State(initialValue: self.state.keypair.privkey_bech32 ?? "")
_settings = ObservedObject(initialValue: state.settings)
}
func textColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white
colorScheme == .light ? Color("DamusBlack") : Color("DamusWhite")
}
func authenticateLocally(completion: @escaping (Bool) -> Void) {
// Need to authenticate only once while ConfigView is presented
guard !has_authenticated_locally else {
completion(true)
return
}
let context = LAContext()
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) {
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: NSLocalizedString("Local authentication to access private key", comment: "Face ID usage description shown when trying to access private key")) { success, error in
DispatchQueue.main.async {
has_authenticated_locally = success
completion(success)
}
}
} else {
// If there's no authentication set up on the device, let the user copy the key without it
has_authenticated_locally = true
completion(true)
}
}
// TODO: (jb55) could be more general but not gonna worry about it atm
func CopyButton(is_pk: Bool) -> some View {
return Button(action: {
let copyKey = {
UIPasteboard.general.string = is_pk ? self.state.keypair.pubkey_bech32 : self.privkey
self.privkey_copied = !is_pk
self.pubkey_copied = is_pk
generator.impactOccurred()
}
if is_pk {
// When trying to copy npub
copyKey()
} else {
// When trying to copy nsec
if has_authenticated_locally {
copyKey()
} else {
authenticateLocally { success in
if success {
copyKey()
}
}
}
}
}) {
let copied = is_pk ? self.pubkey_copied : self.privkey_copied
Image(systemName: copied ? "checkmark.circle" : "doc.on.doc")
}
}
var body: some View {
ZStack(alignment: .leading) {
Form {
Section {
NavigationLink(destination: KeySettingsView(keypair: state.keypair)) {
IconLabel(NSLocalizedString("Keys", comment: "Settings section for managing keys"), img_name: "key.fill", color: .purple)
Section(NSLocalizedString("Public Account ID", comment: "Section title for the user's public account ID.")) {
HStack {
Text(state.keypair.pubkey_bech32)
CopyButton(is_pk: true)
}
NavigationLink(destination: AppearanceSettingsView(settings: settings)) {
IconLabel(NSLocalizedString("Appearance", comment: "Section header for text and appearance settings"), img_name: "textformat", color: .red)
.clipShape(RoundedRectangle(cornerRadius: 5))
}
if let sec = state.keypair.privkey_bech32 {
Section(NSLocalizedString("Secret Account Login Key", comment: "Section title for user's secret account login key.")) {
HStack {
if show_privkey == false || !has_authenticated_locally {
SecureField(NSLocalizedString("Private Key", comment: "Title of the secure field that holds the user's private key."), text: $privkey)
.disabled(true)
} else {
Text(sec)
.clipShape(RoundedRectangle(cornerRadius: 5))
}
CopyButton(is_pk: false)
}
Toggle(NSLocalizedString("Show", comment: "Toggle to show or hide user's secret account login key."), isOn: $show_privkey)
.onChange(of: show_privkey) { newValue in
if newValue {
authenticateLocally { success in
show_privkey = success
}
}
}
}
NavigationLink(destination: NotificationSettingsView(settings: settings)) {
IconLabel(NSLocalizedString("Local Notifications", comment: "Section header for damus local notifications user configuration"), img_name: "bell.fill", color: .blue)
}
NavigationLink(destination: ZapSettingsView(pubkey: state.pubkey, settings: settings)) {
IconLabel(NSLocalizedString("Zaps", comment: "Section header for zap settings"), img_name: "bolt.fill", color: .orange)
}
NavigationLink(destination: TranslationSettingsView(settings: settings)) {
IconLabel(NSLocalizedString("Translation", comment: "Section header for text and appearance settings"), img_name: "globe.americas.fill", color: .green)
}
Section(NSLocalizedString("Wallet Selector", comment: "Section title for selection of wallet.")) {
Toggle(NSLocalizedString("Show wallet selector", comment: "Toggle to show or hide selection of wallet."), isOn: $settings.show_wallet_selector).toggleStyle(.switch)
Picker(NSLocalizedString("Select default wallet", comment: "Prompt selection of user's default wallet"),
selection: $settings.default_wallet) {
ForEach(Wallet.allCases, id: \.self) { wallet in
Text(wallet.model.displayName)
.tag(wallet.model.tag)
}
}
}
Section(NSLocalizedString("Default Zap Amount in sats", comment: "Section title for zap configuration")) {
TextField(String("1000"), text: $default_zap_amount)
.keyboardType(.numberPad)
.onReceive(Just(default_zap_amount)) { newValue in
if let parsed = handle_string_amount(new_value: newValue) {
self.default_zap_amount = String(parsed)
set_default_zap_amount(pubkey: self.state.pubkey, amount: parsed)
}
}
}
Section(NSLocalizedString("Translations", comment: "Section title for selecting the translation service.")) {
Picker(NSLocalizedString("Service", comment: "Prompt selection of translation service provider."), selection: $settings.translation_service) {
ForEach(TranslationService.allCases, id: \.self) { server in
Text(server.model.displayName)
.tag(server.model.tag)
}
}
if settings.translation_service == .libretranslate {
Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $settings.libretranslate_server) {
ForEach(LibreTranslateServer.allCases, id: \.self) { server in
Text(server.model.displayName)
.tag(server.model.tag)
}
}
if settings.libretranslate_server == .custom {
TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_url)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
}
SecureField(NSLocalizedString("API Key (optional)", comment: "Prompt for optional entry of API Key to use translation server."), text: $settings.libretranslate_api_key)
.disableAutocorrection(true)
.disabled(settings.translation_service != .libretranslate)
.autocapitalization(UITextAutocapitalizationType.none)
}
if settings.translation_service == .deepl {
Picker(NSLocalizedString("Plan", comment: "Prompt selection of DeepL subscription plan to perform machine translations on notes"), selection: $settings.deepl_plan) {
ForEach(DeepLPlan.allCases, id: \.self) { server in
Text(server.model.displayName)
.tag(server.model.tag)
}
}
SecureField(NSLocalizedString("API Key (required)", comment: "Prompt for required entry of API Key to use translation server."), text: $settings.deepl_api_key)
.disableAutocorrection(true)
.disabled(settings.translation_service != .deepl)
.autocapitalization(UITextAutocapitalizationType.none)
if settings.deepl_api_key == "" {
Link(NSLocalizedString("Get API Key", comment: "Button to navigate to DeepL website to get a translation API key."), destination: URL(string: "https://www.deepl.com/pro-api")!)
}
}
}
Section(NSLocalizedString("Miscellaneous", comment: "Section header for miscellaneous user configuration")) {
Toggle(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen"), isOn: $settings.left_handed)
.toggleStyle(.switch)
Toggle(NSLocalizedString("Zap Vibration", comment: "Setting to enable vibration on zap"), isOn: $settings.zap_vibration)
.toggleStyle(.switch)
}
Section(NSLocalizedString("Images", comment: "Section title for images configuration.")) {
Toggle(NSLocalizedString("Disable animations", comment: "Button to disable image animation"), isOn: $settings.disable_animation)
.toggleStyle(.switch)
.onChange(of: settings.disable_animation) { _ in
clear_kingfisher_cache()
}
Toggle(NSLocalizedString("Always show images", comment: "Setting to always show and never blur images"), isOn: $settings.always_show_images)
.toggleStyle(.switch)
Button(NSLocalizedString("Clear Cache", comment: "Button to clear image cache.")) {
clear_kingfisher_cache()
}
Picker(NSLocalizedString("Select image uploader", comment: "Prompt selection of user's image uploader"),
selection: $settings.default_image_uploader) {
ForEach(ImageUploader.allCases, id: \.self) { uploader in
Text(uploader.model.displayName)
.tag(uploader.model.tag)
}
}
}
Section(NSLocalizedString("Sign Out", comment: "Section title for signing out")) {
Button(action: {
if state.keypair.privkey == nil {
@@ -99,7 +269,7 @@ struct ConfigView: View {
}
}
.alert(NSLocalizedString("Permanently Delete Account", comment: "Alert for deleting the users account."), isPresented: $confirm_delete_account) {
TextField(String(format: NSLocalizedString("Type %@ to delete", comment: "Text field prompt asking user to type DELETE in all caps to confirm that they want to proceed with deleting their account."), DELETE_KEYWORD), text: $delete_text)
TextField(NSLocalizedString("Type DELETE to delete", comment: "Text field prompt asking user to type the word DELETE to confirm that they want to proceed with deleting their account. The all caps lock DELETE word should not be translated. Everything else should."), text: $delete_text)
Button(NSLocalizedString("Cancel", comment: "Cancel deleting the user."), role: .cancel) {
confirm_delete_account = false
}
@@ -108,12 +278,12 @@ struct ConfigView: View {
return
}
guard delete_text == DELETE_KEYWORD else {
guard delete_text == "DELETE" else {
return
}
let ev = created_deleted_account_profile(keypair: full_kp)
state.postbox.send(ev)
state.pool.send(.event(ev))
notify(.logout, ())
}
}
@@ -132,6 +302,80 @@ struct ConfigView: View {
}
}
var libretranslate_view: some View {
VStack {
Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $settings.libretranslate_server) {
ForEach(LibreTranslateServer.allCases, id: \.self) { server in
Text(server.model.displayName)
.tag(server.model.tag)
}
}
TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_url)
.disableAutocorrection(true)
.disabled(settings.libretranslate_server != .custom)
.autocapitalization(UITextAutocapitalizationType.none)
HStack {
let libretranslate_api_key_placeholder = NSLocalizedString("API Key (optional)", comment: "Prompt for optional entry of API Key to use translation server.")
if show_api_key {
TextField(libretranslate_api_key_placeholder, text: $settings.libretranslate_api_key)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
if settings.libretranslate_api_key != "" {
Button(NSLocalizedString("Hide API Key", comment: "Button to hide the LibreTranslate server API key.")) {
show_api_key = false
}
}
} else {
SecureField(libretranslate_api_key_placeholder, text: $settings.libretranslate_api_key)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
if settings.libretranslate_api_key != "" {
Button(NSLocalizedString("Show API Key", comment: "Button to show the LibreTranslate server API key.")) {
show_api_key = true
}
}
}
}
}
}
var deepl_view: some View {
VStack {
Picker(NSLocalizedString("Plan", comment: "Prompt selection of DeepL subscription plan to perform machine translations on notes"), selection: $settings.deepl_plan) {
ForEach(DeepLPlan.allCases, id: \.self) { server in
Text(server.model.displayName)
.tag(server.model.tag)
}
}
HStack {
let deepl_api_key_placeholder = NSLocalizedString("API Key (required)", comment: "Prompt for required entry of API Key to use translation server.")
if show_api_key {
TextField(deepl_api_key_placeholder, text: $settings.deepl_api_key)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
if settings.deepl_api_key != "" {
Button(NSLocalizedString("Hide API Key", comment: "Button to hide the DeepL translation API key.")) {
show_api_key = false
}
}
} else {
SecureField(deepl_api_key_placeholder, text: $settings.deepl_api_key)
.disableAutocorrection(true)
.autocapitalization(UITextAutocapitalizationType.none)
if settings.deepl_api_key != "" {
Button(NSLocalizedString("Show API Key", comment: "Button to show the DeepL translation API key.")) {
show_api_key = true
}
}
}
if settings.deepl_api_key == "" {
Link(NSLocalizedString("Get API Key", comment: "Button to navigate to DeepL website to get a translation API key."), destination: URL(string: "https://www.deepl.com/pro-api")!)
}
}
}
}
}
struct ConfigView_Previews: PreviewProvider {
+1 -10
View File
@@ -9,12 +9,9 @@ import SwiftUI
struct CreateAccountView: View {
@StateObject var account: CreateAccountModel = CreateAccountModel()
@StateObject var profileUploadViewModel = ProfileUploadingViewModel()
@State var is_light: Bool = false
@State var is_done: Bool = false
@State var reading_eula: Bool = false
@State var profile_image: URL? = nil
func SignupForm<FormContent: View>(@ViewBuilder content: () -> FormContent) -> some View {
return VStack(alignment: .leading, spacing: 10.0, content: content)
@@ -35,7 +32,7 @@ struct CreateAccountView: View {
.font(.title.bold())
.foregroundColor(.white)
ProfilePictureSelector(pubkey: account.pubkey, viewModel: profileUploadViewModel, callback: uploadedProfilePicture(image_url:))
ProfilePictureSelector(pubkey: account.pubkey)
HStack(alignment: .top) {
VStack {
@@ -84,8 +81,6 @@ struct CreateAccountView: View {
self.is_done = true
}
.padding()
.disabled(profileUploadViewModel.isLoading)
.opacity(profileUploadViewModel.isLoading ? 0.5 : 1)
}
.padding(.leading, 14.0)
.padding(.trailing, 20.0)
@@ -96,10 +91,6 @@ struct CreateAccountView: View {
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: BackNav())
}
func uploadedProfilePicture(image_url: URL?) {
account.profile_image = image_url?.absoluteString
}
}
struct BackNav: View {
+1 -4
View File
@@ -130,10 +130,7 @@ struct DMChatView: View {
dms.draft = ""
damus_state.postbox.send(dm)
handle_incoming_dm(ev: dm, our_pubkey: damus_state.pubkey, dms: damus_state.dms, prev_events: NewEventsBits())
damus_state.pool.send(.event(dm))
end_editing()
}
+3 -22
View File
@@ -14,18 +14,8 @@ struct DMView: View {
var is_ours: Bool {
event.pubkey == damus_state.pubkey
}
var Mention: some View {
Group {
if let mention = first_eref_mention(ev: event, privkey: damus_state.keypair.privkey) {
BuilderEventView(damus: damus_state, event_id: mention.ref.id)
} else {
EmptyView()
}
}
}
var DM: some View {
var body: some View {
HStack {
if is_ours {
Spacer(minLength: UIScreen.main.bounds.width * 0.2)
@@ -33,7 +23,7 @@ struct DMView: View {
let should_show_img = should_show_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: .normal, artifacts: .just_content(event.get_content(damus_state.keypair.privkey)), options: [])
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: .normal, artifacts: .just_content(event.get_content(damus_state.keypair.privkey)), truncate: false)
.padding([.top, .leading, .trailing], 10)
.padding([.bottom], 25)
.background(VisualEffectView(effect: UIBlurEffect(style: .prominent))
@@ -46,20 +36,11 @@ struct DMView: View {
.foregroundColor(.gray)
.opacity(0.8)
.offset(x: -10, y: -5), alignment: .bottomTrailing)
if !is_ours {
Spacer(minLength: UIScreen.main.bounds.width * 0.2)
}
}
}
var body: some View {
VStack {
Mention
DM
}
}
}
struct DMView_Previews: PreviewProvider {
+4 -12
View File
@@ -67,7 +67,6 @@ struct EditMetadataView: View {
@Environment(\.colorScheme) var colorScheme
@State var confirm_ln_address: Bool = false
@StateObject var profileUploadViewModel = ProfileUploadingViewModel()
init (damus_state: DamusState) {
self.damus_state = damus_state
@@ -84,7 +83,7 @@ struct EditMetadataView: View {
}
func imageBorderColor() -> Color {
colorScheme == .light ? DamusColors.white : DamusColors.black
colorScheme == .light ? Color("DamusWhite") : Color("DamusBlack")
}
func save() {
@@ -103,7 +102,7 @@ struct EditMetadataView: View {
let m_metadata_ev = make_metadata_event(keypair: damus_state.keypair, metadata: metadata)
if let metadata_ev = m_metadata_ev {
damus_state.postbox.send(metadata_ev)
damus_state.pool.send(.event(metadata_ev))
}
}
@@ -127,7 +126,7 @@ struct EditMetadataView: View {
let pfp_size: CGFloat = 90.0
HStack(alignment: .center) {
ProfilePictureSelector(pubkey: damus_state.pubkey, damus_state: damus_state, viewModel: profileUploadViewModel, callback: uploadedProfilePicture(image_url:))
ProfilePicView(pubkey: damus_state.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles)
.offset(y: -(pfp_size/2.0)) // Increase if set a frame
Spacer()
@@ -202,10 +201,8 @@ struct EditMetadataView: View {
}, footer: {
if let parts = nip05_parts {
Text("'\(parts.username)' at '\(parts.host)' will be used for verification", comment: "Description of how the nip05 identifier would be used for verification.")
} else if !nip05.isEmpty {
Text("'\(nip05)' is an invalid NIP-05 identifier. It should look like an email.", comment: "Description of why the nip05 identifier is invalid.")
} else {
Text("") // without this, the keyboard dismisses unnecessarily when the footer changes state
Text("'\(nip05)' is an invalid NIP-05 identifier. It should look like an email.", comment: "Description of why the nip05 identifier is invalid.")
}
})
@@ -217,7 +214,6 @@ struct EditMetadataView: View {
dismiss()
}
}
.disabled(profileUploadViewModel.isLoading)
.alert(NSLocalizedString("Invalid Tip Address", comment: "Title of alerting as invalid tip address."), isPresented: $confirm_ln_address) {
Button(NSLocalizedString("Ok", comment: "Button to dismiss the alert.")) {
}
@@ -228,10 +224,6 @@ struct EditMetadataView: View {
}
.ignoresSafeArea(edges: .top)
}
func uploadedProfilePicture(image_url: URL?) {
picture = image_url?.absoluteString ?? ""
}
}
struct EditMetadataView_Previews: PreviewProvider {
@@ -1,37 +0,0 @@
//
// EmptyUserSearchView.swift
// damus
//
// Created by eric on 4/3/23.
//
//
// EmptyUserSearchView.swift
// damus
//
// Created by eric on 4/3/23.
//
import SwiftUI
struct EmptyUserSearchView: View {
var body: some View {
VStack {
Image(systemName: "person.fill.questionmark")
.font(.system(size: 35))
.padding()
Text("Could not find the user you're looking for", comment: "Indicates that there are no users found.")
.multilineTextAlignment(.center)
.font(.callout.weight(.medium))
}
.foregroundColor(.gray)
.padding()
}
}
struct EmptyUserSearchView_Previews: PreviewProvider {
static var previews: some View {
EmptyUserSearchView()
}
}
+29 -15
View File
@@ -25,16 +25,7 @@ func eventviewsize_to_font(_ size: EventViewKind) -> Font {
}
}
func eventviewsize_to_uifont(_ size: EventViewKind) -> UIFont {
switch size {
case .small:
return .preferredFont(forTextStyle: .body)
case .normal:
return .preferredFont(forTextStyle: .body)
case .selected:
return .preferredFont(forTextStyle: .title2)
}
}
struct EventView: View {
let event: NostrEvent
@@ -69,7 +60,17 @@ struct EventView: View {
VStack {
if event.known_kind == .boost {
if let inner_ev = event.inner_event {
RepostedEvent(damus: damus, event: event, inner_ev: inner_ev, options: options)
VStack(alignment: .leading) {
let prof = damus.profiles.lookup(id: event.pubkey)
let booster_profile = ProfileView(damus_state: damus, pubkey: event.pubkey)
NavigationLink(destination: booster_profile) {
Reposted(damus: damus, pubkey: event.pubkey, profile: prof)
}
.buttonStyle(PlainButtonStyle())
TextEvent(damus: damus, event: inner_ev, pubkey: inner_ev.pubkey, options: options)
.padding([.top], 1)
}
} else {
EmptyView()
}
@@ -81,7 +82,7 @@ struct EventView: View {
}
} else {
TextEvent(damus: damus, event: event, pubkey: pubkey, options: options)
//.padding([.top], 6)
.padding([.top], 6)
}
}
}
@@ -151,9 +152,22 @@ func format_date(_ created_at: Int64) -> String {
}
func make_actionbar_model(ev: String, damus: DamusState) -> ActionBarModel {
let model = ActionBarModel.empty()
model.update(damus: damus, evid: ev)
return model
let likes = damus.likes.counts[ev]
let boosts = damus.boosts.counts[ev]
let zaps = damus.zaps.event_counts[ev]
let zap_total = damus.zaps.event_totals[ev]
let our_like = damus.likes.our_events[ev]
let our_boost = damus.boosts.our_events[ev]
let our_zap = damus.zaps.our_zaps[ev]
return ActionBarModel(likes: likes ?? 0,
boosts: boosts ?? 0,
zaps: zaps ?? 0,
zap_total: zap_total ?? 0,
our_like: our_like,
our_boost: our_boost,
our_zap: our_zap?.first
)
}
+1 -5
View File
@@ -30,11 +30,7 @@ struct EmbeddedEventView: View {
.minimumScaleFactor(0.75)
.lineLimit(1)
if event_is_reply(event, privkey: damus_state.keypair.privkey) {
ReplyDescription(event: event, profiles: damus_state.profiles)
}
EventBody(damus_state: damus_state, event: event, size: .small, options: [.truncate_content])
EventBody(damus_state: damus_state, event: event, size: .small)
}
}
}
+7 -5
View File
@@ -12,13 +12,11 @@ struct EventBody: View {
let event: NostrEvent
let size: EventViewKind
let should_show_img: Bool
let options: EventViewOptions
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind, should_show_img: Bool? = nil, options: EventViewOptions) {
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind, should_show_img: Bool? = nil) {
self.damus_state = damus_state
self.event = event
self.size = size
self.options = options
self.should_show_img = should_show_img ?? should_show_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
}
@@ -27,13 +25,17 @@ struct EventBody: View {
}
var body: some View {
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: size, artifacts: .just_content(content), options: options)
if event_is_reply(event, privkey: damus_state.keypair.privkey) {
ReplyDescription(event: event, profiles: damus_state.profiles)
}
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: size, artifacts: .just_content(content), truncate: true)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
struct EventBody_Previews: PreviewProvider {
static var previews: some View {
EventBody(damus_state: test_damus_state(), event: test_event, size: .normal, options: [])
EventBody(damus_state: test_damus_state(), event: test_event, size: .normal)
}
}
+2 -2
View File
@@ -102,9 +102,9 @@ struct MenuItems: View {
}
Button(role: .destructive) {
notify(.mute, target_pubkey)
notify(.block, target_pubkey)
} label: {
Label(NSLocalizedString("Mute", comment: "Context menu option for muting users."), systemImage: "exclamationmark.octagon")
Label(NSLocalizedString("Block", comment: "Context menu option for blocking users."), systemImage: "exclamationmark.octagon")
}
}
}
+7 -2
View File
@@ -14,6 +14,7 @@ struct MutedEventView: View {
let selected: Bool
@State var shown: Bool
@Environment(\.colorScheme) var colorScheme
init(damus_state: DamusState, event: NostrEvent, scroller: ScrollViewProxy?, selected: Bool) {
self.damus_state = damus_state
@@ -27,10 +28,14 @@ struct MutedEventView: View {
return !should_show_event(contacts: damus_state.contacts, ev: event)
}
var FillColor: Color {
colorScheme == .light ? Color("DamusLightGrey") : Color("DamusDarkGrey")
}
var MutedBox: some View {
ZStack {
RoundedRectangle(cornerRadius: 20)
.foregroundColor(DamusColors.adaptableGrey)
.foregroundColor(FillColor)
HStack {
Text("Post from a user you've blocked", comment: "Text to indicate that what is being shown is a post from a user who has been blocked.")
@@ -46,7 +51,7 @@ struct MutedEventView: View {
var Event: some View {
Group {
if selected {
SelectedEventView(damus: damus_state, event: event, size: .selected)
SelectedEventView(damus: damus_state, event: event)
} else {
EventView(damus: damus_state, event: event)
}
+5 -15
View File
@@ -10,7 +10,6 @@ import SwiftUI
struct SelectedEventView: View {
let damus: DamusState
let event: NostrEvent
let size: EventViewKind
var pubkey: String {
event.pubkey
@@ -18,10 +17,9 @@ struct SelectedEventView: View {
@StateObject var bar: ActionBarModel
init(damus: DamusState, event: NostrEvent, size: EventViewKind) {
init(damus: DamusState, event: NostrEvent) {
self.damus = damus
self.event = event
self.size = size
self._bar = StateObject(wrappedValue: make_actionbar_model(ev: event.id, damus: damus))
}
@@ -39,24 +37,17 @@ struct SelectedEventView: View {
.padding([.bottom], 4)
}
.padding(.horizontal)
.minimumScaleFactor(0.75)
.lineLimit(1)
if event_is_reply(event, privkey: damus.keypair.privkey) {
ReplyDescription(event: event, profiles: damus.profiles)
.padding(.horizontal)
}
EventBody(damus_state: damus, event: event, size: size, options: [.pad_content])
EventBody(damus_state: damus, event: event, size: .selected)
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
BuilderEventView(damus: damus, event_id: mention.ref.id)
.padding(.horizontal)
}
Text(verbatim: "\(format_date(event.created_at))")
.padding([.top, .leading, .trailing])
.padding(.top, 10)
.font(.footnote)
.foregroundColor(.gray)
@@ -65,13 +56,11 @@ struct SelectedEventView: View {
if !bar.is_empty {
EventDetailBar(state: damus, target: event.id, target_pk: event.pubkey)
.padding(.horizontal)
Divider()
}
EventActionBar(damus_state: damus, event: event)
.padding([.top], 4)
.padding(.horizontal)
Divider()
.padding([.top], 4)
@@ -81,6 +70,7 @@ struct SelectedEventView: View {
guard target == self.event.id else { return }
self.bar.update(damus: self.damus, evid: target)
}
.padding([.leading], 2)
.compositingGroup()
}
}
@@ -88,7 +78,7 @@ struct SelectedEventView: View {
struct SelectedEventView_Previews: PreviewProvider {
static var previews: some View {
SelectedEventView(damus: test_damus_state(), event: test_event, size: .selected)
SelectedEventView(damus: test_damus_state(), event: test_event)
.padding()
}
}
+48 -141
View File
@@ -12,9 +12,6 @@ struct EventViewOptions: OptionSet {
static let no_action_bar = EventViewOptions(rawValue: 1 << 0)
static let no_replying_to = EventViewOptions(rawValue: 1 << 1)
static let no_images = EventViewOptions(rawValue: 1 << 2)
static let wide = EventViewOptions(rawValue: 1 << 3)
static let truncate_content = EventViewOptions(rawValue: 1 << 4)
static let pad_content = EventViewOptions(rawValue: 1 << 5)
}
struct TextEvent: View {
@@ -28,12 +25,51 @@ struct TextEvent: View {
}
var body: some View {
Group {
if options.contains(.wide) {
WideStyle
} else {
ThreadedStyle
HStack(alignment: .top) {
let profile = damus.profiles.lookup(id: pubkey)
let is_anon = event_is_anonymous(ev: event)
VStack {
MaybeAnonPfpView(state: damus, is_anon: is_anon, pubkey: pubkey)
Spacer()
}
VStack(alignment: .leading, spacing: 1) {
HStack(alignment: .center, spacing: 0) {
let pk = is_anon ? "anon" : pubkey
EventProfileName(pubkey: pk, profile: profile, damus: damus, show_friend_confirmed: true, size: .normal)
Text(verbatim: "")
.font(.footnote)
.foregroundColor(.gray)
Text(verbatim: "\(format_relative_time(event.created_at))")
.font(.system(size: 16))
.foregroundColor(.gray)
Spacer()
EventMenuContext(event: event, keypair: damus.keypair, target_pubkey: event.pubkey, bookmarks: damus.bookmarks)
.padding([.bottom], 4)
}
.lineLimit(1)
EventBody(damus_state: damus, event: event, size: .normal)
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
BuilderEventView(damus: damus, event_id: mention.ref.id)
}
if has_action_bar {
Rectangle().frame(height: 2).opacity(0)
EventActionBar(damus_state: damus, event: event)
.padding([.top], 4)
}
}
.padding([.leading], 2)
}
.contentShape(Rectangle())
.background(event_validity_color(event.validity))
@@ -41,127 +77,11 @@ struct TextEvent: View {
.frame(maxWidth: .infinity, minHeight: PFP_SIZE)
.padding([.bottom], 2)
}
func Pfp(is_anon: Bool) -> some View {
MaybeAnonPfpView(state: damus, is_anon: is_anon, pubkey: pubkey)
}
func TopPart(is_anon: Bool) -> some View {
HStack(alignment: .center, spacing: 0) {
ProfileName(is_anon: is_anon)
TimeDot
Time
Spacer()
ContextButton
}
.lineLimit(1)
}
var ReplyPart: some View {
Group {
if event_is_reply(event, privkey: damus.keypair.privkey) {
ReplyDescription(event: event, profiles: damus.profiles)
} else {
EmptyView()
}
}
}
var WideStyle: some View {
VStack(alignment: .leading) {
let is_anon = event_is_anonymous(ev: event)
HStack(spacing: 10) {
Pfp(is_anon: is_anon)
VStack {
TopPart(is_anon: is_anon)
ReplyPart
}
}
.padding(.horizontal)
}
EvBody(options: self.options.union(.pad_content))
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
Mention(mention)
.padding(.horizontal)
}
if has_action_bar {
//EmptyRect
ActionBar
.padding(.horizontal)
}
}
}
var TimeDot: some View {
Text(verbatim: "")
.font(.footnote)
.foregroundColor(.gray)
}
var Time: some View {
Text(verbatim: "\(format_relative_time(event.created_at))")
.font(.system(size: 16))
.foregroundColor(.gray)
}
var ContextButton: some View {
EventMenuContext(event: event, keypair: damus.keypair, target_pubkey: event.pubkey, bookmarks: damus.bookmarks)
.padding([.bottom], 4)
}
func ProfileName(is_anon: Bool) -> some View {
let profile = damus.profiles.lookup(id: pubkey)
let pk = is_anon ? "anon" : pubkey
return EventProfileName(pubkey: pk, profile: profile, damus: damus, show_friend_confirmed: true, size: .normal)
}
func EvBody(options: EventViewOptions) -> some View {
return EventBody(damus_state: damus, event: event, size: .normal, options: options)
}
func Mention(_ mention: Mention) -> some View {
return BuilderEventView(damus: damus, event_id: mention.ref.id)
}
var ActionBar: some View {
return EventActionBar(damus_state: damus, event: event)
.padding([.top], 4)
}
var EmptyRect: some View {
return Rectangle().frame(height: 2).opacity(0)
}
var ThreadedStyle: some View {
HStack(alignment: .top) {
let is_anon = event_is_anonymous(ev: event)
VStack {
Pfp(is_anon: is_anon)
Spacer()
}
VStack(alignment: .leading) {
TopPart(is_anon: is_anon)
ReplyPart
EvBody(options: self.options)
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
Mention(mention)
}
if has_action_bar {
EmptyRect
ActionBar
}
}
.padding([.leading], 2)
}
struct TextEvent_Previews: PreviewProvider {
static var previews: some View {
TextEvent(damus: test_damus_state(), event: test_event, pubkey: "pk", options: [])
}
}
@@ -179,16 +99,3 @@ func event_has_tag(ev: NostrEvent, tag: String) -> Bool {
func event_is_anonymous(ev: NostrEvent) -> Bool {
return ev.known_kind == .zap_request && event_has_tag(ev: ev, tag: "anon")
}
struct TextEvent_Previews: PreviewProvider {
static var previews: some View {
VStack(spacing: 20) {
TextEvent(damus: test_damus_state(), event: test_event, pubkey: "pk", options: [])
.frame(height: 400)
TextEvent(damus: test_damus_state(), event: test_event, pubkey: "pk", options: [.wide])
.frame(height: 400)
}
}
}
-22
View File
@@ -1,22 +0,0 @@
//
// WideEventView.swift
// damus
//
// Created by William Casarin on 2023-03-23.
//
import SwiftUI
struct WideEventView: View {
let event: NostrEvent
var body: some View {
EmptyView()
}
}
struct WideEventView_Previews: PreviewProvider {
static var previews: some View {
WideEventView(event: test_event)
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ struct ZapEvent: View {
if zap.private_request != nil {
Image(systemName: "lock.fill")
.foregroundColor(DamusColors.green)
.foregroundColor(Color("DamusGreen"))
.help(NSLocalizedString("Only you can see this message and who sent it.", comment: "Help text on green lock icon that explains that only the current user can see the message of a zap event and who sent the zap."))
}
}
+3 -3
View File
@@ -50,11 +50,11 @@ struct FollowButtonView: View {
}
func filledTextColor() -> Color {
colorScheme == .light ? DamusColors.white : DamusColors.black
colorScheme == .light ? Color("DamusWhite") : Color("DamusBlack")
}
func fillColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white
colorScheme == .light ? Color("DamusBlack") : Color("DamusWhite")
}
func emptyColor() -> Color {
@@ -62,7 +62,7 @@ struct FollowButtonView: View {
}
func borderColor() -> Color {
colorScheme == .light ? DamusColors.darkGrey : DamusColors.lightGrey
colorScheme == .light ? Color("DamusDarkGrey") : Color("DamusLightGrey")
}
}
+1 -1
View File
@@ -33,7 +33,6 @@ struct FollowersView: View {
LazyVStack(alignment: .leading) {
ForEach(followers.contacts ?? [], id: \.self) { pk in
FollowUserView(target: .pubkey(pk), damus_state: damus_state)
Divider()
}
}
.padding()
@@ -46,6 +45,7 @@ struct FollowersView: View {
followers.unsubscribe()
}
}
}
struct FollowingView: View {
-120
View File
@@ -1,120 +0,0 @@
//
// ImagePicker.swift
// damus
//
// Created by Swift on 3/31/23.
//
import UIKit
import SwiftUI
struct ImagePicker: UIViewControllerRepresentable {
@Environment(\.presentationMode)
private var presentationMode
let sourceType: UIImagePickerController.SourceType
let pubkey: String
var imagesOnly: Bool = false
let onImagePicked: (URL) -> Void
let onVideoPicked: (URL) -> Void
final class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
@Binding private var presentationMode: PresentationMode
private let sourceType: UIImagePickerController.SourceType
private let onImagePicked: (URL) -> Void
private let onVideoPicked: (URL) -> Void
init(presentationMode: Binding<PresentationMode>,
sourceType: UIImagePickerController.SourceType,
onImagePicked: @escaping (URL) -> Void,
onVideoPicked: @escaping (URL) -> Void) {
_presentationMode = presentationMode
self.sourceType = sourceType
self.onImagePicked = onImagePicked
self.onVideoPicked = onVideoPicked
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let videoURL = info[UIImagePickerController.InfoKey.mediaURL] as? URL {
// Handle the selected video
onVideoPicked(videoURL)
} else if let imageURL = info[UIImagePickerController.InfoKey.imageURL] as? URL {
// Handle the selected image
onImagePicked(imageURL)
} else if let cameraImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
if let imageURL = saveImageToTemporaryFolder(image: cameraImage, imageType: "jpeg") {
onImagePicked(imageURL)
}
} else if let editedImage = info[UIImagePickerController.InfoKey.editedImage] as? UIImage {
if let editedImageURL = saveImageToTemporaryFolder(image: editedImage) {
onImagePicked(editedImageURL)
}
}
presentationMode.dismiss()
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
presentationMode.dismiss()
}
func saveImageToTemporaryFolder(image: UIImage, imageType: String = "png") -> URL? {
// Convert UIImage to Data
let imageData: Data?
if imageType.lowercased() == "jpeg" {
imageData = image.jpegData(compressionQuality: 1.0)
} else {
imageData = image.pngData()
}
guard let data = imageData else {
print("Failed to convert UIImage to Data.")
return nil
}
// Generate a temporary URL with a unique filename
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let uniqueImageName = "\(UUID().uuidString).\(imageType)"
let temporaryImageURL = temporaryDirectoryURL.appendingPathComponent(uniqueImageName)
// Save the image data to the temporary URL
do {
try data.write(to: temporaryImageURL)
return temporaryImageURL
} catch {
print("Error saving image data to temporary URL: \(error.localizedDescription)")
return nil
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(presentationMode: presentationMode,
sourceType: sourceType,
onImagePicked: { url in
// Handle the selected image URL
onImagePicked(url)
},
onVideoPicked: { videoURL in
// Handle the selected video URL
onVideoPicked(videoURL)
})
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = sourceType
let mediaUploader = get_media_uploader(pubkey)
picker.mediaTypes = ["public.image", "com.compuserve.gif"]
if mediaUploader.supportsVideo && !imagesOnly {
picker.mediaTypes.append("public.movie")
}
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController,
context: UIViewControllerRepresentableContext<ImagePicker>) {
}
}
+21 -1
View File
@@ -16,6 +16,26 @@ struct ImageView: View {
@State private var selectedIndex = 0
@State var showMenu = true
var navBarView: some View {
VStack {
HStack {
/*
Text(urls[selectedIndex]?.lastPathComponent ?? "")
.bold()
*/
Spacer()
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Image(systemName: "xmark")
})
}
.padding()
}
}
var tabViewIndicator: some View {
HStack(spacing: 10) {
ForEach(urls.indices, id: \.self) { index in
@@ -60,7 +80,7 @@ struct ImageView: View {
.overlay(
VStack {
if showMenu {
NavDismissBarView()
navBarView
Spacer()
if (urls.count > 1) {
+2 -6
View File
@@ -79,15 +79,11 @@ struct LoginView: View {
return
}
// this is a weird way to login anyways
/*
var bootstrap_relays = load_bootstrap_relays(pubkey: nip05.pubkey)
for relay in nip05.relays {
if !(bootstrap_relays.contains { $0 == relay }) {
bootstrap_relays.append(relay)
if !(BOOTSTRAP_RELAYS.contains { $0 == relay }) {
BOOTSTRAP_RELAYS.append(relay)
}
}
*/
save_pubkey(pubkey: nip05.pubkey)
notify(.login, ())
+2 -2
View File
@@ -26,7 +26,7 @@ struct MutelistView: View {
}
damus_state.contacts.set_mutelist(new_ev)
damus_state.postbox.send(new_ev)
damus_state.pool.send(.event(new_ev))
users = get_mutelist_users(new_ev)
} label: {
Label(NSLocalizedString("Delete", comment: "Button to remove a user from their blocklist."), systemImage: "trash")
@@ -43,7 +43,7 @@ struct MutelistView: View {
RemoveAction(pubkey: pubkey)
}
}
.navigationTitle(NSLocalizedString("Muted Users", comment: "Navigation title of view to see list of muted users."))
.navigationTitle(NSLocalizedString("Blocked Users", comment: "Navigation title of view to see list of blocked users."))
}
}
+45 -111
View File
@@ -28,48 +28,49 @@ struct NoteContentView: View {
let show_images: Bool
let size: EventViewKind
let preview_height: CGFloat?
let options: EventViewOptions
let translatable: Bool
let truncate: Bool
@State var artifacts: NoteArtifacts
@State var preview: LinkViewRepresentable?
init(damus_state: DamusState, event: NostrEvent, show_images: Bool, size: EventViewKind, artifacts: NoteArtifacts, options: EventViewOptions) {
init(damus_state: DamusState, event: NostrEvent, show_images: Bool, size: EventViewKind, artifacts: NoteArtifacts, truncate: Bool) {
self.damus_state = damus_state
self.event = event
self.show_images = show_images
self.size = size
self.options = options
self.translatable = damus_state.translations.shouldTranslate(event, state: damus_state)
self._artifacts = State(initialValue: artifacts)
self.preview_height = lookup_cached_preview_size(previews: damus_state.previews, evid: event.id)
self._preview = State(initialValue: load_cached_preview(previews: damus_state.previews, evid: event.id))
self._artifacts = State(initialValue: render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey))
self.truncate = truncate
}
var truncate: Bool {
return options.contains(.truncate_content)
}
var with_padding: Bool {
return options.contains(.pad_content)
}
var truncatedText: some View {
TruncatedText(text: artifacts.content, maxChars: (truncate ? 280 : nil))
.font(eventviewsize_to_font(size))
}
var invoicesView: some View {
InvoicesView(our_pubkey: damus_state.keypair.pubkey, invoices: artifacts.invoices)
}
func MainContent() -> some View {
return VStack(alignment: .leading) {
if size == .selected {
SelectableText(attributedString: artifacts.content)
TranslateView(damus_state: damus_state, event: event)
} else {
TruncatedText(text: artifacts.content, maxChars: (truncate ? 280 : nil))
.font(eventviewsize_to_font(size))
}
var translateView: some View {
TranslateView(damus_state: damus_state, event: event, size: self.size)
}
var previewView: some View {
Group {
if show_images && artifacts.images.count > 0 {
ImageCarousel(urls: artifacts.images)
} else if !show_images && artifacts.images.count > 0 {
ZStack {
ImageCarousel(urls: artifacts.images)
Blur()
.disabled(true)
}
.cornerRadius(10)
}
if artifacts.invoices.count > 0 {
InvoicesView(our_pubkey: damus_state.keypair.pubkey, invoices: artifacts.invoices)
}
if let preview = self.preview, show_images {
if let preview_height {
preview
@@ -84,64 +85,8 @@ struct NoteContentView: View {
}
}
var MainContent: some View {
VStack(alignment: .leading) {
if size == .selected {
if with_padding {
SelectableText(attributedString: artifacts.content, size: self.size)
.padding(.horizontal)
} else {
SelectableText(attributedString: artifacts.content, size: self.size)
}
} else {
if with_padding {
truncatedText
.padding(.horizontal)
} else {
truncatedText
}
}
if translatable {
if with_padding {
translateView
.padding(.horizontal)
} else {
translateView
}
}
if show_images && artifacts.images.count > 0 {
ImageCarousel(previews: damus_state.previews, evid: event.id, urls: artifacts.images)
} else if !show_images && artifacts.images.count > 0 {
ZStack {
ImageCarousel(previews: damus_state.previews, evid: event.id, urls: artifacts.images)
Blur()
.disabled(true)
}
//.cornerRadius(10)
}
if artifacts.invoices.count > 0 {
if with_padding {
invoicesView
.padding(.horizontal)
} else {
invoicesView
}
}
if with_padding {
previewView.padding(.horizontal)
} else {
previewView
}
}
}
var body: some View {
MainContent
MainContent()
.onReceive(handle_notify(.profile_updated)) { notif in
let profile = notif.object as! ProfileUpdate
let blocks = event.blocks(damus_state.keypair.privkey)
@@ -197,14 +142,14 @@ struct NoteContentView: View {
func hashtag_str(_ htag: String) -> AttributedString {
var attributedString = AttributedString(stringLiteral: "#\(htag)")
attributedString.link = URL(string: "damus:t:\(htag)")
attributedString.foregroundColor = DamusColors.purple
attributedString.foregroundColor = Color("DamusPurple")
return attributedString
}
func url_str(_ url: URL) -> AttributedString {
var attributedString = AttributedString(stringLiteral: url.absoluteString)
attributedString.link = url
attributedString.foregroundColor = DamusColors.purple
attributedString.foregroundColor = Color("DamusPurple")
return attributedString
}
@@ -216,13 +161,13 @@ func mention_str(_ m: Mention, profiles: Profiles) -> AttributedString {
let disp = Profile.displayName(profile: profile, pubkey: pk).username
var attributedString = AttributedString(stringLiteral: "@\(disp)")
attributedString.link = URL(string: "damus:\(encode_pubkey_uri(m.ref))")
attributedString.foregroundColor = DamusColors.purple
attributedString.foregroundColor = Color("DamusPurple")
return attributedString
case .event:
let bevid = bech32_note_id(m.ref.ref_id) ?? m.ref.ref_id
var attributedString = AttributedString(stringLiteral: "@\(abbrev_pubkey(bevid))")
attributedString.link = URL(string: "damus:\(encode_event_id_uri(m.ref))")
attributedString.foregroundColor = DamusColors.purple
attributedString.foregroundColor = Color("DamusPurple")
return attributedString
}
}
@@ -232,7 +177,17 @@ struct NoteContentView_Previews: PreviewProvider {
let state = test_damus_state()
let content = "hi there ¯\\_(ツ)_/¯ https://jb55.com/s/Oct12-150217.png 5739a762ef6124dd.jpg"
let artifacts = NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: [])
NoteContentView(damus_state: state, event: NostrEvent(content: content, pubkey: "pk"), show_images: true, size: .normal, artifacts: artifacts, options: [])
NoteContentView(damus_state: state, event: NostrEvent(content: content, pubkey: "pk"), show_images: true, size: .normal, artifacts: artifacts, truncate: false)
}
}
extension View {
func translate_button_style() -> some View {
return self
.font(.footnote)
.contentShape(Rectangle())
.padding([.top, .bottom], 10)
}
}
@@ -262,10 +217,7 @@ func render_blocks(blocks: [Block], profiles: Profiles, privkey: String?) -> Not
.filter({ $0.is_note_mention })
.count == 1
var ind: Int = -1
let txt: AttributedString = blocks.reduce("") { str, block in
ind = ind + 1
switch block {
case .mention(let m):
if m.type == .event && one_note_ref {
@@ -273,14 +225,7 @@ func render_blocks(blocks: [Block], profiles: Profiles, privkey: String?) -> Not
}
return str + mention_str(m, profiles: profiles)
case .text(let txt):
var trimmed = txt
if let prev = blocks[safe: ind-1], case .url(let u) = prev, is_image_url(u) {
trimmed = " " + trim_prefix(trimmed)
}
if let next = blocks[safe: ind+1], case .url(let u) = next, is_image_url(u) {
trimmed = trim_suffix(trimmed)
}
return str + AttributedString(stringLiteral: trimmed)
return str + AttributedString(stringLiteral: txt)
case .hashtag(let htag):
return str + hashtag_str(htag)
case .invoice(let invoice):
@@ -358,14 +303,3 @@ struct TruncatedText: View {
return AttributedString(truncatedAttributedString) + "..."
}
}
// trim suffix whitespace and newlines
func trim_suffix(_ str: String) -> String {
return str.replacingOccurrences(of: "\\s+$", with: "", options: .regularExpression)
}
// trim prefix whitespace and newlines
func trim_prefix(_ str: String) -> String {
return str.replacingOccurrences(of: "^\\s+", with: "", options: .regularExpression)
}
+11 -15
View File
@@ -73,7 +73,7 @@ func event_group_author_name(profiles: Profiles, ind: Int, group: EventGroupType
}
if zap.is_anon {
return NSLocalizedString("Anonymous", comment: "Placeholder author name of the anonymous person who zapped an event.")
return "Anonymous"
}
return event_author_name(profiles: profiles, pubkey: zap.request.ev.pubkey)
@@ -184,12 +184,12 @@ struct EventGroupView: View {
switch group {
case .repost:
Image(systemName: "arrow.2.squarepath")
.foregroundColor(DamusColors.green)
.foregroundColor(Color("DamusGreen"))
case .reaction:
LINEAR_GRADIENT
.mask(Image("shaka-full")
.resizable()
).frame(width: 24, height: 24)
Image("shaka-full")
.resizable()
.frame(width: 24, height: 24)
.foregroundColor(.accentColor)
case .profile_zap(let zapgrp):
ZapIcon(zapgrp)
case .zap(let zapgrp):
@@ -206,21 +206,17 @@ struct EventGroupView: View {
VStack(alignment: .leading) {
ProfilePicturesView(state: state, events: group.events)
GroupDescription
if let event {
let thread = ThreadModel(event: event, damus_state: state)
let dest = ThreadView(state: state, thread: thread)
NavigationLink(destination: dest) {
VStack(alignment: .leading) {
GroupDescription
EventBody(damus_state: state, event: event, size: .normal, options: [.truncate_content])
.padding([.top], 1)
.padding([.trailing])
.foregroundColor(.gray)
}
Text(render_note_content(ev: event, profiles: state.profiles, privkey: state.keypair.privkey).content)
.padding([.top], 1)
.foregroundColor(.gray)
}
.buttonStyle(.plain)
} else {
GroupDescription
}
}
}
@@ -35,14 +35,6 @@ struct NotificationItemView: View {
notification_item_event(events: state.events, notif: item)
}
var options: EventViewOptions {
if state.settings.truncate_mention_text {
return [.wide, .truncate_content]
}
return [.wide]
}
func Item(_ ev: NostrEvent?) -> some View {
Group {
switch item {
@@ -60,12 +52,13 @@ struct NotificationItemView: View {
case .reply(let ev):
NavigationLink(destination: ThreadView(state: state, thread: ThreadModel(event: ev, damus_state: state))) {
EventView(damus: state, event: ev, options: options)
EventView(damus: state, event: ev)
}
.buttonStyle(.plain)
}
ThiccDivider()
Divider()
.padding([.top,.bottom], 5)
}
}
@@ -91,6 +91,7 @@ struct NotificationsView: View {
}
return Color.clear
})
.padding(.horizontal)
}
.coordinateSpace(name: "scroll")
.onReceive(handle_notify(.scroll_to_top)) { notif in
+5 -16
View File
@@ -16,34 +16,23 @@ struct ParticipantsView: View {
var body: some View {
VStack {
Text("Replying to", comment: "Text indicating that the view is used for editing which participants are replied to in a note.")
.font(.headline)
Text("Edit participants", comment: "Text indicating that the view is used for editing which participants are replied to in a note.")
HStack {
Spacer()
Button {
// Remove all "p" refs, keep "e" refs
references = originalReferences.eRefs
} label: {
Text("Remove all", comment: "Button label to remove all participants from a note reply.")
}
.font(.system(size: 14, weight: .bold))
.frame(width: 100, height: 30)
.foregroundColor(.white)
.background(LINEAR_GRADIENT)
.clipShape(Capsule())
.buttonStyle(.borderedProminent)
Spacer()
Button {
references = originalReferences
} label: {
Text("Add all", comment: "Button label to re-add all original participants as profiles to reply to in a note")
}
.font(.system(size: 14, weight: .bold))
.frame(width: 80, height: 30)
.foregroundColor(.white)
.background(LINEAR_GRADIENT)
.clipShape(Capsule())
.buttonStyle(.borderedProminent)
Spacer()
}
VStack {
@@ -67,7 +56,7 @@ struct ParticipantsView: View {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 30))
.foregroundColor(references.contains(participant) ? DamusColors.purple : .gray)
.foregroundColor(references.contains(participant) ? .purple : .gray)
}
.onTapGesture {
if references.contains(participant) {
+2 -2
View File
@@ -10,8 +10,8 @@ import SwiftUI
let BUTTON_SIZE = 57.0
let LINEAR_GRADIENT = LinearGradient(gradient: Gradient(colors: [
DamusColors.purple,
DamusColors.blue
Color("DamusPurple"),
Color("DamusBlue")
]), startPoint: .topTrailing, endPoint: .bottomTrailing)
func PostButton(action: @escaping () -> ()) -> some View {
+74 -114
View File
@@ -16,18 +16,16 @@ let POST_PLACEHOLDER = NSLocalizedString("Type your post here...", comment: "Tex
struct PostView: View {
@State var post: NSMutableAttributedString = NSMutableAttributedString()
@State var cursor: Int = 0
@FocusState var focus: Bool
@State var showPrivateKeyWarning: Bool = false
@State var attach_media: Bool = false
@State var attach_camera: Bool = false
@State var error: String? = nil
@State var originalReferences: [ReferencedId] = []
@State var references: [ReferencedId] = []
@StateObject var image_upload: ImageUploadModel = ImageUploadModel()
let replying_to: NostrEvent?
let references: [ReferencedId]
let damus_state: DamusState
@Environment(\.presentationMode) var presentationMode
@@ -80,25 +78,14 @@ struct PostView: View {
attach_media = true
}, label: {
Image(systemName: "photo")
.padding(6)
})
}
var CameraButton: some View {
Button(action: {
attach_camera = true
}, label: {
Image(systemName: "camera")
.padding(6)
})
}
var AttachmentBar: some View {
HStack(alignment: .center) {
ImageButton
CameraButton
.disabled(image_upload.progress != nil)
}
.disabled(image_upload.progress != nil)
}
var PostButton: some View {
@@ -109,18 +96,16 @@ struct PostView: View {
self.send_post()
}
}
.disabled(is_post_empty)
.font(.system(size: 14, weight: .bold))
.frame(width: 80, height: 30)
.foregroundColor(.white)
.background(LINEAR_GRADIENT)
.opacity(is_post_empty ? 0.5 : 1.0)
.clipShape(Capsule())
}
var TextEntry: some View {
ZStack(alignment: .topLeading) {
TextViewWrapper(attributedText: $post)
TextViewWrapper(attributedText: $post, cursor: $cursor)
.focused($focus)
.textInputAutocapitalization(.sentences)
.onChange(of: post) { _ in
@@ -156,7 +141,9 @@ struct PostView: View {
Spacer()
PostButton
if !is_post_empty {
PostButton
}
}
if let progress = image_upload.progress {
@@ -165,7 +152,7 @@ struct PostView: View {
}
}
.frame(height: 30)
.padding([.bottom], 10)
.padding([.top, .bottom], 4)
}
func append_url(_ url: String) {
@@ -182,11 +169,11 @@ struct PostView: View {
post = combinedAttributedString
}
func handle_upload(media: MediaUpload) {
let uploader = get_media_uploader(damus_state.pubkey)
func handle_upload(image: UIImage) {
let uploader = get_image_uploader(damus_state.pubkey)
Task.init {
let res = await image_upload.start(media: media, uploader: uploader)
let res = await image_upload.start(img: image, uploader: uploader)
switch res {
case .success(let url):
@@ -204,102 +191,75 @@ struct PostView: View {
}
var body: some View {
GeometryReader { (deviceSize: GeometryProxy) in
VStack(alignment: .leading, spacing: 0) {
VStack(alignment: .leading, spacing: 0) {
let searching = get_searching_string(post.string, cursor: cursor)
TopBar
HStack(alignment: .top) {
ProfilePicView(pubkey: damus_state.pubkey, size: 45.0, highlight: .none, profiles: damus_state.profiles)
let searching = get_searching_string(post.string)
TextEntry
}
.frame(maxHeight: searching == nil ? .infinity : 50)
// This if-block observes @ for tagging
if let searching {
UserSearch(damus_state: damus_state, search: searching, post: $post, cursor: $cursor)
.frame(maxHeight: .infinity)
} else {
Divider()
.padding([.bottom], 10)
TopBar
ScrollViewReader { scroller in
ScrollView {
if let replying_to = replying_to {
ReplyView(replying_to: replying_to, damus: damus_state, originalReferences: $originalReferences, references: $references)
}
VStack(alignment: .leading, spacing: 0) {
HStack(alignment: .top) {
ProfilePicView(pubkey: damus_state.pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
.padding(.leading, replying_to != nil ? 15 : 0)
TextEntry
}
.frame(height: deviceSize.size.height*0.78)
.id("post")
}
}
.frame(maxHeight: searching == nil ? .infinity : 70)
.onAppear {
scroll_to_event(scroller: scroller, id: "post", delay: 1.0, animate: true, anchor: .top)
}
}
// This if-block observes @ for tagging
if let searching {
UserSearch(damus_state: damus_state, search: searching, post: $post)
.padding(.leading, replying_to != nil ? 15 : 0)
.frame(maxHeight: .infinity)
} else {
Divider()
.padding([.bottom], 10)
VStack(alignment: .leading) {
AttachmentBar
}
}
AttachmentBar
}
.padding()
.sheet(isPresented: $attach_media) {
ImagePicker(sourceType: .photoLibrary, pubkey: damus_state.pubkey) { img in
handle_upload(media: .image(img))
} onVideoPicked: { url in
handle_upload(media: .video(url))
}
}
.sheet(isPresented: $attach_camera) {
ImagePicker(sourceType: .camera, pubkey: damus_state.pubkey) { img in
handle_upload(media: .image(img))
} onVideoPicked: { url in
handle_upload(media: .video(url))
}
}
.onAppear() {
if let replying_to {
references = gather_reply_ids(our_pubkey: damus_state.pubkey, from: replying_to)
originalReferences = references
if damus_state.drafts.replies[replying_to] == nil {
damus_state.drafts.post = NSMutableAttributedString(string: "")
}
if let p = damus_state.drafts.replies[replying_to] {
post = p
}
} else {
post = damus_state.drafts.post
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.focus = true
}
}
.onDisappear {
if let replying_to, let reply = damus_state.drafts.replies[replying_to], reply.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
damus_state.drafts.replies.removeValue(forKey: replying_to)
} else if replying_to == nil && damus_state.drafts.post.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
damus_state.drafts.post = NSMutableAttributedString(string : "")
}
}
.alert(NSLocalizedString("Note contains \"nsec1\" private key. Are you sure?", comment: "Alert user that they might be attempting to paste a private key and ask them to confirm."), isPresented: $showPrivateKeyWarning, actions: {
Button(NSLocalizedString("No", comment: "Button to cancel out of posting a note after being alerted that it looks like they might be posting a private key."), role: .cancel) {
showPrivateKeyWarning = false
}
Button(NSLocalizedString("Yes, Post with Private Key", comment: "Button to proceed with posting a note even though it looks like they might be posting a private key."), role: .destructive) {
self.send_post()
}
})
}
.padding()
.sheet(isPresented: $attach_media) {
ImagePicker(sourceType: .photoLibrary) { img in
handle_upload(image: img)
}
}
.onAppear() {
if let replying_to {
if damus_state.drafts.replies[replying_to] == nil {
damus_state.drafts.post = NSMutableAttributedString(string: "")
}
if let p = damus_state.drafts.replies[replying_to] {
post = p
}
} else {
post = damus_state.drafts.post
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.focus = true
}
}
.onDisappear {
if let replying_to, let reply = damus_state.drafts.replies[replying_to], reply.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
damus_state.drafts.replies.removeValue(forKey: replying_to)
} else if replying_to == nil && damus_state.drafts.post.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
damus_state.drafts.post = NSMutableAttributedString(string : "")
}
}
.alert(NSLocalizedString("Note contains \"nsec1\" private key. Are you sure?", comment: "Alert user that they might be attempting to paste a private key and ask them to confirm."), isPresented: $showPrivateKeyWarning, actions: {
Button(NSLocalizedString("No", comment: "Button to cancel out of posting a note after being alerted that it looks like they might be posting a private key."), role: .cancel) {
showPrivateKeyWarning = false
}
Button(NSLocalizedString("Yes, Post with Private Key", comment: "Button to proceed with posting a note even though it looks like they might be posting a private key."), role: .destructive) {
self.send_post()
}
})
}
}
func get_searching_string(_ post: String) -> String? {
guard let last_word = post.components(separatedBy: .whitespacesAndNewlines).last else {
func get_searching_string(_ post: String, cursor: Int) -> String? {
guard cursor > 0 else {
return nil
}
guard let last_word = post[...post.index(post.startIndex, offsetBy: cursor - 1)].components(separatedBy: .whitespacesAndNewlines).last else {
return nil
}
@@ -321,6 +281,6 @@ func get_searching_string(_ post: String) -> String? {
struct PostView_Previews: PreviewProvider {
static var previews: some View {
PostView(replying_to: nil, damus_state: test_damus_state())
PostView(replying_to: nil, references: [], damus_state: test_damus_state())
}
}
+37 -21
View File
@@ -22,6 +22,7 @@ struct UserSearch: View {
let search: String
@Binding var post: NSMutableAttributedString
@Binding var cursor: Int
var users: [SearchedUser] {
guard let contacts = damus_state.contacts.event else {
@@ -36,54 +37,68 @@ struct UserSearch: View {
return
}
// Remove all characters after the last '@'
removeCharactersAfterLastAtSymbol()
// Remove all characters after the '@' and before the cursor
let newCursor = removeCharactersAfterAtSymbol()
// Create and append the user tag
let tagAttributedString = createUserTag(for: user, with: pk)
appendUserTag(tagAttributedString)
insertUserTag(tagAttributedString, cursor: newCursor)
cursor = newCursor
}
private func removeCharactersAfterLastAtSymbol() {
while post.string.last != "@" {
post.deleteCharacters(in: NSRange(location: post.length - 1, length: 1))
private func removeCharactersAfterAtSymbol() -> Int {
let newCursor = cursor
guard newCursor > 0 else {
return 0
}
post.deleteCharacters(in: NSRange(location: post.length - 1, length: 1))
var atSymbolOffset = newCursor
while atSymbolOffset > 0 && post.string[post.string.index(post.string.startIndex, offsetBy: atSymbolOffset - 1)] != "@" {
atSymbolOffset -= 1
}
var endOfWordOffset = newCursor
while endOfWordOffset < post.string.count && !post.string[post.string.index(post.string.startIndex, offsetBy: endOfWordOffset)].isWhitespace {
endOfWordOffset += 1
}
post.deleteCharacters(in: NSRange(location: atSymbolOffset - 1, length: endOfWordOffset - atSymbolOffset + 1))
return atSymbolOffset - 1
}
private func createUserTag(for user: SearchedUser, with pk: String) -> NSMutableAttributedString {
let name = Profile.displayName(profile: user.profile, pubkey: pk).username
let tagString = "@\(name)\u{200B} "
let tagString = "\u{200B}@\(name)\u{200B} "
let tagAttributedString = NSMutableAttributedString(string: tagString,
attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18.0),
NSAttributedString.Key.link: "@\(pk)"])
tagAttributedString.removeAttribute(.link, range: NSRange(location: 0, length: 1))
tagAttributedString.removeAttribute(.link, range: NSRange(location: tagAttributedString.length - 2, length: 2))
tagAttributedString.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.label], range: NSRange(location: 0, length: 1))
tagAttributedString.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.label], range: NSRange(location: tagAttributedString.length - 2, length: 2))
return tagAttributedString
}
private func appendUserTag(_ tagAttributedString: NSMutableAttributedString) {
private func insertUserTag(_ tagAttributedString: NSMutableAttributedString, cursor: Int) {
let mutableString = NSMutableAttributedString()
mutableString.append(post)
mutableString.append(tagAttributedString)
mutableString.insert(tagAttributedString, at: cursor)
post = mutableString
}
var body: some View {
ScrollView {
LazyVStack {
Divider()
if users.count == 0 {
EmptyUserSearchView()
} else {
ForEach(users) { user in
UserView(damus_state: damus_state, pubkey: user.pubkey)
.onTapGesture {
on_user_tapped(user: user)
}
}
ForEach(users) { user in
UserView(damus_state: damus_state, pubkey: user.pubkey)
.onTapGesture {
on_user_tapped(user: user)
}
}
}
}
@@ -93,9 +108,10 @@ struct UserSearch: View {
struct UserSearch_Previews: PreviewProvider {
static let search: String = "jb55"
@State static var post: NSMutableAttributedString = NSMutableAttributedString(string: "some @jb55")
@State static var cursor: Int = 0
static var previews: some View {
UserSearch(damus_state: test_damus_state(), search: search, post: $post)
UserSearch(damus_state: test_damus_state(), search: search, post: $post, cursor: $cursor)
}
}
@@ -1,84 +0,0 @@
//
// ProfilePictureEditView.swift
// damus
//
// Created by Joel Klabo on 3/30/23.
//
import SwiftUI
struct EditProfilePictureControl: View {
let pubkey: String
@Binding var profile_image: URL?
@ObservedObject var viewModel: ProfileUploadingViewModel
let callback: (URL?) -> Void
@StateObject var image_upload: ImageUploadModel = ImageUploadModel()
@State private var show_camera = false
@State private var show_library = false
var body: some View {
Menu {
Button(action: {
self.show_library = true
}) {
Text("Choose from Library", comment: "Option to select photo from library")
}
Button(action: {
self.show_camera = true
}) {
Text("Take Photo", comment: "Option to take a photo with the camera")
}
} label: {
if viewModel.isLoading {
ProgressView()
} else {
Image(systemName: "camera")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
.foregroundColor(DamusColors.white)
}
}
.sheet(isPresented: $show_camera) {
ImagePicker(sourceType: .camera, pubkey: pubkey, imagesOnly: true) { img in
handle_upload(media: .image(img))
} onVideoPicked: { url in
print("Cannot upload videos as profile image")
}
}
.sheet(isPresented: $show_library) {
ImagePicker(sourceType: .photoLibrary, pubkey: pubkey, imagesOnly: true) { img in
handle_upload(media: .image(img))
} onVideoPicked: { url in
print("Cannot upload videos as profile image")
}
}
}
private func handle_upload(media: MediaUpload) {
viewModel.isLoading = true
let uploader = get_media_uploader(pubkey)
Task {
let res = await image_upload.start(media: media, uploader: uploader)
switch res {
case .success(let urlString):
let url = URL(string: urlString)
profile_image = url
callback(url)
case .failed(let error):
if let error {
print("Error uploading profile image \(error.localizedDescription)")
} else {
print("Error uploading image :(")
}
callback(nil)
}
viewModel.isLoading = false
}
}
}
+6 -1
View File
@@ -8,6 +8,11 @@
import SwiftUI
struct FollowsYou: View {
@Environment(\.colorScheme) var colorScheme
var fill_color: Color {
colorScheme == .light ? Color("DamusLightGrey") : Color("DamusDarkGrey")
}
var body: some View {
Text("Follows you", comment: "Text to indicate that a user is following your profile.")
@@ -16,7 +21,7 @@ struct FollowsYou: View {
.foregroundColor(.gray)
.background {
RoundedRectangle(cornerRadius: 5.0)
.foregroundColor(DamusColors.adaptableGrey)
.foregroundColor(fill_color)
}
.font(.footnote)
}
+5 -1
View File
@@ -60,7 +60,11 @@ struct ProfileName: View {
var current_nip05: NIP05? {
nip05 ?? damus_state.profiles.is_validated(pubkey)
}
var nip05_color: Color {
return get_nip05_color(pubkey: pubkey, contacts: damus_state.contacts)
}
var current_display_name: DisplayName {
return display_name ?? Profile.displayName(profile: profile, pubkey: pubkey)
}
+1 -55
View File
@@ -32,60 +32,6 @@ func pfp_line_width(_ h: Highlight) -> CGFloat {
}
}
struct EditProfilePictureView: View {
@Binding var url: URL?
let pubkey: String
let size: CGFloat
let highlight: Highlight
var damus_state: DamusState?
var PlaceholderColor: Color {
return id_to_color(pubkey)
}
var Placeholder: some View {
PlaceholderColor
.frame(width: size, height: size)
.clipShape(Circle())
.overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
.padding(2)
}
var body: some View {
ZStack {
Color(uiColor: .systemBackground)
KFAnimatedImage(get_profile_url())
.imageContext(.pfp)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
}
.placeholder { _ in
Placeholder
}
.scaledToFill()
.opacity(0.5)
}
.frame(width: size, height: size)
.clipShape(Circle())
.overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
}
private func get_profile_url() -> URL? {
if let url {
return url
} else if let state = damus_state, let picture = state.profiles.lookup(id: pubkey)?.picture {
return URL(string: picture)
} else {
return url ?? URL(string: robohash(pubkey))
}
}
}
struct InnerProfilePicView: View {
let url: URL?
@@ -172,7 +118,7 @@ func make_preview_profiles(_ pubkey: String) -> Profiles {
let profiles = Profiles()
let picture = "http://cdn.jb55.com/img/red-me.jpg"
let profile = Profile(name: "jb55", display_name: "William Casarin", about: "It's me", picture: picture, banner: "", website: "https://jb55.com", lud06: nil, lud16: nil, nip05: "jb55.com")
let ts_profile = TimestampedProfile(profile: profile, timestamp: 0, event: test_event)
let ts_profile = TimestampedProfile(profile: profile, timestamp: 0)
profiles.add(id: pubkey, profile: ts_profile)
return profiles
}
@@ -7,27 +7,13 @@
import SwiftUI
import Combine
class ProfileUploadingViewModel: ObservableObject {
@Published var isLoading: Bool = false
}
struct ProfilePictureSelector: View {
let pubkey: String
var size: CGFloat = 80.0
var damus_state: DamusState?
@ObservedObject var viewModel: ProfileUploadingViewModel
let callback: (URL?) -> Void
@State var profile_image: URL? = nil
var body: some View {
let highlight: Highlight = .custom(Color.white, 2.0)
ZStack {
EditProfilePictureView(url: $profile_image, pubkey: pubkey, size: size, highlight: highlight, damus_state: damus_state)
EditProfilePictureControl(pubkey: pubkey, profile_image: $profile_image, viewModel: viewModel, callback: callback)
ProfilePicView(pubkey: pubkey, size: 80.0, highlight: highlight, profiles: Profiles())
}
}
}
@@ -35,8 +21,6 @@ struct ProfilePictureSelector: View {
struct ProfilePictureSelector_Previews: PreviewProvider {
static var previews: some View {
let test_pubkey = "ff48854ac6555fed8e439ebb4fa2d928410e0eef13fa41164ec45aaaa132d846"
ProfilePictureSelector(pubkey: test_pubkey, viewModel: ProfileUploadingViewModel()) { _ in
//
}
ProfilePictureSelector(pubkey: test_pubkey)
}
}
+20 -12
View File
@@ -87,11 +87,11 @@ struct EditButton: View {
}
func fillColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white
colorScheme == .light ? Color("DamusBlack") : Color("DamusWhite")
}
func borderColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white
colorScheme == .light ? Color("DamusBlack") : Color("DamusWhite")
}
}
@@ -143,8 +143,12 @@ struct ProfileView: View {
@Environment(\.openURL) var openURL
@Environment(\.presentationMode) var presentationMode
func fillColor() -> Color {
colorScheme == .light ? Color("DamusLightGrey") : Color("DamusDarkGrey")
}
func imageBorderColor() -> Color {
colorScheme == .light ? DamusColors.white : DamusColors.black
colorScheme == .light ? Color("DamusWhite") : Color("DamusBlack")
}
func bannerBlurViewOpacity() -> Double {
@@ -221,8 +225,8 @@ struct ProfileView: View {
notify(.report, target)
}
Button(NSLocalizedString("Mute", comment: "Button to mute a profile."), role: .destructive) {
notify(.mute, profile.pubkey)
Button(NSLocalizedString("Block", comment: "Button to block a profile."), role: .destructive) {
notify(.block, profile.pubkey)
}
}
}
@@ -236,7 +240,7 @@ struct ProfileView: View {
}
.padding(.top, 5)
.padding(.horizontal)
.accentColor(DamusColors.white)
.accentColor(Color("DamusWhite"))
}
func lnButton(lnurl: String, profile: Profile) -> some View {
@@ -323,7 +327,7 @@ struct ProfileView: View {
is_zoomed.toggle()
}
.fullScreenCover(isPresented: $is_zoomed) {
ProfilePicImageView(pubkey: profile.pubkey, profiles: damus_state.profiles) }
ProfileZoomView(pubkey: profile.pubkey, profiles: damus_state.profiles) }
Spacer()
@@ -480,7 +484,7 @@ func test_damus_state() -> DamusState {
let damus = DamusState.empty
let prof = Profile(name: "damus", display_name: "damus", about: "iOS app!", picture: "https://damus.io/img/logo.png", banner: "", website: "https://damus.io", lud06: nil, lud16: "jb55@sendsats.lol", nip05: "damus.io")
let tsprof = TimestampedProfile(profile: prof, timestamp: 0, event: test_event)
let tsprof = TimestampedProfile(profile: prof, timestamp: 0)
damus.profiles.add(id: pubkey, profile: tsprof)
return damus
}
@@ -492,8 +496,12 @@ struct KeyView: View {
@State private var isCopied = false
func fillColor() -> Color {
colorScheme == .light ? Color("DamusLightGrey") : Color("DamusDarkGrey")
}
func keyColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white
colorScheme == .light ? Color("DamusBlack") : Color("DamusWhite")
}
private func copyPubkey(_ pubkey: String) {
@@ -530,7 +538,7 @@ struct KeyView: View {
}
.padding(2)
.padding([.leading, .trailing], 3)
.background(RoundedRectangle(cornerRadius: 11).foregroundColor(DamusColors.adaptableGrey))
.background(RoundedRectangle(cornerRadius: 11).foregroundColor(fillColor()))
if isCopied != true {
Button {
@@ -541,7 +549,7 @@ struct KeyView: View {
} icon: {
Image(systemName: "square.on.square.dashed")
.contentShape(Rectangle())
.foregroundColor(.accentColor)
.foregroundColor(.gray)
.frame(width: 20, height: 20)
}
.labelStyle(IconOnlyLabelStyle())
@@ -555,7 +563,7 @@ struct KeyView: View {
.font(.footnote)
.layoutPriority(1)
}
.foregroundColor(DamusColors.green)
.foregroundColor(Color("DamusGreen"))
}
}
}
@@ -39,11 +39,14 @@ struct ProfileImageContainerView: View {
}
}
struct NavDismissBarView: View {
struct ProfileZoomView: View {
let pubkey: String
let profiles: Profiles
@Environment(\.presentationMode) var presentationMode
var body: some View {
var navBarView: some View {
HStack {
Button(action: {
presentationMode.wrappedValue.dismiss()
@@ -58,14 +61,6 @@ struct NavDismissBarView: View {
}
.padding()
}
}
struct ProfilePicImageView: View {
let pubkey: String
let profiles: Profiles
@Environment(\.presentationMode) var presentationMode
var body: some View {
ZStack {
@@ -84,7 +79,7 @@ struct ProfilePicImageView: View {
presentationMode.wrappedValue.dismiss()
}))
}
.overlay(NavDismissBarView(), alignment: .top)
.overlay(navBarView, alignment: .top)
}
}
@@ -92,7 +87,7 @@ struct ProfileZoomView_Previews: PreviewProvider {
static let pubkey = "ca48854ac6555fed8e439ebb4fa2d928410e0eef13fa41164ec45aaaa132d846"
static var previews: some View {
ProfilePicImageView(
ProfileZoomView(
pubkey: pubkey,
profiles: make_preview_profiles(pubkey))
}
+7 -7
View File
@@ -42,23 +42,23 @@ struct QRCodeView: View {
let profile = damus_state.profiles.lookup(id: damus_state.pubkey)
if (damus_state.profiles.lookup(id: damus_state.pubkey)?.picture) != nil {
ProfilePicView(pubkey: damus_state.pubkey, size: 90.0, highlight: .custom(DamusColors.white, 4.0), profiles: damus_state.profiles)
ProfilePicView(pubkey: damus_state.pubkey, size: 90.0, highlight: .custom(Color("DamusWhite"), 4.0), profiles: damus_state.profiles)
.padding(.top, 50)
} else {
Image(systemName: "person.fill")
.font(.system(size: 60))
.foregroundColor(DamusColors.white)
.foregroundColor(Color("DamusWhite"))
.padding(.top, 50)
}
if let display_name = profile?.display_name {
Text(display_name)
.foregroundColor(DamusColors.white)
.foregroundColor(Color("DamusWhite"))
.font(.system(size: 24, weight: .heavy))
}
if let name = profile?.name {
Text("@" + name)
.foregroundColor(DamusColors.white)
.foregroundColor(Color("DamusWhite"))
.font(.body)
}
@@ -73,19 +73,19 @@ struct QRCodeView: View {
.padding()
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(DamusColors.white, lineWidth: 1))
.stroke(Color("DamusWhite"), lineWidth: 1))
.shadow(radius: 10)
}
Spacer()
Text("Follow me on nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user.")
.foregroundColor(DamusColors.white)
.foregroundColor(Color("DamusWhite"))
.font(.system(size: 24, weight: .heavy))
.padding(.top)
Text("Scan the code", comment: "Text on QR code view to prompt viewer to scan the QR code on screen with their device camera.")
.foregroundColor(DamusColors.white)
.foregroundColor(Color("DamusWhite"))
.font(.system(size: 18, weight: .ultraLight))
Spacer()
+15 -60
View File
@@ -12,33 +12,22 @@ struct RecommendedRelayView: View {
let relay: String
let add_button: Bool
@Binding var showActionButtons: Bool
init(damus: DamusState, relay: String, showActionButtons: Binding<Bool>) {
init(damus: DamusState, relay: String) {
self.damus = damus
self.relay = relay
self.add_button = true
self._showActionButtons = showActionButtons
}
init(damus: DamusState, relay: String, add_button: Bool, showActionButtons: Binding<Bool>) {
init(damus: DamusState, relay: String, add_button: Bool) {
self.damus = damus
self.relay = relay
self.add_button = add_button
self._showActionButtons = showActionButtons
}
var body: some View {
ZStack {
HStack {
if let privkey = damus.keypair.privkey {
if showActionButtons && add_button {
AddButton(privkey: privkey, showText: false)
}
}
RelayType(is_paid: damus.relay_metadata.lookup(relay_id: relay)?.is_paid ?? false)
Text(relay).layoutPriority(1)
if let meta = damus.relay_metadata.lookup(relay_id: relay) {
@@ -48,75 +37,41 @@ struct RecommendedRelayView: View {
EmptyView()
}
.opacity(0.0)
.disabled(showActionButtons)
Spacer()
Image(systemName: "info.circle")
.font(.system(size: 20, weight: .regular))
.foregroundColor(Color.accentColor)
} else {
Spacer()
Image(systemName: "questionmark.circle")
.font(.system(size: 20, weight: .regular))
.foregroundColor(.gray)
}
}
}
.swipeActions {
if add_button {
if let privkey = damus.keypair.privkey {
AddButton(privkey: privkey, showText: false)
.tint(.accentColor)
AddAction(privkey: privkey)
}
}
}
.contextMenu {
CopyAction(relay: relay)
if let privkey = damus.keypair.privkey {
AddButton(privkey: privkey, showText: true)
}
}
}
func CopyAction(relay: String) -> some View {
func AddAction(privkey: String) -> some View {
Button {
UIPasteboard.general.setValue(relay, forPasteboardType: "public.plain-text")
} label: {
Label(NSLocalizedString("Copy", comment: "Button to copy a relay server address."), systemImage: "doc.on.doc")
}
}
func AddButton(privkey: String, showText: Bool) -> some View {
Button(action: {
add_action(privkey: privkey)
}) {
if showText {
Text(NSLocalizedString("Connect", comment: "Button to connect to recommended relay server."))
guard let ev_before_add = damus.contacts.event else {
return
}
Image(systemName: "plus.circle.fill")
.font(.system(size: 20, weight: .medium))
.foregroundColor(.accentColor)
.padding(.leading, 5)
guard let ev_after_add = add_relay(ev: ev_before_add, privkey: privkey, current_relays: damus.pool.descriptors, relay: relay, info: .rw) else {
return
}
process_contact_event(state: damus, ev: ev_after_add)
damus.pool.send(.event(ev_after_add))
} label: {
Label(NSLocalizedString("Add Relay", comment: "Button to add recommended relay server."), systemImage: "plus.circle")
}
}
func add_action(privkey: String) {
guard let ev_before_add = damus.contacts.event else {
return
}
guard let ev_after_add = add_relay(ev: ev_before_add, privkey: privkey, current_relays: damus.pool.descriptors, relay: relay, info: .rw) else {
return
}
process_contact_event(state: damus, ev: ev_after_add)
damus.postbox.send(ev_after_add)
.tint(.accentColor)
}
}
struct RecommendedRelayView_Previews: PreviewProvider {
static var previews: some View {
RecommendedRelayView(damus: test_damus_state(), relay: "wss://relay.damus.io", showActionButtons: .constant(false))
RecommendedRelayView(damus: test_damus_state(), relay: "wss://relay.damus.io")
}
}
+55 -105
View File
@@ -10,8 +10,8 @@ import SwiftUI
struct RelayConfigView: View {
let state: DamusState
@State var new_relay: String = ""
@State var show_add_relay: Bool = false
@State var relays: [RelayDescriptor]
@State private var showActionButtons = false
@Environment(\.dismiss) var dismiss
@@ -23,8 +23,8 @@ struct RelayConfigView: View {
var recommended: [RelayDescriptor] {
let rs: [RelayDescriptor] = []
return BOOTSTRAP_RELAYS.reduce(into: rs) { xs, x in
if state.pool.get_relay(x) == nil, let url = URL(string: x) {
xs.append(RelayDescriptor(url: url, info: .rw))
if state.pool.get_relay(x) == nil {
xs.append(RelayDescriptor(url: URL(string: x)!, info: .rw))
}
}
}
@@ -37,122 +37,72 @@ struct RelayConfigView: View {
.onReceive(handle_notify(.switched_timeline)) { _ in
dismiss()
}
.sheet(isPresented: $show_add_relay) {
AddRelayView(show_add_relay: $show_add_relay, relay: $new_relay) { m_relay in
guard var relay = m_relay else {
return
}
if relay.starts(with: "wss://") == false && relay.starts(with: "ws://") == false {
relay = "wss://" + relay
}
if relay.hasSuffix("/") {
relay.removeLast();
}
guard let url = URL(string: relay) else {
return
}
guard let ev = state.contacts.event else {
return
}
guard let privkey = state.keypair.privkey else {
return
}
let info = RelayInfo.rw
guard (try? state.pool.add_relay(url, info: info)) != nil else {
return
}
state.pool.connect(to: [relay])
guard let new_ev = add_relay(ev: ev, privkey: privkey, current_relays: state.pool.descriptors, relay: relay, info: info) else {
return
}
process_contact_event(state: state, ev: ev)
state.pool.send(.event(new_ev))
}
}
}
var MainContent: some View {
Form {
Section {
AddRelayView(relay: $new_relay)
} header: {
HStack {
Text(NSLocalizedString("Connect To Relay", comment: "Label for section for adding a relay server."))
.font(.system(size: 18, weight: .heavy))
.padding(.bottom, 5)
}
} footer: {
VStack {
HStack {
Spacer()
if(!new_relay.isEmpty) {
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of view adding user inputted relay.")) {
new_relay = ""
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
.font(.system(size: 14, weight: .bold))
.frame(width: 80, height: 30)
.foregroundColor(.white)
.background(LINEAR_GRADIENT)
.clipShape(Capsule())
.padding(EdgeInsets(top: 15, leading: 0, bottom: 0, trailing: 0))
Button(NSLocalizedString("Add", comment: "Button to confirm adding user inputted relay.")) {
if new_relay.starts(with: "wss://") == false && new_relay.starts(with: "ws://") == false {
new_relay = "wss://" + new_relay
}
if new_relay.hasSuffix("/") {
new_relay.removeLast();
}
guard let url = URL(string: new_relay) else {
return
}
guard let ev = state.contacts.event else {
return
}
guard let privkey = state.keypair.privkey else {
return
}
let info = RelayInfo.rw
guard (try? state.pool.add_relay(url, info: info)) != nil else {
return
}
state.pool.connect(to: [new_relay])
guard let new_ev = add_relay(ev: ev, privkey: privkey, current_relays: state.pool.descriptors, relay: new_relay, info: info) else {
return
}
process_contact_event(state: state, ev: ev)
state.pool.send(.event(new_ev))
new_relay = ""
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
.font(.system(size: 14, weight: .bold))
.frame(width: 80, height: 30)
.foregroundColor(.white)
.background(LINEAR_GRADIENT)
.clipShape(Capsule())
.padding(EdgeInsets(top: 15, leading: 0, bottom: 0, trailing: 0))
}
}
}
}
Section {
List(Array(relays), id: \.url) { relay in
RelayView(state: state, relay: relay.url.absoluteString, showActionButtons: $showActionButtons)
RelayView(state: state, relay: relay.url.absoluteString)
}
} header: {
HStack {
Text(NSLocalizedString("Connected Relays", comment: "Section title for relay servers that are connected."))
.font(.system(size: 18, weight: .heavy))
.padding(.bottom, 5)
Text("Relays", comment: "Header text for relay server list for configuration.")
Spacer()
Button(action: { show_add_relay = true }) {
Image(systemName: "plus")
.foregroundColor(.accentColor)
}
}
}
if recommended.count > 0 {
Section {
Section(NSLocalizedString("Recommended Relays", comment: "Section title for recommend relay servers that could be added as part of configuration")) {
List(recommended, id: \.url) { r in
RecommendedRelayView(damus: state, relay: r.url.absoluteString, showActionButtons: $showActionButtons)
}
} header: {
Text(NSLocalizedString("Recommended Relays", comment: "Section title for recommend relay servers that could be added as part of configuration"))
.font(.system(size: 18, weight: .heavy))
.padding(.bottom, 5)
}
}
}
.navigationTitle(NSLocalizedString("Relays", comment: "Title of relays view"))
.navigationBarTitleDisplayMode(.large)
.toolbar {
if state.keypair.privkey != nil {
if showActionButtons {
Button("Done") {
showActionButtons.toggle()
}
} else {
Button("Edit") {
showActionButtons.toggle()
RecommendedRelayView(damus: state, relay: r.url.absoluteString)
}
}
}

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