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 ## [1.3.0-7] - 2023-03-24
- New experimental timeline view - New experimental timeline view
@@ -153,10 +57,6 @@
- Add image uploader (Swift) - Add image uploader (Swift)
- Add option to always show images (never blur) (William Casarin) - 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 ### Changed
@@ -179,9 +79,6 @@
- Extend user tagging search to all local profiles (William Casarin) - Extend user tagging search to all local profiles (William Casarin)
- Vibrate when a zap is received (Swift) - Vibrate when a zap is received (Swift)
- New and Improved Share sheet (ericholguin) - New and Improved Share sheet (ericholguin)
- Bulgarian translations (elsat)
- Persian translations (Mahdi Taghizadeh)
- Ukrainian translations (Valeriia Khudiakova, Tony B)
### Changed ### Changed
@@ -280,8 +177,6 @@
- Customized zaps (William Casarin) - Customized zaps (William Casarin)
- Add new Notifications View (William Casarin) - Add new Notifications View (William Casarin)
- Bookmarking (Joel Klabo) - Bookmarking (Joel Klabo)
- Chinese, Traditional (Hong Kong) translations (rasputin)
- Chinese, Traditional (Taiwan) translations (rasputin)
### Changed ### Changed
@@ -307,9 +202,6 @@
- Added the ability to select text on posts (OlegAba) - Added the ability to select text on posts (OlegAba)
- Added Posts or Post & Replies selector to Profile (ericholguin) - Added Posts or Post & Replies selector to Profile (ericholguin)
- Improved profile navbar (OlegAba) - Improved profile navbar (OlegAba)
- Czech translations (Martin Gabrhel)
- Indonesian translations (johnybergzy)
- Russian translations (Tony B)
### Changed ### Changed
@@ -358,6 +250,7 @@
### Added ### Added
- Relay Filtering (William Casarin) - Relay Filtering (William Casarin)
- Japanese translations (Terry Yiu)
- Add password autofill on account login and creation (Terry Yiu) - Add password autofill on account login and creation (Terry Yiu)
- Show if relay is paid (William Casarin) - Show if relay is paid (William Casarin)
- Add "Follows You" indicator on profile (William Casarin) - Add "Follows You" indicator on profile (William Casarin)
@@ -370,10 +263,6 @@
- Copy invoice button (Joel Klabo) - Copy invoice button (Joel Klabo)
- Receive Lightning Zaps (William Casarin) - Receive Lightning Zaps (William Casarin)
- Allow text selection in bio (Suhail Saqan) - 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 ### Changed
@@ -408,7 +297,6 @@
- LibreTranslate note translations (Terry Yiu) - LibreTranslate note translations (Terry Yiu)
- Added support for account deletion (William Casarin) - Added support for account deletion (William Casarin)
- User tagging and autocompletion in posts (Swift) - User tagging and autocompletion in posts (Swift)
- Polish translations (pysiak)
### Changed ### Changed
@@ -431,8 +319,7 @@
### Added ### Added
- Arabic translations (Barodane) - Added Arabic and Portuguese translations (Barodane, Antonio Chagas)
- Portuguese translations (Antonio Chagas)
- Add QRCode view for sharing your pubkey (ericholguin) - Add QRCode view for sharing your pubkey (ericholguin)
- Added nostr: uri handling (William Casarin) - Added nostr: uri handling (William Casarin)
@@ -459,8 +346,7 @@
### Added ### Added
- Reposts view (Terry Yiu) - Reposts view (Terry Yiu)
- Italian translations (Nicolò Carcagnì) - Translations for it_IT, it_CH, fr_FR, de_DE, de_AT and lv_LV (Nicolò Carcagnì, Solobalbo, Gregor, Peter Gerstbach, SYX)
- Latvian translations (SYX)
- Added ability to block users (William Casarin) - Added ability to block users (William Casarin)
- Added a way to report content (William Casarin) - Added a way to report content (William Casarin)
- Stretchable profile cover header (Swift) - Stretchable profile cover header (Swift)
@@ -487,9 +373,7 @@
- Show website on profiles (William Casarin) - Show website on profiles (William Casarin)
- Add the ability to choose participants when replying (Joel Klabo) - Add the ability to choose participants when replying (Joel Klabo)
- German translations (Gregor, Peter Gerstbach) - Translations for de_AT, de_DE, tr_TR, fr_FR (Gregor, Peter Gerstbach, Taylan Benli, Solobalbo)
- Turkish translations (Taylan Benli)
- French (France) translations (Solobalbo)
- Add DM Message Requests (William Casarin) - 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 */; }; 3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */; };
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */; }; 3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */; };
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; }; 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 */; }; 3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; }; 3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.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 */; }; 4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F8E280F640A000448DE /* ThreadModel.swift */; };
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F90280F6528000448DE /* ChatView.swift */; }; 4C0A3F91280F6528000448DE /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F90280F6528000448DE /* ChatView.swift */; };
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.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 */; }; 4C216F32286E388800040376 /* DMChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F31286E388800040376 /* DMChatView.swift */; };
4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; }; 4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; };
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F352870A9A700040376 /* InputDismissKeyboard.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 */; }; 4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFBA2804A34C0006080F /* ProofOfWork.swift */; };
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FF7D42823313F009601DB /* Mentions.swift */; }; 4C7FF7D52823313F009601DB /* Mentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FF7D42823313F009601DB /* Mentions.swift */; };
4C8682872814DE470026224F /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8682862814DE470026224F /* ProfileView.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 */; }; 4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD152839DB54008EE7EF /* NostrMetadata.swift */; };
4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD17283A9EE5008EE7EF /* LoginView.swift */; }; 4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD17283A9EE5008EE7EF /* LoginView.swift */; };
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD19283AA67F008EE7EF /* Bech32.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 */; }; 4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */; };
4CB9D4A92992D2F400A9A7E4 /* FollowsYou.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */; }; 4CB9D4A92992D2F400A9A7E4 /* FollowsYou.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */; };
4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.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 */; }; 4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAE6297EFA7B00430951 /* Zap.swift */; };
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */; }; 4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */; };
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEC297F0B9E00430951 /* Highlight.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 */; }; 4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */; };
4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */; }; 4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */; };
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.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 */; }; 4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F8CC281352B30009DFBB /* Notifications.swift */; };
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */; }; 4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */; };
4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9E0285287B800C00DD9 /* TextFieldAlert.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 */; }; 4CFF8F6329CC9AD7008DB934 /* ImageContextMenuModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */; };
4CFF8F6729CC9E3A008DB934 /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6629CC9E3A008DB934 /* ImageView.swift */; }; 4CFF8F6729CC9E3A008DB934 /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6629CC9E3A008DB934 /* ImageView.swift */; };
4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6829CC9ED1008DB934 /* ImageContainerView.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 */; }; 4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.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 */; }; 5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; }; 5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
5CF72FC229B9142F00124A13 /* ShareAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF72FC129B9142F00124A13 /* ShareAction.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 */; }; 643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643EA5C7296B764E005081BB /* RelayFilterView.swift */; };
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; }; 647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; };
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FBD06E296255C400D9D3B2 /* Theme.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 */; }; DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; };
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; }; E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; };
E9E4ED0B295867B900DD7078 /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadView.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 */; }; F75BA12D29A1855400E10810 /* BookmarksManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75BA12C29A1855400E10810 /* BookmarksManager.swift */; };
F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75BA12E29A18EF500E10810 /* BookmarksView.swift */; }; F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75BA12E29A18EF500E10810 /* BookmarksView.swift */; };
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E91298B0F0700AB113A /* RelayDetailView.swift */; }; F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E91298B0F0700AB113A /* RelayDetailView.swift */; };
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E96298B1FDF00AB113A /* NIPURLBuilder.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 */; }; F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */; };
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParicipantsView.swift */; }; F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParicipantsView.swift */; };
/* End PBXBuildFile section */ /* 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; F7F0BA262978E54D009531F3 /* ParicipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParicipantsView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@@ -709,7 +664,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */, 3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */,
5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */,
); );
path = "Empty Views"; path = "Empty Views";
sourceTree = "<group>"; sourceTree = "<group>";
@@ -718,7 +672,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3AA24801297E3DC20090C62D /* RepostView.swift */, 3AA24801297E3DC20090C62D /* RepostView.swift */,
4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */,
); );
path = Reposts; path = Reposts;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -823,23 +776,10 @@
3AA59D1C2999B0400061C48E /* DraftsModel.swift */, 3AA59D1C2999B0400061C48E /* DraftsModel.swift */,
4C54AA0629A540BA003E4487 /* NotificationsModel.swift */, 4C54AA0629A540BA003E4487 /* NotificationsModel.swift */,
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */, 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */,
3A48E23A29D518F000BA313D /* Translations.swift */,
); );
path = Models; path = Models;
sourceTree = "<group>"; 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 */ = { 4C30AC7029A5676F00E2BD5A /* Notifications */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -863,7 +803,6 @@
4C75EFA227FA576C0006080F /* Views */ = { 4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
4C1A9A1B29DDCF8B00516EAC /* Settings */,
4CFF8F6129CC9A80008DB934 /* Images */, 4CFF8F6129CC9A80008DB934 /* Images */,
4CCEB7AC29B53D180078AA28 /* Search */, 4CCEB7AC29B53D180078AA28 /* Search */,
4C30AC7029A5676F00E2BD5A /* Notifications */, 4C30AC7029A5676F00E2BD5A /* Notifications */,
@@ -900,7 +839,6 @@
4C75EFAC28049CFB0006080F /* PostButton.swift */, 4C75EFAC28049CFB0006080F /* PostButton.swift */,
4C75EFA327FA577B0006080F /* PostView.swift */, 4C75EFA327FA577B0006080F /* PostView.swift */,
9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */, 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */,
F757933929D7AECD007DEAC1 /* ImagePicker.swift */,
9C83F89229A937B900136C08 /* TextViewWrapper.swift */, 9C83F89229A937B900136C08 /* TextViewWrapper.swift */,
4CEE2AF8280B2EAC00AB5EEF /* PowView.swift */, 4CEE2AF8280B2EAC00AB5EEF /* PowView.swift */,
4C3AC7A42836987600E1F516 /* MainTabView.swift */, 4C3AC7A42836987600E1F516 /* MainTabView.swift */,
@@ -919,6 +857,7 @@
647D9A8C2968520300A295DE /* SideMenuView.swift */, 647D9A8C2968520300A295DE /* SideMenuView.swift */,
9609F057296E220800069BF3 /* BannerImageView.swift */, 9609F057296E220800069BF3 /* BannerImageView.swift */,
4CB8838E296F781C00DC99E7 /* ReactionsView.swift */, 4CB8838E296F781C00DC99E7 /* ReactionsView.swift */,
6439E013296790CF0020672B /* ProfileZoomView.swift */,
4CF0ABD529817F5B00D66079 /* ReportView.swift */, 4CF0ABD529817F5B00D66079 /* ReportView.swift */,
4CF0ABE42981EE0C00D66079 /* EULAView.swift */, 4CF0ABE42981EE0C00D66079 /* EULAView.swift */,
3AA247FE297E3D900090C62D /* RepostsView.swift */, 3AA247FE297E3D900090C62D /* RepostsView.swift */,
@@ -951,7 +890,6 @@
4C7FF7D628233637009601DB /* Util */ = { 4C7FF7D628233637009601DB /* Util */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
4CE4F0F329D779B5005914DB /* PostBox.swift */,
7C0F392D29B57C8F0039859C /* Extensions */, 7C0F392D29B57C8F0039859C /* Extensions */,
4CE879492995B58700F758CC /* Relays */, 4CE879492995B58700F758CC /* Relays */,
4CF0ABEA29844B2F00D66079 /* AnyCodable */, 4CF0ABEA29844B2F00D66079 /* AnyCodable */,
@@ -984,8 +922,6 @@
3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */, 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */,
4C30AC7729A577AB00E2BD5A /* EventCache.swift */, 4C30AC7729A577AB00E2BD5A /* EventCache.swift */,
4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */, 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */,
4CE4F0F129D4FCFA005914DB /* DebouncedOnChange.swift */,
4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */,
); );
path = Util; path = Util;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1028,7 +964,6 @@
children = ( children = (
4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */, 4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */,
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */, 4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */,
F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */,
4CEE2AF2280B25C500AB5EEF /* ProfilePicView.swift */, 4CEE2AF2280B25C500AB5EEF /* ProfilePicView.swift */,
4C8682862814DE470026224F /* ProfileView.swift */, 4C8682862814DE470026224F /* ProfileView.swift */,
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */, 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */,
@@ -1052,7 +987,6 @@
4CF0ABE6298444FC00D66079 /* MutedEventView.swift */, 4CF0ABE6298444FC00D66079 /* MutedEventView.swift */,
4C3D52B5298DB4E6001C5831 /* ZapEvent.swift */, 4C3D52B5298DB4E6001C5831 /* ZapEvent.swift */,
4C3D52B7298DB5C6001C5831 /* TextEvent.swift */, 4C3D52B7298DB5C6001C5831 /* TextEvent.swift */,
4CFF8F6C29CD022E008DB934 /* WideEventView.swift */,
); );
path = Events; path = Events;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1102,9 +1036,6 @@
4CB883AF297705DD00DC99E7 /* ZapButton.swift */, 4CB883AF297705DD00DC99E7 /* ZapButton.swift */,
4C42812B298C848200DBF26F /* TranslateView.swift */, 4C42812B298C848200DBF26F /* TranslateView.swift */,
7CFF6316299FEFE5005D382A /* SelectableText.swift */, 7CFF6316299FEFE5005D382A /* SelectableText.swift */,
4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */,
4CE4F0F729DB7399005914DB /* ThiccDivider.swift */,
4C1A9A2229DDDB8100516EAC /* IconLabel.swift */,
); );
path = Components; path = Components;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1198,7 +1129,6 @@
children = ( children = (
4CE8794729941DA700F758CC /* RelayFilters.swift */, 4CE8794729941DA700F758CC /* RelayFilters.swift */,
4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */, 4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */,
4CC6193929DC777C006A86D1 /* RelayBootstrap.swift */,
); );
path = Relays; path = Relays;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1259,7 +1189,6 @@
children = ( children = (
4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */, 4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */,
4CFF8F6629CC9E3A008DB934 /* ImageView.swift */, 4CFF8F6629CC9E3A008DB934 /* ImageView.swift */,
6439E013296790CF0020672B /* ProfilePicImageView.swift */,
4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */, 4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */,
); );
path = Images; path = Images;
@@ -1374,35 +1303,32 @@
hasScannedForEncodings = 0; hasScannedForEncodings = 0;
knownRegions = ( knownRegions = (
Base, Base,
ar,
bg,
cs,
de,
"el-GR",
"en-US",
"es-419", "es-419",
"es-ES", "en-US",
fa,
"fr-CA",
"fr-FR",
"hu-HU",
id,
"it-IT",
ja,
ko,
"lv-LV",
nl,
"pl-PL",
"pt-BR",
"pt-PT",
ru,
"sv-SE",
"tr-TR", "tr-TR",
uk, "fr-FR",
vi, "lv-LV",
"it-IT",
de,
"pt-PT",
"pl-PL",
ar,
nl,
"zh-CN", "zh-CN",
"el-GR",
ja,
id,
cs,
ru,
"zh-HK", "zh-HK",
"zh-TW", "zh-TW",
uk,
bg,
fa,
ko,
"hu-HU",
"sv-SE",
"fr-CA",
); );
mainGroup = 4CE6DEDA27F7A08100C66700; mainGroup = 4CE6DEDA27F7A08100C66700;
packageReferences = ( packageReferences = (
@@ -1475,7 +1401,6 @@
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */, 4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */, 4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */,
4C75EFB728049D990006080F /* RelayPool.swift in Sources */, 4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
F757933A29D7AECD007DEAC1 /* ImagePicker.swift in Sources */,
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */, 4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */,
4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */, 4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */,
4CCEB7A929B29DD50078AA28 /* SearchResultsModel.swift in Sources */, 4CCEB7A929B29DD50078AA28 /* SearchResultsModel.swift in Sources */,
@@ -1486,7 +1411,6 @@
4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */, 4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */,
4C363AA228296A7E006E126D /* SearchView.swift in Sources */, 4C363AA228296A7E006E126D /* SearchView.swift in Sources */,
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */, 4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */,
4CC6193A29DC777C006A86D1 /* RelayBootstrap.swift in Sources */,
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */, 4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */,
4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */, 4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */,
4C75EFB92804A2740006080F /* EventView.swift in Sources */, 4C75EFB92804A2740006080F /* EventView.swift in Sources */,
@@ -1565,7 +1489,6 @@
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */, 4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */,
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */, 4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */,
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */, 64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */,
4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */,
4C3EA64928FF597700C48A62 /* bech32.c in Sources */, 4C3EA64928FF597700C48A62 /* bech32.c in Sources */,
4CE879522996B68900F758CC /* RelayType.swift in Sources */, 4CE879522996B68900F758CC /* RelayType.swift in Sources */,
4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */, 4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */,
@@ -1579,7 +1502,6 @@
4CE879582996C45300F758CC /* ZapsView.swift in Sources */, 4CE879582996C45300F758CC /* ZapsView.swift in Sources */,
4C30AC7429A5680900E2BD5A /* EventGroupView.swift in Sources */, 4C30AC7429A5680900E2BD5A /* EventGroupView.swift in Sources */,
4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */, 4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */,
4CFF8F6D29CD022E008DB934 /* WideEventView.swift in Sources */,
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */, 9609F058296E220800069BF3 /* BannerImageView.swift in Sources */,
4C363A94282704FA006E126D /* Post.swift in Sources */, 4C363A94282704FA006E126D /* Post.swift in Sources */,
4C216F32286E388800040376 /* DMChatView.swift in Sources */, 4C216F32286E388800040376 /* DMChatView.swift in Sources */,
@@ -1589,11 +1511,9 @@
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */, 4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */, 4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */, 9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */,
4CE4F0F829DB7399005914DB /* ThiccDivider.swift in Sources */,
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */, 4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */,
4C363A8828236948006E126D /* BlocksView.swift in Sources */, 4C363A8828236948006E126D /* BlocksView.swift in Sources */,
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */, 4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */,
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */, 4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */, 4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */, 4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
@@ -1609,20 +1529,16 @@
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */, 5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */,
4C3EA64F28FF59F200C48A62 /* tal.c in Sources */, 4C3EA64F28FF59F200C48A62 /* tal.c in Sources */,
4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */, 4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */,
5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */,
4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */, 4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */,
4CFF8F6B29CD0079008DB934 /* RepostedEvent.swift in Sources */,
4C8682872814DE470026224F /* ProfileView.swift in Sources */, 4C8682872814DE470026224F /* ProfileView.swift in Sources */,
4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */, 4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */,
4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */, 4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */,
4C1A9A2729DDE31900516EAC /* TranslationSettingsView.swift in Sources */,
4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */, 4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */,
4CF0ABD82981980C00D66079 /* Lists.swift in Sources */, 4CF0ABD82981980C00D66079 /* Lists.swift in Sources */,
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */, 4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */,
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */,
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */, 4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */, 7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
6439E014296790CF0020672B /* ProfilePicImageView.swift in Sources */, 6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */,
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */, 4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */,
4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */, 4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */,
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */, 4C363AA428296DEE006E126D /* SearchModel.swift in Sources */,
@@ -1653,10 +1569,8 @@
4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */, 4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */,
4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */, 4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */,
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */, 4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */,
4CE4F0F229D4FCFA005914DB /* DebouncedOnChange.swift in Sources */,
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */, 4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */, 4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
4C1A9A1A29DCA17E00516EAC /* ReplyCounter.swift in Sources */,
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */, 643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */,
4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */, 4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */,
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */, 4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
@@ -1666,11 +1580,9 @@
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */, 4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */, F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */, 4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */,
3A48E23B29D518F000BA313D /* Translations.swift in Sources */,
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */, 4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
4CF0ABD42980996B00D66079 /* Report.swift in Sources */, 4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
4C06670B28FDE64700038D2A /* damus.c in Sources */, 4C06670B28FDE64700038D2A /* damus.c in Sources */,
4C1A9A2529DDDF2600516EAC /* ZapSettingsView.swift in Sources */,
4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */, 4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */,
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */, 3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */,
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */, 4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
@@ -1681,19 +1593,15 @@
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */, 4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */,
4C75EFA427FA577B0006080F /* PostView.swift in Sources */, 4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */, 4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */,
4C1A9A2129DDD3E100516EAC /* KeySettingsView.swift in Sources */,
4C1A9A1D29DDCF9B00516EAC /* NotificationSettingsView.swift in Sources */,
4C75EFB528049D790006080F /* Relay.swift in Sources */, 4C75EFB528049D790006080F /* Relay.swift in Sources */,
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */, 4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */,
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */, 4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */, 4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */, 4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
7C0F392F29B57CAF0039859C /* Binding+.swift in Sources */, 7C0F392F29B57CAF0039859C /* Binding+.swift in Sources */,
4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */,
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */, 3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */,
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */, 3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */, 4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -1774,9 +1682,6 @@
3AD14EB529C40F38009D2D9C /* hu-HU */, 3AD14EB529C40F38009D2D9C /* hu-HU */,
3AD14EB829C40F3F009D2D9C /* sv-SE */, 3AD14EB829C40F3F009D2D9C /* sv-SE */,
3AD14EBC29C40F47009D2D9C /* fr-CA */, 3AD14EBC29C40F47009D2D9C /* fr-CA */,
3A325AC629C9E0B8002BE7ED /* vi */,
3A325AC929C9E0CF002BE7ED /* es-ES */,
3AC59CA929CDDB78007E04A6 /* pt-BR */,
); );
name = Localizable.stringsdict; name = Localizable.stringsdict;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1809,9 +1714,6 @@
3AD14EB629C40F38009D2D9C /* hu-HU */, 3AD14EB629C40F38009D2D9C /* hu-HU */,
3AD14EB929C40F3F009D2D9C /* sv-SE */, 3AD14EB929C40F3F009D2D9C /* sv-SE */,
3AD14EBB29C40F47009D2D9C /* fr-CA */, 3AD14EBB29C40F47009D2D9C /* fr-CA */,
3A325AC529C9E0B8002BE7ED /* vi */,
3A325AC829C9E0CF002BE7ED /* es-ES */,
3AC59CA829CDDB78007E04A6 /* pt-BR */,
); );
name = InfoPlist.strings; name = InfoPlist.strings;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1845,9 +1747,6 @@
3AD14EB729C40F38009D2D9C /* hu-HU */, 3AD14EB729C40F38009D2D9C /* hu-HU */,
3AD14EBA29C40F3F009D2D9C /* sv-SE */, 3AD14EBA29C40F3F009D2D9C /* sv-SE */,
3AD14EBD29C40F47009D2D9C /* fr-CA */, 3AD14EBD29C40F47009D2D9C /* fr-CA */,
3A325AC429C9E0B8002BE7ED /* vi */,
3A325AC729C9E0CF002BE7ED /* es-ES */,
3AC59CA729CDDB78007E04A6 /* pt-BR */,
); );
name = Localizable.strings; name = Localizable.strings;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1983,7 +1882,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D; DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -1991,9 +1890,7 @@
INFOPLIST_FILE = damus/Info.plist; INFOPLIST_FILE = damus/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Damus; INFOPLIST_KEY_CFBundleDisplayName = Damus;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; 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_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_NSPhotoLibraryAddUsageDescription = "Granting Damus access to your photos allows you to save images.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
@@ -2008,7 +1905,7 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)", "$(PROJECT_DIR)",
); );
MARKETING_VERSION = 1.4.1; MARKETING_VERSION = 1.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2; PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@@ -2027,7 +1924,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3; CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D; DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -2035,9 +1932,7 @@
INFOPLIST_FILE = damus/Info.plist; INFOPLIST_FILE = damus/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Damus; INFOPLIST_KEY_CFBundleDisplayName = Damus;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; 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_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_NSPhotoLibraryAddUsageDescription = "Granting Damus access to your photos allows you to save images.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
@@ -2052,7 +1947,7 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)", "$(PROJECT_DIR)",
); );
MARKETING_VERSION = 1.4.1; MARKETING_VERSION = 1.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2; PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; 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 import SwiftUI
let RECTANGLE_GRADIENT = LinearGradient(gradient: Gradient(colors: [ let RECTANGLE_GRADIENT = LinearGradient(gradient: Gradient(colors: [
DamusColors.purple, Color("DamusPurple"),
DamusColors.blue Color("DamusBlue")
]), startPoint: .leading, endPoint: .trailing) ]), startPoint: .leading, endPoint: .trailing)
struct CustomPicker<SelectionValue: Hashable, Content: View>: View { struct CustomPicker<SelectionValue: Hashable, Content: View>: View {
@@ -56,6 +56,6 @@ struct CustomPicker<SelectionValue: Hashable, Content: View>: View {
} }
func textColor() -> Color { 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 { struct ImageCarousel: View {
var urls: [URL] var urls: [URL]
let evid: String @State var open_sheet: Bool = false
let previews: PreviewCache @State var current_url: URL? = nil
@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
}
var body: some View { var body: some View {
TabView { TabView {
@@ -75,32 +47,31 @@ struct ImageCarousel: View {
Rectangle() Rectangle()
.foregroundColor(Color.clear) .foregroundColor(Color.clear)
.overlay { .overlay {
GeometryReader { geo in KFAnimatedImage(url)
KFAnimatedImage(url) .imageContext(.note)
.callbackQueue(.dispatch(.global(qos:.background))) .cancelOnDisappear(true)
.backgroundDecode(true) .configure { view in
.imageContext(.note) view.framePreloadCount = 3
.cancelOnDisappear(true) }
.configure { view in .aspectRatio(contentMode: .fit)
view.framePreloadCount = 3 .cornerRadius(10)
} .tabItem {
.imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in Text(url.absoluteString)
previews.cache_image_meta(evid: evid, image_fill: fill) }
image_fill = fill .id(url.absoluteString)
} // .contextMenu {
.aspectRatio(contentMode: filling ? .fill : .fit) // Button(NSLocalizedString("Copy Image", comment: "Context menu option to copy an image to clipboard.")) {
.tabItem { // UIPasteboard.general.string = url.absoluteString
Text(url.absoluteString) // }
} // }
.id(url.absoluteString)
}
} }
} }
} }
.fullScreenCover(isPresented: $open_sheet) { .fullScreenCover(isPresented: $open_sheet) {
ImageView(urls: urls) ImageView(urls: urls)
} }
.frame(height: height) .frame(height: 350)
.clipped()
.onTapGesture { .onTapGesture {
open_sheet = true 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 { struct ImageCarousel_Previews: PreviewProvider {
static var previews: some View { 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) .foregroundColor(.gray)
} else { } else {
Image(systemName: "checkmark.circle") Image(systemName: "checkmark.circle")
.foregroundColor(DamusColors.green) .foregroundColor(Color("DamusGreen"))
} }
} }
} }
+9 -34
View File
@@ -24,33 +24,19 @@ struct NIP05Badge: View {
self.clickable = clickable self.clickable = clickable
} }
var nip05_color: Bool { var nip05_color: Color {
return use_nip05_color(pubkey: pubkey, contacts: contacts) return get_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 body: some View { var body: some View {
HStack(spacing: 2) { HStack(spacing: 2) {
Seal Image(systemName: "checkmark.seal.fill")
.font(.footnote)
.foregroundColor(nip05_color)
if show_domain { if show_domain {
if clickable { if clickable {
Text(nip05.host) Text(nip05.host)
.nip05_colorized(gradient: nip05_color) .foregroundColor(nip05_color)
.onTapGesture { .onTapGesture {
if let nip5url = nip05.siteUrl { if let nip5url = nip05.siteUrl {
openURL(nip5url) openURL(nip5url)
@@ -58,7 +44,7 @@ struct NIP05Badge: View {
} }
} else { } else {
Text(nip05.host) Text(nip05.host)
.foregroundColor(.gray) .foregroundColor(nip05_color)
} }
} }
} }
@@ -66,19 +52,8 @@ struct NIP05Badge: View {
} }
} }
extension View { func get_nip05_color(pubkey: String, contacts: Contacts) -> Color {
func nip05_colorized(gradient: Bool) -> some View { return contacts.is_friend_or_self(pubkey) ? .accentColor : .gray
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
} }
struct NIP05Badge_Previews: PreviewProvider { struct NIP05Badge_Previews: PreviewProvider {
+2
View File
@@ -15,10 +15,12 @@ struct Reposted: View {
var body: some View { var body: some View {
HStack(alignment: .center) { HStack(alignment: .center) {
Image(systemName: "arrow.2.squarepath") Image(systemName: "arrow.2.squarepath")
.font(.system(size: 13, weight: .heavy))
.foregroundColor(Color.gray) .foregroundColor(Color.gray)
ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true, show_nip5_domain: false) ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true, show_nip5_domain: false)
.foregroundColor(Color.gray) .foregroundColor(Color.gray)
Text("Reposted", comment: "Text indicating that the post was reposted (i.e. re-shared).") Text("Reposted", comment: "Text indicating that the post was reposted (i.e. re-shared).")
.font(.system(size: 14, weight: .heavy))
.foregroundColor(Color.gray) .foregroundColor(Color.gray)
} }
} }
+1 -3
View File
@@ -15,14 +15,12 @@ struct SelectableText: View {
@State private var selectedTextHeight: CGFloat = .zero @State private var selectedTextHeight: CGFloat = .zero
@State private var selectedTextWidth: CGFloat = .zero @State private var selectedTextWidth: CGFloat = .zero
let size: EventViewKind
var body: some View { var body: some View {
GeometryReader { geo in GeometryReader { geo in
TextViewRepresentable( TextViewRepresentable(
attributedString: attributedString, attributedString: attributedString,
textColor: UIColor.label, textColor: UIColor.label,
font: eventviewsize_to_uifont(size), font: UIFont.preferredFont(forTextStyle: .title2),
fixedWidth: selectedTextWidth, fixedWidth: selectedTextWidth,
height: $selectedTextHeight 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 { struct TranslateView: View {
let damus_state: DamusState let damus_state: DamusState
let event: NostrEvent let event: NostrEvent
let size: EventViewKind
@State var checkingTranslationStatus: Bool = false @State var checkingTranslationStatus: Bool = false
@State var translatable: Bool = true @State var currentLanguage: String = "en"
@State var noteLanguage: String? = nil
@State var noteLanguage: String? @State var translated_note: String? = nil
@State var show_translated_note: Bool @State var show_translated_note: Bool = false
@State var translated_artifacts: NoteArtifacts? @State var translated_artifacts: NoteArtifacts? = nil
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind) {
self.damus_state = damus_state
self.event = event
self.size = size
self._noteLanguage = State(initialValue: damus_state.translations.detectLanguage(event, state: damus_state))
if let translationWithLanguage = damus_state.translations.cachedTranslation(event) {
self._noteLanguage = State(initialValue: translationWithLanguage.language)
let translatedBlocks = event.get_blocks(content: translationWithLanguage.translation)
self._translated_artifacts = State.init(initialValue: render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey))
} else {
self._translated_artifacts = State(initialValue: nil)
}
self._show_translated_note = State(initialValue: damus_state.settings.auto_translate)
}
var TranslateButton: some View { 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 show_translated_note = true
processTranslation()
} }
.translate_button_style() .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 { func Translated(lang: String, artifacts: NoteArtifacts) -> some View {
return Group { 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 show_translated_note = false
} }
.translate_button_style() .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 { func MainContent(note_lang: String) -> some View {
return Group { return Group {
if translatable { let languageName = Locale.current.localizedString(forLanguageCode: note_lang)
let languageName = Locale.current.localizedString(forLanguageCode: note_lang) if let lang = languageName, show_translated_note {
if let languageName, let translated_artifacts, show_translated_note { if checkingTranslationStatus {
Translated(lang: languageName, artifacts: translated_artifacts) CheckingStatus(lang: lang)
} else if !damus_state.settings.auto_translate { } else if let artifacts = translated_artifacts {
TranslateButton Translated(lang: lang, artifacts: artifacts)
} else {
EmptyView()
} }
} else { } else {
EmptyView() TranslateButton
} }
} }
} }
var body: some View { var body: some View {
Group { Group {
if let note_lang = noteLanguage, note_lang != damus_state.translations.targetLanguage { if let note_lang = noteLanguage, noteLanguage != currentLanguage {
MainContent(note_lang: note_lang) MainContent(note_lang: note_lang)
.task {
if show_translated_note {
processTranslation()
}
}
} else { } else {
Text("") Text("")
} }
} }
} .task {
} guard noteLanguage == nil && !checkingTranslationStatus && damus_state.settings.can_translate(damus_state.pubkey) else {
return
}
checkingTranslationStatus = true
extension View { if #available(iOS 16, *) {
func translate_button_style() -> some View { currentLanguage = Locale.current.language.languageCode?.identifier ?? "en"
return self } else {
.font(.footnote) currentLanguage = Locale.current.languageCode ?? "en"
.contentShape(Rectangle()) }
.padding([.top, .bottom], 10)
// 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 { struct TranslateView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let ds = test_damus_state() 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: { }, label: {
Text(link_text) Text(link_text)
.font(.footnote) .font(.footnote)
.foregroundColor(.accentColor)
}) })
} }
} }
+1 -1
View File
@@ -134,7 +134,7 @@ struct ZapButton: View {
struct ZapButton_Previews: PreviewProvider { struct ZapButton_Previews: PreviewProvider {
static var previews: some View { 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) ZapButton(damus_state: test_damus_state(), event: test_event, lnurl: "lnurl", bar: bar)
} }
} }
+68 -73
View File
@@ -8,10 +8,16 @@
import SwiftUI import SwiftUI
import Starscream import Starscream
var BOOTSTRAP_RELAYS = [
"wss://relay.damus.io",
"wss://eden.nostr.land",
"wss://nostr.wine",
"wss://nos.lol",
]
struct TimestampedProfile { struct TimestampedProfile {
let profile: Profile let profile: Profile
let timestamp: Int64 let timestamp: Int64
let event: NostrEvent
} }
enum Sheets: Identifiable { enum Sheets: Identifiable {
@@ -76,9 +82,9 @@ struct ContentView: View {
@State var profile_open: Bool = false @State var profile_open: Bool = false
@State var thread_open: Bool = false @State var thread_open: Bool = false
@State var search_open: Bool = false @State var search_open: Bool = false
@State var muting: String? = nil @State var blocking: String? = nil
@State var confirm_mute: Bool = false @State var confirm_block: Bool = false
@State var user_muted_confirm: Bool = false @State var user_blocked_confirm: Bool = false
@State var confirm_overwrite_mutelist: Bool = false @State var confirm_overwrite_mutelist: Bool = false
@State var current_boost: NostrEvent? = nil @State var current_boost: NostrEvent? = nil
@State var filter_state : FilterState = .posts_and_replies @State var filter_state : FilterState = .posts_and_replies
@@ -141,8 +147,22 @@ struct ContentView: View {
} }
var timelineNavItem: Text { var timelineNavItem: Text {
return Text(timeline_name(selected_timeline)) switch selected_timeline {
.bold() 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 { func MainContent(damus: DamusState) -> some View {
@@ -191,7 +211,7 @@ struct ContentView: View {
Image("damus-home") Image("damus-home")
.resizable() .resizable()
.frame(width:30,height:30) .frame(width:30,height:30)
.shadow(color: DamusColors.purple, radius: 2) .shadow(color: Color("DamusPurple"), radius: 2)
.opacity(isSideBarOpened ? 0 : 1) .opacity(isSideBarOpened ? 0 : 1)
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened) .animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
} else { } else {
@@ -228,9 +248,9 @@ struct ContentView: View {
func MaybeReportView(target: ReportTarget) -> some View { func MaybeReportView(target: ReportTarget) -> some View {
Group { Group {
if let damus_state { if let ds = damus_state {
if let sec = damus_state.keypair.privkey { if let sec = ds.keypair.privkey {
ReportView(postbox: damus_state.postbox, target: target, privkey: sec) ReportView(pool: ds.pool, target: target, privkey: sec)
} else { } else {
EmptyView() EmptyView()
} }
@@ -306,9 +326,9 @@ struct ContentView: View {
case .report(let target): case .report(let target):
MaybeReportView(target: target) MaybeReportView(target: target)
case .post: case .post:
PostView(replying_to: nil, damus_state: damus_state!) PostView(replying_to: nil, references: [], damus_state: damus_state!)
case .reply(let event): case .reply(let event):
PostView(replying_to: event, damus_state: damus_state!) ReplyView(replying_to: event, damus: damus_state!)
case .event: case .event:
EventDetailView() EventDetailView()
case .filter: case .filter:
@@ -369,20 +389,14 @@ struct ContentView: View {
let target = notif.object as! ReportTarget let target = notif.object as! ReportTarget
self.active_sheet = .report(target) self.active_sheet = .report(target)
} }
.onReceive(handle_notify(.mute)) { notif in .onReceive(handle_notify(.block)) { notif in
let pubkey = notif.object as! String let pubkey = notif.object as! String
self.muting = pubkey self.blocking = pubkey
self.confirm_mute = true self.confirm_block = true
} }
.onReceive(handle_notify(.broadcast_event)) { obj in .onReceive(handle_notify(.broadcast_event)) { obj in
let ev = obj.object as! NostrEvent let ev = obj.object as! NostrEvent
guard let ds = self.damus_state else { self.damus_state?.pool.send(.event(ev))
return
}
ds.postbox.send(ev)
if let profile = ds.profiles.profiles[ev.pubkey] {
ds.postbox.send(profile.event)
}
} }
.onReceive(handle_notify(.unfollow)) { notif in .onReceive(handle_notify(.unfollow)) { notif in
guard let privkey = self.privkey else { guard let privkey = self.privkey else {
@@ -396,7 +410,7 @@ struct ContentView: View {
let target = notif.object as! FollowTarget let target = notif.object as! FollowTarget
let pk = target.pubkey 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, our_contacts: damus.contacts.event,
pubkey: damus.pubkey, pubkey: damus.pubkey,
privkey: privkey, privkey: privkey,
@@ -447,16 +461,7 @@ struct ContentView: View {
//let to_relays = tup.1 //let to_relays = tup.1
print("post \(post.content)") print("post \(post.content)")
let new_ev = post_to_event(post: post, privkey: privkey, pubkey: pubkey) let new_ev = post_to_event(post: post, privkey: privkey, pubkey: pubkey)
guard let ds = self.damus_state else { self.damus_state?.pool.send(.event(new_ev))
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)
}
}
case .cancel: case .cancel:
active_sheet = nil active_sheet = nil
print("post cancelled") print("post cancelled")
@@ -474,23 +479,23 @@ struct ContentView: View {
notify(.logout, ()) notify(.logout, ())
} }
} }
.alert(NSLocalizedString("User muted", comment: "Alert message to indicate the user has been muted"), isPresented: $user_muted_confirm, actions: { .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 muted a user was successful.")) { Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to block a user was successful.")) {
user_muted_confirm = false user_blocked_confirm = false
} }
}, message: { }, message: {
if let pubkey = self.muting { if let pubkey = self.blocking {
let profile = damus_state!.profiles.lookup(id: pubkey) let profile = damus_state!.profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: profile, pubkey: pubkey).username 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 { } 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: { .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.")) { Button(NSLocalizedString("Cancel", comment: "Button to cancel out of alert that creates a new mutelist.")) {
confirm_overwrite_mutelist = false 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.")) { Button(NSLocalizedString("Yes, Overwrite", comment: "Text of button that confirms to overwrite the existing mutelist.")) {
@@ -502,7 +507,7 @@ struct ContentView: View {
return return
} }
guard let pubkey = muting else { guard let pubkey = blocking else {
return return
} }
@@ -511,20 +516,20 @@ struct ContentView: View {
} }
damus_state?.contacts.set_mutelist(mutelist) damus_state?.contacts.set_mutelist(mutelist)
ds.postbox.send(mutelist) ds.pool.send(.event(mutelist))
confirm_overwrite_mutelist = false confirm_overwrite_mutelist = false
confirm_mute = false confirm_block = false
user_muted_confirm = true user_blocked_confirm = true
} }
}, message: { }, 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: { .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 muting a user."), role: .cancel) { Button(NSLocalizedString("Cancel", comment: "Alert button to cancel out of alert for blocking a user."), role: .cancel) {
confirm_mute = false 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 { guard let ds = damus_state else {
return return
} }
@@ -535,7 +540,7 @@ struct ContentView: View {
guard let keypair = ds.keypair.to_full() else { guard let keypair = ds.keypair.to_full() else {
return return
} }
guard let pubkey = muting else { guard let pubkey = blocking else {
return return
} }
@@ -543,16 +548,16 @@ struct ContentView: View {
return return
} }
damus_state?.contacts.set_mutelist(ev) damus_state?.contacts.set_mutelist(ev)
ds.postbox.send(ev) ds.pool.send(.event(ev))
} }
} }
}, message: { }, message: {
if let pubkey = muting { if let pubkey = blocking {
let profile = damus_state?.profiles.lookup(id: pubkey) let profile = damus_state?.profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: profile, pubkey: pubkey).username 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 { } 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()) { .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 current_boost = nil
} }
Button(NSLocalizedString("Repost", comment: "Button to confirm reposting a post.")) { 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: { } message: {
Text("Are you sure you want to repost this?", comment: "Alert message to ask if user wants to repost a post.") 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 pool = RelayPool()
let metadatas = RelayMetadatas() let metadatas = RelayMetadatas()
let relay_filters = RelayFilters(our_pubkey: pubkey) let relay_filters = RelayFilters(our_pubkey: pubkey)
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
let new_relay_filters = load_relay_filters(pubkey) == nil let new_relay_filters = load_relay_filters(pubkey) == nil
for relay in bootstrap_relays { for relay in BOOTSTRAP_RELAYS {
if let url = URL(string: relay) { 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) 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) pool.register_handler(sub_id: sub_id, handler: home.handle_event)
let settings = UserSettingsStore()
self.damus_state = DamusState(pool: pool, self.damus_state = DamusState(pool: pool,
keypair: keypair, keypair: keypair,
likes: EventCounter(our_pubkey: pubkey), likes: EventCounter(our_pubkey: pubkey),
@@ -622,16 +622,12 @@ struct ContentView: View {
previews: PreviewCache(), previews: PreviewCache(),
zaps: Zaps(our_pubkey: pubkey), zaps: Zaps(our_pubkey: pubkey),
lnurls: LNUrls(), lnurls: LNUrls(),
settings: settings, settings: UserSettingsStore(),
relay_filters: relay_filters, relay_filters: relay_filters,
relay_metadata: metadatas, relay_metadata: metadatas,
drafts: Drafts(), drafts: Drafts(),
events: EventCache(), events: EventCache(),
bookmarks: BookmarksManager(pubkey: pubkey), bookmarks: BookmarksManager(pubkey: pubkey)
postbox: PostBox(pool: pool),
bootstrap_relays: bootstrap_relays,
replies: ReplyCounter(our_pubkey: pubkey),
translations: Translations(settings)
) )
home.damus_state = self.damus_state! 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?) -> ()) { func find_event(state: DamusState, evid: String, search_type: SearchType, find_from: [String]?, callback: @escaping (NostrEvent?) -> ()) {
if let ev = state.events.lookup(evid) { if let ev = state.events.lookup(evid) {
callback(ev) callback(ev)
@@ -809,8 +806,6 @@ func find_event(state: DamusState, evid: String, search_type: SearchType, find_f
} }
switch ev { switch ev {
case .ok:
break
case .event(_, let ev): case .event(_, let ev):
has_event = true has_event = true
callback(ev) callback(ev)
@@ -837,12 +832,12 @@ func timeline_name(_ timeline: Timeline?) -> String {
} }
switch timeline { switch timeline {
case .home: 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: case .notifications:
return NSLocalizedString("Notifications", comment: "Toolbar label for Notifications view.") return "Notifications"
case .search: case .search:
return NSLocalizedString("Universe 🛸", comment: "Toolbar label for the universal view where posts from all connected relay servers appear.") return "Universe 🛸"
case .dms: 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> <key>NSAllowsArbitraryLoads</key>
<true/> <true/>
</dict> </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> </dict>
</plist> </plist>
+2 -12
View File
@@ -11,40 +11,34 @@ import Foundation
class ActionBarModel: ObservableObject { class ActionBarModel: ObservableObject {
@Published var our_like: NostrEvent? @Published var our_like: NostrEvent?
@Published var our_boost: NostrEvent? @Published var our_boost: NostrEvent?
@Published var our_reply: NostrEvent?
@Published var our_zap: Zap? @Published var our_zap: Zap?
@Published var likes: Int @Published var likes: Int
@Published var boosts: Int @Published var boosts: Int
@Published var zaps: Int @Published var zaps: Int
@Published var zap_total: Int64 @Published var zap_total: Int64
@Published var replies: Int
static func empty() -> ActionBarModel { static func empty() -> ActionBarModel {
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil) return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, 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.likes = likes
self.boosts = boosts self.boosts = boosts
self.zaps = zaps self.zaps = zaps
self.replies = replies
self.zap_total = zap_total self.zap_total = zap_total
self.our_like = our_like self.our_like = our_like
self.our_boost = our_boost self.our_boost = our_boost
self.our_zap = our_zap self.our_zap = our_zap
self.our_reply = our_reply
} }
func update(damus: DamusState, evid: String) { func update(damus: DamusState, evid: String) {
self.likes = damus.likes.counts[evid] ?? 0 self.likes = damus.likes.counts[evid] ?? 0
self.boosts = damus.boosts.counts[evid] ?? 0 self.boosts = damus.boosts.counts[evid] ?? 0
self.zaps = damus.zaps.event_counts[evid] ?? 0 self.zaps = damus.zaps.event_counts[evid] ?? 0
self.replies = damus.replies.get_replies(evid)
self.zap_total = damus.zaps.event_totals[evid] ?? 0 self.zap_total = damus.zaps.event_totals[evid] ?? 0
self.our_like = damus.likes.our_events[evid] self.our_like = damus.likes.our_events[evid]
self.our_boost = damus.boosts.our_events[evid] self.our_boost = damus.boosts.our_events[evid]
self.our_zap = damus.zaps.our_zaps[evid]?.first self.our_zap = damus.zaps.our_zaps[evid]?.first
self.our_reply = damus.replies.our_reply(evid)
self.objectWillChange.send() self.objectWillChange.send()
} }
@@ -60,10 +54,6 @@ class ActionBarModel: ObservableObject {
return our_like != nil return our_like != nil
} }
var replied: Bool {
return our_reply != nil
}
var boosted: Bool { var boosted: Bool {
return our_boost != nil 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 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 { guard let cs = our_contacts else {
return nil return nil
} }
@@ -149,7 +149,7 @@ func unfollow_user(postbox: PostBox, our_contacts: NostrEvent?, pubkey: String,
ev.calculate_id() ev.calculate_id()
ev.sign(privkey: privkey) ev.sign(privkey: privkey)
postbox.send(ev) pool.send(.event(ev))
return ev return ev
} }
-1
View File
@@ -14,7 +14,6 @@ class CreateAccountModel: ObservableObject {
@Published var about: String = "" @Published var about: String = ""
@Published var pubkey: String = "" @Published var pubkey: String = ""
@Published var privkey: String = "" @Published var privkey: String = ""
@Published var profile_image: String? = nil
var pubkey_bech32: String { var pubkey_bech32: String {
return bech32_pubkey(self.pubkey) ?? "" return bech32_pubkey(self.pubkey) ?? ""
+2 -6
View File
@@ -26,10 +26,7 @@ struct DamusState {
let drafts: Drafts let drafts: Drafts
let events: EventCache let events: EventCache
let bookmarks: BookmarksManager let bookmarks: BookmarksManager
let postbox: PostBox
let bootstrap_relays: [String]
let replies: ReplyCounter
let translations: Translations
var pubkey: String { var pubkey: String {
return keypair.pubkey return keypair.pubkey
} }
@@ -39,7 +36,6 @@ struct DamusState {
} }
static var empty: 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: ""))
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))
} }
} }
+1 -1
View File
@@ -1,5 +1,5 @@
// //
// DraftModel.swift // DraftsModel.swift
// damus // damus
// //
// Created by Terry Yiu on 2/12/23. // 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) handle_event(relay_id: relay_id, ev: ev)
case .notice(_): case .notice(_):
break break
case .ok:
break
case .eose(_): case .eose(_):
load_profiles(profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state) 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 { } else if sub_id == self.profiles_id {
damus_state.pool.unsubscribe(sub_id: profiles_id, to: [relay_id]) damus_state.pool.unsubscribe(sub_id: profiles_id, to: [relay_id])
} }
case .ok:
break
} }
} }
} }
-2
View File
@@ -58,8 +58,6 @@ class FollowingModel {
break break
case .nostr_event(let nev): case .nostr_event(let nev):
switch nev { switch nev {
case .ok:
break
case .event(_, let ev): case .event(_, let ev):
if ev.kind == 0 { if ev.kind == 0 {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) 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 { guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
return return
} }
@@ -145,15 +145,9 @@ class HomeModel: ObservableObject {
return return
} }
if handle_last_event(ev: ev, timeline: .notifications) { if handle_last_event(ev: ev, timeline: .notifications) && damus_state.settings.zap_vibration {
if damus_state.settings.zap_vibration { // Generate zap vibration
// Generate zap vibration zap_vibrate(zap_amount: zap.invoice.amount)
zap_vibrate(zap_amount: zap.invoice.amount)
}
if damus_state.settings.zap_notification {
// Create in-app local notification for zap received.
create_in_app_zap_notification(profiles: profiles, zap: zap)
}
} }
return return
@@ -167,7 +161,7 @@ class HomeModel: ObservableObject {
let our_keypair = damus_state.keypair let our_keypair = damus_state.keypair
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) { 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 return
} }
@@ -186,7 +180,7 @@ class HomeModel: ObservableObject {
DispatchQueue.main.async { DispatchQueue.main.async {
self.damus_state.profiles.zappers[ptag] = zapper 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 self.loading = false
break break
case .ok:
break
} }
} }
} }
@@ -465,7 +455,7 @@ class HomeModel: ObservableObject {
return m[kind] return m[kind]
} }
func handle_notification(ev: NostrEvent) { func handle_notification(ev: NostrEvent) {
// don't show notifications from ourselves // don't show notifications from ourselves
guard ev.pubkey != damus_state.pubkey else { guard ev.pubkey != damus_state.pubkey else {
@@ -489,10 +479,7 @@ class HomeModel: ObservableObject {
return return
} }
if handle_last_event(ev: ev, timeline: .notifications) { handle_last_event(ev: ev, timeline: .notifications)
process_local_notification(damus_state: damus_state, event: ev)
}
} }
@discardableResult @discardableResult
@@ -511,13 +498,11 @@ class HomeModel: ObservableObject {
} }
} }
func handle_text_event(sub_id: String, _ ev: NostrEvent) { func handle_text_event(sub_id: String, _ ev: NostrEvent) {
guard should_show_event(contacts: damus_state.contacts, ev: ev) else { guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
return return
} }
damus_state.replies.count_replies(ev)
damus_state.events.insert(ev) damus_state.events.insert(ev)
if sub_id == home_subid { if sub_id == home_subid {
@@ -543,13 +528,9 @@ class HomeModel: ObservableObject {
incoming_dms.append(ev) 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) { if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
self.new_events = notifs self.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 = [] 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) profiles.add(id: ev.pubkey, profile: tprof)
if let nip05 = profile.nip05, old_nip05 != profile.nip05 { 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 { DispatchQueue.main.async {
profiles.validated[ev.pubkey] = validated profiles.validated[ev.pubkey] = validated
profiles.nip05_pubkey[nip05] = ev.pubkey
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile)) 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) { func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
let bootstrap_dict: [String: RelayInfo] = [:] 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 d[r] = .rw
} }
@@ -779,7 +759,6 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
} }
if changed { if changed {
save_bootstrap_relays(pubkey: state.pubkey, relays: Array(new))
notify(.relays_changed, ()) notify(.relays_changed, ())
} }
} }
@@ -949,122 +928,3 @@ func zap_vibrate(zap_amount: Int64) {
vibration_generator.impactOccurred() 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 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 { class ImageUploadModel: NSObject, URLSessionTaskDelegate, ObservableObject {
@Published var progress: Double? = nil @Published var progress: Double? = nil
func start(media: MediaUpload, uploader: MediaUploader) async -> ImageUploadResult { func start(img: UIImage, uploader: ImageUploader) async -> ImageUploadResult {
let res = await create_upload_request(mediaToUpload: media, mediaUploader: uploader, progress: self) let res = await create_image_upload_request(imageToUpload: img, imageUploader: uploader, progress: self)
DispatchQueue.main.async { DispatchQueue.main.async {
self.progress = nil self.progress = nil
} }
-2
View File
@@ -133,8 +133,6 @@ class ProfileModel: ObservableObject, Equatable {
return return
} }
switch resp { switch resp {
case .ok:
break
case .event(_, let ev): case .event(_, let ev):
add_event(ev) add_event(ev)
case .notice(let notice): case .notice(let notice):
-2
View File
@@ -68,8 +68,6 @@ class SearchHomeModel: ObservableObject {
} }
case .notice(let msg): case .notice(let msg):
print("search home notice: \(msg)") print("search home notice: \(msg)")
case .ok:
break
case .eose(let sub_id): case .eose(let sub_id):
loading = false 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): case .event(let ev_subid, let ev):
handle(ev_subid, ev) handle(ev_subid, ev)
return (ev_subid, false) return (ev_subid, false)
case .ok:
return (nil, false)
case .notice(let note): case .notice(let note):
if note.contains("Too many subscription filters") { 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) let the_ev = damus_state.events.upsert(ev)
damus_state.replies.count_replies(the_ev)
damus_state.events.add_replies(ev: the_ev) damus_state.events.add_replies(ev: the_ev)
event_map.insert(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 { func get_image_uploader(_ pubkey: String) -> ImageUploader {
if let defaultMediaUploader = UserDefaults.standard.string(forKey: "default_media_uploader"), if let defaultImageUploader = UserDefaults.standard.string(forKey: "default_image_uploader"),
let defaultMediaUploader = MediaUploader(rawValue: defaultMediaUploader) { let defaultImageUploader = ImageUploader(rawValue: defaultImageUploader) {
return defaultMediaUploader return defaultImageUploader
} else { } else {
return .nostrBuild return .nostrBuild
} }
@@ -98,9 +98,9 @@ class UserSettingsStore: ObservableObject {
} }
} }
@Published var default_media_uploader: MediaUploader { @Published var default_image_uploader: ImageUploader {
didSet { 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 { @Published var translation_service: TranslationService {
didSet { didSet {
UserDefaults.standard.set(translation_service.rawValue, forKey: "translation_service") UserDefaults.standard.set(translation_service.rawValue, forKey: "translation_service")
@@ -265,21 +205,11 @@ class UserSettingsStore: ObservableObject {
show_wallet_selector = should_show_wallet_selector(pubkey) show_wallet_selector = should_show_wallet_selector(pubkey)
always_show_images = UserDefaults.standard.object(forKey: "always_show_images") as? Bool ?? false 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 left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
zap_vibration = UserDefaults.standard.object(forKey: "zap_vibration") 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() 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: // Note from @tyiu:
// Default translation service is disabled by default for now until we gain some confidence that it is working well in production. // 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 { private func clearDeepLApiKey() throws {
try Vault.deletePrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration()) 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 { 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."), 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: "") link: "lightning:", appStoreLink: "lightning:", image: "")
case .strike: 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") appStoreLink: "https://apps.apple.com/us/app/strike-bitcoin-payments/id1488724463", image: "strike")
case .cashapp: 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") appStoreLink: "https://apps.apple.com/us/app/cash-app/id711923939", image: "cashapp")
case .muun: 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: 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") appStoreLink: "https://apps.apple.com/us/app/bluewallet-bitcoin-wallet/id1376878040", image: "bluewallet")
case .walletofsatoshi: 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") appStoreLink: "https://apps.apple.com/us/app/wallet-of-satoshi/id1438599608", image: "walletofsatoshi")
case .zebedee: 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") appStoreLink: "https://apps.apple.com/us/app/zebedee-wallet/id1484394401", image: "zebedee")
case .zeusln: 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") appStoreLink: "https://apps.apple.com/us/app/zeus-ln/id1456038895", image: "zeusln")
case .lnlink: 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") appStoreLink: "https://testflight.apple.com/join/aNY4yuuZ", image: "lnlink")
case .phoenix: 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") appStoreLink: "https://apps.apple.com/us/app/phoenix-wallet/id1544097028", image: "phoenix")
case .breez: 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") appStoreLink: "https://apps.apple.com/us/app/breez-lightning-client-pos/id1463604142", image: "breez")
case .bitcoinbeach: 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") appStoreLink: "https://apps.apple.com/sv/app/bitcoin-beach-wallet/id1531383905", image: "bbw")
case .blixtwallet: 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") appStoreLink: "https://testflight.apple.com/join/EXvGhRzS", image: "blixt-wallet")
case .river: 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") 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 { switch resp {
case .ok:
break
case .notice: case .notice:
break break
case .eose: case .eose:
+1 -10
View File
@@ -98,16 +98,7 @@ struct Profile: Codable {
} }
var website_url: URL? { var website_url: URL? {
if self.website?.trimmingCharacters(in: .whitespacesAndNewlines) == "" { return self.website.flatMap { URL(string: $0) }
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)
}
} }
var lnurl: String? { var lnurl: String? {
+2 -8
View File
@@ -10,7 +10,6 @@ import CommonCrypto
import secp256k1 import secp256k1
import secp256k1_implementation import secp256k1_implementation
import CryptoKit import CryptoKit
import NaturalLanguage
@@ -519,21 +518,16 @@ func make_first_contact_event(keypair: Keypair) -> NostrEvent? {
guard let privkey = keypair.privkey else { guard let privkey = keypair.privkey else {
return nil return nil
} }
let bootstrap_relays = load_bootstrap_relays(pubkey: keypair.pubkey)
let rw_relay_info = RelayInfo(read: true, write: true) let rw_relay_info = RelayInfo(read: true, write: true)
var relays: [String: RelayInfo] = [:] var relays: [String: RelayInfo] = [:]
for relay in BOOTSTRAP_RELAYS {
for relay in bootstrap_relays {
relays[relay] = rw_relay_info relays[relay] = rw_relay_info
} }
let relay_json = encode_json(relays)! let relay_json = encode_json(relays)!
let damus_pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681" let damus_pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
let jb55_pubkey = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245" // lol
let tags = [ let tags = [
["p", damus_pubkey], ["p", damus_pubkey],
["p", jb55_pubkey],
["p", keypair.pubkey] // you're a friend of yourself! ["p", keypair.pubkey] // you're a friend of yourself!
] ]
let ev = NostrEvent(content: relay_json, 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 { 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 import Foundation
struct CommandResult {
let event_id: String
let ok: Bool
let msg: String
}
enum NostrResponse: Decodable { enum NostrResponse: Decodable {
case event(String, NostrEvent) case event(String, NostrEvent)
case notice(String) case notice(String)
case eose(String) case eose(String)
case ok(CommandResult)
var subid: String? { var subid: String? {
switch self { switch self {
case .ok(_):
return nil
case .event(let sub_id, _): case .event(let sub_id, _):
return sub_id return sub_id
case .eose(let sub_id): case .eose(let sub_id):
@@ -57,23 +48,9 @@ enum NostrResponse: Decodable {
let sub_id = try container.decode(String.self) let sub_id = try container.decode(String.self)
self = .eose(sub_id) self = .eose(sub_id)
return 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 { class Profiles {
var profiles: [String: TimestampedProfile] = [:] var profiles: [String: TimestampedProfile] = [:]
var validated: [String: NIP05] = [:] var validated: [String: NIP05] = [:]
var nip05_pubkey: [String: String] = [:]
var zappers: [String: String] = [:] var zappers: [String: String] = [:]
func is_validated(_ pk: String) -> NIP05? { func is_validated(_ pk: String) -> NIP05? {
+1
View File
@@ -256,6 +256,7 @@ class RelayPool {
} }
} }
// handle reconnect logic, etc?
for handler in handlers { for handler in handlers {
handler.callback(relay_id, event) 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 { func parse_display_name(profile: Profile?, pubkey: String) -> DisplayName {
if pubkey == "anon" { if pubkey == "anon" {
return .one(NSLocalizedString("Anonymous", comment: "Placeholder display name of anonymous user.")) return .one("Anonymous")
} }
guard let profile else { 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) let fallback = bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: key, value: nil, table: nil)
return bundle.localizedString(forKey: key, value: fallback, 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 case valid
} }
struct FetchedNIP05 { func validate_nip05(pubkey: String, nip05_str: String) async -> NIP05? {
let response: NIP05Response
let nip05: NIP05Response
}
func fetch_nip05_str(nip05_str: String) async -> NIP05Response? {
guard let nip05 = NIP05.parse(nip05_str) else { guard let nip05 = NIP05.parse(nip05_str) else {
return nil return nil
} }
return await fetch_nip05(nip05: nip05)
}
func fetch_nip05(nip05: NIP05) async -> NIP05Response? {
guard let url = nip05.url else { guard let url = nip05.url else {
return nil return nil
} }
@@ -66,18 +57,6 @@ func fetch_nip05(nip05: NIP05) async -> NIP05Response? {
return nil 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 { guard let stored_pk = decoded.names[nip05.username] else {
return nil return nil
} }
+2 -2
View File
@@ -86,8 +86,8 @@ extension Notification.Name {
static var report: Notification.Name { static var report: Notification.Name {
return Notification.Name("report") return Notification.Name("report")
} }
static var mute: Notification.Name { static var block: Notification.Name {
return Notification.Name("mute") return Notification.Name("block")
} }
static var new_mutes: Notification.Name { static var new_mutes: Notification.Name {
return Notification.Name("new_mutes") return Notification.Name("new_mutes")
-113
View File
@@ -6,116 +6,3 @@
// //
import Foundation 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 { class PreviewCache {
private var previews: [String: Preview] var previews: [String: Preview]
private var image_meta: [String: ImageFill]
func lookup(_ evid: String) -> Preview? { func lookup(_ evid: String) -> Preview? {
return previews[evid] 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?) { func store(evid: String, preview: LPLinkMetadata?) {
switch preview { switch preview {
case .none: case .none:
@@ -50,6 +41,5 @@ class PreviewCache {
init() { init() {
self.previews = [:] 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 self.userSettingsStore = userSettingsStore
} }
/** public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
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? {
switch userSettingsStore.translation_service { switch userSettingsStore.translation_service {
case .libretranslate: case .libretranslate:
return try await translateWithLibreTranslate(text, from: sourceLanguage, to: targetLanguage) return try await translateWithLibreTranslate(text, from: sourceLanguage, to: targetLanguage)
case .deepl: case .deepl:
return try await translateWithDeepL(text, to: targetLanguage) return try await translateWithDeepL(text, from: sourceLanguage, to: targetLanguage)
case .none: case .none:
return nil return text
} }
} }
/** private func translateWithLibreTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
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? {
let url = try makeURL(userSettingsStore.libretranslate_url, path: "/translate") let url = try makeURL(userSettingsStore.libretranslate_url, path: "/translate")
var request = URLRequest(url: url) var request = URLRequest(url: url)
@@ -61,15 +51,10 @@ public struct Translator {
let translatedText: String let translatedText: String
} }
let response: Response = try await decodedData(for: request) let response: Response = try await decodedData(for: request)
let translation = response.translatedText return response.translatedText
return TranslationWithLanguage(translation: translation, language: targetLanguage)
} }
/** private func translateWithDeepL(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
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? {
if userSettingsStore.deepl_api_key == "" { if userSettingsStore.deepl_api_key == "" {
return nil return nil
} }
@@ -83,9 +68,10 @@ public struct Translator {
struct RequestBody: Encodable { struct RequestBody: Encodable {
let text: [String] let text: [String]
let source_lang: String
let target_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) request.httpBody = try encoder.encode(body)
struct Response: Decodable { struct Response: Decodable {
@@ -97,13 +83,7 @@ public struct Translator {
} }
let response: Response = try await decodedData(for: request) let response: Response = try await decodedData(for: request)
return response.translations.map { $0.text }.joined(separator: " ")
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)
} }
private func makeURL(_ baseUrl: String, path: String) throws -> URL { 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 { private extension URLSession {
func data(for request: URLRequest) async throws -> Data { func data(for request: URLRequest) async throws -> Data {
var task: URLSessionDataTask? var task: URLSessionDataTask?
+14 -26
View File
@@ -47,15 +47,10 @@ struct EventActionBar: View {
var body: some View { var body: some View {
HStack { HStack {
if damus_state.keypair.privkey != nil { if damus_state.keypair.privkey != nil {
HStack(spacing: 4) { EventActionButton(img: "bubble.left", col: nil) {
EventActionButton(img: "bubble.left", col: bar.replied ? Color.blue : Color.gray) { notify(.reply, event)
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)
} }
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
} }
Spacer() Spacer()
HStack(spacing: 4) { HStack(spacing: 4) {
@@ -82,10 +77,10 @@ struct EventActionBar: View {
send_like() send_like()
} }
} }
Text(verbatim: "\(bar.likes > 0 ? "\(bar.likes)" : "")") Text(verbatim: "\(bar.likes > 0 ? "\(bar.likes)" : "")")
.font(.footnote.weight(.medium)) .font(.footnote.weight(.medium))
.nip05_colorized(gradient: bar.liked) .foregroundColor(bar.liked ? Color.accentColor : Color.gray)
} }
if let lnurl = self.lnurl { if let lnurl = self.lnurl {
@@ -159,7 +154,7 @@ struct EventActionBar: View {
generator.impactOccurred() 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 amountOfAngleIncrease = 20.0
} }
}) { }) {
if liked { Image(liked ? "shaka-full" : "shaka-line")
LINEAR_GRADIENT .foregroundColor(liked ? .accentColor : .gray)
.mask(Image("shaka-full")
.resizable()
).frame(width: 14, height: 14)
} else {
Image("shaka-line")
.foregroundColor(.gray)
}
} }
.accessibilityLabel(NSLocalizedString("Like", comment: "Accessibility Label for Like button")) .accessibilityLabel(NSLocalizedString("Like", comment: "Accessibility Label for Like button"))
.rotationEffect(Angle(degrees: shouldAnimate ? rotationAngle : 0)) .rotationEffect(Angle(degrees: shouldAnimate ? rotationAngle : 0))
@@ -230,12 +218,12 @@ struct EventActionBar_Previews: PreviewProvider {
let ev = NostrEvent(content: "hi", pubkey: pk) let ev = NostrEvent(content: "hi", pubkey: pk)
let bar = ActionBarModel.empty() 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 = 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, replies: 0, our_like: test_event, our_boost: nil, our_zap: nil, our_reply: 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, replies: 999, our_like: test_event, our_boost: test_event, our_zap: nil, our_reply: 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, replies: 9999, our_like: test_event, our_boost: test_event, our_zap: nil, our_reply: test_event) 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, replies: 9999999, our_like: test_event, our_boost: test_event, our_zap: test_zap, our_reply: test_event) 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, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: 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) { VStack(spacing: 50) {
EventActionBar(damus_state: ds, event: ev, bar: bar) EventActionBar(damus_state: ds, event: ev, bar: bar)
+6 -6
View File
@@ -29,7 +29,7 @@ struct ShareAction: View {
var body: some View { var body: some View {
let col = colorScheme == .light ? DamusColors.mediumGrey : DamusColors.white let col = colorScheme == .light ? Color("DamusMediumGrey") : Color("DamusWhite")
VStack { VStack {
Text("Share Note", comment: "Title text to indicate that the buttons below are meant to be used to share a note with others.") 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) { 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 show_share_action = false
UIPasteboard.general.string = "https://damus.io/" + (bech32_note_id(event.id) ?? event.id) UIPasteboard.general.string = "https://damus.io/" + (bech32_note_id(event.id) ?? event.id)
} }
let bookmarkImg = isBookmarked ? "bookmark.slash" : "bookmark" 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 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 show_share_action = false
self.bookmarks.updateBookmark(event) self.bookmarks.updateBookmark(event)
isBookmarked = self.bookmarks.isBookmarked(event) isBookmarked = self.bookmarks.isBookmarked(event)
@@ -75,10 +75,10 @@ struct ShareAction: View {
}) { }) {
Text(NSLocalizedString("Cancel", comment: "Button to cancel a repost.")) Text(NSLocalizedString("Cancel", comment: "Button to cancel a repost."))
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .center) .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 { .overlay {
RoundedRectangle(cornerRadius: 24) 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)) .padding(EdgeInsets(top: 10, leading: 50, bottom: 25, trailing: 50))
} }
+51 -20
View File
@@ -8,41 +8,72 @@
import SwiftUI import SwiftUI
struct AddRelayView: View { struct AddRelayView: View {
@Binding var show_add_relay: Bool
@Binding var relay: String @Binding var relay: String
let action: (String?) -> Void
var body: some View { var body: some View {
ZStack(alignment: .leading) { VStack(alignment: .leading) {
HStack{ Form {
TextField(NSLocalizedString("wss://some.relay.com", comment: "Placeholder example for relay server address."), text: $relay) Section(NSLocalizedString("Add Relay", comment: "Label for section for adding a relay server.")) {
.padding(2) ZStack(alignment: .leading) {
.padding(.leading, 25) HStack{
.autocorrectionDisabled(true) TextField(NSLocalizedString("wss://some.relay.com", comment: "Placeholder example for relay server address."), text: $relay)
.textInputAutocapitalization(.never) .padding(2)
.padding(.leading, 25)
Label("", systemImage: "xmark.circle.fill") .autocorrectionDisabled(true)
.foregroundColor(.accentColor) .textInputAutocapitalization(.never)
.padding(.trailing, -25.0)
.opacity((relay == "") ? 0.0 : 1.0) Label("", systemImage: "xmark.circle.fill")
.onTapGesture { .foregroundColor(.blue)
self.relay = "" .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") VStack {
.padding(.leading, -10) HStack {
.onTapGesture { Button(NSLocalizedString("Cancel", comment: "Button to cancel out of view adding user inputted relay.")) {
if let pastedrelay = UIPasteboard.general.string { show_add_relay = false
self.relay = pastedrelay 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 { struct AddRelayView_Previews: PreviewProvider {
@State static var show: Bool = true
@State static var relay: String = "" @State static var relay: String = ""
static var previews: some View { 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?) 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 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: "Content-Type: multipart/form-data; boundary=\(boundary)\r\n\r\n")
body.appendString(string: "--\(boundary)\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.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: "\r\n")
body.appendString(string: "--\(boundary)--\r\n") body.appendString(string: "--\(boundary)--\r\n")
return body as Data return body as Data
} }
func create_upload_request(mediaToUpload: MediaUpload, mediaUploader: MediaUploader, progress: URLSessionTaskDelegate) async -> ImageUploadResult {
var mediaData: Data? func create_image_upload_request(imageToUpload: UIImage, imageUploader: ImageUploader, progress: URLSessionTaskDelegate) async -> ImageUploadResult {
guard let url = URL(string: mediaUploader.postAPI) else {
guard let url = URL(string: imageUploader.postAPI) else {
return .failed(nil) return .failed(nil)
} }
@@ -39,26 +40,13 @@ func create_upload_request(mediaToUpload: MediaUpload, mediaUploader: MediaUploa
let boundary = "Boundary-\(UUID().description)" let boundary = "Boundary-\(UUID().description)"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
switch mediaToUpload { // otherwise convert to jpg
case .image(let url): guard let jpegData = imageToUpload.jpegData(compressionQuality: 0.8) else {
do { // somehow failed, just return original
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 {
return .failed(nil) 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 { do {
let (data, _) = try await URLSession.shared.data(for: request, delegate: progress) 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) return .failed(nil)
} }
guard let url = mediaUploader.getMediaURL(from: responseString, mediaIsImage: mediaToUpload.is_image) else { guard let url = imageUploader.getImageURL(from: responseString) else {
print("Upload failed getting media url") print("Upload failed getting image url")
return .failed(nil) return .failed(nil)
} }
@@ -78,6 +66,67 @@ func create_upload_request(mediaToUpload: MediaUpload, mediaUploader: MediaUploa
} catch { } catch {
return .failed(error) 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 { extension NSMutableData {
@@ -89,7 +138,7 @@ extension NSMutableData {
} }
} }
enum MediaUploader: String, CaseIterable, Identifiable { enum ImageUploader: String, CaseIterable, Identifiable {
var id: String { self.rawValue } var id: String { self.rawValue }
case nostrBuild case nostrBuild
case nostrImg case nostrImg
@@ -103,12 +152,12 @@ enum MediaUploader: String, CaseIterable, Identifiable {
} }
} }
var supportsVideo: Bool { var displayImageUploaderName: String {
switch self { switch self {
case .nostrBuild: case .nostrBuild:
return true return "NostrBuild"
case .nostrImg: 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 { switch self {
case .nostrBuild: case .nostrBuild:
guard let startIndex = responseString.range(of: "nostr.build_")?.lowerBound else { guard let startIndex = responseString.range(of: "nostr.build_")?.lowerBound else {
@@ -150,7 +199,7 @@ enum MediaUploader: String, CaseIterable, Identifiable {
return nil return nil
} }
let nostrBuildImageName = responseString[startIndex..<endIndex] 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 return nostrBuildURL
case .nostrImg: case .nostrImg:
+1 -1
View File
@@ -114,7 +114,7 @@ struct ChatView: View {
show_images: show_images, show_images: show_images,
size: .normal, size: .normal,
artifacts: .just_content(event.content), artifacts: .just_content(event.content),
options: []) truncate: false)
if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey { if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey {
let bar = make_actionbar_model(ev: event.id, damus: damus_state) 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 confirm_logout: Bool = false
@State var delete_account_warning: Bool = false @State var delete_account_warning: Bool = false
@State var confirm_delete_account: 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 delete_text: String = ""
@State var default_zap_amount: String
@ObservedObject var settings: UserSettingsStore @ObservedObject var settings: UserSettingsStore
private let DELETE_KEYWORD = "DELETE" let generator = UIImpactFeedbackGenerator(style: .light)
init(state: DamusState) { init(state: DamusState) {
self.state = state 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) _settings = ObservedObject(initialValue: state.settings)
} }
func textColor() -> Color { 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 { var body: some View {
ZStack(alignment: .leading) { ZStack(alignment: .leading) {
Form { Form {
Section { Section(NSLocalizedString("Public Account ID", comment: "Section title for the user's public account ID.")) {
NavigationLink(destination: KeySettingsView(keypair: state.keypair)) { HStack {
IconLabel(NSLocalizedString("Keys", comment: "Settings section for managing keys"), img_name: "key.fill", color: .purple) Text(state.keypair.pubkey_bech32)
CopyButton(is_pk: true)
} }
.clipShape(RoundedRectangle(cornerRadius: 5))
NavigationLink(destination: AppearanceSettingsView(settings: settings)) { }
IconLabel(NSLocalizedString("Appearance", comment: "Section header for text and appearance settings"), img_name: "textformat", color: .red)
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) 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"),
NavigationLink(destination: ZapSettingsView(pubkey: state.pubkey, settings: settings)) { selection: $settings.default_wallet) {
IconLabel(NSLocalizedString("Zaps", comment: "Section header for zap settings"), img_name: "bolt.fill", color: .orange) ForEach(Wallet.allCases, id: \.self) { wallet in
} Text(wallet.model.displayName)
.tag(wallet.model.tag)
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("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")) { Section(NSLocalizedString("Sign Out", comment: "Section title for signing out")) {
Button(action: { Button(action: {
if state.keypair.privkey == nil { 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) { .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) { Button(NSLocalizedString("Cancel", comment: "Cancel deleting the user."), role: .cancel) {
confirm_delete_account = false confirm_delete_account = false
} }
@@ -108,12 +278,12 @@ struct ConfigView: View {
return return
} }
guard delete_text == DELETE_KEYWORD else { guard delete_text == "DELETE" else {
return return
} }
let ev = created_deleted_account_profile(keypair: full_kp) let ev = created_deleted_account_profile(keypair: full_kp)
state.postbox.send(ev) state.pool.send(.event(ev))
notify(.logout, ()) 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 { struct ConfigView_Previews: PreviewProvider {
+1 -10
View File
@@ -9,12 +9,9 @@ import SwiftUI
struct CreateAccountView: View { struct CreateAccountView: View {
@StateObject var account: CreateAccountModel = CreateAccountModel() @StateObject var account: CreateAccountModel = CreateAccountModel()
@StateObject var profileUploadViewModel = ProfileUploadingViewModel()
@State var is_light: Bool = false @State var is_light: Bool = false
@State var is_done: Bool = false @State var is_done: Bool = false
@State var reading_eula: Bool = false @State var reading_eula: Bool = false
@State var profile_image: URL? = nil
func SignupForm<FormContent: View>(@ViewBuilder content: () -> FormContent) -> some View { func SignupForm<FormContent: View>(@ViewBuilder content: () -> FormContent) -> some View {
return VStack(alignment: .leading, spacing: 10.0, content: content) return VStack(alignment: .leading, spacing: 10.0, content: content)
@@ -35,7 +32,7 @@ struct CreateAccountView: View {
.font(.title.bold()) .font(.title.bold())
.foregroundColor(.white) .foregroundColor(.white)
ProfilePictureSelector(pubkey: account.pubkey, viewModel: profileUploadViewModel, callback: uploadedProfilePicture(image_url:)) ProfilePictureSelector(pubkey: account.pubkey)
HStack(alignment: .top) { HStack(alignment: .top) {
VStack { VStack {
@@ -84,8 +81,6 @@ struct CreateAccountView: View {
self.is_done = true self.is_done = true
} }
.padding() .padding()
.disabled(profileUploadViewModel.isLoading)
.opacity(profileUploadViewModel.isLoading ? 0.5 : 1)
} }
.padding(.leading, 14.0) .padding(.leading, 14.0)
.padding(.trailing, 20.0) .padding(.trailing, 20.0)
@@ -96,10 +91,6 @@ struct CreateAccountView: View {
.navigationBarBackButtonHidden(true) .navigationBarBackButtonHidden(true)
.navigationBarItems(leading: BackNav()) .navigationBarItems(leading: BackNav())
} }
func uploadedProfilePicture(image_url: URL?) {
account.profile_image = image_url?.absoluteString
}
} }
struct BackNav: View { struct BackNav: View {
+1 -4
View File
@@ -130,10 +130,7 @@ struct DMChatView: View {
dms.draft = "" dms.draft = ""
damus_state.postbox.send(dm) damus_state.pool.send(.event(dm))
handle_incoming_dm(ev: dm, our_pubkey: damus_state.pubkey, dms: damus_state.dms, prev_events: NewEventsBits())
end_editing() end_editing()
} }
+3 -22
View File
@@ -14,18 +14,8 @@ struct DMView: View {
var is_ours: Bool { var is_ours: Bool {
event.pubkey == damus_state.pubkey event.pubkey == damus_state.pubkey
} }
var Mention: some View { var body: 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 {
HStack { HStack {
if is_ours { if is_ours {
Spacer(minLength: UIScreen.main.bounds.width * 0.2) 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) 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([.top, .leading, .trailing], 10)
.padding([.bottom], 25) .padding([.bottom], 25)
.background(VisualEffectView(effect: UIBlurEffect(style: .prominent)) .background(VisualEffectView(effect: UIBlurEffect(style: .prominent))
@@ -46,20 +36,11 @@ struct DMView: View {
.foregroundColor(.gray) .foregroundColor(.gray)
.opacity(0.8) .opacity(0.8)
.offset(x: -10, y: -5), alignment: .bottomTrailing) .offset(x: -10, y: -5), alignment: .bottomTrailing)
if !is_ours { if !is_ours {
Spacer(minLength: UIScreen.main.bounds.width * 0.2) Spacer(minLength: UIScreen.main.bounds.width * 0.2)
} }
} }
} }
var body: some View {
VStack {
Mention
DM
}
}
} }
struct DMView_Previews: PreviewProvider { struct DMView_Previews: PreviewProvider {
+4 -12
View File
@@ -67,7 +67,6 @@ struct EditMetadataView: View {
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@State var confirm_ln_address: Bool = false @State var confirm_ln_address: Bool = false
@StateObject var profileUploadViewModel = ProfileUploadingViewModel()
init (damus_state: DamusState) { init (damus_state: DamusState) {
self.damus_state = damus_state self.damus_state = damus_state
@@ -84,7 +83,7 @@ struct EditMetadataView: View {
} }
func imageBorderColor() -> Color { func imageBorderColor() -> Color {
colorScheme == .light ? DamusColors.white : DamusColors.black colorScheme == .light ? Color("DamusWhite") : Color("DamusBlack")
} }
func save() { func save() {
@@ -103,7 +102,7 @@ struct EditMetadataView: View {
let m_metadata_ev = make_metadata_event(keypair: damus_state.keypair, metadata: metadata) let m_metadata_ev = make_metadata_event(keypair: damus_state.keypair, metadata: metadata)
if let metadata_ev = m_metadata_ev { 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 let pfp_size: CGFloat = 90.0
HStack(alignment: .center) { 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 .offset(y: -(pfp_size/2.0)) // Increase if set a frame
Spacer() Spacer()
@@ -202,10 +201,8 @@ struct EditMetadataView: View {
}, footer: { }, footer: {
if let parts = nip05_parts { 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.") 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 { } 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() dismiss()
} }
} }
.disabled(profileUploadViewModel.isLoading)
.alert(NSLocalizedString("Invalid Tip Address", comment: "Title of alerting as invalid tip address."), isPresented: $confirm_ln_address) { .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.")) { Button(NSLocalizedString("Ok", comment: "Button to dismiss the alert.")) {
} }
@@ -228,10 +224,6 @@ struct EditMetadataView: View {
} }
.ignoresSafeArea(edges: .top) .ignoresSafeArea(edges: .top)
} }
func uploadedProfilePicture(image_url: URL?) {
picture = image_url?.absoluteString ?? ""
}
} }
struct EditMetadataView_Previews: PreviewProvider { 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 { struct EventView: View {
let event: NostrEvent let event: NostrEvent
@@ -69,7 +60,17 @@ struct EventView: View {
VStack { VStack {
if event.known_kind == .boost { if event.known_kind == .boost {
if let inner_ev = event.inner_event { 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 { } else {
EmptyView() EmptyView()
} }
@@ -81,7 +82,7 @@ struct EventView: View {
} }
} else { } else {
TextEvent(damus: damus, event: event, pubkey: pubkey, options: options) 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 { func make_actionbar_model(ev: String, damus: DamusState) -> ActionBarModel {
let model = ActionBarModel.empty() let likes = damus.likes.counts[ev]
model.update(damus: damus, evid: ev) let boosts = damus.boosts.counts[ev]
return model 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) .minimumScaleFactor(0.75)
.lineLimit(1) .lineLimit(1)
if event_is_reply(event, privkey: damus_state.keypair.privkey) { EventBody(damus_state: damus_state, event: event, size: .small)
ReplyDescription(event: event, profiles: damus_state.profiles)
}
EventBody(damus_state: damus_state, event: event, size: .small, options: [.truncate_content])
} }
} }
} }
+7 -5
View File
@@ -12,13 +12,11 @@ struct EventBody: View {
let event: NostrEvent let event: NostrEvent
let size: EventViewKind let size: EventViewKind
let should_show_img: Bool 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.damus_state = damus_state
self.event = event self.event = event
self.size = size 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) 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 { 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) .frame(maxWidth: .infinity, alignment: .leading)
} }
} }
struct EventBody_Previews: PreviewProvider { struct EventBody_Previews: PreviewProvider {
static var previews: some View { 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) { Button(role: .destructive) {
notify(.mute, target_pubkey) notify(.block, target_pubkey)
} label: { } 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 let selected: Bool
@State var shown: Bool @State var shown: Bool
@Environment(\.colorScheme) var colorScheme
init(damus_state: DamusState, event: NostrEvent, scroller: ScrollViewProxy?, selected: Bool) { init(damus_state: DamusState, event: NostrEvent, scroller: ScrollViewProxy?, selected: Bool) {
self.damus_state = damus_state self.damus_state = damus_state
@@ -27,10 +28,14 @@ struct MutedEventView: View {
return !should_show_event(contacts: damus_state.contacts, ev: event) return !should_show_event(contacts: damus_state.contacts, ev: event)
} }
var FillColor: Color {
colorScheme == .light ? Color("DamusLightGrey") : Color("DamusDarkGrey")
}
var MutedBox: some View { var MutedBox: some View {
ZStack { ZStack {
RoundedRectangle(cornerRadius: 20) RoundedRectangle(cornerRadius: 20)
.foregroundColor(DamusColors.adaptableGrey) .foregroundColor(FillColor)
HStack { 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.") 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 { var Event: some View {
Group { Group {
if selected { if selected {
SelectedEventView(damus: damus_state, event: event, size: .selected) SelectedEventView(damus: damus_state, event: event)
} else { } else {
EventView(damus: damus_state, event: event) EventView(damus: damus_state, event: event)
} }
+5 -15
View File
@@ -10,7 +10,6 @@ import SwiftUI
struct SelectedEventView: View { struct SelectedEventView: View {
let damus: DamusState let damus: DamusState
let event: NostrEvent let event: NostrEvent
let size: EventViewKind
var pubkey: String { var pubkey: String {
event.pubkey event.pubkey
@@ -18,10 +17,9 @@ struct SelectedEventView: View {
@StateObject var bar: ActionBarModel @StateObject var bar: ActionBarModel
init(damus: DamusState, event: NostrEvent, size: EventViewKind) { init(damus: DamusState, event: NostrEvent) {
self.damus = damus self.damus = damus
self.event = event self.event = event
self.size = size
self._bar = StateObject(wrappedValue: make_actionbar_model(ev: event.id, damus: damus)) self._bar = StateObject(wrappedValue: make_actionbar_model(ev: event.id, damus: damus))
} }
@@ -39,24 +37,17 @@ struct SelectedEventView: View {
.padding([.bottom], 4) .padding([.bottom], 4)
} }
.padding(.horizontal)
.minimumScaleFactor(0.75) .minimumScaleFactor(0.75)
.lineLimit(1) .lineLimit(1)
if event_is_reply(event, privkey: damus.keypair.privkey) { EventBody(damus_state: damus, event: event, size: .selected)
ReplyDescription(event: event, profiles: damus.profiles)
.padding(.horizontal)
}
EventBody(damus_state: damus, event: event, size: size, options: [.pad_content])
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) { if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
BuilderEventView(damus: damus, event_id: mention.ref.id) BuilderEventView(damus: damus, event_id: mention.ref.id)
.padding(.horizontal)
} }
Text(verbatim: "\(format_date(event.created_at))") Text(verbatim: "\(format_date(event.created_at))")
.padding([.top, .leading, .trailing]) .padding(.top, 10)
.font(.footnote) .font(.footnote)
.foregroundColor(.gray) .foregroundColor(.gray)
@@ -65,13 +56,11 @@ struct SelectedEventView: View {
if !bar.is_empty { if !bar.is_empty {
EventDetailBar(state: damus, target: event.id, target_pk: event.pubkey) EventDetailBar(state: damus, target: event.id, target_pk: event.pubkey)
.padding(.horizontal)
Divider() Divider()
} }
EventActionBar(damus_state: damus, event: event) EventActionBar(damus_state: damus, event: event)
.padding([.top], 4) .padding([.top], 4)
.padding(.horizontal)
Divider() Divider()
.padding([.top], 4) .padding([.top], 4)
@@ -81,6 +70,7 @@ struct SelectedEventView: View {
guard target == self.event.id else { return } guard target == self.event.id else { return }
self.bar.update(damus: self.damus, evid: target) self.bar.update(damus: self.damus, evid: target)
} }
.padding([.leading], 2)
.compositingGroup() .compositingGroup()
} }
} }
@@ -88,7 +78,7 @@ struct SelectedEventView: View {
struct SelectedEventView_Previews: PreviewProvider { struct SelectedEventView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
SelectedEventView(damus: test_damus_state(), event: test_event, size: .selected) SelectedEventView(damus: test_damus_state(), event: test_event)
.padding() .padding()
} }
} }
+48 -141
View File
@@ -12,9 +12,6 @@ struct EventViewOptions: OptionSet {
static let no_action_bar = EventViewOptions(rawValue: 1 << 0) static let no_action_bar = EventViewOptions(rawValue: 1 << 0)
static let no_replying_to = EventViewOptions(rawValue: 1 << 1) static let no_replying_to = EventViewOptions(rawValue: 1 << 1)
static let no_images = EventViewOptions(rawValue: 1 << 2) 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 { struct TextEvent: View {
@@ -28,12 +25,51 @@ struct TextEvent: View {
} }
var body: some View { var body: some View {
Group { HStack(alignment: .top) {
if options.contains(.wide) { let profile = damus.profiles.lookup(id: pubkey)
WideStyle
} else { let is_anon = event_is_anonymous(ev: event)
ThreadedStyle 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()) .contentShape(Rectangle())
.background(event_validity_color(event.validity)) .background(event_validity_color(event.validity))
@@ -41,127 +77,11 @@ struct TextEvent: View {
.frame(maxWidth: .infinity, minHeight: PFP_SIZE) .frame(maxWidth: .infinity, minHeight: PFP_SIZE)
.padding([.bottom], 2) .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)) struct TextEvent_Previews: PreviewProvider {
static var previews: some View {
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) { TextEvent(damus: test_damus_state(), event: test_event, pubkey: "pk", options: [])
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)
}
} }
} }
@@ -179,16 +99,3 @@ func event_has_tag(ev: NostrEvent, tag: String) -> Bool {
func event_is_anonymous(ev: NostrEvent) -> Bool { func event_is_anonymous(ev: NostrEvent) -> Bool {
return ev.known_kind == .zap_request && event_has_tag(ev: ev, tag: "anon") 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 { if zap.private_request != nil {
Image(systemName: "lock.fill") 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.")) .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 { func filledTextColor() -> Color {
colorScheme == .light ? DamusColors.white : DamusColors.black colorScheme == .light ? Color("DamusWhite") : Color("DamusBlack")
} }
func fillColor() -> Color { func fillColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white colorScheme == .light ? Color("DamusBlack") : Color("DamusWhite")
} }
func emptyColor() -> Color { func emptyColor() -> Color {
@@ -62,7 +62,7 @@ struct FollowButtonView: View {
} }
func borderColor() -> Color { 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) { LazyVStack(alignment: .leading) {
ForEach(followers.contacts ?? [], id: \.self) { pk in ForEach(followers.contacts ?? [], id: \.self) { pk in
FollowUserView(target: .pubkey(pk), damus_state: damus_state) FollowUserView(target: .pubkey(pk), damus_state: damus_state)
Divider()
} }
} }
.padding() .padding()
@@ -46,6 +45,7 @@ struct FollowersView: View {
followers.unsubscribe() followers.unsubscribe()
} }
} }
} }
struct FollowingView: View { 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 private var selectedIndex = 0
@State var showMenu = true @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 { var tabViewIndicator: some View {
HStack(spacing: 10) { HStack(spacing: 10) {
ForEach(urls.indices, id: \.self) { index in ForEach(urls.indices, id: \.self) { index in
@@ -60,7 +80,7 @@ struct ImageView: View {
.overlay( .overlay(
VStack { VStack {
if showMenu { if showMenu {
NavDismissBarView() navBarView
Spacer() Spacer()
if (urls.count > 1) { if (urls.count > 1) {
+2 -6
View File
@@ -79,15 +79,11 @@ struct LoginView: View {
return return
} }
// this is a weird way to login anyways
/*
var bootstrap_relays = load_bootstrap_relays(pubkey: nip05.pubkey)
for relay in nip05.relays { for relay in nip05.relays {
if !(bootstrap_relays.contains { $0 == relay }) { if !(BOOTSTRAP_RELAYS.contains { $0 == relay }) {
bootstrap_relays.append(relay) BOOTSTRAP_RELAYS.append(relay)
} }
} }
*/
save_pubkey(pubkey: nip05.pubkey) save_pubkey(pubkey: nip05.pubkey)
notify(.login, ()) notify(.login, ())
+2 -2
View File
@@ -26,7 +26,7 @@ struct MutelistView: View {
} }
damus_state.contacts.set_mutelist(new_ev) 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) users = get_mutelist_users(new_ev)
} label: { } label: {
Label(NSLocalizedString("Delete", comment: "Button to remove a user from their blocklist."), systemImage: "trash") Label(NSLocalizedString("Delete", comment: "Button to remove a user from their blocklist."), systemImage: "trash")
@@ -43,7 +43,7 @@ struct MutelistView: View {
RemoveAction(pubkey: pubkey) 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 show_images: Bool
let size: EventViewKind let size: EventViewKind
let preview_height: CGFloat? let preview_height: CGFloat?
let options: EventViewOptions let truncate: Bool
let translatable: Bool
@State var artifacts: NoteArtifacts @State var artifacts: NoteArtifacts
@State var preview: LinkViewRepresentable? @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.damus_state = damus_state
self.event = event self.event = event
self.show_images = show_images self.show_images = show_images
self.size = size self.size = size
self.options = options
self.translatable = damus_state.translations.shouldTranslate(event, state: damus_state)
self._artifacts = State(initialValue: artifacts) self._artifacts = State(initialValue: artifacts)
self.preview_height = lookup_cached_preview_size(previews: damus_state.previews, evid: event.id) 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._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._artifacts = State(initialValue: render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey))
self.truncate = truncate
} }
var truncate: Bool { func MainContent() -> some View {
return options.contains(.truncate_content) return VStack(alignment: .leading) {
}
if size == .selected {
var with_padding: Bool { SelectableText(attributedString: artifacts.content)
return options.contains(.pad_content) TranslateView(damus_state: damus_state, event: event)
} } else {
TruncatedText(text: artifacts.content, maxChars: (truncate ? 280 : nil))
var truncatedText: some View { .font(eventviewsize_to_font(size))
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)
}
var translateView: some View { if show_images && artifacts.images.count > 0 {
TranslateView(damus_state: damus_state, event: event, size: self.size) ImageCarousel(urls: artifacts.images)
} } else if !show_images && artifacts.images.count > 0 {
ZStack {
var previewView: some View { ImageCarousel(urls: artifacts.images)
Group { 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 = self.preview, show_images {
if let preview_height { if let preview_height {
preview 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 { var body: some View {
MainContent MainContent()
.onReceive(handle_notify(.profile_updated)) { notif in .onReceive(handle_notify(.profile_updated)) { notif in
let profile = notif.object as! ProfileUpdate let profile = notif.object as! ProfileUpdate
let blocks = event.blocks(damus_state.keypair.privkey) let blocks = event.blocks(damus_state.keypair.privkey)
@@ -197,14 +142,14 @@ struct NoteContentView: View {
func hashtag_str(_ htag: String) -> AttributedString { func hashtag_str(_ htag: String) -> AttributedString {
var attributedString = AttributedString(stringLiteral: "#\(htag)") var attributedString = AttributedString(stringLiteral: "#\(htag)")
attributedString.link = URL(string: "damus:t:\(htag)") attributedString.link = URL(string: "damus:t:\(htag)")
attributedString.foregroundColor = DamusColors.purple attributedString.foregroundColor = Color("DamusPurple")
return attributedString return attributedString
} }
func url_str(_ url: URL) -> AttributedString { func url_str(_ url: URL) -> AttributedString {
var attributedString = AttributedString(stringLiteral: url.absoluteString) var attributedString = AttributedString(stringLiteral: url.absoluteString)
attributedString.link = url attributedString.link = url
attributedString.foregroundColor = DamusColors.purple attributedString.foregroundColor = Color("DamusPurple")
return attributedString return attributedString
} }
@@ -216,13 +161,13 @@ func mention_str(_ m: Mention, profiles: Profiles) -> AttributedString {
let disp = Profile.displayName(profile: profile, pubkey: pk).username let disp = Profile.displayName(profile: profile, pubkey: pk).username
var attributedString = AttributedString(stringLiteral: "@\(disp)") var attributedString = AttributedString(stringLiteral: "@\(disp)")
attributedString.link = URL(string: "damus:\(encode_pubkey_uri(m.ref))") attributedString.link = URL(string: "damus:\(encode_pubkey_uri(m.ref))")
attributedString.foregroundColor = DamusColors.purple attributedString.foregroundColor = Color("DamusPurple")
return attributedString return attributedString
case .event: case .event:
let bevid = bech32_note_id(m.ref.ref_id) ?? m.ref.ref_id let bevid = bech32_note_id(m.ref.ref_id) ?? m.ref.ref_id
var attributedString = AttributedString(stringLiteral: "@\(abbrev_pubkey(bevid))") var attributedString = AttributedString(stringLiteral: "@\(abbrev_pubkey(bevid))")
attributedString.link = URL(string: "damus:\(encode_event_id_uri(m.ref))") attributedString.link = URL(string: "damus:\(encode_event_id_uri(m.ref))")
attributedString.foregroundColor = DamusColors.purple attributedString.foregroundColor = Color("DamusPurple")
return attributedString return attributedString
} }
} }
@@ -232,7 +177,17 @@ struct NoteContentView_Previews: PreviewProvider {
let state = test_damus_state() let state = test_damus_state()
let content = "hi there ¯\\_(ツ)_/¯ https://jb55.com/s/Oct12-150217.png 5739a762ef6124dd.jpg" let content = "hi there ¯\\_(ツ)_/¯ https://jb55.com/s/Oct12-150217.png 5739a762ef6124dd.jpg"
let artifacts = NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: []) 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 }) .filter({ $0.is_note_mention })
.count == 1 .count == 1
var ind: Int = -1
let txt: AttributedString = blocks.reduce("") { str, block in let txt: AttributedString = blocks.reduce("") { str, block in
ind = ind + 1
switch block { switch block {
case .mention(let m): case .mention(let m):
if m.type == .event && one_note_ref { 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) return str + mention_str(m, profiles: profiles)
case .text(let txt): case .text(let txt):
var trimmed = txt return str + AttributedString(stringLiteral: 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)
case .hashtag(let htag): case .hashtag(let htag):
return str + hashtag_str(htag) return str + hashtag_str(htag)
case .invoice(let invoice): case .invoice(let invoice):
@@ -358,14 +303,3 @@ struct TruncatedText: View {
return AttributedString(truncatedAttributedString) + "..." 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 { 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) return event_author_name(profiles: profiles, pubkey: zap.request.ev.pubkey)
@@ -184,12 +184,12 @@ struct EventGroupView: View {
switch group { switch group {
case .repost: case .repost:
Image(systemName: "arrow.2.squarepath") Image(systemName: "arrow.2.squarepath")
.foregroundColor(DamusColors.green) .foregroundColor(Color("DamusGreen"))
case .reaction: case .reaction:
LINEAR_GRADIENT Image("shaka-full")
.mask(Image("shaka-full") .resizable()
.resizable() .frame(width: 24, height: 24)
).frame(width: 24, height: 24) .foregroundColor(.accentColor)
case .profile_zap(let zapgrp): case .profile_zap(let zapgrp):
ZapIcon(zapgrp) ZapIcon(zapgrp)
case .zap(let zapgrp): case .zap(let zapgrp):
@@ -206,21 +206,17 @@ struct EventGroupView: View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
ProfilePicturesView(state: state, events: group.events) ProfilePicturesView(state: state, events: group.events)
GroupDescription
if let event { if let event {
let thread = ThreadModel(event: event, damus_state: state) let thread = ThreadModel(event: event, damus_state: state)
let dest = ThreadView(state: state, thread: thread) let dest = ThreadView(state: state, thread: thread)
NavigationLink(destination: dest) { NavigationLink(destination: dest) {
VStack(alignment: .leading) { Text(render_note_content(ev: event, profiles: state.profiles, privkey: state.keypair.privkey).content)
GroupDescription .padding([.top], 1)
EventBody(damus_state: state, event: event, size: .normal, options: [.truncate_content]) .foregroundColor(.gray)
.padding([.top], 1)
.padding([.trailing])
.foregroundColor(.gray)
}
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} else {
GroupDescription
} }
} }
} }
@@ -35,14 +35,6 @@ struct NotificationItemView: View {
notification_item_event(events: state.events, notif: item) 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 { func Item(_ ev: NostrEvent?) -> some View {
Group { Group {
switch item { switch item {
@@ -60,12 +52,13 @@ struct NotificationItemView: View {
case .reply(let ev): case .reply(let ev):
NavigationLink(destination: ThreadView(state: state, thread: ThreadModel(event: ev, damus_state: state))) { 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) .buttonStyle(.plain)
} }
ThiccDivider() Divider()
.padding([.top,.bottom], 5)
} }
} }
@@ -91,6 +91,7 @@ struct NotificationsView: View {
} }
return Color.clear return Color.clear
}) })
.padding(.horizontal)
} }
.coordinateSpace(name: "scroll") .coordinateSpace(name: "scroll")
.onReceive(handle_notify(.scroll_to_top)) { notif in .onReceive(handle_notify(.scroll_to_top)) { notif in
+5 -16
View File
@@ -16,34 +16,23 @@ struct ParticipantsView: View {
var body: some View { var body: some View {
VStack { VStack {
Text("Replying to", comment: "Text indicating that the view is used for editing which participants are replied to in a note.") Text("Edit participants", comment: "Text indicating that the view is used for editing which participants are replied to in a note.")
.font(.headline)
HStack { HStack {
Spacer() Spacer()
Button { Button {
// Remove all "p" refs, keep "e" refs // Remove all "p" refs, keep "e" refs
references = originalReferences.eRefs references = originalReferences.eRefs
} label: { } label: {
Text("Remove all", comment: "Button label to remove all participants from a note reply.") Text("Remove all", comment: "Button label to remove all participants from a note reply.")
} }
.font(.system(size: 14, weight: .bold)) .buttonStyle(.borderedProminent)
.frame(width: 100, height: 30) Spacer()
.foregroundColor(.white)
.background(LINEAR_GRADIENT)
.clipShape(Capsule())
Button { Button {
references = originalReferences references = originalReferences
} label: { } label: {
Text("Add all", comment: "Button label to re-add all original participants as profiles to reply to in a note") 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)) .buttonStyle(.borderedProminent)
.frame(width: 80, height: 30)
.foregroundColor(.white)
.background(LINEAR_GRADIENT)
.clipShape(Capsule())
Spacer() Spacer()
} }
VStack { VStack {
@@ -67,7 +56,7 @@ struct ParticipantsView: View {
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.font(.system(size: 30)) .font(.system(size: 30))
.foregroundColor(references.contains(participant) ? DamusColors.purple : .gray) .foregroundColor(references.contains(participant) ? .purple : .gray)
} }
.onTapGesture { .onTapGesture {
if references.contains(participant) { if references.contains(participant) {
+2 -2
View File
@@ -10,8 +10,8 @@ import SwiftUI
let BUTTON_SIZE = 57.0 let BUTTON_SIZE = 57.0
let LINEAR_GRADIENT = LinearGradient(gradient: Gradient(colors: [ let LINEAR_GRADIENT = LinearGradient(gradient: Gradient(colors: [
DamusColors.purple, Color("DamusPurple"),
DamusColors.blue Color("DamusBlue")
]), startPoint: .topTrailing, endPoint: .bottomTrailing) ]), startPoint: .topTrailing, endPoint: .bottomTrailing)
func PostButton(action: @escaping () -> ()) -> some View { 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 { struct PostView: View {
@State var post: NSMutableAttributedString = NSMutableAttributedString() @State var post: NSMutableAttributedString = NSMutableAttributedString()
@State var cursor: Int = 0
@FocusState var focus: Bool @FocusState var focus: Bool
@State var showPrivateKeyWarning: Bool = false @State var showPrivateKeyWarning: Bool = false
@State var attach_media: Bool = false @State var attach_media: Bool = false
@State var attach_camera: Bool = false
@State var error: String? = nil @State var error: String? = nil
@State var originalReferences: [ReferencedId] = []
@State var references: [ReferencedId] = []
@StateObject var image_upload: ImageUploadModel = ImageUploadModel() @StateObject var image_upload: ImageUploadModel = ImageUploadModel()
let replying_to: NostrEvent? let replying_to: NostrEvent?
let references: [ReferencedId]
let damus_state: DamusState let damus_state: DamusState
@Environment(\.presentationMode) var presentationMode @Environment(\.presentationMode) var presentationMode
@@ -80,25 +78,14 @@ struct PostView: View {
attach_media = true attach_media = true
}, label: { }, label: {
Image(systemName: "photo") Image(systemName: "photo")
.padding(6)
})
}
var CameraButton: some View {
Button(action: {
attach_camera = true
}, label: {
Image(systemName: "camera")
.padding(6)
}) })
} }
var AttachmentBar: some View { var AttachmentBar: some View {
HStack(alignment: .center) { HStack(alignment: .center) {
ImageButton ImageButton
CameraButton .disabled(image_upload.progress != nil)
} }
.disabled(image_upload.progress != nil)
} }
var PostButton: some View { var PostButton: some View {
@@ -109,18 +96,16 @@ struct PostView: View {
self.send_post() self.send_post()
} }
} }
.disabled(is_post_empty)
.font(.system(size: 14, weight: .bold)) .font(.system(size: 14, weight: .bold))
.frame(width: 80, height: 30) .frame(width: 80, height: 30)
.foregroundColor(.white) .foregroundColor(.white)
.background(LINEAR_GRADIENT) .background(LINEAR_GRADIENT)
.opacity(is_post_empty ? 0.5 : 1.0)
.clipShape(Capsule()) .clipShape(Capsule())
} }
var TextEntry: some View { var TextEntry: some View {
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
TextViewWrapper(attributedText: $post) TextViewWrapper(attributedText: $post, cursor: $cursor)
.focused($focus) .focused($focus)
.textInputAutocapitalization(.sentences) .textInputAutocapitalization(.sentences)
.onChange(of: post) { _ in .onChange(of: post) { _ in
@@ -156,7 +141,9 @@ struct PostView: View {
Spacer() Spacer()
PostButton if !is_post_empty {
PostButton
}
} }
if let progress = image_upload.progress { if let progress = image_upload.progress {
@@ -165,7 +152,7 @@ struct PostView: View {
} }
} }
.frame(height: 30) .frame(height: 30)
.padding([.bottom], 10) .padding([.top, .bottom], 4)
} }
func append_url(_ url: String) { func append_url(_ url: String) {
@@ -182,11 +169,11 @@ struct PostView: View {
post = combinedAttributedString post = combinedAttributedString
} }
func handle_upload(media: MediaUpload) { func handle_upload(image: UIImage) {
let uploader = get_media_uploader(damus_state.pubkey) let uploader = get_image_uploader(damus_state.pubkey)
Task.init { Task.init {
let res = await image_upload.start(media: media, uploader: uploader) let res = await image_upload.start(img: image, uploader: uploader)
switch res { switch res {
case .success(let url): case .success(let url):
@@ -204,102 +191,75 @@ struct PostView: View {
} }
var body: some 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 AttachmentBar
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
}
}
} }
.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? { func get_searching_string(_ post: String, cursor: Int) -> String? {
guard let last_word = post.components(separatedBy: .whitespacesAndNewlines).last else { 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 return nil
} }
@@ -321,6 +281,6 @@ func get_searching_string(_ post: String) -> String? {
struct PostView_Previews: PreviewProvider { struct PostView_Previews: PreviewProvider {
static var previews: some View { 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 let search: String
@Binding var post: NSMutableAttributedString @Binding var post: NSMutableAttributedString
@Binding var cursor: Int
var users: [SearchedUser] { var users: [SearchedUser] {
guard let contacts = damus_state.contacts.event else { guard let contacts = damus_state.contacts.event else {
@@ -36,54 +37,68 @@ struct UserSearch: View {
return return
} }
// Remove all characters after the last '@' // Remove all characters after the '@' and before the cursor
removeCharactersAfterLastAtSymbol() let newCursor = removeCharactersAfterAtSymbol()
// Create and append the user tag // Create and append the user tag
let tagAttributedString = createUserTag(for: user, with: pk) let tagAttributedString = createUserTag(for: user, with: pk)
appendUserTag(tagAttributedString) insertUserTag(tagAttributedString, cursor: newCursor)
cursor = newCursor
} }
private func removeCharactersAfterLastAtSymbol() { private func removeCharactersAfterAtSymbol() -> Int {
while post.string.last != "@" { let newCursor = cursor
post.deleteCharacters(in: NSRange(location: post.length - 1, length: 1))
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 { private func createUserTag(for user: SearchedUser, with pk: String) -> NSMutableAttributedString {
let name = Profile.displayName(profile: user.profile, pubkey: pk).username 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, let tagAttributedString = NSMutableAttributedString(string: tagString,
attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18.0), attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18.0),
NSAttributedString.Key.link: "@\(pk)"]) 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.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)) tagAttributedString.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.label], range: NSRange(location: tagAttributedString.length - 2, length: 2))
return tagAttributedString return tagAttributedString
} }
private func appendUserTag(_ tagAttributedString: NSMutableAttributedString) { private func insertUserTag(_ tagAttributedString: NSMutableAttributedString, cursor: Int) {
let mutableString = NSMutableAttributedString() let mutableString = NSMutableAttributedString()
mutableString.append(post) mutableString.append(post)
mutableString.append(tagAttributedString) mutableString.insert(tagAttributedString, at: cursor)
post = mutableString post = mutableString
} }
var body: some View { var body: some View {
ScrollView { ScrollView {
LazyVStack { LazyVStack {
Divider() ForEach(users) { user in
if users.count == 0 { UserView(damus_state: damus_state, pubkey: user.pubkey)
EmptyUserSearchView() .onTapGesture {
} else { 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 { struct UserSearch_Previews: PreviewProvider {
static let search: String = "jb55" static let search: String = "jb55"
@State static var post: NSMutableAttributedString = NSMutableAttributedString(string: "some @jb55") @State static var post: NSMutableAttributedString = NSMutableAttributedString(string: "some @jb55")
@State static var cursor: Int = 0
static var previews: some View { 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 import SwiftUI
struct FollowsYou: View { struct FollowsYou: View {
@Environment(\.colorScheme) var colorScheme
var fill_color: Color {
colorScheme == .light ? Color("DamusLightGrey") : Color("DamusDarkGrey")
}
var body: some View { var body: some View {
Text("Follows you", comment: "Text to indicate that a user is following your profile.") Text("Follows you", comment: "Text to indicate that a user is following your profile.")
@@ -16,7 +21,7 @@ struct FollowsYou: View {
.foregroundColor(.gray) .foregroundColor(.gray)
.background { .background {
RoundedRectangle(cornerRadius: 5.0) RoundedRectangle(cornerRadius: 5.0)
.foregroundColor(DamusColors.adaptableGrey) .foregroundColor(fill_color)
} }
.font(.footnote) .font(.footnote)
} }
+5 -1
View File
@@ -60,7 +60,11 @@ struct ProfileName: View {
var current_nip05: NIP05? { var current_nip05: NIP05? {
nip05 ?? damus_state.profiles.is_validated(pubkey) 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 { var current_display_name: DisplayName {
return display_name ?? Profile.displayName(profile: profile, pubkey: pubkey) 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 { struct InnerProfilePicView: View {
let url: URL? let url: URL?
@@ -172,7 +118,7 @@ func make_preview_profiles(_ pubkey: String) -> Profiles {
let profiles = Profiles() let profiles = Profiles()
let picture = "http://cdn.jb55.com/img/red-me.jpg" 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 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) profiles.add(id: pubkey, profile: ts_profile)
return profiles return profiles
} }
@@ -7,27 +7,13 @@
import SwiftUI import SwiftUI
import Combine
class ProfileUploadingViewModel: ObservableObject {
@Published var isLoading: Bool = false
}
struct ProfilePictureSelector: View { struct ProfilePictureSelector: View {
let pubkey: String 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 { var body: some View {
let highlight: Highlight = .custom(Color.white, 2.0) let highlight: Highlight = .custom(Color.white, 2.0)
ZStack { ZStack {
EditProfilePictureView(url: $profile_image, pubkey: pubkey, size: size, highlight: highlight, damus_state: damus_state) ProfilePicView(pubkey: pubkey, size: 80.0, highlight: highlight, profiles: Profiles())
EditProfilePictureControl(pubkey: pubkey, profile_image: $profile_image, viewModel: viewModel, callback: callback)
} }
} }
} }
@@ -35,8 +21,6 @@ struct ProfilePictureSelector: View {
struct ProfilePictureSelector_Previews: PreviewProvider { struct ProfilePictureSelector_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let test_pubkey = "ff48854ac6555fed8e439ebb4fa2d928410e0eef13fa41164ec45aaaa132d846" 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 { func fillColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white colorScheme == .light ? Color("DamusBlack") : Color("DamusWhite")
} }
func borderColor() -> Color { 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(\.openURL) var openURL
@Environment(\.presentationMode) var presentationMode @Environment(\.presentationMode) var presentationMode
func fillColor() -> Color {
colorScheme == .light ? Color("DamusLightGrey") : Color("DamusDarkGrey")
}
func imageBorderColor() -> Color { func imageBorderColor() -> Color {
colorScheme == .light ? DamusColors.white : DamusColors.black colorScheme == .light ? Color("DamusWhite") : Color("DamusBlack")
} }
func bannerBlurViewOpacity() -> Double { func bannerBlurViewOpacity() -> Double {
@@ -221,8 +225,8 @@ struct ProfileView: View {
notify(.report, target) notify(.report, target)
} }
Button(NSLocalizedString("Mute", comment: "Button to mute a profile."), role: .destructive) { Button(NSLocalizedString("Block", comment: "Button to block a profile."), role: .destructive) {
notify(.mute, profile.pubkey) notify(.block, profile.pubkey)
} }
} }
} }
@@ -236,7 +240,7 @@ struct ProfileView: View {
} }
.padding(.top, 5) .padding(.top, 5)
.padding(.horizontal) .padding(.horizontal)
.accentColor(DamusColors.white) .accentColor(Color("DamusWhite"))
} }
func lnButton(lnurl: String, profile: Profile) -> some View { func lnButton(lnurl: String, profile: Profile) -> some View {
@@ -323,7 +327,7 @@ struct ProfileView: View {
is_zoomed.toggle() is_zoomed.toggle()
} }
.fullScreenCover(isPresented: $is_zoomed) { .fullScreenCover(isPresented: $is_zoomed) {
ProfilePicImageView(pubkey: profile.pubkey, profiles: damus_state.profiles) } ProfileZoomView(pubkey: profile.pubkey, profiles: damus_state.profiles) }
Spacer() Spacer()
@@ -480,7 +484,7 @@ func test_damus_state() -> DamusState {
let damus = DamusState.empty 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 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) damus.profiles.add(id: pubkey, profile: tsprof)
return damus return damus
} }
@@ -492,8 +496,12 @@ struct KeyView: View {
@State private var isCopied = false @State private var isCopied = false
func fillColor() -> Color {
colorScheme == .light ? Color("DamusLightGrey") : Color("DamusDarkGrey")
}
func keyColor() -> Color { func keyColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white colorScheme == .light ? Color("DamusBlack") : Color("DamusWhite")
} }
private func copyPubkey(_ pubkey: String) { private func copyPubkey(_ pubkey: String) {
@@ -530,7 +538,7 @@ struct KeyView: View {
} }
.padding(2) .padding(2)
.padding([.leading, .trailing], 3) .padding([.leading, .trailing], 3)
.background(RoundedRectangle(cornerRadius: 11).foregroundColor(DamusColors.adaptableGrey)) .background(RoundedRectangle(cornerRadius: 11).foregroundColor(fillColor()))
if isCopied != true { if isCopied != true {
Button { Button {
@@ -541,7 +549,7 @@ struct KeyView: View {
} icon: { } icon: {
Image(systemName: "square.on.square.dashed") Image(systemName: "square.on.square.dashed")
.contentShape(Rectangle()) .contentShape(Rectangle())
.foregroundColor(.accentColor) .foregroundColor(.gray)
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
} }
.labelStyle(IconOnlyLabelStyle()) .labelStyle(IconOnlyLabelStyle())
@@ -555,7 +563,7 @@ struct KeyView: View {
.font(.footnote) .font(.footnote)
.layoutPriority(1) .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 @Environment(\.presentationMode) var presentationMode
var body: some View { var navBarView: some View {
HStack { HStack {
Button(action: { Button(action: {
presentationMode.wrappedValue.dismiss() presentationMode.wrappedValue.dismiss()
@@ -58,14 +61,6 @@ struct NavDismissBarView: View {
} }
.padding() .padding()
} }
}
struct ProfilePicImageView: View {
let pubkey: String
let profiles: Profiles
@Environment(\.presentationMode) var presentationMode
var body: some View { var body: some View {
ZStack { ZStack {
@@ -84,7 +79,7 @@ struct ProfilePicImageView: View {
presentationMode.wrappedValue.dismiss() presentationMode.wrappedValue.dismiss()
})) }))
} }
.overlay(NavDismissBarView(), alignment: .top) .overlay(navBarView, alignment: .top)
} }
} }
@@ -92,7 +87,7 @@ struct ProfileZoomView_Previews: PreviewProvider {
static let pubkey = "ca48854ac6555fed8e439ebb4fa2d928410e0eef13fa41164ec45aaaa132d846" static let pubkey = "ca48854ac6555fed8e439ebb4fa2d928410e0eef13fa41164ec45aaaa132d846"
static var previews: some View { static var previews: some View {
ProfilePicImageView( ProfileZoomView(
pubkey: pubkey, pubkey: pubkey,
profiles: make_preview_profiles(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) let profile = damus_state.profiles.lookup(id: damus_state.pubkey)
if (damus_state.profiles.lookup(id: damus_state.pubkey)?.picture) != nil { 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) .padding(.top, 50)
} else { } else {
Image(systemName: "person.fill") Image(systemName: "person.fill")
.font(.system(size: 60)) .font(.system(size: 60))
.foregroundColor(DamusColors.white) .foregroundColor(Color("DamusWhite"))
.padding(.top, 50) .padding(.top, 50)
} }
if let display_name = profile?.display_name { if let display_name = profile?.display_name {
Text(display_name) Text(display_name)
.foregroundColor(DamusColors.white) .foregroundColor(Color("DamusWhite"))
.font(.system(size: 24, weight: .heavy)) .font(.system(size: 24, weight: .heavy))
} }
if let name = profile?.name { if let name = profile?.name {
Text("@" + name) Text("@" + name)
.foregroundColor(DamusColors.white) .foregroundColor(Color("DamusWhite"))
.font(.body) .font(.body)
} }
@@ -73,19 +73,19 @@ struct QRCodeView: View {
.padding() .padding()
.cornerRadius(10) .cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10) .overlay(RoundedRectangle(cornerRadius: 10)
.stroke(DamusColors.white, lineWidth: 1)) .stroke(Color("DamusWhite"), lineWidth: 1))
.shadow(radius: 10) .shadow(radius: 10)
} }
Spacer() Spacer()
Text("Follow me on nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user.") 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)) .font(.system(size: 24, weight: .heavy))
.padding(.top) .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.") 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)) .font(.system(size: 18, weight: .ultraLight))
Spacer() Spacer()
+15 -60
View File
@@ -12,33 +12,22 @@ struct RecommendedRelayView: View {
let relay: String let relay: String
let add_button: Bool let add_button: Bool
@Binding var showActionButtons: Bool init(damus: DamusState, relay: String) {
init(damus: DamusState, relay: String, showActionButtons: Binding<Bool>) {
self.damus = damus self.damus = damus
self.relay = relay self.relay = relay
self.add_button = true 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.damus = damus
self.relay = relay self.relay = relay
self.add_button = add_button self.add_button = add_button
self._showActionButtons = showActionButtons
} }
var body: some View { var body: some View {
ZStack { ZStack {
HStack { 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) RelayType(is_paid: damus.relay_metadata.lookup(relay_id: relay)?.is_paid ?? false)
Text(relay).layoutPriority(1) Text(relay).layoutPriority(1)
if let meta = damus.relay_metadata.lookup(relay_id: relay) { if let meta = damus.relay_metadata.lookup(relay_id: relay) {
@@ -48,75 +37,41 @@ struct RecommendedRelayView: View {
EmptyView() EmptyView()
} }
.opacity(0.0) .opacity(0.0)
.disabled(showActionButtons)
Spacer() Spacer()
Image(systemName: "info.circle") Image(systemName: "info.circle")
.font(.system(size: 20, weight: .regular))
.foregroundColor(Color.accentColor) .foregroundColor(Color.accentColor)
} else {
Spacer()
Image(systemName: "questionmark.circle")
.font(.system(size: 20, weight: .regular))
.foregroundColor(.gray)
} }
} }
} }
.swipeActions { .swipeActions {
if add_button { if add_button {
if let privkey = damus.keypair.privkey { if let privkey = damus.keypair.privkey {
AddButton(privkey: privkey, showText: false) AddAction(privkey: privkey)
.tint(.accentColor)
} }
} }
} }
.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 { Button {
UIPasteboard.general.setValue(relay, forPasteboardType: "public.plain-text") guard let ev_before_add = damus.contacts.event else {
} label: { return
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."))
} }
Image(systemName: "plus.circle.fill") guard let ev_after_add = add_relay(ev: ev_before_add, privkey: privkey, current_relays: damus.pool.descriptors, relay: relay, info: .rw) else {
.font(.system(size: 20, weight: .medium)) return
.foregroundColor(.accentColor) }
.padding(.leading, 5) 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")
} }
} .tint(.accentColor)
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)
} }
} }
struct RecommendedRelayView_Previews: PreviewProvider { struct RecommendedRelayView_Previews: PreviewProvider {
static var previews: some View { 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 { struct RelayConfigView: View {
let state: DamusState let state: DamusState
@State var new_relay: String = "" @State var new_relay: String = ""
@State var show_add_relay: Bool = false
@State var relays: [RelayDescriptor] @State var relays: [RelayDescriptor]
@State private var showActionButtons = false
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@@ -23,8 +23,8 @@ struct RelayConfigView: View {
var recommended: [RelayDescriptor] { var recommended: [RelayDescriptor] {
let rs: [RelayDescriptor] = [] let rs: [RelayDescriptor] = []
return BOOTSTRAP_RELAYS.reduce(into: rs) { xs, x in return BOOTSTRAP_RELAYS.reduce(into: rs) { xs, x in
if state.pool.get_relay(x) == nil, let url = URL(string: x) { if state.pool.get_relay(x) == nil {
xs.append(RelayDescriptor(url: url, info: .rw)) xs.append(RelayDescriptor(url: URL(string: x)!, info: .rw))
} }
} }
} }
@@ -37,122 +37,72 @@ struct RelayConfigView: View {
.onReceive(handle_notify(.switched_timeline)) { _ in .onReceive(handle_notify(.switched_timeline)) { _ in
dismiss() 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 { var MainContent: some View {
Form { 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 { Section {
List(Array(relays), id: \.url) { relay in List(Array(relays), id: \.url) { relay in
RelayView(state: state, relay: relay.url.absoluteString, showActionButtons: $showActionButtons) RelayView(state: state, relay: relay.url.absoluteString)
} }
} header: { } header: {
HStack { HStack {
Text(NSLocalizedString("Connected Relays", comment: "Section title for relay servers that are connected.")) Text("Relays", comment: "Header text for relay server list for configuration.")
.font(.system(size: 18, weight: .heavy)) Spacer()
.padding(.bottom, 5) Button(action: { show_add_relay = true }) {
Image(systemName: "plus")
.foregroundColor(.accentColor)
}
} }
} }
if recommended.count > 0 { 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 List(recommended, id: \.url) { r in
RecommendedRelayView(damus: state, relay: r.url.absoluteString, showActionButtons: $showActionButtons) RecommendedRelayView(damus: state, relay: r.url.absoluteString)
}
} 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()
} }
} }
} }

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