From 375d454b16b446ff6c0cbac7f728f995351f76b0 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 03:06:37 +0000 Subject: [PATCH 01/76] Translate Localizable.stringsdict in ja 100% translated source file: 'Localizable.stringsdict' on 'ja'. --- damus/ja.lproj/Localizable.stringsdict | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/damus/ja.lproj/Localizable.stringsdict b/damus/ja.lproj/Localizable.stringsdict index d670bb04fe..3349ae6c8d 100644 --- a/damus/ja.lproj/Localizable.stringsdict +++ b/damus/ja.lproj/Localizable.stringsdict @@ -226,6 +226,20 @@ %2$@ sats + word_count + + NSStringLocalizedFormatKey + %#@WORDS@ + WORDS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + %d語 + + zap_notification_no_message NSStringLocalizedFormatKey From e7a948d36283fad48276e2a799e08fc9a2b6f79a Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 03:08:33 +0000 Subject: [PATCH 02/76] Translate InfoPlist.strings in ja 100% translated source file: 'InfoPlist.strings' on 'ja'. --- damus/ja.lproj/InfoPlist.strings | Bin 1128 -> 1364 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/ja.lproj/InfoPlist.strings b/damus/ja.lproj/InfoPlist.strings index f923d8d304bc7a20c5b68a4226d1d1b632c2e34d..82ab37d6c6f977fdba5d0628ea6212be7e92891b 100644 GIT binary patch delta 112 zcmaFCafNHbD@k95RE89WOol`T1qL4=oy1TCWEU}1PM*kQEbPco0E9U}HNFg`48=hC zzDx}I0aMy From 201e4420d1ba818b0964b0e68bfe14dd405f7c69 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 03:20:17 +0000 Subject: [PATCH 03/76] Translate Localizable.strings in ja 100% translated source file: 'Localizable.strings' on 'ja'. --- damus/ja.lproj/Localizable.strings | Bin 89824 -> 96478 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/ja.lproj/Localizable.strings b/damus/ja.lproj/Localizable.strings index 18a3a29c17ddb2f98e5dce064c7690272a0f5581..c7d15c5612cf91b5d64ec0f2e3265f762f28ddac 100644 GIT binary patch delta 4696 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 Oy_UNpH#Jwu&3^%bxU$>; delta 958 zcmb7CUr19?7{8y+c^&Jz)>^}+4pEjm$6O+zwgyp#MVZu=U`FbetFhTOOYNWeWIdSp zWhY7`dWeXK#euHmD(NZ0Bzou}Oe8EKVpvZ>y877}>gL z`Gk(&o&a$RlE*IuC?X(4Db_+!9kdk75q5tEtj9aq5?-`w_D9o~glX>;!a}zYA@oNe z!fj-?&=rA|bUMY-&s&7@w7;9qG0F|mQx@8NLJv-Q@*zs{7P3?&=Qoaqp6vjZ3WEdZuB&;dUlO_gf8CpB2T6vFLx6ZR#f z%JwwDAQ4e$`Dp*8ilUu@mGZmjA0avxiuL1@1Np2j7bJYVWPm0fT|REY#R`qwYJ`bw zCKZBK{!jo_SyZJF-2+;AxCGoPW@!f9!T;-)(xou<^&g_9t5SrfTCvx<9(OHE>~1ND zIwh%y%$w;eBFx;%NKq9=He3#({GbeCz-D$5OxfVGiY-=w38iVBJm!RXQ7+gFS4DQZ z7G^SOUhuegEi=}EZk;kNf6qlQJx(#U#RcnDH9L+tjyX=9aH1GGhv#3EV(WLa{Bavh fNpgH2=m6*I4f22oW>@|0y~q;nP$&C+P^|w6C~_;~ From c469e07ff72b72bd9705f67f78529419ff4515b3 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 09:35:22 +0000 Subject: [PATCH 04/76] Translate Localizable.strings in de 100% translated source file: 'Localizable.strings' on 'de'. --- damus/de.lproj/Localizable.strings | Bin 99250 -> 106596 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/de.lproj/Localizable.strings b/damus/de.lproj/Localizable.strings index 793573d3ff76afaf7d434fc3e6793cbc1028f0fd..9e7ea82781e2fa57dd108dd2a11f4058ddb15eef 100644 GIT binary patch delta 5196 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 QX5i~8$v`vo9$c&W8)t87=>Px# From 22fe9f3dfdd6f6f0b3745192bf34fe215bc02d8d Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 09:36:14 +0000 Subject: [PATCH 05/76] Translate InfoPlist.strings in de 100% translated source file: 'InfoPlist.strings' on 'de'. --- damus/de.lproj/InfoPlist.strings | Bin 1482 -> 1802 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/de.lproj/InfoPlist.strings b/damus/de.lproj/InfoPlist.strings index ccd117722f27ff348d4e9dd4cfc2536f9b543eea..0380a85cc50d62d96c52339b67a38c44ea546e6c 100644 GIT binary patch delta 160 zcmX@b-NiTIm8362DnklGCPN~F0)r2bPGTqmvWplhC;KxS3p+9t0AUVLjW0tfLorZ3 zd2%+B_T&T1cHyZ&9WdRg40%AEAl*4YGM}M@Ap@u<8>k|U;SbO>1+X0;9pONEkd3K8 d(R83qNkEn^LoiTBA`k=ZnEannapS88EC7fAB{u*7 delta 11 ScmeC;JH Date: Mon, 25 Sep 2023 09:37:28 +0000 Subject: [PATCH 06/76] Translate Localizable.stringsdict in de 100% translated source file: 'Localizable.stringsdict' on 'de'. --- damus/de.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/damus/de.lproj/Localizable.stringsdict b/damus/de.lproj/Localizable.stringsdict index fdb93cc603..79bb8d567c 100644 --- a/damus/de.lproj/Localizable.stringsdict +++ b/damus/de.lproj/Localizable.stringsdict @@ -258,6 +258,22 @@ %2$@ Sats + word_count + + NSStringLocalizedFormatKey + %#@WORDS@ + WORDS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d Wort + other + %d Wörter + + zap_notification_no_message NSStringLocalizedFormatKey From 9dac31d7131444502542ac22683f47f98b349e00 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 15:37:24 +0000 Subject: [PATCH 07/76] Translate Localizable.stringsdict in nl 100% translated source file: 'Localizable.stringsdict' on 'nl'. --- damus/nl.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/damus/nl.lproj/Localizable.stringsdict b/damus/nl.lproj/Localizable.stringsdict index 0ae363bc42..4331669b92 100644 --- a/damus/nl.lproj/Localizable.stringsdict +++ b/damus/nl.lproj/Localizable.stringsdict @@ -258,6 +258,22 @@ %2$@ sats + word_count + + NSStringLocalizedFormatKey + %#@WORDS@ + WORDS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d woord + other + %d woorden + + zap_notification_no_message NSStringLocalizedFormatKey From 61612121f44303ff9ee8a8bcd085882f2d22fd8b Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 15:37:45 +0000 Subject: [PATCH 08/76] Translate InfoPlist.strings in nl 100% translated source file: 'InfoPlist.strings' on 'nl'. --- damus/nl.lproj/InfoPlist.strings | Bin 1424 -> 1764 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/nl.lproj/InfoPlist.strings b/damus/nl.lproj/InfoPlist.strings index 1e3bd0e8f9a26811bb138eb620f177dd77953226..2236c5dd03a03d5d648a068485137928326e8e15 100644 GIT binary patch delta 203 zcmbQh{e*YID@k95RE89WOol`T1qL4=oy1TCWEU}1PL^jf7ItJP0Ky!g8efJ|hGL+6 z^5j@1?a9xWHR|&i^1(XOfjUco>=GcI1yrfPkPA1n49F^Cr~;Y@63YSV&tm|o&j<1q zfTC$YGYfz+Ah}|o35h@qvZoj*k_S{#0;ClfvVmfGKnN4d2eFwL@+N;|RNVM#84Cam CZY>-D delta 11 ScmaFDJAr$`tIc*y%UA#&`UIo^ From 19857c12b71d2ad60dd54f81cae45f5998be2cbc Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Tue, 26 Sep 2023 11:56:49 +0000 Subject: [PATCH 09/76] Translate Localizable.stringsdict in hu_HU 100% translated source file: 'Localizable.stringsdict' on 'hu_HU'. --- damus/hu-HU.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/damus/hu-HU.lproj/Localizable.stringsdict b/damus/hu-HU.lproj/Localizable.stringsdict index d4a2369401..262613c2f3 100644 --- a/damus/hu-HU.lproj/Localizable.stringsdict +++ b/damus/hu-HU.lproj/Localizable.stringsdict @@ -258,6 +258,22 @@ %2$@ sats + word_count + + NSStringLocalizedFormatKey + %#@WORDS@ + WORDS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d Szó + other + %d Szavak + + zap_notification_no_message NSStringLocalizedFormatKey From f3449ecaed419ad3a24126a7a53f32cab958cb03 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Tue, 26 Sep 2023 11:58:14 +0000 Subject: [PATCH 10/76] Translate InfoPlist.strings in hu_HU 100% translated source file: 'InfoPlist.strings' on 'hu_HU'. --- damus/hu-HU.lproj/InfoPlist.strings | Bin 1442 -> 1762 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/hu-HU.lproj/InfoPlist.strings b/damus/hu-HU.lproj/InfoPlist.strings index 7e1af8bb45ee609408ede2d7ed57be2755ebef45..609d53ff20170e7bebd9dd667eb6cc4b7f52d943 100644 GIT binary patch delta 170 zcmZ3){fKwMD@k95RE89WOol`T1qL4=oy1TCWEU}1POfD!7ItJP0Ky!g8efJ|hGL+6 z^5jG&?a6VBT(NmTb=g2&Ae|r;Squ*uN`PvrfHX*@0+7xD!bG46`9PU`u Date: Tue, 26 Sep 2023 12:07:48 +0000 Subject: [PATCH 11/76] Translate Localizable.strings in hu_HU 100% translated source file: 'Localizable.strings' on 'hu_HU'. --- damus/hu-HU.lproj/Localizable.strings | Bin 98942 -> 106300 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/hu-HU.lproj/Localizable.strings b/damus/hu-HU.lproj/Localizable.strings index fece6d53aff4d59405214fa60859eb0c5d8adbed..c33ac88a89a4b02d5b8258a26c6612a273bfb4ed 100644 GIT binary patch delta 4891 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 N8tpj02Jc}8_Fsf5rtbg% delta 1051 zcma)5ZAep57(VY@W8TJ^^KPxOyzWPljwKS&=g=hcPbLMG7IU*{3QJ`!KSo4E!5UG~ z3%(&CgCyva@I*I6GWt;lB@+FJAhPh61zACag!SHASid3;_uO;N`@H9z=Xu^Ua^E(0 z(q{QFce|>LQf=VE!Lnk!5cXnrolQ=cowQJC8tig*-IawnF{sFF<&8X?*yg~oF%{eP zIx(})DgAq|L2g1ciMWW0Ui>sxXof6x0i3CG%SUwsIT#5SQODe>aeGdF&_ z;=oLUlfno#bdGzWl6GE%LVO-}(}Ts}#?tj6R3i1L9k!#rQI%8CG+7 zRRn}4x&hYHBlFSKhiB>r?{xyfPT~Kg&;|mWR&0~jZ8f-W>?4rndQ04ghw9PocDlK2C6t;gQ1{+-% z?}|JRc(TH30>i402H>q=|z}>Ih31Gif$)|4TeWVqjrqY z0B7zL>m%jRZ>8D*cz6G=y@svrTnPWL!@M6`gy&j3bXEl)-P#UHUWxE?))xN!rj)8$7zB8`c6%o&Z%Z=!K_?^xLN)sQv_wPeOD6 From 945604afce47b0509ea65db7cd5f569989e39ca7 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 05:50:30 +0000 Subject: [PATCH 12/76] Translate InfoPlist.strings in sv_SE 100% translated source file: 'InfoPlist.strings' on 'sv_SE'. --- damus/sv-SE.lproj/InfoPlist.strings | Bin 1434 -> 1744 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/sv-SE.lproj/InfoPlist.strings b/damus/sv-SE.lproj/InfoPlist.strings index 8e5ac02ecf7a810dfb456cb69ca5c98375319ec0..0120a6f827432f2425fb34316bfded9d91f94159 100644 GIT binary patch delta 152 zcmbQmeSvqvD@k95RE89WOol`T1qL4=oy1TCWEU}1PM*nPEbPco0E9U}HNFg`48=hC zlPX a$zwJBxe5tIY~b8(07y6a Date: Wed, 27 Sep 2023 06:03:00 +0000 Subject: [PATCH 13/76] Translate Localizable.strings in sv_SE 100% translated source file: 'Localizable.strings' on 'sv_SE'. --- damus/sv-SE.lproj/Localizable.strings | Bin 97254 -> 104462 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/sv-SE.lproj/Localizable.strings b/damus/sv-SE.lproj/Localizable.strings index cd1d04b2d0d6a7e904a0afe5aa4c25d2ed0699cd..a706646c1e3c5e33f76df538bb6c796923cbef9f 100644 GIT binary patch delta 4955 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 Date: Wed, 27 Sep 2023 06:03:31 +0000 Subject: [PATCH 14/76] Translate Localizable.stringsdict in sv_SE 100% translated source file: 'Localizable.stringsdict' on 'sv_SE'. --- damus/sv-SE.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/damus/sv-SE.lproj/Localizable.stringsdict b/damus/sv-SE.lproj/Localizable.stringsdict index 03f522bd30..de72aac13d 100644 --- a/damus/sv-SE.lproj/Localizable.stringsdict +++ b/damus/sv-SE.lproj/Localizable.stringsdict @@ -258,6 +258,22 @@ %2$@ sats + word_count + + NSStringLocalizedFormatKey + %#@WORDS@ + WORDS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d Ord + other + %d Ord + + zap_notification_no_message NSStringLocalizedFormatKey From 433d186f679b0a240b8d30d9ba644ca9ccd67747 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Thu, 28 Sep 2023 08:55:56 +0000 Subject: [PATCH 15/76] Translate Localizable.strings in nl 100% translated source file: 'Localizable.strings' on 'nl'. --- damus/nl.lproj/Localizable.strings | Bin 97906 -> 105236 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/nl.lproj/Localizable.strings b/damus/nl.lproj/Localizable.strings index d4c7043e9644af105567d9cfc3509175302324fb..828aaaa8e7e41e2322429741ba4442d618a2cad0 100644 GIT binary patch delta 5050 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 Xt-!EQUsb!HGU&G;T^T+DjnaPr9W=fu delta 989 zcmb7CZAg<*6uxI}V|yKQ&G|uzzKWFO(qSJ+MFkmUnGB1Z5zdb-ovvv*DJ!U;LP7l4 z5wD0WF$t?bi(9=Q{qRSG&rq^#aKkNcC&dwRIqFRm1PKo;hpw*YGlbSA$*H^KI$ZyPzH{YZMf@ZYmTd}R; z42ElU;&{~n@a_bVaMhy|A=lRQ6SdTipIMpAF0ood^3l#uca5~%gtM0MP}^S0oM(|< zJgk{&WvX(~irmyhPW*=(lJH!t(YZb+DvZE2P;8~x!kTFJH!HI$x|a#evjNB-B_gwI z%2d%Ce-yzN#qtrU%e=J<(s169%7aBT#F^UzIMZH&nQcGCO#38=?haYcy%#_d<$Zhg z820ZMbweX6(l}H2MwV4rJeY^0qYD@m8JKv=BrK0!rsKFO6Ky>@3_p-WK`O}g}U0JYT=eWc}W@y6RrR2UwP~(AY3GJ8E2`w}$SCqV) z;@Xtl{Ng^?!i!HqCjTA}$xxzbEO2!*+V4urat<8QDdv0_(fdflU^)589N)YUqzs`kP@F;0>=u8E%0maeUww6e+$ta47x{ DkY+oH From 641049f6b40a90d92b59c2d160e7e4a9cf0fdbdb Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Fri, 29 Sep 2023 08:50:47 +0000 Subject: [PATCH 16/76] Translate Localizable.stringsdict in el_GR 100% translated source file: 'Localizable.stringsdict' on 'el_GR'. --- damus/el-GR.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/damus/el-GR.lproj/Localizable.stringsdict b/damus/el-GR.lproj/Localizable.stringsdict index 118c4aeab5..b20d51bf82 100644 --- a/damus/el-GR.lproj/Localizable.stringsdict +++ b/damus/el-GR.lproj/Localizable.stringsdict @@ -258,6 +258,22 @@ %2$@ sats + word_count + + NSStringLocalizedFormatKey + %#@WORDS@ + WORDS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d Λέξη + other + %d Λέξεις + + zap_notification_no_message NSStringLocalizedFormatKey From aaf587c3a9273094f3186ec9b9d15e460a674d59 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Fri, 29 Sep 2023 08:55:05 +0000 Subject: [PATCH 17/76] Translate InfoPlist.strings in pl_PL 100% translated source file: 'InfoPlist.strings' on 'pl_PL'. --- damus/pl-PL.lproj/InfoPlist.strings | Bin 1450 -> 1806 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/pl-PL.lproj/InfoPlist.strings b/damus/pl-PL.lproj/InfoPlist.strings index 9e82534b368e53a92c97f5c99bf00f3f8df49fb6..e023d6945945539d269613883ee3faa77b2df5b1 100644 GIT binary patch delta 179 zcmZ3*-N!fKm8362DnklGCPN~F0)r2bPGTqmvWplhC+jmC3p+9t0AUVLjW0tfLorZ3 zd2%|F_T&Z3CiW=|`9P5p21&*Opk5G90jL{fLJpA32g;-}WCK|W47otPIY53UkOi^r uGeh}gMixaikp4s<2AN$6RGAMnr-Y##?1CyFnFrL9$T0a2qr%2l2Uq|YLn$Bt delta 11 ScmeCumqC; From 481280f006fa61eb2c011ed0c9b8468ed253a115 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Fri, 29 Sep 2023 08:55:24 +0000 Subject: [PATCH 18/76] Translate InfoPlist.strings in el_GR 100% translated source file: 'InfoPlist.strings' on 'el_GR'. --- damus/el-GR.lproj/InfoPlist.strings | Bin 1520 -> 1756 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/el-GR.lproj/InfoPlist.strings b/damus/el-GR.lproj/InfoPlist.strings index c68505bc85220e2209326d8230718e63edc8a46b..418e20b39839847970783ec7854658a2ff33514f 100644 GIT binary patch delta 117 zcmeyseTR3#D@k95RE89WOol`T1qL4=oy1TCWEU}1PWEOp7ItJP0Ky!g8efJ|hGL+6 z^5lukncCZ#_cALmY+~NYyoq@?klfF_gLxhEF6Qk((E~uT$^rmhh7-jA From 74446560437eb10999ca3d8b82218ab0abe681a8 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Fri, 29 Sep 2023 09:16:08 +0000 Subject: [PATCH 19/76] Translate Localizable.strings in el_GR 100% translated source file: 'Localizable.strings' on 'el_GR'. --- damus/el-GR.lproj/Localizable.strings | Bin 99912 -> 107136 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/el-GR.lproj/Localizable.strings b/damus/el-GR.lproj/Localizable.strings index 8ddca2439554c74225148333975cc94fe2757096..03db3f7298160ce223d21cc7af497118d61d663d 100644 GIT binary patch delta 4923 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 K;k$M782<;bbnf>6 delta 1085 zcma)4Z){Ul6u+mhZ60s6c|5zemO3A0>9n0jN-}UxQ*<8|Se6F{!*DLet&^I%mbRdq zL~vO&j=C65+$B2xWG03zb5!r}v1E~sX(Y11P_p>JkbTq`P?1l@pyxf5_{o@?ciuVo zoOj+izu)iBm+lK?x8u*-mGcQyH#B2?%8&X+x9HnYcJQ_lXu{VYaf^wKQ#HKu5GbO( zb*F~Lf(Pp^`f#N)jImA+b}h&_zNm?*ox`xIz|t(scC!JT?|c~hb~TFNbKgi<=?$Wi z(zr7K5uV%+UL3#b!RC|~r#qs5XQNz?fDe<8$^50mpkmULv93wQp{r4MpaywVObe{&2pJbTK6`MpkjGNriEte5G0eFy^ZAwTy4)VgLRoo(j_18^TEk5S=k z5ggpz&y6~eajeHD3Oyr$x==(leReIcj)Hr0Bh%R{Y!5BcPhN?Z$%8?ylIkt}#$h(Ua`7hADPHi&( zw4kVDkD#--o!>`siN5Wp-|1x2>{VQyx*v-8YvKVs@YW1(Bg}Y*3!V}`yzPX$p@C!H zda=Dh6RqVz!0u0<72k~>m-%>+_-s6SbQLD6I$x=Qu$Y{9*MXHS3L1}xu-fAk6CeHV z5Yv-EK=reThkTv?KACH#_CarvSuhL>;p@Y)eCF6Uz6^Xp}|xOuBY ztrmRz&=3h}@w|M-07or$7Ze-{X*hCN$2~vQS(HA=oAyH(=i_7;I0@Xyfoyhdg$Wn#`YmWqZi7*Um)`@g8SREb4d2uS zm-y`zxbes>jki4yYQ3epe@iQCtFpu*bo&H75q3htrQYzmucUd&loq5}X^!xzS{O-Z yq|?$D(kJBUY+9PZE0-EDe>`k{oq?EY{xJYaz@;C3rsn`SR7+OvX5t;VaqnNa08-`v From 7390808630903adf155986522c2664d11a35290e Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Fri, 29 Sep 2023 09:32:07 +0000 Subject: [PATCH 20/76] Translate Localizable.strings in pl_PL 100% translated source file: 'Localizable.strings' on 'pl_PL'. --- damus/pl-PL.lproj/Localizable.strings | Bin 97980 -> 105264 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/pl-PL.lproj/Localizable.strings b/damus/pl-PL.lproj/Localizable.strings index 0a09e117e9ab3640b2568a435453149c29d9ddce..e85bea4a7d8c6a7bddef67540de6002694ada85e 100644 GIT binary patch delta 5218 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 DLZ82w delta 989 zcma)4T}YE*6n@X9Ykn>L)TK2{zLqS}8P00`kQMd^mF9(+R@r@y^PTsc=RG{6oK8MSfmIk4bR+=O9Y686_gdLL`RC(*h1(Ph(n*nZqr zJ`df?O7K916({!?v0{%MJNohhu7HBuiz z^4h7F5FM(`rZP>`Uw|@x)PJ%26Kw~mJ*`9MtOM;Xl4Yj3}GLN*kl!@EGOTL*o# zR$WGQd;<(5gugfEUDfaXpnfY8b-NX3x>4mN{Y5icx-v*LT!uwU6Mdz%i5Wh zqNVg%Sru^!;?EP=*$QhWZihjkHyTe3y9?^^X(W?mDN!f9;FMKo8~8zWcfe4ZI(7mk GGJgR&`!hBG From c0377d630bc727ab21623253eeb934b0fff344fb Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Fri, 29 Sep 2023 09:32:15 +0000 Subject: [PATCH 21/76] Translate Localizable.strings in pl_PL 100% translated source file: 'Localizable.strings' on 'pl_PL'. --- damus/pl-PL.lproj/Localizable.strings | Bin 105264 -> 105272 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/pl-PL.lproj/Localizable.strings b/damus/pl-PL.lproj/Localizable.strings index e85bea4a7d8c6a7bddef67540de6002694ada85e..ae91e6ac3ea67fe814d2bd9263b9e235b56a1948 100644 GIT binary patch delta 40 wcmdn6jcvy^whehNdG#0+7!n!s7}6PltjUEhl_zi7X*BuFMUl;SUWRN005mTU$N&HU delta 28 jcmdn7jco%E<-KH8V9;Rjo4oP4^5jiBjW%C+nXwT7wU7;l From 36c0307ebd293d110a22e3ca0189b9470cc450d5 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Fri, 29 Sep 2023 09:32:25 +0000 Subject: [PATCH 22/76] Translate Localizable.strings in pl_PL 100% translated source file: 'Localizable.strings' on 'pl_PL'. --- damus/pl-PL.lproj/Localizable.strings | Bin 105272 -> 105224 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/pl-PL.lproj/Localizable.strings b/damus/pl-PL.lproj/Localizable.strings index ae91e6ac3ea67fe814d2bd9263b9e235b56a1948..6eab9086762ef7aa5c056bf51e7715912e700517 100644 GIT binary patch delta 18 acmdn7jjdxF+lIWClQ-=&+I-<<#zp{Lw+eLt delta 38 scmeC!# Date: Sat, 30 Sep 2023 00:05:03 +0000 Subject: [PATCH 23/76] Translate Localizable.stringsdict in es_ES 100% translated source file: 'Localizable.stringsdict' on 'es_ES'. --- damus/es-ES.lproj/Localizable.stringsdict | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/damus/es-ES.lproj/Localizable.stringsdict b/damus/es-ES.lproj/Localizable.stringsdict index d9971a1da6..1ec07b2e15 100644 --- a/damus/es-ES.lproj/Localizable.stringsdict +++ b/damus/es-ES.lproj/Localizable.stringsdict @@ -56,6 +56,24 @@ Siguiendo + imports_count + + NSStringLocalizedFormatKey + %#@IMPORTS@ + IMPORTS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + Importación + many + Importaciones + other + Importaciones + + reacted_tagged_in_3 NSStringLocalizedFormatKey @@ -272,6 +290,24 @@ %2$@ sats + word_count + + NSStringLocalizedFormatKey + %#@WORDS@ + WORDS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d Palabra + many + %d Palabras + other + %d Palabras + + zap_notification_no_message NSStringLocalizedFormatKey From 88fc8e41f7729f471b99393a0a78ccfde936672a Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Sat, 30 Sep 2023 00:27:25 +0000 Subject: [PATCH 24/76] Translate InfoPlist.strings in es_ES 100% translated source file: 'InfoPlist.strings' on 'es_ES'. --- damus/es-ES.lproj/InfoPlist.strings | Bin 1378 -> 1710 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/es-ES.lproj/InfoPlist.strings b/damus/es-ES.lproj/InfoPlist.strings index dcb7e8df750143f0f9071c0d6faff65a4f8d0ab1..5912eede35ae9c61046f11b884a63c0534f4c556 100644 GIT binary patch delta 180 zcmaFFwT^efD@k95RE89WOol`T1qL4=oy1TCWEU}1P8MV`7ItJP0Ky!g8efJ|hGL+6 z^5l&y+LPlL&9algs&jy3K0^smMKaioT%fufAU~5K7s=!Tu$dq`a)7FffpjX6ECGt9 mK=~;^5e1;VsX+URfMO{?bCQ8Dli@Q%-sJmCiW^^5umAvJO(@v_ delta 11 ScmZ3-`-p48tIcLi6)XTB1_X%! From b2584476ac166e939479075094db0a124c967b20 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Sat, 30 Sep 2023 00:34:41 +0000 Subject: [PATCH 25/76] Translate Localizable.strings in es_ES 100% translated source file: 'Localizable.strings' on 'es_ES'. --- damus/es-ES.lproj/Localizable.strings | Bin 93294 -> 105930 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/es-ES.lproj/Localizable.strings b/damus/es-ES.lproj/Localizable.strings index 5b297c6ad43f86c98567cfc1c8c72724b5506347..ede0ec2a4630a045673540ae1117c3388d3bd465 100644 GIT binary patch delta 9729 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 Date: Mon, 2 Oct 2023 17:46:19 +0000 Subject: [PATCH 26/76] Translate InfoPlist.strings in zh_CN 100% translated source file: 'InfoPlist.strings' on 'zh_CN'. --- damus/zh-CN.lproj/InfoPlist.strings | Bin 1012 -> 1218 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/zh-CN.lproj/InfoPlist.strings b/damus/zh-CN.lproj/InfoPlist.strings index f90b15c9799a861e90b854023c651d4d6b200fe3..170d40288cd795a78fd18a4870188d63a0f40acc 100644 GIT binary patch delta 101 zcmeyueu#6zJxO1NRE89WOol`T1qL4=oy1TCWEU}1PEKSp7ItJP0Ky!g8efJ|hGL+6 z^5lig+A90{GgBC*H+Jstem6D2zol$aRIvZ#xTk(?evdoXrr4!!D`HI8xX+9k0M#)e A-~a#s delta 11 ScmX@a`GtMLz0GSF#h3vh3 Date: Mon, 2 Oct 2023 17:46:53 +0000 Subject: [PATCH 27/76] Translate InfoPlist.strings in zh_HK 100% translated source file: 'InfoPlist.strings' on 'zh_HK'. --- damus/zh-HK.lproj/InfoPlist.strings | Bin 1012 -> 1218 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/zh-HK.lproj/InfoPlist.strings b/damus/zh-HK.lproj/InfoPlist.strings index a6ef60722272389575dc74201eff53f702b09d35..411114ad953f0591f17fa413332179518651643c 100644 GIT binary patch delta 101 zcmeyueu#6zJxO1NRE89WOol`T1qL4=oy1TCWEU}1PEKSp7ItJP0Ky!g8efJ|hGL+6 z^5lig+A1LvGE*3)H+E`u`G+R>x0Fqax-x5Z+*7|czsH?xQ|wX?6nQ6Z+-JrN0IgLa AI{*Lx delta 11 ScmX@a`GtMLz0GSF#h3vh3 Date: Mon, 2 Oct 2023 17:59:57 +0000 Subject: [PATCH 28/76] Translate Localizable.strings in zh_CN 100% translated source file: 'Localizable.strings' on 'zh_CN'. --- damus/zh-CN.lproj/Localizable.strings | Bin 86548 -> 93042 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/zh-CN.lproj/Localizable.strings b/damus/zh-CN.lproj/Localizable.strings index 132d554c456f9535921155439087a9f89bcc64ae..4cf19dccbdbe3c95f917577e8675a0b92ebcee90 100644 GIT binary patch delta 4506 zcmbVPdr(y86~7JOzr`PPi0ofRS+3sXNbQgt)mnu1?r&WwuvC zm;?Ys1FC_iCdCLZGE zHsPlpf7|if%GKh#G~S8^^Z36FM>}woo;=sUdGIv)-wvDeqv7l$4Lr;@W|Aanhu;d$ z>!*{s;@s{%W#g@t`{Dka7@zamhdN<(Br^)u5_IE*Tr;d$YK7BRHOpGKPTc9ivw3`a z@mYiCc~E{MzHgeawqDeawBxx2q7}`!isx4N?}jViCWiAUqXYHp;5y;f(xhGpiJoW?pXeK*M9VDUlV)*VF&HBa8qj{F zfrEv-@5RF9(}d!sObeH@R6}Vxxa81hdC^1UrCKqT6fpwl3+O$M82=9ZmF%at7GXg# zGeKRvzRd3=+#+GPRuo0Rao|`wny0~VFPt3pb9(>~LR!1K-)(2rpx>VcqaSKvrz-}U zGd1vTkq!(+c^NB3EeX+qB6urgXF2TDD%m(ppHUW5`mv%&UOo%074`Q`gZ zzl^AmI0&3hI~4TmO9N3fHSkCWKX0Q31kPG+qZkE$q%ESA!o4hJ3hv$iMint#g0Fj1 zJQIVfBo)$ZEMzvSVX#OONy5n_VzXv$tMC|!G+NeM)&En~b-14=hMbj%HE1_q7p>I}SWe_C%Iv*To zG!bN`7d0aeZiL-CY}Nw*5u8(`fPT9fc1J1Wes z<(=%Ypm(wyI9nL(JN?S)An3_93+*~)hMjIhZ!!kkiwUp=-IRv!ZKw#D=f*KYIGNmz zzgxuLjob`;7gfbIis=PtFfjo?Apx^j+~dKuP59=2io9DiD-kE{+~BJpznoDP))=@j zw5ewsydG+Xu@{}NX!mDfG}&2Bud;+lnRe}VxZb$ zut-GjpK>dRli{Utizce3uWKT^t7jQ)Qqd>x1fDHsvyd^J#TD@AUSc8AvkSlEa)kmU z!u3DPHMvefM}BjJYbYoO2EWE^{T2|2e-QltP-{V5k;>uTdzSA3SArCelt<;mjVB{}q26+T zCY1{bJmhrh(Ue)ady{}p2|$EH5vW9Lk_V*t5fGEyi_}JJb;=F;DCK zNsVwhpRI=CYii+6E=%>rTsmg}-B()SelGS?hw{yka?uDK`Fg0VSHtRShJa#Yfy-lR zcwt-zH^*wimEPYA@7+jFQEH7p?H#Kv{&`=QTyNmyIxAd%$pH5^s^H-Y`!p}-pv|Cj zX?(YDoUue|FdFnJMJQl0LDGU~xmAQ+-^51*J+F-(X}|p>og+KN#wpoU_3JoRjDK8eG)c3@SikOZy=eZVVoS8*~AhuFeTog8w;)N z#7y6c`AE$hjZt<`?$dvwb-B2fq9j6mDL%z}0q$1J9vNuur3TeL9Hn9#gRz?EfQR64 zYI~`1L%hJf^R^k(!V%V}U7?P2N{eycD;x=Drddxe_cxBJiJ|}z4(H0;NlFYm!`T5g z)%m$p%BZNxye8Qhc)NMx5K)sH|Le~a3!cz|t}kM*z9~Cp_^OuuJ{TU=n&IyIW{mS> z8;f3)I92w57e3k-Yc9p6&4b!0hJE>q5B9=XXX^05DAuPITy}OTg7{f!GOrY1KVNph zJdJMK8ObbmgLv^4cP5?d*0qf zBd@rEoFuEd=)t-RC7~7BmA8AaiMYO$#Sc5Ij90_xi#fw*lG!31+>SE~x*T?Q;@WJ6 z)dMl#&o3c3hZ*hucnSkhed97rn)j9 zALx#xD$zQUI@F)~^p?aVXROq;_~q?#e^B|?^f{k?c%A2@W{$8yd8wQy&C*Sc%7uJu F{vW^BaHs$P delta 870 zcma))Ur19?9LLXZZu5GX!W~Tsfj;SFBYcx)wzC`1qzCUrtN7T$9b}Y9=7A zo3xWI(n10luF1n}o6J7gTrrsL$U*Z)COuIBdi2lh@Oq;fFE~W3@rjsNZs3A4ELb$r zEhi+P)F|L;o&g)?&AY-fTzOuN%UJ^b?gj(;jcQyB3)tb%qit4;`X9-vwPqZ1q|k|6 zka0BMfWbN&hGqm-;v53BKM--$C9si(aTDD>2ikI(NMwL`c)MOc>*X!``DiC?WT(=} zZFKQU2kGRBPSV4*5?8hH5!pFUQ%fNgo3wg-mLk8t~gqa zTqcoRnx6)HHD10L#8O8Z^l(%exa!s7t9lU&`+`gukO75(TKx7|&zyluz|s3o7P>qa z%QkOtOO`w1D$d``ba$In)KUxuI1>ECLieocxUDub`8PTxE+03nGBNhYtB@-LH=brc68N5d!K0JZK1QE8IE3+V0=lwc;z zsL^sT1x@WbW%UpY3UdWu5va!sZ>hfsw0|Ei_2t99qDaINPtiyENH?E#MV~T#|$XP% From 077d1aa1fd1106a74d3b0742b7c07223f63f8706 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 18:00:07 +0000 Subject: [PATCH 29/76] Translate Localizable.strings in zh_CN 100% translated source file: 'Localizable.strings' on 'zh_CN'. --- damus/zh-CN.lproj/Localizable.strings | Bin 93042 -> 93044 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/zh-CN.lproj/Localizable.strings b/damus/zh-CN.lproj/Localizable.strings index 4cf19dccbdbe3c95f917577e8675a0b92ebcee90..576936b2c2e8f0ed7872b927e8311a422fc69208 100644 GIT binary patch delta 29 lcmex#jrGel)(u_OY{&bb&`T*vM4mSV* delta 23 fcmexzjrG$t)(u_OEKl-crZ!Ko-af&a@rgbFlPe27 From 7f3cc8b7a1155ad4c6cb09d3400c383c73e8518c Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 18:02:15 +0000 Subject: [PATCH 30/76] Translate Localizable.strings in zh_CN 100% translated source file: 'Localizable.strings' on 'zh_CN'. --- damus/zh-CN.lproj/Localizable.strings | Bin 93044 -> 93044 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/zh-CN.lproj/Localizable.strings b/damus/zh-CN.lproj/Localizable.strings index 576936b2c2e8f0ed7872b927e8311a422fc69208..da6a22341f64b68b3c3d508ef9a0b1f0a66fb91a 100644 GIT binary patch delta 19 bcmexzjrGel)(t<(Sme_KrfvRHR`CS@Y$6K2 delta 19 bcmexzjrGel)(t<(Sn8)Pi`)FCtl|p*ar_Hv From dda94cc1c1a10d95c90af4181a2b06e27d84c89a Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 18:06:25 +0000 Subject: [PATCH 31/76] Translate Localizable.strings in zh_TW 100% translated source file: 'Localizable.strings' on 'zh_TW'. --- damus/zh-TW.lproj/Localizable.strings | Bin 86484 -> 92980 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/zh-TW.lproj/Localizable.strings b/damus/zh-TW.lproj/Localizable.strings index 12a658af03d1a4092b675bc758224ed6a3f9d6a0..5df1872824ee9deec66a262aaf542864a1842148 100644 GIT binary patch delta 4620 zcmbVPdr(y86~7F zK2gCut2NTiwc~gz*Wz!oE(if_wH~tSb#UgS3NG1o2^}2IO-;f7;wJdr3~8U43VE)L zYvH{3Y{Pe1Tn?w^EZkB$5}%g^w#G4@TZA7A_n-FRiss!H>N2*zvP-xW!4|^m8eO!L zqjN-4=@-^nQxkcv3r}vt(Jj-~LvFcVs4rysP+Jr3ziBH{3mrP96ZqLIg<0WjfhpZ8 zZr352(SZJSpownro97zQSD?4i;nM8Mw>Y~C9g__=R*&u3x7t4V z$kV(A#`@y@Sqr|?3P+-tUbx|4TKIInF{U2ZctkfE@EgM->|D&^s@l13d}!es&?t%) z&(-7hEqGuP{_mXh(JdNFrt%oUPI26g|I30_(_4%_tp{haA(CS6#7{T;ZADr?j}e#8 z3hW#>>HGGU9sO(Vd#=o?XWO8*zvn7+5A-|`LRu}{3z%^=<~@t7hMc3RaH3cX7hl%; z>xz#v8C|fycrGj{v9fw7yJ~`zAyt%13`V>7?S=k_rYN3bCb`3T1*e0>!RbadJbIvp z!zH}GucUMa+?~w}2MU>*at|e?+B69}Zf-M1tOXN5(P$A?@U38um8 zh#XEa)otQo&74!1$U&dR{}>KOn{_h~3-Gg*CgEZ()3BFdWMy2ed{)DT%&PRkUp5GX zW@Zs;<5(ix%h5r;!%kqI1-04Xc~4*(E$Kp+30OHI`CQ`2R7ixgMrMXmhdyf>8N_Ky zxHql(WZfj#|1`$}x|Z+-lbI5u(~T!I;UbEY1YClh#2KFJK^xrMCY&j<3Ub~`GE1N( zfI3AB=^cSwjk9?FPRD)*?|71-?#Jq>j7|y@P9P<4Vx}fD3#ls>!0?)Q;m&-fi}oO+ zc_fch0zX`>F!=e3gUPU?Sq&FGN8t4OSg8Gr%IrdJ5GRO<6cS?Bl*S0r7G@Ue;+e); z^nLRbl5qKrh&`l5qG;Z^t^$Wo)vrVjN$1mGNrnUim7v?eZNT|mK~hSlC}+y}NNsW) z45nW(1xfhtVm1@*-4BIZR}10e&Kn|H6g+(0kOaHSbwg1)4W#H~~II7oh=f)hJ538ZH)bulfc)=w0NuZ#}LOYY61Ot&eV;EEq)R7Ooer78t|iyW2X*&@mluk$fn@qMOT;nS`Y8OpD~no*5}$19j2HJFG}-6i8lbZ`=Q_L{-xGr~z(~00q)5`*T%IB9FfkPjzp4|C7PI&)o5K4t>_>P`7JX$7W9s4Fgi+`WW8r40k)Wss zTOyKQ_n(i;K5Uy^@hq&=zI)FFE#eDu%I8c+CQib9#8r zt%sCGbp(M1^981N&1UK|NE*reKsPYeXTXIw)1vG+n}R8=VGdKGe9ll?WYa$?J%^s( zGSKt2d!G-+jy~_#o=Ay>&PE;V+pUel+KCaRlGBJ1Le0)HB^LgFh_$1^STkVatf^nd zSDOBU)nXfU$3tzkFxJs*Ssn! zm&W}m=QQCzSWBsu5Jrlb0gjbx;O@n6Jt?75qcW>*Z1*RBx;ZkSO*4rFL>g>*NSP8H z^0Pu@GQ0B;(+d;*!Ofx!y$_S0>?95TLl5gC;$&;4nhJ<{5GUvuOPM9VlnvWF_ZO*cj7{hBFo#Cfw37TN26>S`sKoLBxkex04{ZTMxZo@zlDa zOKvznY=G8e&9t5&6aBFvg-LV*LM-)kR0~K*ycrTIsaF$j!^@XNef7v6p7%= z(DXBvQ3}|pygW^xA8aFCex_^$Z(i!?ISu;|o31%}>XqU4?_$kfjQ2CUWVrlhg0T|o zR|{G#@BLuEeGrDPXMp2Zy3s>1tU@JdEbNNV8P7r^G}Z<~8@%!}fP~)5tD+ zNG|PFSAKu}!H%Cg@@oFPojo4CnZR;YRJuod(^-)g?j#w7OL^?B)I@cC%K`x4pm!5ugqgvQBabtk9MN@UPL)Lf;bdDpU)D~gTpJ`_y~Sd zNu`$310!2af-Q=vtW_cwsS%=bw;aEv?rM`F_S6u*Z9nN7#Wwfb7*P3a!rdiIl@PdK zK5-o(<33{AQpJB+L-g<#VOC MbYrBl61Wxo51o{IFaQ7m delta 906 zcma)4T}YEr7(Qoi_W8BG)>^un=*Kc0Hcdl{bV+{~w#F(K)4)dRG%abvG+jRuB^700 zKGLx+YV-po5!T7R=t2~viwFt&A?hNro309Ubx~rSZ+RVYIDGH-zR!8j^E~f){Lb{| zmdWsYZpi6I)Ln|inoR!OoonFbMW^vY!gBJIvhf`#{rLNdsviwIqgzz5=-X zZvMd^NT5kT-kf49+rR)>NguEUr2pxT$svOF^}1T zkx~nyGxok=Cr)+BSm(9CwbhJxoe>8Ea!LaUqjq??VR|2W19lYNbFv4ksf?atEBg2N zF;XCLPw+IwtM4-A_DcLh^_5Iy#Zx!CiAp+1lVE&UL~Vj~R4mbt{H-(#B`u=TNSXzr znH&*VRiMIR>6ruUqlY?Bvrxg{Dw*}Jqc-$xm)EPLj>H7ANucJD$^QCHDp@Ud9cdT$ zwULPEazy+e7L`tRazD*Rr8^7JHx`t%_oKW>;z~yj)dMIUUQB*gaQ3x=0jHI>#d0YR z9yd#UXJkHiW>DgVm#!J~kznZQ3a(tPF(7ndEqeN=_|O$s78GL+v&dA!u8T6_?zbnC+nPj2NF&DX=3&z>BX%Wh2oI<8V~P72m}RmSH`D3`?2C<> zwNV>2n&7{YuX!A_#0VvxUiH5-(q|J9)BK0ONiMqQf+4Q1a-ck+Fkd#6mg$F_BE3;; zCy5M{#zY32Lo2mSD($3fbO)8R(@y%xh{9YuJmVH^Dvwr5+RI|P{kwAZE7Vd#==X}!-op|C%T$~)B9Xi`*(I9&8TcTe_*#Zxq)^otfhj^ a;(^Pisa~4DXnx=lyB(w+&0kG>m;3?f{TogI From 94d448e8d4f9c2712c9f4c8452ebc3491fc1a30b Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 18:06:45 +0000 Subject: [PATCH 32/76] Translate InfoPlist.strings in zh_TW 100% translated source file: 'InfoPlist.strings' on 'zh_TW'. --- damus/zh-TW.lproj/InfoPlist.strings | Bin 1012 -> 1218 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/zh-TW.lproj/InfoPlist.strings b/damus/zh-TW.lproj/InfoPlist.strings index a6ef60722272389575dc74201eff53f702b09d35..411114ad953f0591f17fa413332179518651643c 100644 GIT binary patch delta 101 zcmeyueu#6zJxO1NRE89WOol`T1qL4=oy1TCWEU}1PEKSp7ItJP0Ky!g8efJ|hGL+6 z^5lig+A1LvGE*3)H+E`u`G+R>x0Fqax-x5Z+*7|czsH?xQ|wX?6nQ6Z+-JrN0IgLa AI{*Lx delta 11 ScmX@a`GtMLz0GSF#h3vh3 Date: Tue, 3 Oct 2023 13:51:03 +0000 Subject: [PATCH 33/76] Translate InfoPlist.strings in sw 100% translated source file: 'InfoPlist.strings' on 'sw'. --- damus/sw.lproj/InfoPlist.strings | Bin 1468 -> 1778 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/sw.lproj/InfoPlist.strings b/damus/sw.lproj/InfoPlist.strings index 11ee46998fd73da3a81b5ef144f474952d6fed08..5ce92090dce446df28de5ada120b40347469821f 100644 GIT binary patch delta 158 zcmdnP{fT$ND^6dARE89WOoqhC6Ip~O>o6M#J2DgiVGdB-m!XuQ7$}!KIiJ~FFq0vV zA(0^i$S(oXSqzzzEm@_5bAjw^ut*Y+rNB@L=I29M3Jke0^MNAS4CP=o89*`zXpRCy V6_{VjkPI|870d&gyYb{_765XBBUu0d delta 11 ScmeywyN7$itIY~bmskKH00is+ From f56b35972d11aae2dae13c0d43ef0d8684e92c91 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 16:51:33 +0000 Subject: [PATCH 34/76] Translate Localizable.strings in es_419 100% translated source file: 'Localizable.strings' on 'es_419'. --- damus/es-419.lproj/Localizable.strings | Bin 98798 -> 106146 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/es-419.lproj/Localizable.strings b/damus/es-419.lproj/Localizable.strings index e3413ce92f179144a7a85a3b0f2baa1f27d1ca3a..dd643d1c78ccfb8fc74951030dbbc1beae0ea490 100644 GIT binary patch delta 4882 zcmb7I4Qx}_6@C|oV4TD;{)uA;2^CPe-qVKi2>oET4fu7qE*u>ZKRsIsw;wM6&ov5fgx0?itLlTFMF z_sUDBwo5%yMA{^EDYNG7o&@!CGNH_Ggx2Qbtgs|YiES9&;eb~^H#3=gdzoWer4+&+ zS*p_HVM|3`QX2%{&V!jB=0pEg3*H{)zgxvzP(06SkflDD_AeW}RAGU8%{hEYH+xpO zUU4h~${twwnZ+yz-db*aZZ_%;O1=2gj(_{`H!QW`vse$ScekPCVW}Ti=~I?Eq%Nsj z>W6DlA5;hIRir_jjPKydJ}D~YS)8iL0lh9>Oy0OM{LT@z};#5 z%=0Xb2g;cR*7cj=-1l-KFQ8}yMM7v=5Eb{Mg0R$y;@hN+xI#wtNCUXjg(qbEjN<1D zc$s#t7Yz>Lh3%?C4DJH3?~LHyrYO&vO;PI zdeqM-U+rejES)=2l>21n;QkV3RZcdYHy|8k__)F{m#m?Hw5cJbtdbqu(Y7wMj$+ZJ zVnJ4)df?6#2i!ASJd_eOQflD)@r;Z*NE2oq_OEqi=uW`twJt-u6orwCPPkI;2Dy1A zl+H83spb|K_og{Ts!|L_eccc_=YSJ8>=4>+;r<$y4o0^b-Y>SIpc(F-u)?R!ri?K( zI)nzGD^NMdci`FCU|Ih>f_~=0@4zuyNysEX%~h)`q^=~q+`hc9@h#Onp#dnXUoZNzm#M^LpP zUX8%l7b<(!?_m&Xn&FIK0;pKDgv6-?#&T`EHH$5RvW{HeR$)jYLlLY~GzW!5Y53tz zt4(qL=!Oph+f8H`Oy7ABdf&D}*$$rRjexJscF0VaP6P&|rWZNukc ze5SJ$sUWTif5`}vq`#Y`5`0!&#N;#_vX#hjnMd{O^@@-@IOHi8`YBL)xDla>RzN3d zFCoqMGqxD&e`192TmNy1O^pDymXqlf47O%Y~-TZfC_>mz0*aClEXRUP=( z74h)^*4g-o>A*fBW-C?|^#Oh1)=Go$vvscQTC|H?74&kGkJ*_GTCb*g#P~q5Pc=p5 z2L9W6mX{}^PU1E4LNH!}P++9k;i^llCoxw?NP&iLY;fH$lUs6`Q5he(Wq@OERKSu> zJA4$i!_ZkP?7VD0aM|dgxq&cGMrv(7F*m^KcYIlOXat2ff<+kRV;h)rq5d}7q?#bg zCLcRhRsHx(ZAnI@V$wnk^SyIe9vo9LVf_0R<*Up8wyVY4+nfeR4w>4ihl;tW1GQ?!r)JB-|Sz_A`9iOHlz6@O-h4&^3yvdHM?*)lfcrft{4kDR+*)F`u{E`LcO<1!<%_V1$ zU^ddkpC;_A5B9F}DGw($xoP?_PHw~iZiUKIHol^V*?Endxl_koxU}36+vsNJjVVUk z;QWM%FDhU&lFK|;zzULI;fr1m)V+&+yOY zlhXuCXnAeSQ$n*W0gm8H(AK7yREJO>-!g~UJTi)=3h4M98Wwq7R6k^?nA~;eVfk5l zNwD+YQhF<#%hF?S_}MG$ac0e@TBL5Sv9?B-`gcR!&1jP_&?Tk`?e$T2gp6}OYFu0$ zEIsSO2<61`%hck$6u=74=4 z(G-zNI|NU=4EWyy{!9&X&7oM*6cj=_18MDp>-Rvg@ab0{-*A;9*T_%X&xY1WBduVEGo9GH<4d)UH?7PB2cka3hfx-tc4Q zDoD+vc<-hBiTQ(ia0IptIUQQbt)&&*SKof&=#Vq362DP2yD+X{{*NE3uPPQIsU@*x zVRpvGZ`qlVUk@`snm~k|;7xk9BX%+$FG_lIya+D(i=J{6bZCSla;Kx6b*WWKWTW}8 l^pg)ilO-{I6CR0HlJ|?F5EVM+-o*{ zA4W456yw(FpBSlDX8smVta*313Q`5?H()odoAn}nLOz~vm`Ih2K-H_fun+XUrEq5$Rz1r` z*DIS|x)QonN*)KRp4H|78U From ce5855fe3d9242ad85eb92f492f006488835c372 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 16:52:02 +0000 Subject: [PATCH 35/76] Translate InfoPlist.strings in es_419 100% translated source file: 'InfoPlist.strings' on 'es_419'. --- damus/es-419.lproj/InfoPlist.strings | Bin 1352 -> 1684 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/es-419.lproj/InfoPlist.strings b/damus/es-419.lproj/InfoPlist.strings index b19dba187586a01ddd62d8a7efb168b45b0bd578..9c36f20b4e90c1300e0596448aa3a1212faef1a4 100644 GIT binary patch delta 200 zcmX@XHHCM=D@k95RE89WOol`T1qL4=oy1TCWEU}1PIhE57ItJP0Ky!g8efJ|hGL+6 z^5lsu+LPlL4eCpPA__qDAU!!iGM}LYs4f|7QZ7&~2guK4$VD>00BkbIrW~N^Vj!Ig zBujvzDNue2P(%S}dn(Y@BA{3b(41r-%w+h?kjJ3JV9mhApwFNMR1+}yHly&yS8*%= DQwb{3 delta 11 ScmbQjdxC4itIcLiaV!8F$^<(A From ca2960cc73edaaeee0dda134fb7e4a8a8af90f15 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 16:52:27 +0000 Subject: [PATCH 36/76] Translate Localizable.stringsdict in es_419 100% translated source file: 'Localizable.stringsdict' on 'es_419'. --- damus/es-419.lproj/Localizable.stringsdict | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/damus/es-419.lproj/Localizable.stringsdict b/damus/es-419.lproj/Localizable.stringsdict index cce3a7aa35..5d3b9303c6 100644 --- a/damus/es-419.lproj/Localizable.stringsdict +++ b/damus/es-419.lproj/Localizable.stringsdict @@ -290,6 +290,24 @@ %2$@ sats + word_count + + NSStringLocalizedFormatKey + %#@WORDS@ + WORDS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d Palabra + many + %d Palabras + other + %d Palabras + + zap_notification_no_message NSStringLocalizedFormatKey From 3365c7283233c8042b2b797e9c4172ea914844be Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 16:54:02 +0000 Subject: [PATCH 37/76] Translate Localizable.strings in es_419 100% translated source file: 'Localizable.strings' on 'es_419'. --- damus/es-419.lproj/Localizable.strings | Bin 106146 -> 106134 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/es-419.lproj/Localizable.strings b/damus/es-419.lproj/Localizable.strings index dd643d1c78ccfb8fc74951030dbbc1beae0ea490..bfe8e99cdf3f60015e805b064787fdd5e02dde33 100644 GIT binary patch delta 17 ZcmZ3qmu=c!wuUW?%c8e$ie~)a1OP~-2kZa< delta 29 lcmbQXmu=BrwuUW?%c8jz7;+dg8HyQ781lC-jb{Al1OT2}3N`=$ From e62ee11b06966c62239200caa35a3cbb4d465f36 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 20:23:46 +0000 Subject: [PATCH 38/76] Translate Localizable.strings in de 100% translated source file: 'Localizable.strings' on 'de'. --- damus/de.lproj/Localizable.strings | Bin 106596 -> 106596 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/de.lproj/Localizable.strings b/damus/de.lproj/Localizable.strings index 9e7ea82781e2fa57dd108dd2a11f4058ddb15eef..83ae04958ef1e5a9ba0658006ae3f39ad62ab4d8 100644 GIT binary patch delta 18 acmaEIfbGcvwhimvOzwCswfV%GIhz4$7z=d( delta 20 ecmV+v0PFwczy{>N2C%N|0c4ZG>KU`h?3}ZE1Pl=X From 9f2eafc3cbc6076a58e01a2478f9dbc5e3efa4f4 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 20:26:25 +0000 Subject: [PATCH 39/76] Translate Localizable.strings in de 100% translated source file: 'Localizable.strings' on 'de'. --- damus/de.lproj/Localizable.strings | Bin 106596 -> 106604 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/de.lproj/Localizable.strings b/damus/de.lproj/Localizable.strings index 83ae04958ef1e5a9ba0658006ae3f39ad62ab4d8..a88061b98213b3c4dee659a851a793fc0cd140c5 100644 GIT binary patch delta 42 vcmaEIfbGozwuUW?ehcIh8FCpE7#taj7?K!L88R74fMgzn5`#4Z7Z3sfB;N`r delta 34 ncmaEJfbGcvwuUW?ehb7L8HyN^7*ZKB8A^bp5`#4Z7Z3sf(((wy From 641b255a71a744fcdbee7acaa2292571716be4e8 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 21:35:08 +0000 Subject: [PATCH 40/76] Translate InfoPlist.strings in pt_PT 100% translated source file: 'InfoPlist.strings' on 'pt_PT'. --- damus/pt-PT.lproj/InfoPlist.strings | Bin 1468 -> 1816 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/pt-PT.lproj/InfoPlist.strings b/damus/pt-PT.lproj/InfoPlist.strings index 47ff370a81b35b9ff9f04f711a3a49a4fee472e8..e8ec1c2882a7d60c9ed97bc06f1bc80dc289def8 100644 GIT binary patch delta 247 zcmX|5F$%&!5FA1}!N$t&tgX}+2$qJt!Q5RDh+Mc!3M;=Lr12Z}zQPv>7GhrE>1CW3@NlScfWe-Bg~&SMG_11CY>Sqsp{78OMBWY|Tb@g*chnuVYp99T zSn^4o9FbvV)!madc=9IdWmBb_8omLYPD)qY8seJO=bx(Ogk&<*R;rF`1&ObxWk=GH X6#QE(VI)!@?(xP8VJP#(ZG7ZkB%3tI delta 11 ScmbQiw}*SetIcjqmskKD#00qj From 04917cfbe4986dd695b62690b8cfebd15fd02c79 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 23:18:35 +0000 Subject: [PATCH 41/76] Translate Localizable.strings in pt_PT 100% translated source file: 'Localizable.strings' on 'pt_PT'. --- damus/pt-PT.lproj/Localizable.strings | Bin 61510 -> 105422 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/pt-PT.lproj/Localizable.strings b/damus/pt-PT.lproj/Localizable.strings index 16cb4f37187d7e8f47d011c662b52eb0ce9c8d8e..2cb6b6256511d767984d143d43ae6753e7ec0ebc 100644 GIT binary patch literal 105422 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 u(e1mpg$L&mF_K&Tt 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 From 7f5707294c04223371becb1d7f937fab4b63d5f6 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Sun, 8 Oct 2023 13:47:07 +0000 Subject: [PATCH 42/76] Translate Localizable.stringsdict in pl_PL 100% translated source file: 'Localizable.stringsdict' on 'pl_PL'. --- damus/pl-PL.lproj/Localizable.stringsdict | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/damus/pl-PL.lproj/Localizable.stringsdict b/damus/pl-PL.lproj/Localizable.stringsdict index 41ce42fcbe..1760152014 100644 --- a/damus/pl-PL.lproj/Localizable.stringsdict +++ b/damus/pl-PL.lproj/Localizable.stringsdict @@ -322,6 +322,26 @@ %2$@ satsa + word_count + + NSStringLocalizedFormatKey + %#@WORDS@ + WORDS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d słowo + few + %d słowa + many + %d słów + other + %d słowa + + zap_notification_no_message NSStringLocalizedFormatKey From 7bed47c919f545794d93198891b78a4995e59e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Sat, 7 Oct 2023 01:21:53 +0000 Subject: [PATCH 43/76] test: Setup Snapshot testing library and add a snapshot test (testTextWrapperViewWillWrapText) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change adds `https://github.com/pointfreeco/swift-snapshot-testing` as a package dependency and links it to the `damusTests` target. It also adds one snapshot test to demonstrate its usefulness, by adding coverage to one particular aspect that we have never been able to test before: Whether or not the post text editor will wrap the text once the text gets long. Testing of the test ------------------- PASS iOS: 17.0 Device: Simulator Damus: This commit Test steps: 1. Run `testTextWrapperViewWillWrapText`. PASS 2. Change TextViewWrapper.swift and remove this line: ``` textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) ``` 3. Rerun. It fails. PASS (This is expected) Closes: https://github.com/damus-io/damus/issues/1562 Signed-off-by: Daniel D’Aquino Signed-off-by: William Casarin --- damus.xcodeproj/project.pbxproj | 27 +++++++++++++++++++ .../xcshareddata/swiftpm/Package.resolved | 18 +++++++++++++ damusTests/PostViewTests.swift | 25 +++++++++++++++++ 3 files changed, 70 insertions(+) diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 3d922da936..2e84006263 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -435,6 +435,8 @@ D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */; }; D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC02AC4750B0080BA88 /* MentionView.swift */; }; D7870BC32AC47EBC0080BA88 /* EventLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */; }; + D7A343EE2AD0D77C00CED48B /* InlineSnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D7A343ED2AD0D77C00CED48B /* InlineSnapshotTesting */; }; + D7A343F02AD0D77C00CED48B /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D7A343EF2AD0D77C00CED48B /* SnapshotTesting */; }; D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */; }; D7FF94002AC7AC5300FD969D /* RelayURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */; }; E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */; }; @@ -1164,6 +1166,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D7A343EE2AD0D77C00CED48B /* InlineSnapshotTesting in Frameworks */, + D7A343F02AD0D77C00CED48B /* SnapshotTesting in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2382,6 +2386,10 @@ 4CE6DEF527F7A08200C66700 /* PBXTargetDependency */, ); name = damusTests; + packageProductDependencies = ( + D7A343ED2AD0D77C00CED48B /* InlineSnapshotTesting */, + D7A343EF2AD0D77C00CED48B /* SnapshotTesting */, + ); productName = damusTests; productReference = 4CE6DEF327F7A08200C66700 /* damusTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; @@ -2470,6 +2478,7 @@ 4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */, 4CCF9AB02A1FE80B00E03CFB /* XCRemoteSwiftPackageReference "GSPlayer" */, 4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */, + D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */, ); productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */; projectDirPath = ""; @@ -3490,6 +3499,14 @@ minimumVersion = 0.2.26; }; }; + D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swift-snapshot-testing"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.14.1; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -3508,6 +3525,16 @@ package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */; productName = secp256k1; }; + D7A343ED2AD0D77C00CED48B /* InlineSnapshotTesting */ = { + isa = XCSwiftPackageProductDependency; + package = D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */; + productName = InlineSnapshotTesting; + }; + D7A343EF2AD0D77C00CED48B /* SnapshotTesting */ = { + isa = XCSwiftPackageProductDependency; + package = D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */; + productName = SnapshotTesting; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 4CE6DEDB27F7A08100C66700 /* Project object */; diff --git a/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c8409240c6..1990dd3508 100644 --- a/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -33,6 +33,24 @@ "state" : { "revision" : "76bb7971da7fbf429de1c84f1244adf657242fee" } + }, + { + "identity" : "swift-snapshot-testing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-snapshot-testing", + "state" : { + "revision" : "5b356adceabff6ca027f6574aac79e9fee145d26", + "version" : "1.14.1" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "74203046135342e4a4a627476dd6caf8b28fe11b", + "version" : "509.0.0" + } } ], "version" : 2 diff --git a/damusTests/PostViewTests.swift b/damusTests/PostViewTests.swift index bab02ac364..9ca04d203b 100644 --- a/damusTests/PostViewTests.swift +++ b/damusTests/PostViewTests.swift @@ -6,6 +6,8 @@ // import Foundation import XCTest +import SnapshotTesting +import SwiftUI @testable import damus import SwiftUI @@ -19,6 +21,29 @@ final class PostViewTests: XCTestCase { // Put teardown code here. This method is called after the invocation of each test method in the class. } + func testTextWrapperViewWillWrapText() { + // Setup test variables to be passed into the TextViewWrapper + let tagModel: TagModel = TagModel() + var textHeight: CGFloat? = nil + let textHeightBinding: Binding = Binding(get: { + return textHeight + }, set: { newValue in + textHeight = newValue + }) + + // Setup the test view + let textEditorView = TextViewWrapper( + attributedText: .constant(NSMutableAttributedString(string: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")), + textHeight: textHeightBinding, + cursorIndex: 9, + updateCursorPosition: { _ in return } + ).environmentObject(tagModel) + let hostView = UIHostingController(rootView: textEditorView) + + // Run snapshot check + assertSnapshot(matching: hostView, as: .image(on: .iPhoneSe(.portrait))) + } + /// Based on https://github.com/damus-io/damus/issues/1375 /// Tests whether the editor properly handles mention links after they have been added, to avoid manual editing of attributed links func testMentionLinkEditorHandling() throws { From 05dee129b503f3fd500687e4ccfbd719e3383cfc Mon Sep 17 00:00:00 2001 From: ericholguin Date: Fri, 6 Oct 2023 22:13:47 -0600 Subject: [PATCH 44/76] relays: allow users to hide the recommended relay view Closes: https://github.com/damus-io/damus/pull/1587 Signed-off-by: William Casarin --- damus/Views/Relays/RelayConfigView.swift | 37 ++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/damus/Views/Relays/RelayConfigView.swift b/damus/Views/Relays/RelayConfigView.swift index 9f01d41152..2fbb3d4996 100644 --- a/damus/Views/Relays/RelayConfigView.swift +++ b/damus/Views/Relays/RelayConfigView.swift @@ -12,6 +12,7 @@ struct RelayConfigView: View { @State var relays: [RelayDescriptor] @State private var showActionButtons = false @State var show_add_relay: Bool = false + @SceneStorage("RelayConfigView.show_recommended") var show_recommended : Bool = true @Environment(\.dismiss) var dismiss @@ -43,8 +44,41 @@ struct RelayConfigView: View { VStack { Divider() - if recommended.count > 0 { + if showActionButtons && !show_recommended { VStack { + Button(action: { + withAnimation(.easeOut(duration: 0.2)) { + show_recommended.toggle() + } + }) { + Text("Show recommended relays", comment: "Button to show recommended relays.") + .foregroundStyle(DamusLightGradient.gradient) + .padding(10) + .background { + RoundedRectangle(cornerRadius: 15) + .stroke(DamusLightGradient.gradient) + } + } + .padding(.top, 10) + } + } + + if recommended.count > 0 && show_recommended { + VStack { + HStack(alignment: .top) { + Spacer() + Button(action: { + withAnimation(.easeOut(duration: 0.2)) { + show_recommended.toggle() + } + }) { + Image(systemName: "xmark.circle") + .font(.system(size: 18)) + .foregroundStyle(DamusLightGradient.gradient) + } + .padding([.top, .trailing], 8) + } + Text("Recommended relays", comment: "Title for view of recommended relays.") .foregroundStyle(DamusLightGradient.gradient) .padding(10) @@ -52,7 +86,6 @@ struct RelayConfigView: View { RoundedRectangle(cornerRadius: 15) .stroke(DamusLightGradient.gradient) } - .padding(.vertical) HStack(spacing: 20) { ForEach(recommended, id: \.url) { r in From 6cdf2dca53d8082b4e9886b4779aadde9fa51014 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Wed, 11 Oct 2023 16:19:17 +0000 Subject: [PATCH 45/76] Translate InfoPlist.strings in lv_LV 100% translated source file: 'InfoPlist.strings' on 'lv_LV'. --- damus/lv-LV.lproj/InfoPlist.strings | Bin 1496 -> 1828 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/lv-LV.lproj/InfoPlist.strings b/damus/lv-LV.lproj/InfoPlist.strings index 625249165b05d480e840a367f6a1dc17f0aab39c..4901b3a7951322ccdf1b585ae410e972a62ff06b 100644 GIT binary patch delta 160 zcmcb?y@YSVD@k95RE89WOol`T1qL4=oy1TCWEU}1PBvsR7ItJP0Ky!g8efJ|hGL+6 z^5lFb?a3RMy~0a@;w3 Date: Wed, 11 Oct 2023 17:19:54 +0000 Subject: [PATCH 46/76] Translate Localizable.strings in lv_LV 100% translated source file: 'Localizable.strings' on 'lv_LV'. --- damus/lv-LV.lproj/Localizable.strings | Bin 84670 -> 105002 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/damus/lv-LV.lproj/Localizable.strings b/damus/lv-LV.lproj/Localizable.strings index 57db50a3e1b854092e6df35f9f8931190d487225..a817dd206c1d13a9787cecf0e26a38dbff544158 100644 GIT binary patch delta 19576 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 Date: Wed, 11 Oct 2023 17:28:36 +0000 Subject: [PATCH 47/76] Translate Localizable.stringsdict in lv_LV 100% translated source file: 'Localizable.stringsdict' on 'lv_LV'. --- damus/lv-LV.lproj/Localizable.stringsdict | 106 ++++++++++++++++------ 1 file changed, 80 insertions(+), 26 deletions(-) diff --git a/damus/lv-LV.lproj/Localizable.stringsdict b/damus/lv-LV.lproj/Localizable.stringsdict index 1c7fd44c7a..91fa6a42b3 100644 --- a/damus/lv-LV.lproj/Localizable.stringsdict +++ b/damus/lv-LV.lproj/Localizable.stringsdict @@ -2,6 +2,24 @@ + followed_by_three_and_others + + NSStringLocalizedFormatKey + %#@OTHERS@ + OTHERS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + zero + Sekots pēc %2$@, %3$@, %4$@ & %1$d citiem + one + Sekots pēc %2$@, %3$@, %4$@ & %1$d citiem + other + Sekots pēc %2$@, %3$@, %4$@ & %1$d citiem + + followers_count NSStringLocalizedFormatKey @@ -38,6 +56,24 @@ Sekojat + imports_count + + NSStringLocalizedFormatKey + %#@IMPORTS@ + IMPORTS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + zero + Importi + one + Importi + other + Importi + + reacted_tagged_in_3 NSStringLocalizedFormatKey @@ -49,14 +85,14 @@ NSStringFormatValueTypeKey d zero - %2$@ un %1$d citi reaģēja uz ziņu, kurā esat atzīmēts + %2$@ un %1$d citi reaģēja uz ierakstu, kurā bijāt atzīmēts one - %2$@ un %1$d cits reaģēja uz ziņu, kurā esat atzīmēts + %2$@ un %1$d citi reaģēja uz ierakstu, kurā bijāt atzīmēts other - %2$@ un %1$d citi reaģēja uz ziņu, kurā esat atzīmēts + %2$@ un %1$d citi reaģēja uz ierakstu, kurā bijāt atzīmēts - reacted_your_post_3 + reacted_your_note_3 NSStringLocalizedFormatKey %#@REACTED@ @@ -67,11 +103,11 @@ NSStringFormatValueTypeKey d zero - %2$@ un %1$d citi reaģēja tavai ziņai + %2$@ un %1$d citi reaģēja uz jūsu ierakstu one - %2$@ un %1$d cits reaģēja tavai ziņai + %2$@ un %1$d citi reaģēja uz jūsu ierakstu other - %2$@ un %1$d citi reaģēja tavai ziņai + %2$@ un %1$d citi reaģēja uz jūsu ierakstu reacted_your_profile_3 @@ -157,14 +193,14 @@ NSStringFormatValueTypeKey d zero - %2$@ un %1$d citi atkārtoti publicēja ziņu, kurā bijāt atzīmēts + %2$@ un %1$d citi atkārtoti pārpublicēja ierakstu, kurā bijāt atzīmēts one - %2$@ un %1$d cits atkārtoti publicēja ziņu, kurā bijāt atzīmēts + %2$@ un %1$d citi atkārtoti pārpublicēja ierakstu, kurā bijāt atzīmēts other - %2$@ un %1$d citi atkārtoti publicēja ziņu, kurā bijāt atzīmēts + %2$@ un %1$d citi atkārtoti pārpublicēja ierakstu, kurā bijāt atzīmēts - reposted_your_post_3 + reposted_your_note_3 NSStringLocalizedFormatKey %#@REPOSTED@ @@ -175,11 +211,11 @@ NSStringFormatValueTypeKey d zero - %2$@ un %1$d citi atkārtoti publicēja tavu ziņu + %2$@ un %1$d citi pārpublicēja jūsu ierakstu one - %2$@ un %1$d cits atkārtoti publicēja tavu ziņu + %2$@ un %1$d citi pārpublicēja jūsu ierakstu other - %2$@ un %1$d citi atkārtoti publicēja tavu ziņu + %2$@ un %1$d citi pārpublicēja jūsu ierakstu reposted_your_profile_3 @@ -254,6 +290,24 @@ %2$@ sati + word_count + + NSStringLocalizedFormatKey + %#@WORDS@ + WORDS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + zero + %d Vārdi + one + %d Vārdi + other + %d Vārdi + + zap_notification_no_message NSStringLocalizedFormatKey @@ -283,11 +337,11 @@ NSStringFormatValueTypeKey @ zero - Jūs saņēmāt %2$@ satus no %3$@: "%4$@" + Jūs saņēmāt %2$@ satus no %3$@: "%4$@" one - Jūs saņēmāt %2$@ satu no %3$@: "%4$@" + Jūs saņēmāt %2$@ satus no %3$@: "%4$@" other - Jūs saņēmāt %2$@ satus no %3$@: "%4$@" + Jūs saņēmāt %2$@ satus no %3$@: "%4$@" zapped_tagged_in_3 @@ -301,14 +355,14 @@ NSStringFormatValueTypeKey d zero - %2$@ un %1$d citi sazapoja ziņu, kurā bijāt atzīmēts + %2$@ un %1$d citi zapoja ierakstam, kurā bijāt atzīmēts one - %2$@ un %1$d cits sazapoja ziņu, kurā bijāt atzīmēts + %2$@ un %1$d citi zapoja ierakstam, kurā bijāt atzīmēts other %2$@ un %1$d citi zapoja ierakstam, kurā bijāt atzīmēts - zapped_your_post_3 + zapped_your_note_3 NSStringLocalizedFormatKey %#@ZAPPED@ @@ -319,11 +373,11 @@ NSStringFormatValueTypeKey d zero - %2$@ un %1$d citi sazapoja tavai ziņai + %2$@ un %1$d citi zapoja jūsu ierakstam one - %2$@ un %1$d cits sazapoja tavai ziņai + %2$@ un %1$d citi zapoja jūsu ierakstam other - %2$@ un %1$d citi sazapoja tavam ierakstam + %2$@ un %1$d citi zapoja jūsu ierakstam zapped_your_profile_3 @@ -337,11 +391,11 @@ NSStringFormatValueTypeKey d zero - %2$@ un %1$d citi sazapoja tavam profilam + %2$@ un %1$d citi zapoja jums one - %2$@ un %1$d cits sazapoja tavam profilam + %2$@ un %1$d citi zapoja jums other - %2$@ un %1$d citi zapoja tavam profilam + %2$@ un %1$d citi zapoja jums zaps_count From 76f6ed0f86adffd4327de3be82dbb006b28a8537 Mon Sep 17 00:00:00 2001 From: ericholguin Date: Sat, 14 Oct 2023 15:23:10 -0600 Subject: [PATCH 48/76] colors: add an adapatable white color --- .../Contents.json | 38 +++++++++++++++++++ damus/Components/DamusColors.swift | 1 + 2 files changed, 39 insertions(+) create mode 100644 damus/Assets.xcassets/Colors/DamusAdaptableWhite.colorset/Contents.json diff --git a/damus/Assets.xcassets/Colors/DamusAdaptableWhite.colorset/Contents.json b/damus/Assets.xcassets/Colors/DamusAdaptableWhite.colorset/Contents.json new file mode 100644 index 0000000000..9c0e331e97 --- /dev/null +++ b/damus/Assets.xcassets/Colors/DamusAdaptableWhite.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/damus/Components/DamusColors.swift b/damus/Components/DamusColors.swift index 4eb7f43d49..259e388ca2 100644 --- a/damus/Components/DamusColors.swift +++ b/damus/Components/DamusColors.swift @@ -11,6 +11,7 @@ import SwiftUI class DamusColors { static let adaptableGrey = Color("DamusAdaptableGrey") static let adaptableBlack = Color("DamusAdaptableBlack") + static let adaptableWhite = Color("DamusAdaptableWhite") static let white = Color("DamusWhite") static let black = Color("DamusBlack") static let brown = Color("DamusBrown") From cf243e39c928f37d59d6078da42d1caab3a597c3 Mon Sep 17 00:00:00 2001 From: ericholguin Date: Sat, 14 Oct 2023 15:23:49 -0600 Subject: [PATCH 49/76] ui: improve status view Changelog-Changed: Improved status view design Closes: https://github.com/damus-io/damus/pull/1606 --- damus/Components/Status/UserStatusSheet.swift | 134 +++++++++++++----- damus/ContentView.swift | 3 +- 2 files changed, 97 insertions(+), 40 deletions(-) diff --git a/damus/Components/Status/UserStatusSheet.swift b/damus/Components/Status/UserStatusSheet.swift index dec711352d..10620e1b7a 100644 --- a/damus/Components/Status/UserStatusSheet.swift +++ b/damus/Components/Status/UserStatusSheet.swift @@ -46,17 +46,26 @@ enum StatusDuration: CustomStringConvertible, CaseIterable { } let formatter = DateComponentsFormatter() - formatter.unitsStyle = .full + formatter.unitsStyle = .abbreviated formatter.allowedUnits = [.minute, .hour, .day, .weekOfMonth] return formatter.string(from: timeInterval) ?? "\(timeInterval) seconds" } } +enum Fields{ + case status + case link +} + struct UserStatusSheet: View { + let damus_state: DamusState let postbox: PostBox let keypair: Keypair @State var duration: StatusDuration = .never + @State var show_link: Bool = false + + @FocusState var focusedTextField : Fields? @ObservedObject var status: UserStatusModel @Environment(\.dismiss) var dismiss @@ -86,45 +95,15 @@ struct UserStatusSheet: View { } var body: some View { - VStack(alignment: .leading, spacing: 20) { - Text("Set Status", comment: "Title of view that allows the user to set their profile status (e.g. working, studying, coding)") - .font(.largeTitle) - - TextField(text: status_binding, label: { - Text("📋 Working", comment: "Placeholder as an example of what the user could set as their profile status.") - }) - - HStack { - Image("link") - - TextField(text: url_binding, label: { - Text("https://example.com", comment: "Placeholder as an example of what the user could set so that the link is opened when the status is tapped.") - }) - } - + VStack { HStack { - Text("Clear status", comment: "Label to prompt user to select an expiration time for the profile status to clear.") - - Spacer() - - Picker(NSLocalizedString("Duration", comment: "Label for profile status expiration duration picker."), selection: $duration) { - ForEach(StatusDuration.allCases, id: \.self) { d in - Text(verbatim: d.description) - .tag(d) - } - } - } - - Toggle(isOn: $status.playing_enabled, label: { - Text("Broadcast music playing on Apple Music", comment: "Toggle to enable or disable broadcasting what music is being played on Apple Music in their profile status.") - }) - - HStack(alignment: .center) { Button(action: { dismiss() }, label: { Text("Cancel", comment: "Cancel button text for dismissing profile status settings view.") + .padding(10) }) + .buttonStyle(NeutralButtonStyle()) Spacer() @@ -140,21 +119,98 @@ struct UserStatusSheet: View { dismiss() }, label: { - Text("Save", comment: "Save button text for saving profile status settings.") + Text("Share", comment: "Save button text for saving profile status settings.") }) - .buttonStyle(GradientButtonStyle()) + .buttonStyle(GradientButtonStyle(padding: 10)) + } + .padding() + + Divider() + + ZStack { + ProfilePicView(pubkey: keypair.pubkey, size: 120.0, highlight: .custom(DamusColors.white, 3.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) + .padding(.top, 130) + + VStack(spacing: 0) { + HStack { + TextField(NSLocalizedString("Staying humble...", comment: "Placeholder as an example of what the user could set as their profile status."), text: status_binding, axis: .vertical) + .focused($focusedTextField, equals: Fields.status) + .task { + self.focusedTextField = .status + } + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + .lineLimit(3) + .frame(width: 175) + } + .padding(10) + .background(DamusColors.adaptableWhite) + .cornerRadius(15) + .shadow(color: DamusColors.neutral3, radius: 15) + + Circle() + .fill(DamusColors.adaptableWhite) + .frame(width: 12, height: 12) + .padding(.trailing, 140) + + Circle() + .fill(DamusColors.adaptableWhite) + .frame(width: 7, height: 7) + .padding(.trailing, 120) + + } + .padding(.leading, 60) + } + + VStack { + HStack { + Image("link") + .foregroundColor(DamusColors.neutral3) + + TextField(text: url_binding, label: { + Text("Add an external link", comment: "Placeholder as an example of what the user could set so that the link is opened when the status is tapped.") + }) + .focused($focusedTextField, equals: Fields.link) + } + .padding(10) + .cornerRadius(12) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(DamusColors.neutral3, lineWidth: 1) + ) + } + .padding() + + Toggle(isOn: $status.playing_enabled, label: { + Text("Broadcast music playing on Apple Music", comment: "Toggle to enable or disable broadcasting what music is being played on Apple Music in their profile status.") + }) + .tint(DamusColors.purple) + .padding(.horizontal) + + HStack { + Text("Clear status", comment: "Label to prompt user to select an expiration time for the profile status to clear.") + + Spacer() + + Picker(NSLocalizedString("Duration", comment: "Label for profile status expiration duration picker."), selection: $duration) { + ForEach(StatusDuration.allCases, id: \.self) { d in + Text(verbatim: d.description) + .tag(d) + } + } + .pickerStyle(.segmented) } - .padding([.top], 30) + .padding() Spacer() } - .padding(30) + .padding(.top) } } struct UserStatusSheet_Previews: PreviewProvider { static var previews: some View { - UserStatusSheet(postbox: test_damus_state.postbox, keypair: test_keypair, status: .init()) + UserStatusSheet(damus_state: test_damus_state, postbox: test_damus_state.postbox, keypair: test_keypair, status: .init()) } } diff --git a/damus/ContentView.swift b/damus/ContentView.swift index dc897f09fa..2042773641 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -312,7 +312,8 @@ struct ContentView: View { case .post(let action): PostView(action: action, damus_state: damus_state!) case .user_status: - UserStatusSheet(postbox: damus_state!.postbox, keypair: damus_state!.keypair, status: damus_state!.profiles.profile_data(damus_state!.pubkey).status) + UserStatusSheet(damus_state: damus_state!, postbox: damus_state!.postbox, keypair: damus_state!.keypair, status: damus_state!.profiles.profile_data(damus_state!.pubkey).status) + .presentationDragIndicator(.visible) case .event: EventDetailView() case .zap(let zapsheet): From 439f9974c5e99a7b3626b9a3a02b1ffbf38e6792 Mon Sep 17 00:00:00 2001 From: Jericho Hasselbush Date: Wed, 11 Oct 2023 08:17:28 -0400 Subject: [PATCH 50/76] login: add nsec qr-scanning - Allow scanning of QR codes, and if detects a nsec, will provide it to the login prompt. - If nsec is found, provides option to keep nsec in keychain; default is to not store - User stays logged in until they logout, or app is force-quit if nsec is not stored. damusApp.swift: Obtains keypair from the notification generated to allow login. LoginView.swift: New views allowing for adding and logic handling the QR reader in QRScanNSECView.swift to enable QR scan for nsec. QRScanNSECView.swift: New view to scan for QR code. The sparkling magnifying glass is enabled if the view calling the QR view changes the privKeyFound bound variable. Tipjar: npub1el277q4kesp8vhs7rq6qkwnhpxfp345u7tnuxykwr67d9wg0wvyslam5n0 Closes: https://github.com/damus-io/damus/issues/1291 Changelog-Added: Add QR scan nsec logins. Signed-off-by: Jericho Hasselbush Reviewed-by: William Casarin Signed-off-by: William Casarin --- damus.xcodeproj/project.pbxproj | 4 + damus/Views/LoginView.swift | 156 ++++++++++++++++++++++++------- damus/Views/QRScanNSECView.swift | 66 +++++++++++++ damus/damusApp.swift | 3 + 4 files changed, 194 insertions(+), 35 deletions(-) create mode 100644 damus/Views/QRScanNSECView.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 2e84006263..3769bb59f8 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -419,6 +419,7 @@ 9609F058296E220800069BF3 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; }; 9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C83F89229A937B900136C08 /* TextViewWrapper.swift */; }; 9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; }; + ADFE73552AD4793100EC7326 /* QRScanNSECView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADFE73542AD4793100EC7326 /* QRScanNSECView.swift */; }; BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; }; BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; }; BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; }; @@ -1113,6 +1114,7 @@ 9609F057296E220800069BF3 /* BannerImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerImageView.swift; sourceTree = ""; }; 9C83F89229A937B900136C08 /* TextViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewWrapper.swift; sourceTree = ""; }; 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachMediaUtility.swift; sourceTree = ""; }; + ADFE73542AD4793100EC7326 /* QRScanNSECView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRScanNSECView.swift; sourceTree = ""; }; BA3759892ABCCDE30018D73B /* ImageResizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageResizer.swift; sourceTree = ""; }; BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureProcessor.swift; sourceTree = ""; }; BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCaptureProcessor.swift; sourceTree = ""; }; @@ -1685,6 +1687,7 @@ 4C3AC79E2833115300E1F516 /* FollowButtonView.swift */, 4C3AC79C2833036D00E1F516 /* FollowingView.swift */, 4C90BD17283A9EE5008EE7EF /* LoginView.swift */, + ADFE73542AD4793100EC7326 /* QRScanNSECView.swift */, 4C363A8D28236FE4006E126D /* NoteContentView.swift */, 4C75EFAC28049CFB0006080F /* PostButton.swift */, 4C75EFA327FA577B0006080F /* PostView.swift */, @@ -2564,6 +2567,7 @@ 4C4793042A993DC000489948 /* midl.c in Sources */, 4C4793012A993CDA00489948 /* mdb.c in Sources */, 4CE9FBBA2A6B3C63007E485C /* nostrdb.c in Sources */, + ADFE73552AD4793100EC7326 /* QRScanNSECView.swift in Sources */, 4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */, 5CF72FC229B9142F00124A13 /* ShareAction.swift in Sources */, 4C32B9522A9AD44700DC3548 /* Message.swift in Sources */, diff --git a/damus/Views/LoginView.swift b/damus/Views/LoginView.swift index d5901f2228..cd9e741ef0 100644 --- a/damus/Views/LoginView.swift +++ b/damus/Views/LoginView.swift @@ -30,6 +30,13 @@ enum ParsedKey { } return false } + + var is_priv: Bool { + if case .priv = self { + return true + } + return false + } } struct LoginView: View { @@ -37,6 +44,7 @@ struct LoginView: View { @State var is_pubkey: Bool = false @State var error: String? = nil @State private var credential_handler = CredentialHandler() + @State private var shouldSaveKey: Bool = true var nav: NavigationCoordinator func get_error(parsed_key: ParsedKey?) -> String? { @@ -57,7 +65,7 @@ struct LoginView: View { SignInHeader() .padding(.top, 100) - SignInEntry(key: $key) + SignInEntry(key: $key, shouldSaveKey: $shouldSaveKey) let parsed = parse_key(key) @@ -83,7 +91,7 @@ struct LoginView: View { Button(action: { Task { do { - try await process_login(p, is_pubkey: is_pubkey) + try await process_login(p, is_pubkey: is_pubkey, shouldSaveKey: shouldSaveKey) } catch { self.error = error.localizedDescription } @@ -168,37 +176,39 @@ enum LoginError: LocalizedError { } } -func process_login(_ key: ParsedKey, is_pubkey: Bool) async throws { - switch key { - case .priv(let priv): - try handle_privkey(priv) - case .pub(let pub): - try clear_saved_privkey() - save_pubkey(pubkey: pub) - - case .nip05(let id): - guard let nip05 = await get_nip05_pubkey(id: id) else { - throw LoginError.nip05_failed - } - - // this is a weird way to login anyways - /* - var bootstrap_relays = load_bootstrap_relays(pubkey: nip05.pubkey) - for relay in nip05.relays { - if !(bootstrap_relays.contains { $0 == relay }) { - bootstrap_relays.append(relay) - } - } - */ - save_pubkey(pubkey: nip05.pubkey) - - case .hex(let hexstr): - if is_pubkey, let pubkey = hex_decode_pubkey(hexstr) { +func process_login(_ key: ParsedKey, is_pubkey: Bool, shouldSaveKey: Bool = true) async throws { + if shouldSaveKey { + switch key { + case .priv(let priv): + try handle_privkey(priv) + case .pub(let pub): try clear_saved_privkey() + save_pubkey(pubkey: pub) + + case .nip05(let id): + guard let nip05 = await get_nip05_pubkey(id: id) else { + throw LoginError.nip05_failed + } - save_pubkey(pubkey: pubkey) - } else if let privkey = hex_decode_privkey(hexstr) { - try handle_privkey(privkey) + // this is a weird way to login anyways + /* + var bootstrap_relays = load_bootstrap_relays(pubkey: nip05.pubkey) + for relay in nip05.relays { + if !(bootstrap_relays.contains { $0 == relay }) { + bootstrap_relays.append(relay) + } + } + */ + save_pubkey(pubkey: nip05.pubkey) + + case .hex(let hexstr): + if is_pubkey, let pubkey = hex_decode_pubkey(hexstr) { + try clear_saved_privkey() + + save_pubkey(pubkey: pubkey) + } else if let privkey = hex_decode_privkey(hexstr) { + try handle_privkey(privkey) + } } } @@ -213,7 +223,16 @@ func process_login(_ key: ParsedKey, is_pubkey: Bool) async throws { save_pubkey(pubkey: pk) } - guard let keypair = get_saved_keypair() else { + func handle_transient_privkey(_ key: ParsedKey) -> Keypair? { + if case let .priv(priv) = key, let pubkey = privkey_to_pubkey(privkey: priv) { + return Keypair(pubkey: pubkey, privkey: priv) + } + return nil + } + + let keypair = shouldSaveKey ? get_saved_keypair() : handle_transient_privkey(key) + + guard let keypair = keypair else { return } @@ -265,11 +284,15 @@ func get_nip05_pubkey(id: String) async -> NIP05User? { struct KeyInput: View { let title: String let key: Binding + let shouldSaveKey: Binding + var privKeyFound: Binding @State private var is_secured: Bool = true - init(_ title: String, key: Binding) { + init(_ title: String, key: Binding, shouldSaveKey: Binding, privKeyFound: Binding) { self.title = title self.key = key + self.shouldSaveKey = shouldSaveKey + self.privKeyFound = privKeyFound } var body: some View { @@ -281,6 +304,8 @@ struct KeyInput: View { self.key.wrappedValue = pastedkey } } + SignInScan(shouldSaveKey: shouldSaveKey, loginKey: key, privKeyFound: privKeyFound) + if is_secured { SecureField("", text: key) .nsecLoginStyle(key: key.wrappedValue, title: title) @@ -323,18 +348,79 @@ struct SignInHeader: View { struct SignInEntry: View { let key: Binding - + let shouldSaveKey: Binding + @State private var privKeyFound: Bool = false var body: some View { VStack(alignment: .leading) { Text("Enter your account key", comment: "Prompt for user to enter an account key to login.") .fontWeight(.medium) .padding(.top, 30) - KeyInput(NSLocalizedString("nsec1...", comment: "Prompt for user to enter in an account key to login. This text shows the characters the key could start with if it was a private key."), key: key) + KeyInput(NSLocalizedString("nsec1...", comment: "Prompt for user to enter in an account key to login. This text shows the characters the key could start with if it was a private key."), + key: key, + shouldSaveKey: shouldSaveKey, + privKeyFound: $privKeyFound) + if privKeyFound { + Toggle("Save Key in Secure Keychain", isOn: shouldSaveKey) + } } } } +struct SignInScan: View { + @State var showQR: Bool = false + @State var qrkey: ParsedKey? + @Binding var shouldSaveKey: Bool + @Binding var loginKey: String + @Binding var privKeyFound: Bool + let generator = UINotificationFeedbackGenerator() + + var body: some View { + VStack { + Button(action: { showQR.toggle() }, label: { + Image(systemName: "qrcode.viewfinder")}) + .foregroundColor(.gray) + + } + .sheet(isPresented: $showQR, onDismiss: { + if qrkey == nil { resetView() }} + ) { + QRScanNSECView(showQR: $showQR, + privKeyFound: $privKeyFound, + codeScannerCompletion: { scannerCompletion($0) }) + } + .onChange(of: showQR) { show in + if showQR { resetView() } + } + } + + func handleQRString(_ string: String) { + qrkey = parse_key(string) + if let key = qrkey, key.is_priv { + loginKey = string + privKeyFound = true + shouldSaveKey = false + generator.notificationOccurred(.success) + } + } + + func scannerCompletion(_ result: Result) { + switch result { + case .success(let success): + handleQRString(success.string) + case .failure: + return + } + } + + func resetView() { + loginKey = "" + qrkey = nil + privKeyFound = false + shouldSaveKey = true + } +} + struct CreateAccountPrompt: View { var nav: NavigationCoordinator var body: some View { diff --git a/damus/Views/QRScanNSECView.swift b/damus/Views/QRScanNSECView.swift new file mode 100644 index 0000000000..eddca24218 --- /dev/null +++ b/damus/Views/QRScanNSECView.swift @@ -0,0 +1,66 @@ +// +// QRScanNSECView.swift +// damus +// +// Created by Jericho Hasselbush on 9/29/23. +// + +import SwiftUI +import VisionKit + +struct QRScanNSECView: View { + @Binding var showQR: Bool + @Binding var privKeyFound: Bool + var codeScannerCompletion: (Result) -> Void + var body: some View { + ZStack { + ZStack { + DamusGradient() + } + VStack { + Text("Scan Your Private Key QR", + comment: "Text to prompt scanning a QR code of a user's privkey to login to their profile.") + .padding(.top, 50) + .font(.system(size: 24, weight: .heavy)) + + Spacer() + CodeScannerView(codeTypes: [.qr], + scanMode: .continuous, + scanInterval: 2.0, + showViewfinder: false, + simulatedData: "", + shouldVibrateOnSuccess: false, + isTorchOn: false, + isGalleryPresented: .constant(false), + videoCaptureDevice: .default(for: .video), + completion: codeScannerCompletion) + .scaledToFit() + .frame(width: 300, height: 300) + .cornerRadius(10) + .overlay(RoundedRectangle(cornerRadius: 10).stroke(DamusColors.white, lineWidth: 5.0)) + .shadow(radius: 10) + + Button(action: { showQR = false }) { + VStack { + Image(systemName: privKeyFound ? "sparkle.magnifyingglass" : "magnifyingglass") + .font(privKeyFound ? .title : .title3) + }} + .padding(.top) + .buttonStyle(GradientButtonStyle()) + + Spacer() + + Spacer() + } + } + } +} + +#Preview { + @State var showQR = true + @State var privKeyFound = false + @State var shouldSaveKey = true + return QRScanNSECView(showQR: $showQR, + privKeyFound: $privKeyFound, + codeScannerCompletion: { _ in }) +} diff --git a/damus/damusApp.swift b/damus/damusApp.swift index 7300b1b0e3..a629200a93 100644 --- a/damus/damusApp.swift +++ b/damus/damusApp.swift @@ -32,6 +32,9 @@ struct MainView: View { .onReceive(handle_notify(.login)) { notif in needs_setup = false keypair = get_saved_keypair() + if keypair == nil, let tempkeypair = notif.to_full()?.to_keypair() { + keypair = tempkeypair + } } } } From 82fba88cc4131f1b2d67c9ac5b56793d634a48e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Sat, 14 Oct 2023 00:42:27 +0000 Subject: [PATCH 51/76] storage: set disk cache expiry dates for images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds expiry dates for images added to the Kingfisher cache. The expiry date depends on the context of the image: - Images from notes expire after a week - Images from profile banners expire after two weeks - Profile pictures never expire. Test ---- Device: iPhone 14 Pro (Simulator), iOS: 17.0 Special remarks: Requires minor local mods and debugger connection Steps: 1. Locally change the note image expiry to 5 seconds 2. Set a breakpoint in `removeExpiredValues` function in `DiskStorage.swift` in Kingfisher 3. Disable breakpoints for now 4. Start Damus and go to the profile feed of someone new 5. Scroll down through the images for about a minute 6. Turn on breakpoints 7. Switch to a different app in the simulator (Make Damus go to background mode) 8. Wait for a few seconds. Debugger should hit the breakpoint set. PASS 9. Take note of the fileURLs of the images being deleted 10. Go to that directory where the fileURLs are in via Finder 11. Look at some of the images being deleted. Perhaps save a copy for comparison. 12. Turn off breakpoints, resume execution and go back to Damus 13. Scroll back up. Some images there should match the images being automatically deleted from the cache. PASS Closes: https://github.com/damus-io/damus/issues/1565 Changelog-Added: Add expiry date for images in cache to be auto-deleted after a preset time to save space on storage Signed-off-by: Daniel D’Aquino Reviewed-by: William Casarin Signed-off-by: William Casarin --- damus/Util/Extensions/KFOptionSetter+.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/damus/Util/Extensions/KFOptionSetter+.swift b/damus/Util/Extensions/KFOptionSetter+.swift index e675144800..264fa77abe 100644 --- a/damus/Util/Extensions/KFOptionSetter+.swift +++ b/damus/Util/Extensions/KFOptionSetter+.swift @@ -28,6 +28,18 @@ extension KFOptionSetter { options.scaleFactor = UIScreen.main.scale options.onlyLoadFirstFrame = disable_animation + switch imageContext { + case .pfp: + options.diskCacheExpiration = .never + break + case .banner: + options.diskCacheExpiration = .days(14) + break + case .note: + options.diskCacheExpiration = .days(7) + break + } + return self } From edb23e4e7016465ad7e9238673fe7d039e2b2b2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Fri, 13 Oct 2023 12:24:55 -0700 Subject: [PATCH 52/76] ui: prefix username with `@` character, fix spacing Closes: https://github.com/damus-io/damus/issues/1559 Changelog-Fixed: Add more spacing between display name and username, and prefix username with `@` character --- damus/Views/Profile/EventProfileName.swift | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/damus/Views/Profile/EventProfileName.swift b/damus/Views/Profile/EventProfileName.swift index bde6c2a206..28cec949de 100644 --- a/damus/Views/Profile/EventProfileName.swift +++ b/damus/Views/Profile/EventProfileName.swift @@ -66,12 +66,14 @@ struct EventProfileName: View { .font(.body.weight(.bold)) case .both(username: let username, displayName: let displayName): - Text(verbatim: displayName) - .font(.body.weight(.bold)) - - Text(verbatim: username) - .foregroundColor(.gray) - .font(eventviewsize_to_font(size, font_size: damus_state.settings.font_size)) + HStack(spacing: 6) { + Text(verbatim: displayName) + .font(.body.weight(.bold)) + + Text(verbatim: "@\(username)") + .foregroundColor(.gray) + .font(eventviewsize_to_font(size, font_size: damus_state.settings.font_size)) + } } /* From 3b76fcb743f1e9bb4049228a302b4254bccc9921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Fri, 13 Oct 2023 12:28:41 -0700 Subject: [PATCH 53/76] test: Add basic snapshot test coverage for EventView This commit adds a basic snapshot test for EventView, and also adds some testing infrastructure to help with mocking NostrDB behavior. Test ---- PASS Device: iOS 17.0 Simulator iOS: 17.0 Damus: This commit Steps: Run `EventViewTests` Results: Snapshot matches baseline reference added --- damus.xcodeproj/project.pbxproj | 18 ++++- damusTests/EventViewTests.swift | 46 ++++++++++++ damusTests/Mocking/MockDamusState.swift | 66 ++++++++++++++++++ damusTests/Mocking/MockProfiles.swift | 28 ++++++++ .../testBasicEventViewLayout.1.png | Bin 0 -> 140396 bytes 5 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 damusTests/EventViewTests.swift create mode 100644 damusTests/Mocking/MockDamusState.swift create mode 100644 damusTests/Mocking/MockProfiles.swift create mode 100644 damusTests/__Snapshots__/EventViewTests/testBasicEventViewLayout.1.png diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 3769bb59f8..ad962ff851 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -430,6 +430,9 @@ BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; }; D2277EEA2A089BD5006C3807 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; }; D71DC1EC2A9129C3006E207C /* PostViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71DC1EB2A9129C3006E207C /* PostViewTests.swift */; }; + D72A2D022AD9C136002AFF62 /* EventViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */; }; + D72A2D052AD9C1B5002AFF62 /* MockDamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D042AD9C1B5002AFF62 /* MockDamusState.swift */; }; + D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */; }; D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; }; D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; }; D723C38E2AB8D83400065664 /* ContentFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D723C38D2AB8D83400065664 /* ContentFilters.swift */; }; @@ -1128,6 +1131,9 @@ D2277EE92A089BD5006C3807 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; D71DC1EB2A9129C3006E207C /* PostViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostViewTests.swift; sourceTree = ""; }; D723C38D2AB8D83400065664 /* ContentFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentFilters.swift; sourceTree = ""; }; + D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventViewTests.swift; sourceTree = ""; }; + D72A2D042AD9C1B5002AFF62 /* MockDamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDamusState.swift; sourceTree = ""; }; + D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProfiles.swift; sourceTree = ""; }; D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContentViewTests.swift; sourceTree = ""; }; D7870BC02AC4750B0080BA88 /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = ""; }; D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventLoaderView.swift; sourceTree = ""; }; @@ -2155,6 +2161,7 @@ 4CE6DEF627F7A08200C66700 /* damusTests */ = { isa = PBXGroup; children = ( + D72A2D032AD9C165002AFF62 /* Mocking */, 4C9B0DEC2A65A74000CBDA21 /* Util */, 4C0C03962A61E2670098B3B8 /* Fixtures */, 4C7D097D2A0C58B900943473 /* WalletConnectTests.swift */, @@ -2185,6 +2192,7 @@ 4C684A562A7FFAE6005E6031 /* UrlTests.swift */, D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */, D71DC1EB2A9129C3006E207C /* PostViewTests.swift */, + D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */, D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */, ); path = damusTests; @@ -2313,12 +2321,13 @@ path = Camera; sourceTree = ""; }; - BA3759952ABCCF360018D73B /* Camera */ = { + D72A2D032AD9C165002AFF62 /* Mocking */ = { isa = PBXGroup; children = ( - BA3759962ABCCF360018D73B /* CameraPreview.swift */, + D72A2D042AD9C1B5002AFF62 /* MockDamusState.swift */, + D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */, ); - path = Camera; + path = Mocking; sourceTree = ""; }; F71694E82A66221E001F4053 /* Onboarding */ = { @@ -2975,12 +2984,14 @@ 3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */, 501F8C822A0224EB001AFC1D /* KeychainStorageTests.swift in Sources */, 3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */, + D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */, 4C4F14A72A2A61A30045A0B9 /* NostrScriptTests.swift in Sources */, D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */, 4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */, 4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */, 4C7D097E2A0C58B900943473 /* WalletConnectTests.swift in Sources */, 4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */, + D72A2D022AD9C136002AFF62 /* EventViewTests.swift in Sources */, 4CB8839A297322D200DC99E7 /* DMTests.swift in Sources */, D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */, 4C9054852A6AEAA000811EEC /* NdbTests.swift in Sources */, @@ -2988,6 +2999,7 @@ F944F56E29EA9CCC0067B3BF /* DamusParseContentTests.swift in Sources */, 3A5E47C72A4A76C800C0D090 /* TrieTests.swift in Sources */, 4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */, + D72A2D052AD9C1B5002AFF62 /* MockDamusState.swift in Sources */, 4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */, 4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */, 50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */, diff --git a/damusTests/EventViewTests.swift b/damusTests/EventViewTests.swift new file mode 100644 index 0000000000..ccd3c356ef --- /dev/null +++ b/damusTests/EventViewTests.swift @@ -0,0 +1,46 @@ +// +// EventViewTests.swift +// damusTests +// +// Created by Daniel D’Aquino on 2023-10-13. +// + +import Foundation +import XCTest +import SnapshotTesting +import SwiftUI +@testable import damus + +final class EventViewTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testBasicEventViewLayout() { + let test_mock_damus_state = generate_test_damus_state( + mock_profile_info: [ + // Manually mock some profile info so that we have a more realistic-looking note + jack_keypair.pubkey: Profile( + name: "jack", + display_name: "Jack Dorsey" + ) + ] + ) + let test_note = NostrEvent( + content: "Nostr is the super app. Because it’s actually an ecosystem of apps, all of which make each other better. People haven’t grasped that yet. They will when it’s more accessible and onboarding is more straightforward and intuitive.", + keypair: jack_keypair, + createdAt: UInt32(Date.init(timeIntervalSinceNow: -60).timeIntervalSince1970) + )! + + let eventViewTest = EventView(damus: test_mock_damus_state, event: test_note).padding() + let hostView = UIHostingController(rootView: eventViewTest) + + // Run snapshot check + assertSnapshot(matching: hostView, as: .image(on: .iPhone13(.portrait))) + } +} diff --git a/damusTests/Mocking/MockDamusState.swift b/damusTests/Mocking/MockDamusState.swift new file mode 100644 index 0000000000..9d49c9ab18 --- /dev/null +++ b/damusTests/Mocking/MockDamusState.swift @@ -0,0 +1,66 @@ +// +// MockDamusState.swift +// damusTests +// +// Created by Daniel D’Aquino on 2023-10-13. +// + +import Foundation +@testable import damus + +// Generates a test damus state with configurable mock parameters +func generate_test_damus_state( + mock_profile_info: [Pubkey: Profile]? +) -> DamusState { + // Create a unique temporary directory + var tempDir: String! + do { + let fileManager = FileManager.default + let temp = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString) + try fileManager.createDirectory(at: temp, withIntermediateDirectories: true, attributes: nil) + tempDir = temp.absoluteString + } catch { + tempDir = "." + } + + print("opening \(tempDir!)") + let ndb = Ndb(path: tempDir)! + let our_pubkey = test_pubkey + let pool = RelayPool(ndb: ndb) + let settings = UserSettingsStore() + + let profiles: Profiles = { + guard let mock_profile_info, let profiles: Profiles = MockProfiles(mocked_profiles: mock_profile_info, ndb: ndb) else { + return Profiles.init(ndb: ndb) + } + return profiles + }() + + let damus = DamusState(pool: pool, + keypair: test_keypair, + likes: .init(our_pubkey: our_pubkey), + boosts: .init(our_pubkey: our_pubkey), + contacts: .init(our_pubkey: our_pubkey), + profiles: profiles, + dms: .init(our_pubkey: our_pubkey), + previews: .init(), + zaps: .init(our_pubkey: our_pubkey), + lnurls: .init(), + settings: settings, + relay_filters: .init(our_pubkey: our_pubkey), + relay_model_cache: .init(), + drafts: .init(), + events: .init(ndb: ndb), + bookmarks: .init(pubkey: our_pubkey), + postbox: .init(pool: pool), + bootstrap_relays: .init(), + replies: .init(our_pubkey: our_pubkey), + muted_threads: .init(keypair: test_keypair), + wallet: .init(settings: settings), + nav: .init(), + music: .init(onChange: {_ in }), + video: .init(), + ndb: ndb) + + return damus +} diff --git a/damusTests/Mocking/MockProfiles.swift b/damusTests/Mocking/MockProfiles.swift new file mode 100644 index 0000000000..591ba2f6ef --- /dev/null +++ b/damusTests/Mocking/MockProfiles.swift @@ -0,0 +1,28 @@ +// +// MockProfiles.swift +// damusTests +// +// Created by Daniel D’Aquino on 2023-10-13. +// + +import Foundation +@testable import damus + +// A Mockable `Profiles` class that can be used for testing. +// Note: Not all methods are mocked. You might need to implement a method depending on the test you are writing. +class MockProfiles: Profiles { + var mocked_profiles: [Pubkey: Profile] = [:] + var ndb: Ndb + + init?(mocked_profiles: [Pubkey : Profile], ndb: Ndb) { + self.mocked_profiles = mocked_profiles + self.ndb = ndb + super.init(ndb: ndb) + } + + override func lookup(id: Pubkey) -> NdbTxn { + return NdbTxn(ndb: self.ndb) { txn in + return self.mocked_profiles[id] + } + } +} diff --git a/damusTests/__Snapshots__/EventViewTests/testBasicEventViewLayout.1.png b/damusTests/__Snapshots__/EventViewTests/testBasicEventViewLayout.1.png new file mode 100644 index 0000000000000000000000000000000000000000..51454c1c24cc6ae4860d08004ab2734371c6b6b0 GIT binary patch literal 140396 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 ZAL*eczW~&V$vgl6 literal 0 HcmV?d00001 From 06eb9d4a0e20132b04eb619db95eb42fe627288c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Tue, 17 Oct 2023 01:36:14 +0000 Subject: [PATCH 54/76] dm: Do not show DMs from muted users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commits causes DMs from muted users to be filtered out. It also fixes an issue where the DM list would appear completely blank in certain scenarios. Testing ------- CONDITIONAL PASS Device: iPhone 14 Pro simulator iOS: 17.0 Damus: This commit Setup: - Three test accounts (A, B, and C). "A" will be the account running on the device under test. - Account "A" should start with no DMs 1. Send a direct message from "B" to "A", and reply from "A". 2. Go to DMs -> DMs tab. Conversation with "B" should appear. PASS 3. Mute user "B" (I did it via profiles page). 4. Go back to DMs view via back button. DMs from "B" should not appear. PASS 5. Since there are no DMs, the screen should display "Nothing to see here". PASS 5. Close Damus app via iOS app switcher and reopen Damus 6. Check DMs list. Should only show "Nothing to see here". PASS 7. Send a DM from account "C" to "A" and reply. 8. Go back to DMs -> DMs tab. Only the message from account "C" should appear. 9. Unmute user "B" 10. Go back to DMs. Messages from "B" and "C" should appear. PASS Notes: - There was one instance when the first DM from account "C" appeared in the "DMs" tab (Not "requests") momentarily. After a bit it went into requests as expected. - When unmuting user "B", I had to refresh the DM list by switching tabs, meaning that the view did not immediately update. Upon inspection, the two behaviors above are not caused by this change, so this is a conditional pass. Closes: https://github.com/damus-io/damus/issues/1350 Changelog-Fixed: Do not show DMs from muted users Signed-off-by: Daniel D’Aquino Reviewed-by: William Casarin Signed-off-by: William Casarin --- damus/Views/DirectMessagesView.swift | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/damus/Views/DirectMessagesView.swift b/damus/Views/DirectMessagesView.swift index 352cef7103..2ff3da747b 100644 --- a/damus/Views/DirectMessagesView.swift +++ b/damus/Views/DirectMessagesView.swift @@ -22,11 +22,12 @@ struct DirectMessagesView: View { func MainContent(requests: Bool) -> some View { ScrollView { LazyVStack(spacing: 0) { - if model.dms.isEmpty, !model.loading { + let dms = requests ? model.message_requests : model.friend_dms + let filtered_dms = filter_dms(dms: dms) + if filtered_dms.isEmpty, !model.loading { EmptyTimelineView() } else { - let dms = requests ? model.message_requests : model.friend_dms - ForEach(dms, id: \.pubkey) { dm in + ForEach(filtered_dms, id: \.pubkey) { dm in MaybeEvent(dm) .padding(.top, 10) } @@ -36,6 +37,12 @@ struct DirectMessagesView: View { } } + func filter_dms(dms: [DirectMessageModel]) -> [DirectMessageModel] { + return dms.filter({ dm in + return damus_state.settings.friend_filter.filter(contacts: damus_state.contacts, pubkey: dm.pubkey) && !damus_state.contacts.is_muted(dm.pubkey) + }) + } + var options: EventViewOptions { if self.damus_state.settings.translate_dms { return [.truncate_content, .no_action_bar] @@ -46,8 +53,7 @@ struct DirectMessagesView: View { func MaybeEvent(_ model: DirectMessageModel) -> some View { Group { - let ok = damus_state.settings.friend_filter.filter(contacts: damus_state.contacts, pubkey: model.pubkey) - if ok, let ev = model.events.last { + if let ev = model.events.last { EventView(damus: damus_state, event: ev, pubkey: model.pubkey, options: options) .onTapGesture { self.model.set_active_dm_model(model) From 2aaedd077e038dff997c04352e653606a6b7ef7c Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 19:16:01 +0000 Subject: [PATCH 55/76] Translate Localizable.stringsdict in pl_PL 100% translated source file: 'Localizable.stringsdict' on 'pl_PL'. --- damus/pl-PL.lproj/Localizable.stringsdict | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/damus/pl-PL.lproj/Localizable.stringsdict b/damus/pl-PL.lproj/Localizable.stringsdict index 1760152014..c90faecd95 100644 --- a/damus/pl-PL.lproj/Localizable.stringsdict +++ b/damus/pl-PL.lproj/Localizable.stringsdict @@ -377,9 +377,9 @@ few Otrzymano %2$@ satoshi od %3$@: "%4$@" many - Otrzymano %2$@ satoshi od %3$@: "%4$@" + Otrzymano %2$@ satoshi od %3$@: "%4$@" other - Otrzymano %2$@ satoshi od %3$@: "%4$@" + Otrzymano %2$@ satoshi od %3$@: "%4$@" zapped_tagged_in_3 From 63fee80c53cb9ebc970a47953031fb4308c3e61e Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 19:16:15 +0000 Subject: [PATCH 56/76] Translate Localizable.stringsdict in pl_PL 100% translated source file: 'Localizable.stringsdict' on 'pl_PL'. --- damus/pl-PL.lproj/Localizable.stringsdict | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/damus/pl-PL.lproj/Localizable.stringsdict b/damus/pl-PL.lproj/Localizable.stringsdict index c90faecd95..2202e87886 100644 --- a/damus/pl-PL.lproj/Localizable.stringsdict +++ b/damus/pl-PL.lproj/Localizable.stringsdict @@ -375,7 +375,7 @@ one Otrzymano %2$@ satoshi od %3$@: "%4$@" few - Otrzymano %2$@ satoshi od %3$@: "%4$@" + Otrzymano %2$@ satoshi od %3$@: "%4$@" many Otrzymano %2$@ satoshi od %3$@: "%4$@" other From 0277303da727cbef77100cbdb92a178a0f2183ea Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 19:16:26 +0000 Subject: [PATCH 57/76] Translate Localizable.stringsdict in pl_PL 100% translated source file: 'Localizable.stringsdict' on 'pl_PL'. --- damus/pl-PL.lproj/Localizable.stringsdict | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/damus/pl-PL.lproj/Localizable.stringsdict b/damus/pl-PL.lproj/Localizable.stringsdict index 2202e87886..65e57ba4bb 100644 --- a/damus/pl-PL.lproj/Localizable.stringsdict +++ b/damus/pl-PL.lproj/Localizable.stringsdict @@ -373,7 +373,7 @@ NSStringFormatValueTypeKey @ one - Otrzymano %2$@ satoshi od %3$@: "%4$@" + Otrzymano %2$@ satoshi od %3$@: "%4$@" few Otrzymano %2$@ satoshi od %3$@: "%4$@" many From 7c984899047440ff1ef343619d83cdc795f31d89 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Thu, 19 Oct 2023 19:04:52 +0200 Subject: [PATCH 58/76] nav: fix pushing duplicate routes Skip push if matches top route. Fixes: https://github.com/damus-io/damus/issues/104 Closes: https://github.com/damus-io/damus/pull/1625 Reviewed-by: William Casarin Signed-off-by: William Casarin --- damus/Util/Router.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/damus/Util/Router.swift b/damus/Util/Router.swift index d7d72d25c1..3017d11c7a 100644 --- a/damus/Util/Router.swift +++ b/damus/Util/Router.swift @@ -218,6 +218,9 @@ class NavigationCoordinator: ObservableObject { @Published var path = [Route]() func push(route: Route) { + guard route != path.last else { + return + } path.append(route) } From bf43842590a0f62aafcd0f811d0634b396ab7f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Fri, 20 Oct 2023 18:15:58 +0000 Subject: [PATCH 59/76] onboarding: Suggest first post during onboarding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Testing of standard flow ------------------------ PASS Device: iPhone 14 Pro simulator iOS: 17.0 Damus: This commit Steps: 1. Delete and reinstall Damus 2. Go through onboarding until suggested users appear 3. Click "continue". Should slide into the post view. PASS 4. Post view should look similar to the Figma design file, but with examples as placeholders. PASS 5. Examples should switch every 3 seconds. PASS 6. Typing a first character causes the #introductions hashtag to be automatically added. PASS 7. Uploading an image makes progress view show up and not break layout. PASS 8. Clicking on "post" should post this note and dismiss onboarding view. PASS Testing of other flows ---------------------- PASS Device: iPhone 14 Pro simulator iOS: 17.0 Damus: This commit Special remark: Made local change to always show the onboarding suggestions, and speed up testing Coverage: 1. Clicking "skip" on suggested users view will skip into the post view. PASS 2. Clicking "cancel" on post view and then going to the normal post view reveals a blank draft. PASS 3. Clicking "cancel" dismisses onboarding view and does not post anything. PASS 4. Normal post view looks normal (not broken). PASS 5. Changing initial suggested post during onboarding, cancelling the post, and then re-entering normal post view reveals the draft with user modifications. PASS Changelog-Added: Suggest first post during onboarding Closes: https://github.com/damus-io/damus/issues/1338 Signed-off-by: Daniel D’Aquino Signed-off-by: William Casarin --- damus.xcodeproj/project.pbxproj | 29 ++--- damus/ContentView.swift | 16 +-- damus/Models/DraftsModel.swift | 9 +- .../OnboardingSuggestionsView.swift | 123 ++++++++++++++++++ .../Views/Onboarding/SuggestedUsersView.swift | 77 ----------- damus/Views/PostView.swift | 78 ++++++++--- damus/Views/TextViewWrapper.swift | 23 +++- damusTests/PostViewTests.swift | 3 +- 8 files changed, 236 insertions(+), 122 deletions(-) create mode 100644 damus/Views/Onboarding/OnboardingSuggestionsView.swift delete mode 100644 damus/Views/Onboarding/SuggestedUsersView.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index ad962ff851..e4cce35523 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -380,10 +380,6 @@ 4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; }; 50088DA129E8271A008A1FDF /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50088DA029E8271A008A1FDF /* WebSocket.swift */; }; 501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C7F2A0220E1001AFC1D /* KeychainStorage.swift */; }; - BA3759922ABCCEBA0018D73B /* CameraService+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */; }; - BA3759932ABCCEBA0018D73B /* CameraModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759902ABCCEBA0018D73B /* CameraModel.swift */; }; - BA3759942ABCCEBA0018D73B /* CameraService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759912ABCCEBA0018D73B /* CameraService.swift */; }; - BA3759972ABCCF360018D73B /* CameraPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759962ABCCF360018D73B /* CameraPreview.swift */; }; 501F8C822A0224EB001AFC1D /* KeychainStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */; }; 504323A72A34915F006AE6DC /* RelayModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504323A62A34915F006AE6DC /* RelayModel.swift */; }; 504323A92A3495B6006AE6DC /* RelayModelCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504323A82A3495B6006AE6DC /* RelayModelCache.swift */; }; @@ -423,6 +419,9 @@ BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; }; BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; }; BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; }; + BA3759922ABCCEBA0018D73B /* CameraService+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */; }; + BA3759932ABCCEBA0018D73B /* CameraModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759902ABCCEBA0018D73B /* CameraModel.swift */; }; + BA3759942ABCCEBA0018D73B /* CameraService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759912ABCCEBA0018D73B /* CameraService.swift */; }; BA3759972ABCCF360018D73B /* CameraPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759962ABCCF360018D73B /* CameraPreview.swift */; }; BA4AB0AE2A63B9270070A32A /* AddEmojiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4AB0AD2A63B9270070A32A /* AddEmojiView.swift */; }; BA4AB0B02A63B94D0070A32A /* EmojiListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4AB0AF2A63B94D0070A32A /* EmojiListItemView.swift */; }; @@ -433,9 +432,9 @@ D72A2D022AD9C136002AFF62 /* EventViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */; }; D72A2D052AD9C1B5002AFF62 /* MockDamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D042AD9C1B5002AFF62 /* MockDamusState.swift */; }; D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */; }; + D723C38E2AB8D83400065664 /* ContentFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D723C38D2AB8D83400065664 /* ContentFilters.swift */; }; D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; }; D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; }; - D723C38E2AB8D83400065664 /* ContentFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D723C38D2AB8D83400065664 /* ContentFilters.swift */; }; D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */; }; D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC02AC4750B0080BA88 /* MentionView.swift */; }; D7870BC32AC47EBC0080BA88 /* EventLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */; }; @@ -446,7 +445,7 @@ E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */; }; E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; }; E9E4ED0B295867B900DD7078 /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadView.swift */; }; - F71694EA2A662232001F4053 /* SuggestedUsersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71694E92A662232001F4053 /* SuggestedUsersView.swift */; }; + F71694EA2A662232001F4053 /* OnboardingSuggestionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71694E92A662232001F4053 /* OnboardingSuggestionsView.swift */; }; F71694EC2A662292001F4053 /* SuggestedUsersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71694EB2A662292001F4053 /* SuggestedUsersViewModel.swift */; }; F71694EE2A6624F9001F4053 /* suggested_users.json in Resources */ = {isa = PBXBuildFile; fileRef = F71694ED2A6624F9001F4053 /* suggested_users.json */; }; F71694F22A67314D001F4053 /* SuggestedUserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71694F12A67314D001F4053 /* SuggestedUserView.swift */; }; @@ -944,10 +943,6 @@ 4C9B0DED2A65A75F00CBDA21 /* AttrStringTestExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttrStringTestExtensions.swift; sourceTree = ""; }; 4C9B0DF22A65C46800CBDA21 /* ProfileEditButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileEditButton.swift; sourceTree = ""; }; 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayName.swift; sourceTree = ""; }; - BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CameraService+Extensions.swift"; sourceTree = ""; }; - BA3759902ABCCEBA0018D73B /* CameraModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraModel.swift; sourceTree = ""; }; - BA3759912ABCCEBA0018D73B /* CameraService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraService.swift; sourceTree = ""; }; - BA3759962ABCCF360018D73B /* CameraPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPreview.swift; sourceTree = ""; }; 4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfileName.swift; sourceTree = ""; }; 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapView.swift; sourceTree = ""; }; 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeAnonPfpView.swift; sourceTree = ""; }; @@ -1121,9 +1116,10 @@ BA3759892ABCCDE30018D73B /* ImageResizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageResizer.swift; sourceTree = ""; }; BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureProcessor.swift; sourceTree = ""; }; BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCaptureProcessor.swift; sourceTree = ""; }; + BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CameraService+Extensions.swift"; sourceTree = ""; }; + BA3759902ABCCEBA0018D73B /* CameraModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraModel.swift; sourceTree = ""; }; + BA3759912ABCCEBA0018D73B /* CameraService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraService.swift; sourceTree = ""; }; BA3759962ABCCF360018D73B /* CameraPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPreview.swift; sourceTree = ""; }; - D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = ""; }; - D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = ""; }; BA4AB0AD2A63B9270070A32A /* AddEmojiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEmojiView.swift; sourceTree = ""; }; BA4AB0AF2A63B94D0070A32A /* EmojiListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiListItemView.swift; sourceTree = ""; }; BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = ""; }; @@ -1134,6 +1130,8 @@ D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventViewTests.swift; sourceTree = ""; }; D72A2D042AD9C1B5002AFF62 /* MockDamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDamusState.swift; sourceTree = ""; }; D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProfiles.swift; sourceTree = ""; }; + D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = ""; }; + D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = ""; }; D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContentViewTests.swift; sourceTree = ""; }; D7870BC02AC4750B0080BA88 /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = ""; }; D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventLoaderView.swift; sourceTree = ""; }; @@ -1142,7 +1140,7 @@ E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = ""; }; E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = ""; }; E9E4ED0A295867B900DD7078 /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = ""; }; - F71694E92A662232001F4053 /* SuggestedUsersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedUsersView.swift; sourceTree = ""; }; + F71694E92A662232001F4053 /* OnboardingSuggestionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingSuggestionsView.swift; sourceTree = ""; }; F71694EB2A662292001F4053 /* SuggestedUsersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedUsersViewModel.swift; sourceTree = ""; }; F71694ED2A6624F9001F4053 /* suggested_users.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = suggested_users.json; sourceTree = ""; }; F71694F12A67314D001F4053 /* SuggestedUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedUserView.swift; sourceTree = ""; }; @@ -1261,7 +1259,6 @@ 4C3EA66C28FF782800C48A62 /* amount.c */, 4C3EA66E28FF787100C48A62 /* overflows.h */, 4C3EA67228FF79F600C48A62 /* structeq.h */, - BA3759952ABCCF360018D73B /* Camera */, 4C3EA67328FF7A2600C48A62 /* cppmagic.h */, 4C3EA67428FF7A5A00C48A62 /* take.c */, 4C3EA67628FF7A9800C48A62 /* talstr.c */, @@ -2333,7 +2330,7 @@ F71694E82A66221E001F4053 /* Onboarding */ = { isa = PBXGroup; children = ( - F71694E92A662232001F4053 /* SuggestedUsersView.swift */, + F71694E92A662232001F4053 /* OnboardingSuggestionsView.swift */, F71694F12A67314D001F4053 /* SuggestedUserView.swift */, F71694EB2A662292001F4053 /* SuggestedUsersViewModel.swift */, F71694ED2A6624F9001F4053 /* suggested_users.json */, @@ -2836,7 +2833,7 @@ BA3759942ABCCEBA0018D73B /* CameraService.swift in Sources */, 4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */, 4CF0ABD82981980C00D66079 /* Lists.swift in Sources */, - F71694EA2A662232001F4053 /* SuggestedUsersView.swift in Sources */, + F71694EA2A662232001F4053 /* OnboardingSuggestionsView.swift in Sources */, 4C12536A2A76D3850004F4B8 /* RelaysChangedNotify.swift in Sources */, 4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */, 5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */, diff --git a/damus/ContentView.swift b/damus/ContentView.swift index 2042773641..edda8d50a8 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -26,7 +26,7 @@ enum Sheets: Identifiable { case select_wallet(SelectWallet) case filter case user_status - case suggestedUsers + case onboardingSuggestions static func zap(target: ZapTarget, lnurl: String) -> Sheets { return .zap(ZapSheet(target: target, lnurl: lnurl)) @@ -45,7 +45,7 @@ enum Sheets: Identifiable { case .zap(let sheet): return "zap-" + hex_encode(sheet.target.id) case .select_wallet: return "select-wallet" case .filter: return "filter" - case .suggestedUsers: return "suggested-users" + case .onboardingSuggestions: return "onboarding-suggestions" } } } @@ -74,7 +74,7 @@ struct ContentView: View { @State private var isSideBarOpened = false var home: HomeModel = HomeModel() @StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator() - @AppStorage("has_seen_suggested_users") private var hasSeenSuggestedUsers = false + @AppStorage("has_seen_suggested_users") private var hasSeenOnboardingSuggestions = false let sub_id = UUID().description @Environment(\.colorScheme) var colorScheme @@ -300,9 +300,9 @@ struct ContentView: View { self.connect() try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers) setup_notifications() - if !hasSeenSuggestedUsers { - active_sheet = .suggestedUsers - hasSeenSuggestedUsers = true + if !hasSeenOnboardingSuggestions { + active_sheet = .onboardingSuggestions + hasSeenOnboardingSuggestions = true } } .sheet(item: $active_sheet) { item in @@ -329,8 +329,8 @@ struct ContentView: View { } else { RelayFilterView(state: damus_state!, timeline: timeline) } - case .suggestedUsers: - SuggestedUsersView(model: SuggestedUsersViewModel(damus_state: damus_state!)) + case .onboardingSuggestions: + OnboardingSuggestionsView(model: SuggestedUsersViewModel(damus_state: damus_state!)) } } .onOpenURL { url in diff --git a/damus/Models/DraftsModel.swift b/damus/Models/DraftsModel.swift index 97fdc6d534..ab71d839fb 100644 --- a/damus/Models/DraftsModel.swift +++ b/damus/Models/DraftsModel.swift @@ -7,7 +7,7 @@ import Foundation -class DraftArtifacts { +class DraftArtifacts: Equatable { var content: NSMutableAttributedString var media: [UploadedMedia] @@ -15,6 +15,13 @@ class DraftArtifacts { self.content = content self.media = media } + + static func == (lhs: DraftArtifacts, rhs: DraftArtifacts) -> Bool { + return ( + lhs.media == rhs.media && + lhs.content.string == rhs.content.string // Comparing the text content is not perfect but acceptable in this case because attributes for our post editor are determined purely from text content + ) + } } class Drafts: ObservableObject { diff --git a/damus/Views/Onboarding/OnboardingSuggestionsView.swift b/damus/Views/Onboarding/OnboardingSuggestionsView.swift new file mode 100644 index 0000000000..3a4eb3075e --- /dev/null +++ b/damus/Views/Onboarding/OnboardingSuggestionsView.swift @@ -0,0 +1,123 @@ +// +// OnboardingSuggestionsView.swift +// damus +// +// Created by klabo on 7/17/23. +// + +import SwiftUI + +fileprivate let first_post_example_1: String = NSLocalizedString("Hello everybody!\n\nThis is my first post on Damus, I am happy to meet you all 🤙. What’s up?\n\n#introductions", comment: "First post example given to the user during onboarding, as a suggestion as to what they could post first") +fileprivate let first_post_example_2: String = NSLocalizedString("This is my first post on Nostr 💜. I love drawing and folding Origami!\n\nNice to meet you all! #introductions #plebchain ", comment: "First post example given to the user during onboarding, as a suggestion as to what they could post first") +fileprivate let first_post_example_3: String = NSLocalizedString("For #Introductions! I’m a software developer.\n\nMy side interests include languages and I am striving to be a #polyglot - I am a native English speaker and can speak French, German and Japanese.", comment: "First post example given to the user during onboarding, as a suggestion as to what they could post first") +fileprivate let first_post_example_4: String = NSLocalizedString("Howdy! I’m a graphic designer during the day and coder at night, but I’m also trying to spend more time outdoors.\n\nHope to meet folks who are on their own journeys to a peaceful and free life!", comment: "First post example given to the user during onboarding, as a suggestion as to what they could post first") + +struct OnboardingSuggestionsView: View { + + @StateObject var model: SuggestedUsersViewModel + @State var current_page: Int = 0 + let first_post_examples: [String] = [first_post_example_1, first_post_example_2, first_post_example_3, first_post_example_4] + let initial_text_suffix: String = "\n\n#introductions" + + @Environment(\.presentationMode) private var presentationMode + + func next_page() { + withAnimation { + current_page += 1 + } + } + + var body: some View { + NavigationView { + TabView(selection: $current_page) { + SuggestedUsersPageView(model: model, next_page: self.next_page) + .navigationTitle(NSLocalizedString("Who to Follow", comment: "Title for a screen displaying suggestions of who to follow")) + .navigationBarTitleDisplayMode(.inline) + .navigationBarItems(leading: Button(action: { + self.next_page() + }, label: { + Text(NSLocalizedString("Skip", comment: "Button to dismiss the suggested users screen")) + .font(.subheadline.weight(.semibold)) + })) + .tag(0) + + PostView( + action: .posting(.user(model.damus_state.pubkey)), + damus_state: model.damus_state, + prompt_view: { + AnyView( + HStack { + Image(systemName: "sparkles") + Text(NSLocalizedString("Add your first post", comment: "Prompt given to the user during onboarding, suggesting them to write their first post")) + } + .foregroundColor(.secondary) + .font(.callout) + .padding(.top, 10) + ) + }, + placeholder_messages: self.first_post_examples, + initial_text_suffix: self.initial_text_suffix + ) + .tag(1) + } + .tabViewStyle(.page(indexDisplayMode: .never)) + } + } +} + +fileprivate struct SuggestedUsersPageView: View { + var model: SuggestedUsersViewModel + var next_page: (() -> Void) + + var body: some View { + VStack { + List { + ForEach(model.groups) { group in + Section { + ForEach(group.users, id: \.self) { pk in + if let user = model.suggestedUser(pubkey: pk) { + SuggestedUserView(user: user, damus_state: model.damus_state) + } + } + } header: { + SuggestedUsersSectionHeader(group: group, model: model) + } + } + } + .listStyle(.plain) + + Spacer() + + Button(action: { + self.next_page() + }) { + Text(NSLocalizedString("Continue", comment: "Button to dismiss suggested users view and continue to the main app")) + .frame(minWidth: 300, maxWidth: .infinity, alignment: .center) + } + .buttonStyle(GradientButtonStyle()) + .padding([.leading, .trailing], 24) + .padding(.bottom, 16) + } + } +} + +struct SuggestedUsersSectionHeader: View { + let group: SuggestedUserGroup + let model: SuggestedUsersViewModel + var body: some View { + HStack { + Text(group.title.uppercased()) + Spacer() + Button(NSLocalizedString("Follow All", comment: "Button to follow all users in this section")) { + model.follow(pubkeys: group.users) + } + .font(.subheadline.weight(.semibold)) + } + } +} + +struct SuggestedUsersView_Previews: PreviewProvider { + static var previews: some View { + OnboardingSuggestionsView(model: SuggestedUsersViewModel(damus_state: test_damus_state)) + } +} diff --git a/damus/Views/Onboarding/SuggestedUsersView.swift b/damus/Views/Onboarding/SuggestedUsersView.swift deleted file mode 100644 index 4ce268ccf5..0000000000 --- a/damus/Views/Onboarding/SuggestedUsersView.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// SuggestedUsersView.swift -// damus -// -// Created by klabo on 7/17/23. -// - -import SwiftUI - -struct SuggestedUsersView: View { - - @StateObject var model: SuggestedUsersViewModel - - @Environment(\.presentationMode) private var presentationMode - - var body: some View { - NavigationView { - VStack { - List { - ForEach(model.groups) { group in - Section { - ForEach(group.users, id: \.self) { pk in - if let user = model.suggestedUser(pubkey: pk) { - SuggestedUserView(user: user, damus_state: model.damus_state) - } - } - } header: { - SuggestedUsersSectionHeader(group: group, model: model) - } - } - } - .listStyle(.plain) - - Spacer() - - Button(action: { - presentationMode.wrappedValue.dismiss() - }) { - Text(NSLocalizedString("Continue", comment: "Button to dismiss suggested users view and continue to the main app")) - .frame(minWidth: 300, maxWidth: .infinity, alignment: .center) - } - .buttonStyle(GradientButtonStyle()) - .padding([.leading, .trailing], 24) - .padding(.bottom, 16) - } - .navigationTitle(NSLocalizedString("Who to Follow", comment: "Title for a screen displaying suggestions of who to follow")) - .navigationBarTitleDisplayMode(.inline) - .navigationBarItems(trailing: Button(action: { - presentationMode.wrappedValue.dismiss() - }, label: { - Text(NSLocalizedString("Skip", comment: "Button to dismiss the suggested users screen")) - .font(.subheadline.weight(.semibold)) - })) - } - } -} - -struct SuggestedUsersSectionHeader: View { - let group: SuggestedUserGroup - let model: SuggestedUsersViewModel - var body: some View { - HStack { - Text(group.title.uppercased()) - Spacer() - Button(NSLocalizedString("Follow All", comment: "Button to follow all users in this section")) { - model.follow(pubkeys: group.users) - } - .font(.subheadline.weight(.semibold)) - } - } -} - -struct SuggestedUsersView_Previews: PreviewProvider { - static var previews: some View { - SuggestedUsersView(model: SuggestedUsersViewModel(damus_state: test_damus_state)) - } -} diff --git a/damus/Views/PostView.swift b/damus/Views/PostView.swift index ffcc300307..451007e9a4 100644 --- a/damus/Views/PostView.swift +++ b/damus/Views/PostView.swift @@ -62,9 +62,28 @@ struct PostView: View { @StateObject var image_upload: ImageUploadModel = ImageUploadModel() @StateObject var tagModel: TagModel = TagModel() + + @State private var current_placeholder_index = 0 let action: PostAction let damus_state: DamusState + let prompt_view: (() -> AnyView)? + let placeholder_messages: [String] + let initial_text_suffix: String? + + init( + action: PostAction, + damus_state: DamusState, + prompt_view: (() -> AnyView)? = nil, + placeholder_messages: [String]? = nil, + initial_text_suffix: String? = nil + ) { + self.action = action + self.damus_state = damus_state + self.prompt_view = prompt_view + self.placeholder_messages = placeholder_messages ?? [POST_PLACEHOLDER] + self.initial_text_suffix = initial_text_suffix + } @Environment(\.presentationMode) var presentationMode @@ -151,12 +170,10 @@ struct PostView: View { } } .disabled(posting_disabled) - .font(.system(size: 14, weight: .bold)) - .frame(width: 80, height: 30) - .foregroundColor(.white) - .background(LINEAR_GRADIENT) .opacity(posting_disabled ? 0.5 : 1.0) - .clipShape(Capsule()) + .bold() + .buttonStyle(GradientButtonStyle(padding: 10)) + } func isEmpty() -> Bool { @@ -214,12 +231,19 @@ struct PostView: View { var TextEntry: some View { ZStack(alignment: .topLeading) { - TextViewWrapper(attributedText: $post, textHeight: $textHeight, cursorIndex: newCursorIndex, getFocusWordForMention: { word, range in - focusWordAttributes = (word, range) - self.newCursorIndex = nil - }, updateCursorPosition: { newCursorIndex in - self.newCursorIndex = newCursorIndex - }) + TextViewWrapper( + attributedText: $post, + textHeight: $textHeight, + initialTextSuffix: initial_text_suffix, + cursorIndex: newCursorIndex, + getFocusWordForMention: { word, range in + focusWordAttributes = (word, range) + self.newCursorIndex = nil + }, + updateCursorPosition: { newCursorIndex in + self.newCursorIndex = newCursorIndex + } + ) .environmentObject(tagModel) .focused($focus) .textInputAutocapitalization(.sentences) @@ -230,22 +254,33 @@ struct PostView: View { .frame(height: get_valid_text_height()) if post.string.isEmpty { - Text(POST_PLACEHOLDER) + Text(self.placeholder_messages[self.current_placeholder_index]) .padding(.top, 8) .padding(.leading, 4) .foregroundColor(Color(uiColor: .placeholderText)) .allowsHitTesting(false) } } + .onAppear { + // Schedule a timer to switch messages every 3 seconds + Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { timer in + withAnimation { + self.current_placeholder_index = (self.current_placeholder_index + 1) % self.placeholder_messages.count + } + } + } } var TopBar: some View { VStack { HStack(spacing: 5.0) { - Button(NSLocalizedString("Cancel", comment: "Button to cancel out of posting a note.")) { + Button(action: { self.cancel() - } - .foregroundColor(.primary) + }, label: { + Text(NSLocalizedString("Cancel", comment: "Button to cancel out of posting a note.")) + .padding(10) + }) + .buttonStyle(NeutralButtonStyle()) if let error { Text(error) @@ -261,9 +296,14 @@ struct PostView: View { ProgressView(value: progress, total: 1.0) .progressViewStyle(.linear) } + + Divider() + .foregroundColor(DamusColors.neutral3) + .padding(.top, 5) } .frame(height: 30) .padding() + .padding(.top, 15) } func handle_upload(media: MediaUpload) { @@ -312,7 +352,12 @@ struct PostView: View { HStack(alignment: .top) { ProfilePicView(pubkey: damus_state.pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) - TextEntry + VStack(alignment: .leading) { + if let prompt_view { + prompt_view() + } + TextEntry + } } .id("post") @@ -360,6 +405,7 @@ struct PostView: View { } Editor(deviceSize: deviceSize) + .padding(.top, 5) } } .frame(maxHeight: searching == nil ? deviceSize.size.height : 70) diff --git a/damus/Views/TextViewWrapper.swift b/damus/Views/TextViewWrapper.swift index 9ebd140dab..35434d898c 100644 --- a/damus/Views/TextViewWrapper.swift +++ b/damus/Views/TextViewWrapper.swift @@ -11,6 +11,7 @@ struct TextViewWrapper: UIViewRepresentable { @Binding var attributedText: NSMutableAttributedString @EnvironmentObject var tagModel: TagModel @Binding var textHeight: CGFloat? + let initialTextSuffix: String? let cursorIndex: Int? var getFocusWordForMention: ((String?, NSRange?) -> Void)? = nil @@ -74,25 +75,41 @@ struct TextViewWrapper: UIViewRepresentable { } func makeCoordinator() -> Coordinator { - Coordinator(attributedText: $attributedText, getFocusWordForMention: getFocusWordForMention, updateCursorPosition: updateCursorPosition) + Coordinator(attributedText: $attributedText, getFocusWordForMention: getFocusWordForMention, updateCursorPosition: updateCursorPosition, initialTextSuffix: initialTextSuffix) } class Coordinator: NSObject, UITextViewDelegate { @Binding var attributedText: NSMutableAttributedString var getFocusWordForMention: ((String?, NSRange?) -> Void)? = nil let updateCursorPosition: ((Int) -> Void) + let initialTextSuffix: String? + var initialTextSuffixWasAdded: Bool = false init(attributedText: Binding, getFocusWordForMention: ((String?, NSRange?) -> Void)?, - updateCursorPosition: @escaping ((Int) -> Void) + updateCursorPosition: @escaping ((Int) -> Void), + initialTextSuffix: String? ) { _attributedText = attributedText self.getFocusWordForMention = getFocusWordForMention self.updateCursorPosition = updateCursorPosition + self.initialTextSuffix = initialTextSuffix } func textViewDidChange(_ textView: UITextView) { - attributedText = NSMutableAttributedString(attributedString: textView.attributedText) + if let initialTextSuffix, !self.initialTextSuffixWasAdded { + self.initialTextSuffixWasAdded = true + var mutable = NSMutableAttributedString(attributedString: textView.attributedText) + let originalRange = textView.selectedRange + addUnattributedText(initialTextSuffix, to: &mutable, inRange: originalRange) + attributedText = mutable + DispatchQueue.main.async { + self.updateCursorPosition(originalRange.location) + } + } + else { + attributedText = NSMutableAttributedString(attributedString: textView.attributedText) + } processFocusedWordForMention(textView: textView) } diff --git a/damusTests/PostViewTests.swift b/damusTests/PostViewTests.swift index 9ca04d203b..963cb98e07 100644 --- a/damusTests/PostViewTests.swift +++ b/damusTests/PostViewTests.swift @@ -35,6 +35,7 @@ final class PostViewTests: XCTestCase { let textEditorView = TextViewWrapper( attributedText: .constant(NSMutableAttributedString(string: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")), textHeight: textHeightBinding, + initialTextSuffix: nil, cursorIndex: 9, updateCursorPosition: { _ in return } ).environmentObject(tagModel) @@ -157,7 +158,7 @@ func checkMentionLinkEditorHandling( if let expectedNewCursorIndex { XCTAssertEqual(newCursorIndex, expectedNewCursorIndex) } - }) + }, initialTextSuffix: nil) let textView = UITextView() textView.attributedText = content From 7ae66b84907c52685d18c6cbfe49c23f658b2d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Fri, 20 Oct 2023 18:16:13 +0000 Subject: [PATCH 60/76] ui: Add suggested hashtags to universe view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds a suggested hashtag section to the universe view tab. The method for suggesting hashtags is currently simple: 1. It contains a list of many possible hashtags that we could recommend 2. It calculates how many users have been talking about it in the events fetched by the Universe tab 3. It selects the Top-N most mentioned suggested hashtags in the Universe tab This has the following properties: 1. It has some spam resistance as it ranks by unique users mentioning the tag (instead of events) 2. It is a simple way to curate good hashtags 3. It shows the ones with the most amount of people talking about it among the notes fecthed in the Universe view Testing ------- PASS Device: iPhone 14 Pro simulator iOS: 17.0 Damus: This commit Coverage: 1. Suggested hashtags are displayed 2. Layout looks similar to Figma 3. User count goes up (does not stay at zero) 4. Clicking on a suggested hashtag takes you to that hashtag view 5. Only the top 5 hashtags are displayed Notes: The counts seem low, probably because there are not enough notes loaded in Universe View Changelog-Added: Add suggested hashtags to universe view Closes: https://github.com/damus-io/damus/issues/1569 Signed-off-by: Daniel D’Aquino Signed-off-by: William Casarin --- damus.xcodeproj/project.pbxproj | 4 + damus/Models/SearchHomeModel.swift | 2 +- damus/Views/SearchHomeView.swift | 13 +++ damus/Views/SuggestedHashtagsView.swift | 135 ++++++++++++++++++++++++ 4 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 damus/Views/SuggestedHashtagsView.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index e4cce35523..936fd40b36 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -435,6 +435,7 @@ D723C38E2AB8D83400065664 /* ContentFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D723C38D2AB8D83400065664 /* ContentFilters.swift */; }; D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; }; D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; }; + D783A63F2AD4E53D00658DDA /* SuggestedHashtagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */; }; D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */; }; D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC02AC4750B0080BA88 /* MentionView.swift */; }; D7870BC32AC47EBC0080BA88 /* EventLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */; }; @@ -1132,6 +1133,7 @@ D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProfiles.swift; sourceTree = ""; }; D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = ""; }; D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = ""; }; + D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedHashtagsView.swift; sourceTree = ""; }; D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContentViewTests.swift; sourceTree = ""; }; D7870BC02AC4750B0080BA88 /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = ""; }; D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventLoaderView.swift; sourceTree = ""; }; @@ -1719,6 +1721,7 @@ 50DA11252A16A23F00236234 /* Launch.storyboard */, 5C513FCB2984ACA60072348F /* QRCodeView.swift */, 643EA5C7296B764E005081BB /* RelayFilterView.swift */, + D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */, ); path = Views; sourceTree = ""; @@ -2818,6 +2821,7 @@ 4CC14FF52A740BB7007AEB17 /* NoteId.swift in Sources */, 4C19AE512A5CEF7C00C90DB7 /* NostrScript.swift in Sources */, 4C32B95E2A9AD44700DC3548 /* FlatBufferObject.swift in Sources */, + D783A63F2AD4E53D00658DDA /* SuggestedHashtagsView.swift in Sources */, 4C3EA64F28FF59F200C48A62 /* tal.c in Sources */, 4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */, 5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */, diff --git a/damus/Models/SearchHomeModel.swift b/damus/Models/SearchHomeModel.swift index b655b3e397..1fd3d3729a 100644 --- a/damus/Models/SearchHomeModel.swift +++ b/damus/Models/SearchHomeModel.swift @@ -17,7 +17,7 @@ class SearchHomeModel: ObservableObject { let damus_state: DamusState let base_subid = UUID().description let profiles_subid = UUID().description - let limit: UInt32 = 250 + let limit: UInt32 = 500 //let multiple_events_per_pubkey: Bool = false init(damus_state: DamusState) { diff --git a/damus/Views/SearchHomeView.swift b/damus/Views/SearchHomeView.swift index ac0649c7f9..9308539b55 100644 --- a/damus/Views/SearchHomeView.swift +++ b/damus/Views/SearchHomeView.swift @@ -74,6 +74,19 @@ struct SearchHomeView: View { } return preferredLanguages.contains(note_lang) + }, + content: { + AnyView(VStack { + SuggestedHashtagsView(damus_state: damus_state, max_items: 5, events: model.events) + HStack { + Image(systemName: "bubble.fill") + Text(NSLocalizedString("All recent notes", comment: "A label indicating that the notes being displayed below it are all recent notes")) + Spacer() + } + .foregroundColor(.secondary) + .padding(.top, 20) + .padding(.horizontal) + }) } ) .refreshable { diff --git a/damus/Views/SuggestedHashtagsView.swift b/damus/Views/SuggestedHashtagsView.swift new file mode 100644 index 0000000000..1cddfa5a9e --- /dev/null +++ b/damus/Views/SuggestedHashtagsView.swift @@ -0,0 +1,135 @@ +// +// SuggestedHashtagsView.swift +// damus +// +// Created by Daniel D’Aquino on 2023-10-09. +// + +import SwiftUI + +// Currently we have a hardcoded list of possible hashtags that might be nice to suggest, +// and we suggest the top-N ones most active in the past day. +// This might be simple and effective until we find a more sophisticated way to let the user discover new hashtags +let DEFAULT_SUGGESTED_HASHTAGS: [String] = [ + "grownostr", "damus", "zapathon", "introductions", "plebchain", "bitcoin", "food", + "coffeechain", "nostr", "asknostr", "bounty", "freedom", "freedomtech", "foodstr", + "memestr", "memes", "music", "musicstr", "art", "artstr" +] + +struct SuggestedHashtagsView: View { + struct HashtagWithUserCount: Hashable { + var hashtag: String + var count: Int + } + + let damus_state: DamusState + @StateObject var events: EventHolder + var item_limit: Int? + let suggested_hashtags: [String] + var hashtags_with_count_to_display: [HashtagWithUserCount] { + get { + let all_items = self.suggested_hashtags + .map({ hashtag in + return HashtagWithUserCount( + hashtag: hashtag, + count: self.users_talking_about(hashtag: Hashtag(hashtag: hashtag)) + ) + }) + .sorted(by: { a, b in + a.count > b.count + }) + guard let item_limit else { + return all_items + } + return Array(all_items.prefix(item_limit)) + } + } + + init(damus_state: DamusState, suggested_hashtags: [String]? = nil, max_items item_limit: Int? = nil, events: EventHolder) { + self.damus_state = damus_state + self.suggested_hashtags = suggested_hashtags ?? DEFAULT_SUGGESTED_HASHTAGS + self.item_limit = item_limit + _events = StateObject.init(wrappedValue: events) + } + + var body: some View { + VStack { + HStack { + Image(systemName: "sparkles") + Text(NSLocalizedString("Suggested hashtags", comment: "A label indicating that the items below it are suggested hashtags")) + Spacer() + } + .foregroundColor(.secondary) + .padding(.bottom, 10) + + ForEach(hashtags_with_count_to_display, + id: \.self) { hashtag_with_count in + SuggestedHashtagView(damus_state: damus_state, hashtag: hashtag_with_count.hashtag, count: hashtag_with_count.count) + } + } + .padding() + } + + private struct SuggestedHashtagView: View { // Purposefully private to SuggestedHashtagsView because it assumes the same 24h window + let damus_state: DamusState + let hashtag: String + let count: Int + + init(damus_state: DamusState, hashtag: String, count: Int) { + self.damus_state = damus_state + self.hashtag = hashtag + self.count = count + } + + var body: some View { + HStack { + SingleCharacterAvatar(character: "#") + + VStack(alignment: .leading, spacing: 10) { + Text("#\(hashtag)") + .bold() + + Text(self.count != 1 ? String( + format: NSLocalizedString("%d users talking about it", comment: "A label indicating how many users have been talking about a hashtag"), + self.count + ) : NSLocalizedString("1 user talking about it", comment: "A label indicating 1 user has been talking about a hashtag")) + .foregroundStyle(.secondary) + } + + Spacer() + } + .onTapGesture { + let search_model = SearchModel(state: damus_state, search: NostrFilter.init(hashtag: [hashtag])) + damus_state.nav.push(route: Route.Search(search: search_model)) + } + } + } + + func users_talking_about(hashtag: Hashtag) -> Int { + return self.events.all_events + .filter({ $0.referenced_hashtags.contains(hashtag)}) + .reduce(Set([]), { authors, note in + return authors.union([note.pubkey]) + }) + .count + } +} + +struct SuggestedHashtagsView_Previews: PreviewProvider { + static var previews: some View { + let time_window: TimeInterval = 24 * 60 * 60 // 1 day + let search_model = SearchModel( + state: test_damus_state, + search: NostrFilter.init( + since: UInt32(Date.now.timeIntervalSince1970 - time_window), + hashtag: ["nostr", "bitcoin", "zapathon"] + ) + ) + + SuggestedHashtagsView( + damus_state: test_damus_state, + events: search_model.events + ) + } +} + From 45904e1bf2871f15f16d229117f12f3c793f4d09 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Sat, 21 Oct 2023 19:25:21 +0200 Subject: [PATCH 61/76] nav: compare searches for navigation decisions In 7c98489, routes are compared to the stack top before push. Problem is, search comparison is not looking at the NostrFilter. Instead, hash value involves two UUID-based fields (sub_id, profiles_subid), so equality will always fail and result in a "duplicated push". As I do not know the context of such fields to deliberately drop them, this patch is sent as a draft. The basic idea is using the filter for comparison, so I added a Hashable extension to NostrFilter where the subject of the comparison may be fine-tuned. Adding `hashtag` resolves #1367 but it's only a starting point. Signed-off-by: Davide De Rosa Reviewed-by: William Casarin Signed-off-by: William Casarin --- damus.xcodeproj/project.pbxproj | 4 ++++ damus/Models/NostrFilter+Hashable.swift | 19 +++++++++++++++++++ damus/Util/Router.swift | 3 +-- 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 damus/Models/NostrFilter+Hashable.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 936fd40b36..af675c8983 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 0E8A4BB72AE4359200065E81 /* NostrFilter+Hashable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8A4BB62AE4359200065E81 /* NostrFilter+Hashable.swift */; }; 3165648B295B70D500C64604 /* LinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3165648A295B70D500C64604 /* LinkView.swift */; }; 3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */; }; 3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAEC294FCCFC00EE4006 /* Constants.swift */; }; @@ -481,6 +482,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 0E8A4BB62AE4359200065E81 /* NostrFilter+Hashable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NostrFilter+Hashable.swift"; sourceTree = ""; }; 3165648A295B70D500C64604 /* LinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkView.swift; sourceTree = ""; }; 3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyTimelineView.swift; sourceTree = ""; }; 3169CAEC294FCCFC00EE4006 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Constants.swift; path = damus/Util/Constants.swift; sourceTree = SOURCE_ROOT; }; @@ -1295,6 +1297,7 @@ 4C363A9928283854006E126D /* Reply.swift */, 4C363A9B282838B9006E126D /* EventRef.swift */, 4C363AA328296DEE006E126D /* SearchModel.swift */, + 0E8A4BB62AE4359200065E81 /* NostrFilter+Hashable.swift */, 4C3AC79A28306D7B00E1F516 /* Contacts.swift */, 4C285C85283892E7008A31F1 /* CreateAccountModel.swift */, 4C63334F283D40E500B1C9C3 /* HomeModel.swift */, @@ -2574,6 +2577,7 @@ 4C4793062A993E5300489948 /* json_parser.c in Sources */, 4C4793052A993E3200489948 /* builder.c in Sources */, 4C4793042A993DC000489948 /* midl.c in Sources */, + 0E8A4BB72AE4359200065E81 /* NostrFilter+Hashable.swift in Sources */, 4C4793012A993CDA00489948 /* mdb.c in Sources */, 4CE9FBBA2A6B3C63007E485C /* nostrdb.c in Sources */, ADFE73552AD4793100EC7326 /* QRScanNSECView.swift in Sources */, diff --git a/damus/Models/NostrFilter+Hashable.swift b/damus/Models/NostrFilter+Hashable.swift new file mode 100644 index 0000000000..6c6b8dbc6f --- /dev/null +++ b/damus/Models/NostrFilter+Hashable.swift @@ -0,0 +1,19 @@ +// +// NostrFilter+Hashable.swift +// damus +// +// Created by Davide De Rosa on 10/21/23. +// + +import Foundation + +// FIXME: fine-tune here what's involved in comparing search filters +extension NostrFilter: Hashable { + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.hashtag == rhs.hashtag + } + + func hash(into hasher: inout Hasher) { + hasher.combine(hashtag) + } +} diff --git a/damus/Util/Router.swift b/damus/Util/Router.swift index 3017d11c7a..ac6731e700 100644 --- a/damus/Util/Router.swift +++ b/damus/Util/Router.swift @@ -188,8 +188,7 @@ enum Route: Hashable { hasher.combine(reactions.target) case .Search(let search): hasher.combine("search") - hasher.combine(search.sub_id) - hasher.combine(search.profiles_subid) + hasher.combine(search.search) case .EULA: hasher.combine("eula") case .Login: From 17331301da473ad21c623bc55b7374afd8a320dc Mon Sep 17 00:00:00 2001 From: William Casarin Date: Sun, 22 Oct 2023 09:35:00 +0800 Subject: [PATCH 62/76] Clarified camera and mic usage strings Fixes: https://github.com/damus-io/damus/issues/1628 --- damus/Info.plist | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/damus/Info.plist b/damus/Info.plist index b4e50bf497..00aada81dd 100644 --- a/damus/Info.plist +++ b/damus/Info.plist @@ -67,10 +67,10 @@ NSCameraUsageDescription - Damus needs access to your camera if you want to upload photos from it + Damus needs access to your camera for creating photo posts NSAppleMusicUsageDescription Damus needs access to your media library for playback statuses NSMicrophoneUsageDescription - Damus needs access to your microphone if you want to upload recorded videos from it + Damus needs access to your microphone for creating video recording posts From 4ed79ff3c3869d3db70c9cfd81ed78defac02156 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Sun, 22 Oct 2023 10:38:11 +0800 Subject: [PATCH 63/76] ui: reduce size of event menu hitbox Changelog-Fixed: Reduce size of event menu hitbox --- damus/Views/Events/EventMenu.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/damus/Views/Events/EventMenu.swift b/damus/Views/Events/EventMenu.swift index 0c2aead794..efadd05675 100644 --- a/damus/Views/Events/EventMenu.swift +++ b/damus/Views/Events/EventMenu.swift @@ -34,10 +34,10 @@ struct EventMenuContext: View { Menu { MenuItems(event: event, keypair: keypair, target_pubkey: target_pubkey, bookmarks: bookmarks, muted_threads: muted_threads, settings: settings) } label: { - Color.clear + Color.pink } // Hitbox frame size - .frame(width: 100, height: 70) + .frame(width: 50, height: 35) ) } .padding([.bottom], 4) From e70f270c5c8d05cd192212769b514ca5581bd012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Sat, 21 Oct 2023 04:44:36 +0000 Subject: [PATCH 64/76] zaps: Improve discoverability of profile zaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit via zappability badges and profile action sheets This commit improves discoverability of zaps with the following changes: 1. New zap icon appears on profile pictures of events where the author of such event has zaps setup 2. Clicking on a profile picture from an event shows an action sheet that makes it easier to see a preview of their profile, and a zap button Testing ------- Devices: - iPhone 14 Pro simulator - iPad 10 simulator iOS: - 17.0.1 - 16.4 Damus: This commit Coverage: 1. Checked that zap icon appears on profile pictures on events in different feeds and threads 2. Checked that this zap icon only appears for profiles that have zaps enabled 3. Checked that profile action sheet looks good on both light mode and dark mode 4. Checked that long descriptions are truncated and the "see more" "see less" buttons work 5. Checked that clicking "see more" or "see less" adapts the size of the action sheet (on iPhone) 6. Checked that action sheet looks good whether or not the user has a website link setup 7. Checked that long presses on the zap button in the action sheet bring the same options as the normal profile view 8. Checked all the buttons in the action sheet take the user to the expected place 9. Checked that the original profile view looks good (on both light and dark mode) Notes: - Action sheet cannot be resized on iPad. - Could not test on Mac Catalyst because there seems to be a crash on the creation of a new account Reference ticket: https://github.com/damus-io/damus/issues/1596 Changelog-Added: Improve discoverability of profile zaps with zappability badges and profile action sheets Signed-off-by: Daniel D’Aquino Signed-off-by: William Casarin --- damus.xcodeproj/project.pbxproj | 9 ++ damus/Components/NeutralButtonStyle.swift | 14 ++ damus/Components/SelectableText.swift | 11 ++ damus/Components/WebsiteLink.swift | 30 +++- damus/ContentView.swift | 4 + damus/Views/Events/EventProfile.swift | 4 +- damus/Views/Profile/AboutView.swift | 12 +- damus/Views/Profile/MaybeAnonPfpView.swift | 6 +- damus/Views/Profile/ProfileNameView.swift | 80 +---------- damus/Views/Profile/ProfilePicView.swift | 53 ++++--- damus/Views/Profile/ProfileView.swift | 38 +---- damus/Views/ProfileActionSheetView.swift | 154 +++++++++++++++++++++ damus/Views/PubkeyView.swift | 83 +++++++++++ damus/Views/Zaps/ZapButtonView.swift | 92 ++++++++++++ 14 files changed, 453 insertions(+), 137 deletions(-) create mode 100644 damus/Views/ProfileActionSheetView.swift create mode 100644 damus/Views/Zaps/ZapButtonView.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index af675c8983..5c988c4ea6 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -430,6 +430,7 @@ BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; }; D2277EEA2A089BD5006C3807 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; }; D71DC1EC2A9129C3006E207C /* PostViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71DC1EB2A9129C3006E207C /* PostViewTests.swift */; }; + D723C38E2AB8D83400065664 /* ContentFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D723C38D2AB8D83400065664 /* ContentFilters.swift */; }; D72A2D022AD9C136002AFF62 /* EventViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */; }; D72A2D052AD9C1B5002AFF62 /* MockDamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D042AD9C1B5002AFF62 /* MockDamusState.swift */; }; D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */; }; @@ -437,6 +438,8 @@ D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; }; D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; }; D783A63F2AD4E53D00658DDA /* SuggestedHashtagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */; }; + D76874F32AE3632B00FB0F68 /* ZapButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76874F22AE3632B00FB0F68 /* ZapButtonView.swift */; }; + D77BFA0B2AE3051200621634 /* ProfileActionSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */; }; D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */; }; D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC02AC4750B0080BA88 /* MentionView.swift */; }; D7870BC32AC47EBC0080BA88 /* EventLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */; }; @@ -1136,6 +1139,8 @@ D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = ""; }; D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = ""; }; D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedHashtagsView.swift; sourceTree = ""; }; + D76874F22AE3632B00FB0F68 /* ZapButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButtonView.swift; sourceTree = ""; }; + D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileActionSheetView.swift; sourceTree = ""; }; D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContentViewTests.swift; sourceTree = ""; }; D7870BC02AC4750B0080BA88 /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = ""; }; D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventLoaderView.swift; sourceTree = ""; }; @@ -1725,6 +1730,7 @@ 5C513FCB2984ACA60072348F /* QRCodeView.swift */, 643EA5C7296B764E005081BB /* RelayFilterView.swift */, D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */, + D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */, ); path = Views; sourceTree = ""; @@ -2236,6 +2242,7 @@ 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */, 4CA3FA0F29F593D000FDB3C3 /* ZapTypePicker.swift */, 4C73C5132A4437C10062CAC0 /* ZapUserView.swift */, + D76874F22AE3632B00FB0F68 /* ZapButtonView.swift */, ); path = Zaps; sourceTree = ""; @@ -2960,6 +2967,8 @@ E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */, 4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */, 4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */, + D76874F32AE3632B00FB0F68 /* ZapButtonView.swift in Sources */, + D77BFA0B2AE3051200621634 /* ProfileActionSheetView.swift in Sources */, 4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */, 3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */, 3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */, diff --git a/damus/Components/NeutralButtonStyle.swift b/damus/Components/NeutralButtonStyle.swift index 3d61b19e49..f7aa797f46 100644 --- a/damus/Components/NeutralButtonStyle.swift +++ b/damus/Components/NeutralButtonStyle.swift @@ -20,6 +20,20 @@ struct NeutralButtonStyle: ButtonStyle { } } +struct NeutralCircleButtonStyle: ButtonStyle { + func makeBody(configuration: Self.Configuration) -> some View { + return configuration.label + .padding(20) + .background(DamusColors.neutral1) + .cornerRadius(9999) + .overlay( + RoundedRectangle(cornerRadius: 9999) + .stroke(DamusColors.neutral3, lineWidth: 1) + ) + .scaleEffect(configuration.isPressed ? 0.95 : 1) + } +} + struct NeutralButtonStyle_Previews: PreviewProvider { static var previews: some View { diff --git a/damus/Components/SelectableText.swift b/damus/Components/SelectableText.swift index 51346f3977..975d5810ba 100644 --- a/damus/Components/SelectableText.swift +++ b/damus/Components/SelectableText.swift @@ -11,12 +11,19 @@ import SwiftUI struct SelectableText: View { let attributedString: AttributedString + let textAlignment: NSTextAlignment @State private var selectedTextHeight: CGFloat = .zero @State private var selectedTextWidth: CGFloat = .zero let size: EventViewKind + init(attributedString: AttributedString, textAlignment: NSTextAlignment? = nil, size: EventViewKind) { + self.attributedString = attributedString + self.textAlignment = textAlignment ?? NSTextAlignment.natural + self.size = size + } + var body: some View { GeometryReader { geo in TextViewRepresentable( @@ -24,6 +31,7 @@ struct SelectableText: View { textColor: UIColor.label, font: eventviewsize_to_uifont(size), fixedWidth: selectedTextWidth, + textAlignment: self.textAlignment, height: $selectedTextHeight ) .padding([.leading, .trailing], -1.0) @@ -48,6 +56,7 @@ struct SelectableText: View { let textColor: UIColor let font: UIFont let fixedWidth: CGFloat + let textAlignment: NSTextAlignment @Binding var height: CGFloat @@ -61,12 +70,14 @@ struct SelectableText: View { view.textContainerInset = .zero view.textContainerInset.left = 1.0 view.textContainerInset.right = 1.0 + view.textAlignment = textAlignment return view } func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext) { let mutableAttributedString = createNSAttributedString() uiView.attributedText = mutableAttributedString + uiView.textAlignment = self.textAlignment let newHeight = mutableAttributedString.height(containerWidth: fixedWidth) diff --git a/damus/Components/WebsiteLink.swift b/damus/Components/WebsiteLink.swift index 90063dd003..8bb23f534d 100644 --- a/damus/Components/WebsiteLink.swift +++ b/damus/Components/WebsiteLink.swift @@ -9,33 +9,57 @@ import SwiftUI struct WebsiteLink: View { let url: URL + let style: StyleVariant @Environment(\.openURL) var openURL + + init(url: URL, style: StyleVariant? = nil) { + self.url = url + self.style = style ?? .normal + } var body: some View { HStack { Image("link") - .foregroundColor(.gray) - .font(.footnote) + .resizable() + .frame(width: 16, height: 16) + .foregroundColor(self.style == .accent ? .white : .gray) + .padding(.vertical, 5) + .padding([.leading], 10) Button(action: { openURL(url) }, label: { Text(link_text) .font(.footnote) - .foregroundColor(.accentColor) + .foregroundColor(self.style == .accent ? .white : .accentColor) .truncationMode(.tail) .lineLimit(1) }) + .padding(.vertical, 5) + .padding([.trailing], 10) } + .background( + self.style == .accent ? + AnyView(RoundedRectangle(cornerRadius: 50).fill(PinkGradient)) + : AnyView(Color.clear) + ) } var link_text: String { url.host ?? url.absoluteString } + + enum StyleVariant { + case normal + case accent + } } struct WebsiteLink_Previews: PreviewProvider { static var previews: some View { WebsiteLink(url: URL(string: "https://jb55.com")!) + .previewDisplayName("Normal") + WebsiteLink(url: URL(string: "https://jb55.com")!, style: .accent) + .previewDisplayName("Accent") } } diff --git a/damus/ContentView.swift b/damus/ContentView.swift index edda8d50a8..2ab1cd687c 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -22,6 +22,7 @@ enum Sheets: Identifiable { case post(PostAction) case report(ReportTarget) case event(NostrEvent) + case profile_action(Pubkey) case zap(ZapSheet) case select_wallet(SelectWallet) case filter @@ -42,6 +43,7 @@ enum Sheets: Identifiable { case .user_status: return "user_status" case .post(let action): return "post-" + (action.ev?.id.hex() ?? "") case .event(let ev): return "event-" + ev.id.hex() + case .profile_action(let pubkey): return "profile-action-" + pubkey.npub case .zap(let sheet): return "zap-" + hex_encode(sheet.target.id) case .select_wallet: return "select-wallet" case .filter: return "filter" @@ -316,6 +318,8 @@ struct ContentView: View { .presentationDragIndicator(.visible) case .event: EventDetailView() + case .profile_action(let pubkey): + ProfileActionSheetView(damus_state: damus_state!, pubkey: pubkey) case .zap(let zapsheet): CustomizeZapView(state: damus_state!, target: zapsheet.target, lnurl: zapsheet.lnurl) case .select_wallet(let select): diff --git a/damus/Views/Events/EventProfile.swift b/damus/Views/Events/EventProfile.swift index 6d8071209d..60525b1333 100644 --- a/damus/Views/Events/EventProfile.swift +++ b/damus/Views/Events/EventProfile.swift @@ -37,9 +37,9 @@ struct EventProfile: View { var body: some View { HStack(alignment: .center, spacing: 10) { - ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles, disable_animation: disable_animation) + ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles, disable_animation: disable_animation, show_zappability: true) .onTapGesture { - damus_state.nav.push(route: .ProfileByKey(pubkey: pubkey)) + notify(.present_sheet(Sheets.profile_action(pubkey))) } VStack(alignment: .leading, spacing: 0) { diff --git a/damus/Views/Profile/AboutView.swift b/damus/Views/Profile/AboutView.swift index 948f86a220..905b8001d5 100644 --- a/damus/Views/Profile/AboutView.swift +++ b/damus/Views/Profile/AboutView.swift @@ -10,15 +10,23 @@ import SwiftUI struct AboutView: View { let state: DamusState let about: String - let max_about_length = 280 + let max_about_length: Int + let text_alignment: NSTextAlignment @State var show_full_about: Bool = false @State private var about_string: AttributedString? = nil + init(state: DamusState, about: String, max_about_length: Int? = nil, text_alignment: NSTextAlignment? = nil) { + self.state = state + self.about = about + self.max_about_length = max_about_length ?? 280 + self.text_alignment = text_alignment ?? .natural + } + var body: some View { Group { if let about_string { let truncated_about = show_full_about ? about_string : about_string.truncateOrNil(maxLength: max_about_length) - SelectableText(attributedString: truncated_about ?? about_string, size: .subheadline) + SelectableText(attributedString: truncated_about ?? about_string, textAlignment: self.text_alignment, size: .subheadline) if truncated_about != nil { if show_full_about { diff --git a/damus/Views/Profile/MaybeAnonPfpView.swift b/damus/Views/Profile/MaybeAnonPfpView.swift index 0b4256e4d5..62c1239349 100644 --- a/damus/Views/Profile/MaybeAnonPfpView.swift +++ b/damus/Views/Profile/MaybeAnonPfpView.swift @@ -21,16 +21,16 @@ struct MaybeAnonPfpView: View { } var body: some View { - Group { + ZStack { if is_anon { Image("question") .resizable() .font(.largeTitle) .frame(width: size, height: size) } else { - ProfilePicView(pubkey: pubkey, size: size, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation) + ProfilePicView(pubkey: pubkey, size: size, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation, show_zappability: true) .onTapGesture { - state.nav.push(route: Route.ProfileByKey(pubkey: pubkey)) + notify(.present_sheet(Sheets.profile_action(pubkey))) } } } diff --git a/damus/Views/Profile/ProfileNameView.swift b/damus/Views/Profile/ProfileNameView.swift index eb1821d76c..44c585b4a2 100644 --- a/damus/Views/Profile/ProfileNameView.swift +++ b/damus/Views/Profile/ProfileNameView.swift @@ -7,84 +7,6 @@ import SwiftUI -fileprivate struct KeyView: View { - let pubkey: Pubkey - - @Environment(\.colorScheme) var colorScheme - - @State private var isCopied = false - - func keyColor() -> Color { - colorScheme == .light ? DamusColors.black : DamusColors.white - } - - private func copyPubkey(_ pubkey: String) { - UIPasteboard.general.string = pubkey - UIImpactFeedbackGenerator(style: .medium).impactOccurred() - withAnimation { - isCopied = true - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - withAnimation { - isCopied = false - } - } - } - } - - func pubkey_context_menu(pubkey: Pubkey) -> some View { - return self.contextMenu { - Button { - UIPasteboard.general.string = pubkey.npub - } label: { - Label(NSLocalizedString("Copy Account ID", comment: "Context menu option for copying the ID of the account that created the note."), image: "copy2") - } - } - } - - var body: some View { - let bech32 = pubkey.npub - - HStack { - Text(verbatim: "\(abbrev_pubkey(bech32, amount: 16))") - .font(.footnote) - .foregroundColor(keyColor()) - .padding(5) - .padding([.leading, .trailing], 5) - .background(RoundedRectangle(cornerRadius: 11).foregroundColor(DamusColors.adaptableGrey)) - - if isCopied { - HStack { - Image("check-circle") - .resizable() - .frame(width: 20, height: 20) - Text(NSLocalizedString("Copied", comment: "Label indicating that a user's key was copied.")) - .font(.footnote) - .layoutPriority(1) - } - .foregroundColor(DamusColors.green) - } else { - HStack { - Button { - copyPubkey(bech32) - } label: { - Label { - Text("Public key", comment: "Label indicating that the text is a user's public account key.") - } icon: { - Image("copy2") - .resizable() - .contentShape(Rectangle()) - .foregroundColor(.accentColor) - .frame(width: 20, height: 20) - } - .labelStyle(IconOnlyLabelStyle()) - .symbolRenderingMode(.hierarchical) - } - } - } - } - } -} - struct ProfileNameView: View { let pubkey: Pubkey let damus: DamusState @@ -116,7 +38,7 @@ struct ProfileNameView: View { Spacer() - KeyView(pubkey: pubkey) + PubkeyView(pubkey: pubkey) .pubkey_context_menu(pubkey: pubkey) } } diff --git a/damus/Views/Profile/ProfilePicView.swift b/damus/Views/Profile/ProfilePicView.swift index 54afbbc532..779b7e843d 100644 --- a/damus/Views/Profile/ProfilePicView.swift +++ b/damus/Views/Profile/ProfilePicView.swift @@ -69,38 +69,59 @@ struct ProfilePicView: View { let highlight: Highlight let profiles: Profiles let disable_animation: Bool + let zappability_indicator: Bool @State var picture: String? - init(pubkey: Pubkey, size: CGFloat, highlight: Highlight, profiles: Profiles, disable_animation: Bool, picture: String? = nil) { + init(pubkey: Pubkey, size: CGFloat, highlight: Highlight, profiles: Profiles, disable_animation: Bool, picture: String? = nil, show_zappability: Bool? = nil) { self.pubkey = pubkey self.profiles = profiles self.size = size self.highlight = highlight self._picture = State(initialValue: picture) self.disable_animation = disable_animation + self.zappability_indicator = show_zappability ?? false + } + + func get_lnurl() -> String? { + return profiles.lookup_with_timestamp(pubkey).unsafeUnownedValue?.lnurl } var body: some View { - InnerProfilePicView(url: get_profile_url(picture: picture, pubkey: pubkey, profiles: profiles), fallbackUrl: URL(string: robohash(pubkey)), pubkey: pubkey, size: size, highlight: highlight, disable_animation: disable_animation) - .onReceive(handle_notify(.profile_updated)) { updated in - guard updated.pubkey == self.pubkey else { - return - } - - switch updated { - case .manual(_, let profile): - if let pic = profile.picture { - self.picture = pic + ZStack (alignment: Alignment(horizontal: .trailing, vertical: .bottom)) { + InnerProfilePicView(url: get_profile_url(picture: picture, pubkey: pubkey, profiles: profiles), fallbackUrl: URL(string: robohash(pubkey)), pubkey: pubkey, size: size, highlight: highlight, disable_animation: disable_animation) + .onReceive(handle_notify(.profile_updated)) { updated in + guard updated.pubkey == self.pubkey else { + return } - case .remote(pubkey: let pk): - let profile_txn = profiles.lookup(id: pk) - let profile = profile_txn.unsafeUnownedValue - if let pic = profile?.picture { - self.picture = pic + + switch updated { + case .manual(_, let profile): + if let pic = profile.picture { + self.picture = pic + } + case .remote(pubkey: let pk): + let profile_txn = profiles.lookup(id: pk) + let profile = profile_txn.unsafeUnownedValue + if let pic = profile?.picture { + self.picture = pic + } } } + + if self.zappability_indicator, let lnurl = self.get_lnurl(), lnurl != "" { + Image("zap.fill") + .resizable() + .frame( + width: size * 0.24, + height: size * 0.24 + ) + .padding(size * 0.04) + .foregroundColor(.white) + .background(Color.orange) + .clipShape(Circle()) } + } } } diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift index 583a82e485..7e8966b477 100644 --- a/damus/Views/Profile/ProfileView.swift +++ b/damus/Views/Profile/ProfileView.swift @@ -221,39 +221,13 @@ struct ProfileView: View { .accentColor(DamusColors.white) } - func lnButton(lnurl: String, unownedProfile: Profile?, pubkey: Pubkey) -> some View { - let reactions = unownedProfile?.reactions ?? true - let button_img = reactions ? "zap.fill" : "zap" - let lud16 = unownedProfile?.lud16 - - return Button(action: { [lnurl] in - present_sheet(.zap(target: .profile(self.profile.pubkey), lnurl: lnurl)) - }) { - Image(button_img) - .foregroundColor(button_img == "zap.fill" ? .orange : Color.primary) + func lnButton(unownedProfile: Profile?, record: ProfileRecord?) -> some View { + return ZapButtonView(unownedProfileRecord: record, profileModel: self.profile) { reactions_enabled, lud16, lnurl in + Image(reactions_enabled ? "zap.fill" : "zap") + .foregroundColor(reactions_enabled ? .orange : Color.primary) .profile_button_style(scheme: colorScheme) - .contextMenu { [lud16, reactions, lnurl] in - if reactions == false { - Text("OnlyZaps Enabled", comment: "Non-tappable text in context menu that shows up when the zap button on profile is long pressed to indicate that the user has enabled OnlyZaps, meaning that they would like to be only zapped and not accept reactions to their notes.") - } - - if let lud16 { - Button { - UIPasteboard.general.string = lud16 - } label: { - Label(lud16, image: "copy2") - } - } else { - Button { - UIPasteboard.general.string = lnurl - } label: { - Label(NSLocalizedString("Copy LNURL", comment: "Context menu option for copying a user's Lightning URL."), image: "copy") - } - } - } - + .cornerRadius(24) } - .cornerRadius(24) } var dmButton: some View { @@ -283,7 +257,7 @@ struct ProfileView: View { let lnurl = record.lnurl, lnurl != "" { - lnButton(lnurl: lnurl, unownedProfile: profile, pubkey: pubkey) + lnButton(unownedProfile: profile, record: record) } dmButton diff --git a/damus/Views/ProfileActionSheetView.swift b/damus/Views/ProfileActionSheetView.swift new file mode 100644 index 0000000000..029e5f5c3e --- /dev/null +++ b/damus/Views/ProfileActionSheetView.swift @@ -0,0 +1,154 @@ +// +// ProfileActionSheetView.swift +// damus +// +// Created by Daniel D’Aquino on 2023-10-20. +// + +import SwiftUI + +struct ProfileActionSheetView: View { + let damus_state: DamusState + let pfp_size: CGFloat = 90.0 + + @StateObject var profile: ProfileModel + @StateObject var zap_button_model: ZapButtonModel = ZapButtonModel() + @State private var sheetHeight: CGFloat = .zero + + @Environment(\.dismiss) var dismiss + @Environment(\.colorScheme) var colorScheme + @Environment(\.presentationMode) var presentationMode + + init(damus_state: DamusState, pubkey: Pubkey) { + self.damus_state = damus_state + self._profile = StateObject(wrappedValue: ProfileModel(pubkey: pubkey, damus: damus_state)) + } + + func imageBorderColor() -> Color { + colorScheme == .light ? DamusColors.white : DamusColors.black + } + + func profile_data() -> ProfileRecord? { + let profile_txn = damus_state.profiles.lookup_with_timestamp(profile.pubkey) + return profile_txn.unsafeUnownedValue + } + + func get_profile() -> Profile? { + return self.profile_data()?.profile + } + + var dmButton: some View { + let dm_model = damus_state.dms.lookup_or_create(profile.pubkey) + return VStack(alignment: .center, spacing: 10) { + Button( + action: { + damus_state.nav.push(route: Route.DMChat(dms: dm_model)) + dismiss() + }, + label: { + Image("messages") + .profile_button_style(scheme: colorScheme) + } + ) + .buttonStyle(NeutralCircleButtonStyle()) + Text(NSLocalizedString("Message", comment: "Button label that allows the user to start a direct message conversation with the user shown on-screen")) + .foregroundStyle(.secondary) + .font(.caption) + } + } + + var zapButton: some View { + if let lnurl = self.profile_data()?.lnurl, lnurl != "" { + return AnyView( + VStack(alignment: .center, spacing: 10) { + ZapButtonView(damus_state: damus_state, pubkey: self.profile.pubkey, action: { dismiss() }) { reactions_enabled, lud16, lnurl in + Image(reactions_enabled ? "zap.fill" : "zap") + .foregroundColor(reactions_enabled ? .orange : Color.primary) + .profile_button_style(scheme: colorScheme) + } + .buttonStyle(NeutralCircleButtonStyle()) + + Text(NSLocalizedString("Zap", comment: "Button label that allows the user to zap (i.e. send a Bitcoin tip via the lightning network) the user shown on-screen")) + .foregroundStyle(.secondary) + .font(.caption) + } + ) + } + else { + return AnyView(EmptyView()) + } + } + + var profileName: some View { + let display_name = Profile.displayName(profile: self.get_profile(), pubkey: self.profile.pubkey).displayName + return HStack(alignment: .center, spacing: 10) { + Text(display_name) + .font(.title) + } + } + + var body: some View { + VStack(alignment: .center) { + ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) + if let url = self.profile_data()?.profile?.website_url { + WebsiteLink(url: url, style: .accent) + .padding(.top, -15) + } + + profileName + + PubkeyView(pubkey: profile.pubkey) + + if let about = self.profile_data()?.profile?.about { + AboutView(state: damus_state, about: about, max_about_length: 140, text_alignment: .center) + .padding(.top) + } + + HStack(spacing: 20) { + self.dmButton + self.zapButton + } + .padding() + + Button( + action: { + damus_state.nav.push(route: Route.ProfileByKey(pubkey: profile.pubkey)) + dismiss() + }, + label: { + HStack { + Spacer() + Text(NSLocalizedString("View full profile", comment: "A button label that allows the user to see the full profile of the profile they are previewing")) + Image(systemName: "arrow.up.right") + Spacer() + } + + } + ) + + .buttonStyle(NeutralCircleButtonStyle()) + } + .padding() + .padding(.top, 20) + .overlay { + GeometryReader { geometry in + Color.clear.preference(key: InnerHeightPreferenceKey.self, value: geometry.size.height) + } + } + .onPreferenceChange(InnerHeightPreferenceKey.self) { newHeight in + sheetHeight = newHeight + } + .presentationDetents([.height(sheetHeight)]) + } +} + +struct InnerHeightPreferenceKey: PreferenceKey { + static var defaultValue: CGFloat = .zero + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = nextValue() + } +} + +#Preview { + ProfileActionSheetView(damus_state: test_damus_state, pubkey: test_pubkey) +} diff --git a/damus/Views/PubkeyView.swift b/damus/Views/PubkeyView.swift index 2a4fcfaef4..6d31f4b059 100644 --- a/damus/Views/PubkeyView.swift +++ b/damus/Views/PubkeyView.swift @@ -7,6 +7,89 @@ import SwiftUI +struct PubkeyView: View { + let pubkey: Pubkey + + @Environment(\.colorScheme) var colorScheme + + @State private var isCopied = false + + func keyColor() -> Color { + colorScheme == .light ? DamusColors.black : DamusColors.white + } + + private func copyPubkey(_ pubkey: String) { + UIPasteboard.general.string = pubkey + UIImpactFeedbackGenerator(style: .medium).impactOccurred() + withAnimation { + isCopied = true + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + withAnimation { + isCopied = false + } + } + } + } + + func pubkey_context_menu(pubkey: Pubkey) -> some View { + return self.contextMenu { + Button { + UIPasteboard.general.string = pubkey.npub + } label: { + Label(NSLocalizedString("Copy Account ID", comment: "Context menu option for copying the ID of the account that created the note."), image: "copy2") + } + } + } + + var body: some View { + let bech32 = pubkey.npub + + HStack { + Text(verbatim: "\(abbrev_pubkey(bech32, amount: 16))") + .font(.footnote) + .foregroundColor(keyColor()) + .padding(5) + .padding([.leading], 5) + + HStack { + if isCopied { + Image("check-circle") + .resizable() + .foregroundColor(DamusColors.green) + .frame(width: 20, height: 20) + Text(NSLocalizedString("Copied", comment: "Label indicating that a user's key was copied.")) + .font(.footnote) + .layoutPriority(1) + .foregroundColor(DamusColors.green) + } else { + Button { + copyPubkey(bech32) + } label: { + Label { + Text("Public key", comment: "Label indicating that the text is a user's public account key.") + } icon: { + Image("copy2") + .resizable() + .contentShape(Rectangle()) + .foregroundColor(colorScheme == .light ? DamusColors.darkGrey : DamusColors.lightGrey) + .frame(width: 20, height: 20) + } + .labelStyle(IconOnlyLabelStyle()) + .symbolRenderingMode(.hierarchical) + + } + } + } + .padding([.trailing], 10) + } + .background(RoundedRectangle(cornerRadius: 11).foregroundColor(colorScheme == .light ? DamusColors.adaptableGrey : DamusColors.neutral1)) + } +} + +#Preview { + PubkeyView(pubkey: test_pubkey) +} + func abbrev_pubkey(_ pubkey: String, amount: Int = 8) -> String { return pubkey.prefix(amount) + ":" + pubkey.suffix(amount) } diff --git a/damus/Views/Zaps/ZapButtonView.swift b/damus/Views/Zaps/ZapButtonView.swift new file mode 100644 index 0000000000..07c91341af --- /dev/null +++ b/damus/Views/Zaps/ZapButtonView.swift @@ -0,0 +1,92 @@ +// +// ZapButtonView.swift +// damus +// +// Created by Daniel D’Aquino on 2023-10-20. +// + +import SwiftUI + +struct ZapButtonView: View { + typealias ContentViewFunction = (_ reactions_enabled: Bool, _ lud16: String?, _ lnurl: String?) -> Content + typealias ActionFunction = () -> Void + + let pubkey: Pubkey + @ViewBuilder let label: ContentViewFunction + let action: ActionFunction? + + let reactions_enabled: Bool + let lud16: String? + let lnurl: String? + + init(pubkey: Pubkey, reactions_enabled: Bool, lud16: String?, lnurl: String?, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) { + self.pubkey = pubkey + self.label = label + self.action = action + self.reactions_enabled = reactions_enabled + self.lud16 = lud16 + self.lnurl = lnurl + } + + init(damus_state: DamusState, pubkey: Pubkey, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) { + self.pubkey = pubkey + self.label = label + self.action = action + + let profile_txn = damus_state.profiles.lookup_with_timestamp(pubkey) + let record = profile_txn.unsafeUnownedValue + self.reactions_enabled = record?.profile?.reactions ?? true + self.lud16 = record?.profile?.lud06 + self.lnurl = record?.lnurl + } + + init(unownedProfileRecord: ProfileRecord?, profileModel: ProfileModel, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) { + self.pubkey = profileModel.pubkey + self.label = label + self.action = action + + self.reactions_enabled = unownedProfileRecord?.profile?.reactions ?? true + self.lud16 = unownedProfileRecord?.profile?.lud16 + self.lnurl = unownedProfileRecord?.lnurl + } + + var body: some View { + Button( + action: { + if let lnurl { + present_sheet(.zap(target: .profile(self.pubkey), lnurl: lnurl)) + } + action?() + }, + label: { + self.label(self.reactions_enabled, self.lud16, self.lnurl) + } + ) + .contextMenu { + if self.reactions_enabled == false { + Text("OnlyZaps Enabled", comment: "Non-tappable text in context menu that shows up when the zap button on profile is long pressed to indicate that the user has enabled OnlyZaps, meaning that they would like to be only zapped and not accept reactions to their notes.") + } + + if let lud16 { + Button { + UIPasteboard.general.string = lud16 + } label: { + Label(lud16, image: "copy2") + } + } else { + Button { + UIPasteboard.general.string = lnurl + } label: { + Label(NSLocalizedString("Copy LNURL", comment: "Context menu option for copying a user's Lightning URL."), image: "copy") + } + } + } + .disabled(lnurl == nil) + } +} + +#Preview { + ZapButtonView(pubkey: test_pubkey, reactions_enabled: true, lud16: make_test_profile().lud16, lnurl: "test@sendzaps.lol", label: { reactions_enabled, lud16, lnurl in + Image("zap.fill") + }) +} From 2b102671e5c4aaee7436148adf8ec6c307fd40f2 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Sun, 22 Oct 2023 10:41:36 +0800 Subject: [PATCH 65/76] smol fix --- damus/Views/Events/EventMenu.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/damus/Views/Events/EventMenu.swift b/damus/Views/Events/EventMenu.swift index efadd05675..183824f56e 100644 --- a/damus/Views/Events/EventMenu.swift +++ b/damus/Views/Events/EventMenu.swift @@ -34,7 +34,7 @@ struct EventMenuContext: View { Menu { MenuItems(event: event, keypair: keypair, target_pubkey: target_pubkey, bookmarks: bookmarks, muted_threads: muted_threads, settings: settings) } label: { - Color.pink + Color.clear } // Hitbox frame size .frame(width: 50, height: 35) From 29915159db115eebe8cd68d08febae805e6ad6ba Mon Sep 17 00:00:00 2001 From: William Casarin Date: Sun, 22 Oct 2023 14:27:56 +0800 Subject: [PATCH 66/76] v1.6 (24) changelog --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ damus.xcodeproj/project.pbxproj | 4 ++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03bb95a1a1..0cbfffd171 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,30 @@ +## [1.6-24] - 2023-10-22 - AppStore Rejection Cope + +### Added + +- Improve discoverability of profile zaps with zappability badges and profile action sheets (Daniel D’Aquino) +- Add suggested hashtags to universe view (Daniel D’Aquino) +- Suggest first post during onboarding (Daniel D’Aquino) +- Add expiry date for images in cache to be auto-deleted after a preset time to save space on storage (Daniel D’Aquino) +- Add QR scan nsec logins. (Jericho Hasselbush) + + +### Changed + +- Improved status view design (ericholguin) +- Improve clear cache functionality (Daniel D’Aquino) + + +### Fixed + +- Reduce size of event menu hitbox (William Casarin) +- Do not show DMs from muted users (Daniel D’Aquino) +- Add more spacing between display name and username, and prefix username with `@` character (Daniel D’Aquino) +- Broadcast quoted notes when posting a note with quotes (Daniel D’Aquino) + + +[1.6-24]: https://github.com/damus-io/damus/releases/tag/v1.6-24 + ## [1.6-23] - 2023-10-06 - Appstore Release ### Added diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 5c988c4ea6..ad4dbd4e78 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -3290,7 +3290,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 23; + CURRENT_PROJECT_VERSION = 24; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; DEVELOPMENT_TEAM = XK7H4JAB3D; ENABLE_PREVIEWS = YES; @@ -3339,7 +3339,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 23; + CURRENT_PROJECT_VERSION = 24; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; DEVELOPMENT_TEAM = XK7H4JAB3D; ENABLE_PREVIEWS = YES; From 502ceee6d456d3583824d19f550e18eba4cdff8e Mon Sep 17 00:00:00 2001 From: William Casarin Date: Mon, 23 Oct 2023 10:34:45 +0800 Subject: [PATCH 67/76] ndb: update bindings This adds new bindings from nostrdb related to tracking reaction stats. We aren't using this yet but it's a part of the latest nostrdb update. --- nostrdb/bindings/c/meta_builder.h | 27 ++++- nostrdb/bindings/c/meta_json_parser.h | 160 +++++++++++++++++++++++--- nostrdb/bindings/c/meta_reader.h | 5 + nostrdb/bindings/c/meta_verifier.h | 5 + nostrdb/bindings/swift/NdbMeta.swift | 71 ++++++++++++ 5 files changed, 250 insertions(+), 18 deletions(-) create mode 100644 nostrdb/bindings/swift/NdbMeta.swift diff --git a/nostrdb/bindings/c/meta_builder.h b/nostrdb/bindings/c/meta_builder.h index 1ae48f3413..ad850bd53f 100644 --- a/nostrdb/bindings/c/meta_builder.h +++ b/nostrdb/bindings/c/meta_builder.h @@ -20,19 +20,31 @@ static const flatbuffers_voffset_t __NdbEventMeta_required[] = { 0 }; typedef flatbuffers_ref_t NdbEventMeta_ref_t; static NdbEventMeta_ref_t NdbEventMeta_clone(flatbuffers_builder_t *B, NdbEventMeta_table_t t); -__flatbuffers_build_table(flatbuffers_, NdbEventMeta, 1) +__flatbuffers_build_table(flatbuffers_, NdbEventMeta, 6) -#define __NdbEventMeta_formal_args , int32_t v0 -#define __NdbEventMeta_call_args , v0 +#define __NdbEventMeta_formal_args ,\ + int32_t v0, int32_t v1, int32_t v2, int32_t v3, int32_t v4, int64_t v5 +#define __NdbEventMeta_call_args ,\ + v0, v1, v2, v3, v4, v5 static inline NdbEventMeta_ref_t NdbEventMeta_create(flatbuffers_builder_t *B __NdbEventMeta_formal_args); __flatbuffers_build_table_prolog(flatbuffers_, NdbEventMeta, NdbEventMeta_file_identifier, NdbEventMeta_type_identifier) __flatbuffers_build_scalar_field(0, flatbuffers_, NdbEventMeta_received_at, flatbuffers_int32, int32_t, 4, 4, INT32_C(0), NdbEventMeta) +__flatbuffers_build_scalar_field(1, flatbuffers_, NdbEventMeta_reactions, flatbuffers_int32, int32_t, 4, 4, INT32_C(0), NdbEventMeta) +__flatbuffers_build_scalar_field(2, flatbuffers_, NdbEventMeta_quotes, flatbuffers_int32, int32_t, 4, 4, INT32_C(0), NdbEventMeta) +__flatbuffers_build_scalar_field(3, flatbuffers_, NdbEventMeta_reposts, flatbuffers_int32, int32_t, 4, 4, INT32_C(0), NdbEventMeta) +__flatbuffers_build_scalar_field(4, flatbuffers_, NdbEventMeta_zaps, flatbuffers_int32, int32_t, 4, 4, INT32_C(0), NdbEventMeta) +__flatbuffers_build_scalar_field(5, flatbuffers_, NdbEventMeta_zap_total, flatbuffers_int64, int64_t, 8, 8, INT64_C(0), NdbEventMeta) static inline NdbEventMeta_ref_t NdbEventMeta_create(flatbuffers_builder_t *B __NdbEventMeta_formal_args) { if (NdbEventMeta_start(B) - || NdbEventMeta_received_at_add(B, v0)) { + || NdbEventMeta_zap_total_add(B, v5) + || NdbEventMeta_received_at_add(B, v0) + || NdbEventMeta_reactions_add(B, v1) + || NdbEventMeta_quotes_add(B, v2) + || NdbEventMeta_reposts_add(B, v3) + || NdbEventMeta_zaps_add(B, v4)) { return 0; } return NdbEventMeta_end(B); @@ -42,7 +54,12 @@ static NdbEventMeta_ref_t NdbEventMeta_clone(flatbuffers_builder_t *B, NdbEventM { __flatbuffers_memoize_begin(B, t); if (NdbEventMeta_start(B) - || NdbEventMeta_received_at_pick(B, t)) { + || NdbEventMeta_zap_total_pick(B, t) + || NdbEventMeta_received_at_pick(B, t) + || NdbEventMeta_reactions_pick(B, t) + || NdbEventMeta_quotes_pick(B, t) + || NdbEventMeta_reposts_pick(B, t) + || NdbEventMeta_zaps_pick(B, t)) { return 0; } __flatbuffers_memoize_end(B, t, NdbEventMeta_end(B)); diff --git a/nostrdb/bindings/c/meta_json_parser.h b/nostrdb/bindings/c/meta_json_parser.h index b008106284..94fcebd093 100644 --- a/nostrdb/bindings/c/meta_json_parser.h +++ b/nostrdb/bindings/c/meta_json_parser.h @@ -33,16 +33,14 @@ static const char *NdbEventMeta_parse_json_table(flatcc_json_parser_t *ctx, cons uint64_t w; *result = 0; - if (flatcc_builder_start_table(ctx->ctx, 1)) goto failed; + if (flatcc_builder_start_table(ctx->ctx, 6)) goto failed; buf = flatcc_json_parser_object_start(ctx, buf, end, &more); while (more) { buf = flatcc_json_parser_symbol_start(ctx, buf, end); w = flatcc_json_parser_symbol_part(buf, end); - if (w == 0x7265636569766564) { /* descend "received" */ - buf += 8; - w = flatcc_json_parser_symbol_part(buf, end); - if ((w & 0xffffff0000000000) == 0x5f61740000000000) { /* "_at" */ - buf = flatcc_json_parser_match_symbol(ctx, (mark = buf), end, 3); + if (w < 0x7265636569766564) { /* branch "received" */ + if ((w & 0xffffffffffff0000) == 0x71756f7465730000) { /* "quotes" */ + buf = flatcc_json_parser_match_symbol(ctx, (mark = buf), end, 6); if (mark != buf) { int32_t val = 0; static flatcc_json_parser_integral_symbol_f *symbolic_parsers[] = { @@ -54,18 +52,154 @@ static const char *NdbEventMeta_parse_json_table(flatcc_json_parser_t *ctx, cons if (buf == mark || buf == end) goto failed; } if (val != INT32_C(0) || (ctx->flags & flatcc_json_parser_f_force_add)) { - if (!(pval = flatcc_builder_table_add(ctx->ctx, 0, 4, 4))) goto failed; + if (!(pval = flatcc_builder_table_add(ctx->ctx, 2, 4, 4))) goto failed; flatbuffers_int32_write_to_pe(pval, val); } } else { - buf = flatcc_json_parser_unmatched_symbol(ctx, buf, end); + goto pfguard1; } - } else { /* "_at" */ + } else { /* "quotes" */ + goto pfguard1; + } /* "quotes" */ + goto endpfguard1; +pfguard1: + if (w == 0x7265616374696f6e) { /* descend "reaction" */ + buf += 8; + w = flatcc_json_parser_symbol_part(buf, end); + if ((w & 0xff00000000000000) == 0x7300000000000000) { /* "s" */ + buf = flatcc_json_parser_match_symbol(ctx, (mark = buf), end, 1); + if (mark != buf) { + int32_t val = 0; + static flatcc_json_parser_integral_symbol_f *symbolic_parsers[] = { + meta_local_json_parser_enum, + meta_global_json_parser_enum, 0 }; + buf = flatcc_json_parser_int32(ctx, (mark = buf), end, &val); + if (mark == buf) { + buf = flatcc_json_parser_symbolic_int32(ctx, (mark = buf), end, symbolic_parsers, &val); + if (buf == mark || buf == end) goto failed; + } + if (val != INT32_C(0) || (ctx->flags & flatcc_json_parser_f_force_add)) { + if (!(pval = flatcc_builder_table_add(ctx->ctx, 1, 4, 4))) goto failed; + flatbuffers_int32_write_to_pe(pval, val); + } + } else { + buf = flatcc_json_parser_unmatched_symbol(ctx, buf, end); + } + } else { /* "s" */ + buf = flatcc_json_parser_unmatched_symbol(ctx, buf, end); + } /* "s" */ + } else { /* descend "reaction" */ buf = flatcc_json_parser_unmatched_symbol(ctx, buf, end); - } /* "_at" */ - } else { /* descend "received" */ - buf = flatcc_json_parser_unmatched_symbol(ctx, buf, end); - } /* descend "received" */ + } /* descend "reaction" */ +endpfguard1: + (void)0; + } else { /* branch "received" */ + if (w < 0x7265706f73747300) { /* branch "reposts" */ + if (w == 0x7265636569766564) { /* descend "received" */ + buf += 8; + w = flatcc_json_parser_symbol_part(buf, end); + if ((w & 0xffffff0000000000) == 0x5f61740000000000) { /* "_at" */ + buf = flatcc_json_parser_match_symbol(ctx, (mark = buf), end, 3); + if (mark != buf) { + int32_t val = 0; + static flatcc_json_parser_integral_symbol_f *symbolic_parsers[] = { + meta_local_json_parser_enum, + meta_global_json_parser_enum, 0 }; + buf = flatcc_json_parser_int32(ctx, (mark = buf), end, &val); + if (mark == buf) { + buf = flatcc_json_parser_symbolic_int32(ctx, (mark = buf), end, symbolic_parsers, &val); + if (buf == mark || buf == end) goto failed; + } + if (val != INT32_C(0) || (ctx->flags & flatcc_json_parser_f_force_add)) { + if (!(pval = flatcc_builder_table_add(ctx->ctx, 0, 4, 4))) goto failed; + flatbuffers_int32_write_to_pe(pval, val); + } + } else { + buf = flatcc_json_parser_unmatched_symbol(ctx, buf, end); + } + } else { /* "_at" */ + buf = flatcc_json_parser_unmatched_symbol(ctx, buf, end); + } /* "_at" */ + } else { /* descend "received" */ + buf = flatcc_json_parser_unmatched_symbol(ctx, buf, end); + } /* descend "received" */ + } else { /* branch "reposts" */ + if (w < 0x7a61705f746f7461) { /* branch "zap_tota" */ + if ((w & 0xffffffffffffff00) == 0x7265706f73747300) { /* "reposts" */ + buf = flatcc_json_parser_match_symbol(ctx, (mark = buf), end, 7); + if (mark != buf) { + int32_t val = 0; + static flatcc_json_parser_integral_symbol_f *symbolic_parsers[] = { + meta_local_json_parser_enum, + meta_global_json_parser_enum, 0 }; + buf = flatcc_json_parser_int32(ctx, (mark = buf), end, &val); + if (mark == buf) { + buf = flatcc_json_parser_symbolic_int32(ctx, (mark = buf), end, symbolic_parsers, &val); + if (buf == mark || buf == end) goto failed; + } + if (val != INT32_C(0) || (ctx->flags & flatcc_json_parser_f_force_add)) { + if (!(pval = flatcc_builder_table_add(ctx->ctx, 3, 4, 4))) goto failed; + flatbuffers_int32_write_to_pe(pval, val); + } + } else { + buf = flatcc_json_parser_unmatched_symbol(ctx, buf, end); + } + } else { /* "reposts" */ + buf = flatcc_json_parser_unmatched_symbol(ctx, buf, end); + } /* "reposts" */ + } else { /* branch "zap_tota" */ + if (w == 0x7a61705f746f7461) { /* descend "zap_tota" */ + buf += 8; + w = flatcc_json_parser_symbol_part(buf, end); + if ((w & 0xff00000000000000) == 0x6c00000000000000) { /* "l" */ + buf = flatcc_json_parser_match_symbol(ctx, (mark = buf), end, 1); + if (mark != buf) { + int64_t val = 0; + static flatcc_json_parser_integral_symbol_f *symbolic_parsers[] = { + meta_local_json_parser_enum, + meta_global_json_parser_enum, 0 }; + buf = flatcc_json_parser_int64(ctx, (mark = buf), end, &val); + if (mark == buf) { + buf = flatcc_json_parser_symbolic_int64(ctx, (mark = buf), end, symbolic_parsers, &val); + if (buf == mark || buf == end) goto failed; + } + if (val != INT64_C(0) || (ctx->flags & flatcc_json_parser_f_force_add)) { + if (!(pval = flatcc_builder_table_add(ctx->ctx, 5, 8, 8))) goto failed; + flatbuffers_int64_write_to_pe(pval, val); + } + } else { + buf = flatcc_json_parser_unmatched_symbol(ctx, buf, end); + } + } else { /* "l" */ + buf = flatcc_json_parser_unmatched_symbol(ctx, buf, end); + } /* "l" */ + } else { /* descend "zap_tota" */ + if ((w & 0xffffffff00000000) == 0x7a61707300000000) { /* "zaps" */ + buf = flatcc_json_parser_match_symbol(ctx, (mark = buf), end, 4); + if (mark != buf) { + int32_t val = 0; + static flatcc_json_parser_integral_symbol_f *symbolic_parsers[] = { + meta_local_json_parser_enum, + meta_global_json_parser_enum, 0 }; + buf = flatcc_json_parser_int32(ctx, (mark = buf), end, &val); + if (mark == buf) { + buf = flatcc_json_parser_symbolic_int32(ctx, (mark = buf), end, symbolic_parsers, &val); + if (buf == mark || buf == end) goto failed; + } + if (val != INT32_C(0) || (ctx->flags & flatcc_json_parser_f_force_add)) { + if (!(pval = flatcc_builder_table_add(ctx->ctx, 4, 4, 4))) goto failed; + flatbuffers_int32_write_to_pe(pval, val); + } + } else { + buf = flatcc_json_parser_unmatched_symbol(ctx, buf, end); + } + } else { /* "zaps" */ + buf = flatcc_json_parser_unmatched_symbol(ctx, buf, end); + } /* "zaps" */ + } /* descend "zap_tota" */ + } /* branch "zap_tota" */ + } /* branch "reposts" */ + } /* branch "received" */ buf = flatcc_json_parser_object_end(ctx, buf, end, &more); } if (ctx->error) goto failed; diff --git a/nostrdb/bindings/c/meta_reader.h b/nostrdb/bindings/c/meta_reader.h index e72746d0d2..90f2223eed 100644 --- a/nostrdb/bindings/c/meta_reader.h +++ b/nostrdb/bindings/c/meta_reader.h @@ -47,6 +47,11 @@ __flatbuffers_offset_vec_at(NdbEventMeta_table_t, vec, i, 0) __flatbuffers_table_as_root(NdbEventMeta) __flatbuffers_define_scalar_field(0, NdbEventMeta, received_at, flatbuffers_int32, int32_t, INT32_C(0)) +__flatbuffers_define_scalar_field(1, NdbEventMeta, reactions, flatbuffers_int32, int32_t, INT32_C(0)) +__flatbuffers_define_scalar_field(2, NdbEventMeta, quotes, flatbuffers_int32, int32_t, INT32_C(0)) +__flatbuffers_define_scalar_field(3, NdbEventMeta, reposts, flatbuffers_int32, int32_t, INT32_C(0)) +__flatbuffers_define_scalar_field(4, NdbEventMeta, zaps, flatbuffers_int32, int32_t, INT32_C(0)) +__flatbuffers_define_scalar_field(5, NdbEventMeta, zap_total, flatbuffers_int64, int64_t, INT64_C(0)) #include "flatcc_epilogue.h" diff --git a/nostrdb/bindings/c/meta_verifier.h b/nostrdb/bindings/c/meta_verifier.h index d2b2df7224..db801cf6f2 100644 --- a/nostrdb/bindings/c/meta_verifier.h +++ b/nostrdb/bindings/c/meta_verifier.h @@ -15,6 +15,11 @@ static int NdbEventMeta_verify_table(flatcc_table_verifier_descriptor_t *td) { int ret; if ((ret = flatcc_verify_field(td, 0, 4, 4) /* received_at */)) return ret; + if ((ret = flatcc_verify_field(td, 1, 4, 4) /* reactions */)) return ret; + if ((ret = flatcc_verify_field(td, 2, 4, 4) /* quotes */)) return ret; + if ((ret = flatcc_verify_field(td, 3, 4, 4) /* reposts */)) return ret; + if ((ret = flatcc_verify_field(td, 4, 4, 4) /* zaps */)) return ret; + if ((ret = flatcc_verify_field(td, 5, 8, 8) /* zap_total */)) return ret; return flatcc_verify_ok; } diff --git a/nostrdb/bindings/swift/NdbMeta.swift b/nostrdb/bindings/swift/NdbMeta.swift new file mode 100644 index 0000000000..f452ee6d33 --- /dev/null +++ b/nostrdb/bindings/swift/NdbMeta.swift @@ -0,0 +1,71 @@ +// automatically generated by the FlatBuffers compiler, do not modify +// swiftlint:disable all +// swiftformat:disable all + +import FlatBuffers + +public struct NdbEventMeta: FlatBufferObject, Verifiable { + + static func validateVersion() { FlatBuffersVersion_23_5_26() } + public var __buffer: ByteBuffer! { return _accessor.bb } + private var _accessor: Table + + private init(_ t: Table) { _accessor = t } + public init(_ bb: ByteBuffer, o: Int32) { _accessor = Table(bb: bb, position: o) } + + private enum VTOFFSET: VOffset { + case receivedAt = 4 + case reactions = 6 + case quotes = 8 + case reposts = 10 + case zaps = 12 + case zapTotal = 14 + var v: Int32 { Int32(self.rawValue) } + var p: VOffset { self.rawValue } + } + + public var receivedAt: Int32 { let o = _accessor.offset(VTOFFSET.receivedAt.v); return o == 0 ? 0 : _accessor.readBuffer(of: Int32.self, at: o) } + public var reactions: Int32 { let o = _accessor.offset(VTOFFSET.reactions.v); return o == 0 ? 0 : _accessor.readBuffer(of: Int32.self, at: o) } + public var quotes: Int32 { let o = _accessor.offset(VTOFFSET.quotes.v); return o == 0 ? 0 : _accessor.readBuffer(of: Int32.self, at: o) } + public var reposts: Int32 { let o = _accessor.offset(VTOFFSET.reposts.v); return o == 0 ? 0 : _accessor.readBuffer(of: Int32.self, at: o) } + public var zaps: Int32 { let o = _accessor.offset(VTOFFSET.zaps.v); return o == 0 ? 0 : _accessor.readBuffer(of: Int32.self, at: o) } + public var zapTotal: Int64 { let o = _accessor.offset(VTOFFSET.zapTotal.v); return o == 0 ? 0 : _accessor.readBuffer(of: Int64.self, at: o) } + public static func startNdbEventMeta(_ fbb: inout FlatBufferBuilder) -> UOffset { fbb.startTable(with: 6) } + public static func add(receivedAt: Int32, _ fbb: inout FlatBufferBuilder) { fbb.add(element: receivedAt, def: 0, at: VTOFFSET.receivedAt.p) } + public static func add(reactions: Int32, _ fbb: inout FlatBufferBuilder) { fbb.add(element: reactions, def: 0, at: VTOFFSET.reactions.p) } + public static func add(quotes: Int32, _ fbb: inout FlatBufferBuilder) { fbb.add(element: quotes, def: 0, at: VTOFFSET.quotes.p) } + public static func add(reposts: Int32, _ fbb: inout FlatBufferBuilder) { fbb.add(element: reposts, def: 0, at: VTOFFSET.reposts.p) } + public static func add(zaps: Int32, _ fbb: inout FlatBufferBuilder) { fbb.add(element: zaps, def: 0, at: VTOFFSET.zaps.p) } + public static func add(zapTotal: Int64, _ fbb: inout FlatBufferBuilder) { fbb.add(element: zapTotal, def: 0, at: VTOFFSET.zapTotal.p) } + public static func endNdbEventMeta(_ fbb: inout FlatBufferBuilder, start: UOffset) -> Offset { let end = Offset(offset: fbb.endTable(at: start)); return end } + public static func createNdbEventMeta( + _ fbb: inout FlatBufferBuilder, + receivedAt: Int32 = 0, + reactions: Int32 = 0, + quotes: Int32 = 0, + reposts: Int32 = 0, + zaps: Int32 = 0, + zapTotal: Int64 = 0 + ) -> Offset { + let __start = NdbEventMeta.startNdbEventMeta(&fbb) + NdbEventMeta.add(receivedAt: receivedAt, &fbb) + NdbEventMeta.add(reactions: reactions, &fbb) + NdbEventMeta.add(quotes: quotes, &fbb) + NdbEventMeta.add(reposts: reposts, &fbb) + NdbEventMeta.add(zaps: zaps, &fbb) + NdbEventMeta.add(zapTotal: zapTotal, &fbb) + return NdbEventMeta.endNdbEventMeta(&fbb, start: __start) + } + + public static func verify(_ verifier: inout Verifier, at position: Int, of type: T.Type) throws where T: Verifiable { + var _v = try verifier.visitTable(at: position) + try _v.visit(field: VTOFFSET.receivedAt.p, fieldName: "receivedAt", required: false, type: Int32.self) + try _v.visit(field: VTOFFSET.reactions.p, fieldName: "reactions", required: false, type: Int32.self) + try _v.visit(field: VTOFFSET.quotes.p, fieldName: "quotes", required: false, type: Int32.self) + try _v.visit(field: VTOFFSET.reposts.p, fieldName: "reposts", required: false, type: Int32.self) + try _v.visit(field: VTOFFSET.zaps.p, fieldName: "zaps", required: false, type: Int32.self) + try _v.visit(field: VTOFFSET.zapTotal.p, fieldName: "zapTotal", required: false, type: Int64.self) + _v.finish() + } +} + From 1cf898e0b255d4d4e1f3efadd0120c8e390d8a57 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Mon, 23 Oct 2023 10:28:42 +0800 Subject: [PATCH 68/76] ndb: update nostrdb This includes the new profile fetched_at logic and reaction stats. When receiving new profiles, nostrdb will record when it was last received in a new database. This database is a mapping from Pubkey to timestamp. You can manually read/write to this table using: ndb_read_last_profile_fetch ndb_write_last_profile_fetch This patch also includes the new reaction counting metadata table. It is not used yet (but reactions are still counted!) Changelog-Added: Added reaction counters to nostrdb Changelog-Added: Record when profile is last fetched in nostrdb --- nostrdb/nostrdb.c | 359 ++++++++++++++++++++++++++++++++++++---------- nostrdb/nostrdb.h | 7 +- 2 files changed, 291 insertions(+), 75 deletions(-) diff --git a/nostrdb/nostrdb.c b/nostrdb/nostrdb.c index d805393de5..97a11809e0 100644 --- a/nostrdb/nostrdb.c +++ b/nostrdb/nostrdb.c @@ -16,6 +16,8 @@ #include "bindings/c/profile_json_parser.h" #include "bindings/c/profile_builder.h" +#include "bindings/c/meta_builder.h" +#include "bindings/c/meta_reader.h" #include "bindings/c/profile_verifier.h" #include "secp256k1.h" #include "secp256k1_ecdh.h" @@ -155,8 +157,7 @@ static void ndb_make_search_key(struct ndb_search_key *key, unsigned char *id, key->search[sizeof(key->search) - 1] = '\0'; } -static int ndb_write_profile_search_index(struct ndb_lmdb *lmdb, - MDB_txn *txn, +static int ndb_write_profile_search_index(struct ndb_txn *txn, struct ndb_search_key *index_key, uint64_t profile_key) { @@ -168,7 +169,9 @@ static int ndb_write_profile_search_index(struct ndb_lmdb *lmdb, val.mv_data = &profile_key; val.mv_size = sizeof(profile_key); - if ((rc = mdb_put(txn, lmdb->dbs[NDB_DB_PROFILE_SEARCH], &key, &val, 0))) { + if ((rc = mdb_put(txn->mdb_txn, txn->lmdb->dbs[NDB_DB_PROFILE_SEARCH], + &key, &val, 0))) + { ndb_debug("ndb_write_profile_search_index failed: %s\n", mdb_strerror(rc)); return 0; @@ -179,8 +182,7 @@ static int ndb_write_profile_search_index(struct ndb_lmdb *lmdb, // map usernames and display names to profile keys for user searching -static int ndb_write_profile_search_indices(struct ndb_lmdb *lmdb, - MDB_txn *txn, +static int ndb_write_profile_search_indices(struct ndb_txn *txn, struct ndb_note *note, uint64_t profile_key, void *profile_root) @@ -199,8 +201,7 @@ static int ndb_write_profile_search_indices(struct ndb_lmdb *lmdb, if (name) { ndb_make_search_key(&index, note->pubkey, note->created_at, name); - if (!ndb_write_profile_search_index(lmdb, txn, &index, - profile_key)) + if (!ndb_write_profile_search_index(txn, &index, profile_key)) return 0; } @@ -211,19 +212,30 @@ static int ndb_write_profile_search_indices(struct ndb_lmdb *lmdb, } ndb_make_search_key(&index, note->pubkey, note->created_at, display_name); - if (!ndb_write_profile_search_index(lmdb, txn, &index, - profile_key)) + if (!ndb_write_profile_search_index(txn, &index, profile_key)) return 0; } return 1; } -int ndb_begin_query(struct ndb *ndb, struct ndb_txn *txn) + +static int _ndb_begin_query(struct ndb *ndb, struct ndb_txn *txn, int flags) { - txn->ndb = ndb; + txn->lmdb = &ndb->lmdb; MDB_txn **mdb_txn = (MDB_txn **)&txn->mdb_txn; - return mdb_txn_begin(ndb->lmdb.env, NULL, 0, mdb_txn) == 0; + return mdb_txn_begin(txn->lmdb->env, NULL, flags, mdb_txn) == 0; +} + +int ndb_begin_query(struct ndb *ndb, struct ndb_txn *txn) +{ + return _ndb_begin_query(ndb, txn, MDB_RDONLY); +} + +// this should only be used in migrations, etc +static int ndb_begin_rw_query(struct ndb *ndb, struct ndb_txn *txn) +{ + return _ndb_begin_query(ndb, txn, 0); } @@ -243,8 +255,8 @@ static int ndb_migrate_user_search_indices(struct ndb *ndb) size_t len; int count; - if (!ndb_begin_query(ndb, &txn)) { - fprintf(stderr, "ndb_migrate_user_search_indices: ndb_begin_query failed\n"); + if (!ndb_begin_rw_query(ndb, &txn)) { + fprintf(stderr, "ndb_migrate_user_search_indices: ndb_begin_rw_query failed\n"); return 0; } @@ -268,8 +280,7 @@ static int ndb_migrate_user_search_indices(struct ndb *ndb) return 0; } - if (!ndb_write_profile_search_indices(&ndb->lmdb, txn.mdb_txn, - note, profile_key, + if (!ndb_write_profile_search_indices(&txn, note, profile_key, profile_root)) { fprintf(stderr, "ndb_migrate_user_search_indices: ndb_write_profile_search_indices failed\n"); @@ -282,7 +293,8 @@ static int ndb_migrate_user_search_indices(struct ndb *ndb) fprintf(stderr, "migrated %d profiles to include search indices\n", count); mdb_cursor_close(cur); - mdb_txn_commit(txn.mdb_txn); + + ndb_end_query(&txn); return 1; } @@ -442,11 +454,15 @@ struct ndb_writer_ndb_meta { uint64_t version; }; +// Used in the writer thread when writing ndb_profile_fetch_record's +// kv = pubkey: recor struct ndb_writer_last_fetch { unsigned char pubkey[32]; uint64_t fetched_at; }; +// The different types of messages that the writer thread can write to the +// database struct ndb_writer_msg { enum ndb_writer_msgtype type; union { @@ -457,9 +473,10 @@ struct ndb_writer_msg { }; }; -void ndb_end_query(struct ndb_txn *txn) +int ndb_end_query(struct ndb_txn *txn) { - mdb_txn_abort(txn->mdb_txn); + // this works on read or write queries. + return mdb_txn_commit(txn->mdb_txn) == 0; } int ndb_note_verify(void *ctx, unsigned char pubkey[32], unsigned char id[32], @@ -504,18 +521,21 @@ static int ndb_writer_queue_note(struct ndb_writer *writer, return prot_queue_push(&writer->inbox, &msg); } -static void ndb_writer_last_profile_fetch(struct ndb_lmdb *lmdb, MDB_txn *txn, - struct ndb_writer_last_fetch *w) +static void ndb_writer_last_profile_fetch(struct ndb_txn *txn, + const unsigned char *pubkey, + uint64_t fetched_at) { int rc; MDB_val key, val; - key.mv_data = (unsigned char*)&w->pubkey; - key.mv_size = sizeof(w->pubkey); - val.mv_data = &w->fetched_at; - val.mv_size = sizeof(w->fetched_at); + key.mv_data = (unsigned char*)pubkey; + key.mv_size = 32; + val.mv_data = &fetched_at; + val.mv_size = sizeof(fetched_at); - if ((rc = mdb_put(txn, lmdb->dbs[NDB_DB_PROFILE_LAST_FETCH], &key, &val, 0))) { + if ((rc = mdb_put(txn->mdb_txn, txn->lmdb->dbs[NDB_DB_PROFILE_LAST_FETCH], + &key, &val, 0))) + { ndb_debug("write version to ndb_meta failed: %s\n", mdb_strerror(rc)); return; @@ -524,6 +544,46 @@ static void ndb_writer_last_profile_fetch(struct ndb_lmdb *lmdb, MDB_txn *txn, //fprintf(stderr, "writing version %" PRIu64 "\n", version); } + +// We just received a profile that we haven't processed yet, but it could +// be an older one! Make sure we only write last fetched profile if it's a new +// one +// +// To do this, we first check the latest profile in the database. If the +// created_date for this profile note is newer, then we write a +// last_profile_fetch record, otherwise we do not. +// +// WARNING: This function is only valid when called from the writer thread +static int ndb_maybe_write_last_profile_fetch(struct ndb_txn *txn, + struct ndb_note *note) +{ + size_t len; + uint64_t profile_key, note_key; + void *root; + struct ndb_note *last_profile; + NdbProfileRecord_table_t record; + + if ((root = ndb_get_profile_by_pubkey(txn, note->pubkey, &len, &profile_key))) { + record = NdbProfileRecord_as_root(root); + note_key = NdbProfileRecord_note_key(record); + last_profile = ndb_get_note_by_key(txn, note_key, &len); + if (last_profile == NULL) { + return 0; + } + + // found profile, let's see if it's newer than ours + if (note->created_at > last_profile->created_at) { + // this is a new profile note, record last fetched time + ndb_writer_last_profile_fetch(txn, note->pubkey, time(NULL)); + } + } else { + // couldn't fetch profile. record last fetched time + ndb_writer_last_profile_fetch(txn, note->pubkey, time(NULL)); + } + + return 1; +} + int ndb_write_last_profile_fetch(struct ndb *ndb, const unsigned char *pubkey, uint64_t fetched_at) { @@ -536,8 +596,8 @@ int ndb_write_last_profile_fetch(struct ndb *ndb, const unsigned char *pubkey, } // get some value based on a clustered id key -int ndb_get_tsid(MDB_txn *txn, struct ndb_lmdb *lmdb, enum ndb_dbs db, - const unsigned char *id, MDB_val *val) +int ndb_get_tsid(struct ndb_txn *txn, enum ndb_dbs db, const unsigned char *id, + MDB_val *val) { MDB_val k, v; MDB_cursor *cur; @@ -550,7 +610,7 @@ int ndb_get_tsid(MDB_txn *txn, struct ndb_lmdb *lmdb, enum ndb_dbs db, k.mv_data = &tsid; k.mv_size = sizeof(tsid); - mdb_cursor_open(txn, lmdb->dbs[db], &cur); + mdb_cursor_open(txn->mdb_txn, txn->lmdb->dbs[db], &cur); // Position cursor at the next key greater than or equal to the specified key if (mdb_cursor_get(cur, &k, &v, MDB_SET_RANGE)) { @@ -582,7 +642,7 @@ static void *ndb_lookup_by_key(struct ndb_txn *txn, uint64_t key, k.mv_data = &key; k.mv_size = sizeof(key); - if (mdb_get(txn->mdb_txn, txn->ndb->lmdb.dbs[store], &k, &v)) { + if (mdb_get(txn->mdb_txn, txn->lmdb->dbs[store], &k, &v)) { ndb_debug("ndb_get_profile_by_pubkey: mdb_get note failed\n"); return NULL; } @@ -602,7 +662,7 @@ static void *ndb_lookup_tsid(struct ndb_txn *txn, enum ndb_dbs ind, if (len) *len = 0; - if (!ndb_get_tsid(txn->mdb_txn, &txn->ndb->lmdb, ind, pk, &k)) { + if (!ndb_get_tsid(txn, ind, pk, &k)) { //ndb_debug("ndb_get_profile_by_pubkey: ndb_get_tsid failed\n"); return 0; } @@ -610,7 +670,7 @@ static void *ndb_lookup_tsid(struct ndb_txn *txn, enum ndb_dbs ind, if (primkey) *primkey = *(uint64_t*)k.mv_data; - if (mdb_get(txn->mdb_txn, txn->ndb->lmdb.dbs[store], &k, &v)) { + if (mdb_get(txn->mdb_txn, txn->lmdb->dbs[store], &k, &v)) { ndb_debug("ndb_get_profile_by_pubkey: mdb_get note failed\n"); return 0; } @@ -638,7 +698,7 @@ static inline uint64_t ndb_get_indexkey_by_id(struct ndb_txn *txn, { MDB_val k; - if (!ndb_get_tsid(txn->mdb_txn, &txn->ndb->lmdb, db, id, &k)) + if (!ndb_get_tsid(txn, db, id, &k)) return 0; return *(uint32_t*)k.mv_data; @@ -664,37 +724,52 @@ void *ndb_get_profile_by_key(struct ndb_txn *txn, uint64_t key, size_t *len) return ndb_lookup_by_key(txn, key, NDB_DB_PROFILE, len); } -uint64_t ndb_read_last_profile_fetch(struct ndb_txn *txn, uint64_t profile_key) +uint64_t +ndb_read_last_profile_fetch(struct ndb_txn *txn, const unsigned char *pubkey) { - size_t len; - void *ret = ndb_lookup_by_key(txn, profile_key, NDB_DB_PROFILE_LAST_FETCH, &len); - if (ret == NULL) + MDB_val k, v; + + k.mv_data = (unsigned char*)pubkey; + k.mv_size = 32; + + if (mdb_get(txn->mdb_txn, txn->lmdb->dbs[NDB_DB_PROFILE_LAST_FETCH], &k, &v)) { + //ndb_debug("ndb_read_last_profile_fetch: mdb_get note failed\n"); return 0; - assert(len == sizeof(uint64_t)); - return *((uint64_t*)ret); + } + + return *((uint64_t*)v.mv_data); } -static int ndb_has_note(MDB_txn *txn, struct ndb_lmdb *lmdb, const unsigned char *id) +static int ndb_has_note(struct ndb_txn *txn, const unsigned char *id) { MDB_val val; - if (!ndb_get_tsid(txn, lmdb, NDB_DB_NOTE_ID, id, &val)) + if (!ndb_get_tsid(txn, NDB_DB_NOTE_ID, id, &val)) return 0; return 1; } +static void ndb_txn_from_mdb(struct ndb_txn *txn, struct ndb_lmdb *lmdb, + MDB_txn *mdb_txn) +{ + txn->lmdb = lmdb; + txn->mdb_txn = mdb_txn; +} + static enum ndb_idres ndb_ingester_json_controller(void *data, const char *hexid) { unsigned char id[32]; struct ndb_ingest_controller *c = data; + struct ndb_txn txn; hex_decode(hexid, 64, id, sizeof(id)); // let's see if we already have it - if (!ndb_has_note(c->read_txn, c->lmdb, id)) + ndb_txn_from_mdb(&txn, c->lmdb, c->read_txn); + if (!ndb_has_note(&txn, id)) return NDB_IDRES_CONT; return NDB_IDRES_STOP; @@ -785,8 +860,12 @@ static int ndb_ingester_process_note(secp256k1_context *ctx, size_t note_size, struct ndb_writer_msg *out) { + //printf("ndb_ingester_process_note "); + //print_hex(note->id, 32); + //printf("\n"); + // Verify! If it's an invalid note we don't need to - // bothter writing it to the database + // bother writing it to the database if (!ndb_note_verify(ctx, note->pubkey, note->id, note->sig)) { ndb_debug("signature verification failed\n"); return 0; @@ -957,8 +1036,8 @@ int ndb_search_profile(struct ndb_txn *txn, struct ndb_search *search, const cha k.mv_size = sizeof(s); if ((rc = mdb_cursor_open(txn->mdb_txn, - txn->ndb->lmdb.dbs[NDB_DB_PROFILE_SEARCH], - cursor))) { + txn->lmdb->dbs[NDB_DB_PROFILE_SEARCH], + cursor))) { printf("search_profile: cursor opened failed: %s\n", mdb_strerror(rc)); return 0; @@ -1040,7 +1119,7 @@ static int ndb_search_key_cmp(const MDB_val *a, const MDB_val *b) return 0; } -static int ndb_write_profile(struct ndb_lmdb *lmdb, MDB_txn *txn, +static int ndb_write_profile(struct ndb_txn *txn, struct ndb_writer_profile *profile, uint64_t note_key) { @@ -1071,11 +1150,11 @@ static int ndb_write_profile(struct ndb_lmdb *lmdb, MDB_txn *txn, //assert(NdbProfileRecord_verify_as_root(flatbuf, flatbuf_len) == 0); // get dbs - profile_db = lmdb->dbs[NDB_DB_PROFILE]; - pk_db = lmdb->dbs[NDB_DB_PROFILE_PK]; + profile_db = txn->lmdb->dbs[NDB_DB_PROFILE]; + pk_db = txn->lmdb->dbs[NDB_DB_PROFILE_PK]; // get new key - profile_key = ndb_get_last_key(txn, profile_db) + 1; + profile_key = ndb_get_last_key(txn->mdb_txn, profile_db) + 1; // write profile to profile store key.mv_data = &profile_key; @@ -1084,7 +1163,7 @@ static int ndb_write_profile(struct ndb_lmdb *lmdb, MDB_txn *txn, val.mv_size = flatbuf_len; //ndb_debug("profile_len %ld\n", profile->profile_len); - if ((rc = mdb_put(txn, profile_db, &key, &val, 0))) { + if ((rc = mdb_put(txn->mdb_txn, profile_db, &key, &val, 0))) { ndb_debug("write profile to db failed: %s\n", mdb_strerror(rc)); return 0; } @@ -1097,14 +1176,20 @@ static int ndb_write_profile(struct ndb_lmdb *lmdb, MDB_txn *txn, val.mv_data = &profile_key; val.mv_size = sizeof(profile_key); - if ((rc = mdb_put(txn, pk_db, &key, &val, 0))) { + // write last fetched record + if (!ndb_maybe_write_last_profile_fetch(txn, note)) { + ndb_debug("failed to write last profile fetched record\n"); + return 0; + } + + if ((rc = mdb_put(txn->mdb_txn, pk_db, &key, &val, 0))) { ndb_debug("write profile_pk(%" PRIu64 ") to db failed: %s\n", profile_key, mdb_strerror(rc)); return 0; } // write name, display_name profile search indices - if (!ndb_write_profile_search_indices(lmdb, txn, note, profile_key, + if (!ndb_write_profile_search_indices(txn, note, profile_key, flatbuf)) { ndb_debug("failed to write profile search indices\n"); return 0; @@ -1113,7 +1198,127 @@ static int ndb_write_profile(struct ndb_lmdb *lmdb, MDB_txn *txn, return 1; } -static uint64_t ndb_write_note(struct ndb_lmdb *lmdb, MDB_txn *txn, +// find the last id tag in a note (e, p, etc) +static unsigned char *ndb_note_last_id_tag(struct ndb_note *note, char type) +{ + unsigned char *last = NULL; + struct ndb_iterator iter; + struct ndb_str str; + + // get the liked event id (last id) + ndb_tags_iterate_start(note, &iter); + + while (ndb_tags_iterate_next(&iter)) { + if (iter.tag->count < 2) + continue; + + str = ndb_note_str(note, &iter.tag->strs[0]); + + // assign liked to the last e tag + if (str.flag == NDB_PACKED_STR && str.str[0] == type) { + str = ndb_note_str(note, &iter.tag->strs[1]); + if (str.flag == NDB_PACKED_ID) + last = str.id; + } + } + + return last; +} + +void *ndb_get_note_meta(struct ndb_txn *txn, const unsigned char *id, size_t *len) +{ + MDB_val k, v; + + k.mv_data = (unsigned char*)id; + k.mv_size = 32; + + if (mdb_get(txn->mdb_txn, txn->lmdb->dbs[NDB_DB_META], &k, &v)) { + ndb_debug("ndb_get_note_meta: mdb_get note failed\n"); + return NULL; + } + + if (len) + *len = v.mv_size; + + return v.mv_data; +} + +// When receiving a reaction note, look for the liked id and increase the +// reaction counter in the note metadata database +// +// TODO: I found some bugs when implementing this feature. If the same note id +// is processed multiple times in the same ingestion block, then it will count +// the like twice. This is because it hasn't been written to the DB yet and the +// ingestor doesn't know about notes that are being processed at the same time. +// One fix for this is to maintain a hashtable in the ingestor and make sure +// the same note is not processed twice. +// +// I'm not sure how common this would be, so I'm not going to worry about it +// for now, but it's something to keep in mind. +static int ndb_write_reaction_stats(struct ndb_txn *txn, struct ndb_note *note) +{ + size_t len; + void *root; + int reactions, rc; + MDB_val key, val; + NdbEventMeta_table_t meta; + unsigned char *liked = ndb_note_last_id_tag(note, 'e'); + + if (liked == NULL) + return 0; + + root = ndb_get_note_meta(txn, liked, &len); + + flatcc_builder_t builder; + flatcc_builder_init(&builder); + NdbEventMeta_start_as_root(&builder); + + // no meta record, let's make one + if (root == NULL) { + NdbEventMeta_reactions_add(&builder, 1); + } else { + // clone existing and add to it + meta = NdbEventMeta_as_root(root); + + reactions = NdbEventMeta_reactions_get(meta); + NdbEventMeta_clone(&builder, meta); + NdbEventMeta_reactions_add(&builder, reactions + 1); + } + + NdbProfileRecord_end_as_root(&builder); + root = flatcc_builder_finalize_aligned_buffer(&builder, &len); + assert(((uint64_t)root % 8) == 0); + + if (root == NULL) { + ndb_debug("failed to create note metadata record\n"); + return 0; + } + + // metadata is keyed on id because we want to collect stats regardless + // if we have the note yet or not + key.mv_data = liked; + key.mv_size = 32; + + val.mv_data = root; + val.mv_size = len; + + // write the new meta record + //ndb_debug("writing stats record for "); + //print_hex(liked, 32); + //ndb_debug("\n"); + + if ((rc = mdb_put(txn->mdb_txn, txn->lmdb->dbs[NDB_DB_META], &key, &val, 0))) { + ndb_debug("write reaction stats to db failed: %s\n", mdb_strerror(rc)); + return 0; + } + + free(root); + + return 1; +} + + +static uint64_t ndb_write_note(struct ndb_txn *txn, struct ndb_writer_note *note) { int rc; @@ -1123,11 +1328,11 @@ static uint64_t ndb_write_note(struct ndb_lmdb *lmdb, MDB_txn *txn, MDB_val key, val; // get dbs - note_db = lmdb->dbs[NDB_DB_NOTE]; - id_db = lmdb->dbs[NDB_DB_NOTE_ID]; + note_db = txn->lmdb->dbs[NDB_DB_NOTE]; + id_db = txn->lmdb->dbs[NDB_DB_NOTE_ID]; // get new key - note_key = ndb_get_last_key(txn, note_db) + 1; + note_key = ndb_get_last_key(txn->mdb_txn, note_db) + 1; // write note to event store key.mv_data = ¬e_key; @@ -1135,7 +1340,7 @@ static uint64_t ndb_write_note(struct ndb_lmdb *lmdb, MDB_txn *txn, val.mv_data = note->note; val.mv_size = note->note_len; - if ((rc = mdb_put(txn, note_db, &key, &val, 0))) { + if ((rc = mdb_put(txn->mdb_txn, note_db, &key, &val, 0))) { ndb_debug("write note to db failed: %s\n", mdb_strerror(rc)); return 0; } @@ -1148,17 +1353,21 @@ static uint64_t ndb_write_note(struct ndb_lmdb *lmdb, MDB_txn *txn, val.mv_data = ¬e_key; val.mv_size = sizeof(note_key); - if ((rc = mdb_put(txn, id_db, &key, &val, 0))) { + if ((rc = mdb_put(txn->mdb_txn, id_db, &key, &val, 0))) { ndb_debug("write note id index to db failed: %s\n", mdb_strerror(rc)); return 0; } + if (note->note->kind == 7) { + ndb_write_reaction_stats(txn, note->note); + } + return note_key; } // only to be called from the writer thread -static void ndb_write_version(struct ndb_lmdb *lmdb, MDB_txn *txn, uint64_t version) +static void ndb_write_version(struct ndb_txn *txn, uint64_t version) { int rc; MDB_val key, val; @@ -1171,7 +1380,7 @@ static void ndb_write_version(struct ndb_lmdb *lmdb, MDB_txn *txn, uint64_t vers val.mv_data = &version; val.mv_size = sizeof(version); - if ((rc = mdb_put(txn, lmdb->dbs[NDB_DB_NDB_META], &key, &val, 0))) { + if ((rc = mdb_put(txn->mdb_txn, txn->lmdb->dbs[NDB_DB_NDB_META], &key, &val, 0))) { ndb_debug("write version to ndb_meta failed: %s\n", mdb_strerror(rc)); return; @@ -1186,11 +1395,13 @@ static void *ndb_writer_thread(void *data) struct ndb_writer_msg msgs[THREAD_QUEUE_BATCH], *msg; int i, popped, done, any_note; uint64_t note_nkey; - MDB_txn *txn; + MDB_txn *mdb_txn = NULL; + struct ndb_txn txn; + ndb_txn_from_mdb(&txn, writer->lmdb, mdb_txn); done = 0; while (!done) { - txn = NULL; + txn.mdb_txn = NULL; popped = prot_queue_pop_all(&writer->inbox, msgs, THREAD_QUEUE_BATCH); //ndb_debug("writer popped %d items\n", popped); @@ -1206,7 +1417,7 @@ static void *ndb_writer_thread(void *data) } } - if (any_note && mdb_txn_begin(writer->lmdb->env, NULL, 0, &txn)) + if (any_note && mdb_txn_begin(txn.lmdb->env, NULL, 0, (MDB_txn **)&txn.mdb_txn)) { fprintf(stderr, "writer thread txn_begin failed"); // should definitely not happen unless DB is full @@ -1224,33 +1435,37 @@ static void *ndb_writer_thread(void *data) continue; case NDB_WRITER_PROFILE: note_nkey = - ndb_write_note(writer->lmdb, txn, &msg->note); + ndb_write_note(&txn, &msg->note); if (msg->profile.record.builder) { // only write if parsing didn't fail - ndb_write_profile(writer->lmdb, txn, - &msg->profile, + ndb_write_profile(&txn, &msg->profile, note_nkey); } break; case NDB_WRITER_NOTE: - ndb_write_note(writer->lmdb, txn, &msg->note); + ndb_write_note(&txn, &msg->note); + //printf("wrote note "); + //print_hex(msg->note.note->id, 32); + //printf("\n"); break; case NDB_WRITER_DBMETA: - ndb_write_version(writer->lmdb, txn, msg->ndb_meta.version); + ndb_write_version(&txn, msg->ndb_meta.version); break; case NDB_WRITER_PROFILE_LAST_FETCH: - ndb_writer_last_profile_fetch(writer->lmdb, txn, &msg->last_fetch); + ndb_writer_last_profile_fetch(&txn, + msg->last_fetch.pubkey, + msg->last_fetch.fetched_at + ); break; } } // commit writes - if (any_note && mdb_txn_commit(txn)) { + if (any_note && !ndb_end_query(&txn)) { fprintf(stderr, "writer thread txn commit failed"); assert(false); } - // free notes for (i = 0; i < popped; i++) { msg = &msgs[i]; @@ -1459,7 +1674,7 @@ static int ndb_init_lmdb(const char *filename, struct ndb_lmdb *lmdb, size_t map } // note metadata db - if ((rc = mdb_dbi_open(txn, "meta", MDB_CREATE | MDB_INTEGERKEY, &lmdb->dbs[NDB_DB_META]))) { + if ((rc = mdb_dbi_open(txn, "meta", MDB_CREATE, &lmdb->dbs[NDB_DB_META]))) { fprintf(stderr, "mdb_dbi_open meta failed, error %d\n", rc); return 0; } diff --git a/nostrdb/nostrdb.h b/nostrdb/nostrdb.h index 3c1ad3d399..62006864d4 100644 --- a/nostrdb/nostrdb.h +++ b/nostrdb/nostrdb.h @@ -40,7 +40,7 @@ struct ndb_search { // required to keep a read struct ndb_txn { - struct ndb *ndb; + struct ndb_lmdb *lmdb; void *mdb_txn; }; @@ -196,15 +196,16 @@ int ndb_begin_query(struct ndb *, struct ndb_txn *); int ndb_search_profile(struct ndb_txn *txn, struct ndb_search *search, const char *query); int ndb_search_profile_next(struct ndb_search *search); void ndb_search_profile_end(struct ndb_search *search); -void ndb_end_query(struct ndb_txn *); +int ndb_end_query(struct ndb_txn *); int ndb_write_last_profile_fetch(struct ndb *ndb, const unsigned char *pubkey, uint64_t fetched_at); -uint64_t ndb_read_last_profile_fetch(struct ndb_txn *txn, uint64_t profile_key); +uint64_t ndb_read_last_profile_fetch(struct ndb_txn *txn, const unsigned char *pubkey); void *ndb_get_profile_by_pubkey(struct ndb_txn *txn, const unsigned char *pubkey, size_t *len, uint64_t *primkey); void *ndb_get_profile_by_key(struct ndb_txn *txn, uint64_t key, size_t *len); uint64_t ndb_get_notekey_by_id(struct ndb_txn *txn, const unsigned char *id); uint64_t ndb_get_profilekey_by_pubkey(struct ndb_txn *txn, const unsigned char *id); struct ndb_note *ndb_get_note_by_id(struct ndb_txn *txn, const unsigned char *id, size_t *len, uint64_t *primkey); struct ndb_note *ndb_get_note_by_key(struct ndb_txn *txn, uint64_t key, size_t *len); +void *ndb_get_note_meta(struct ndb_txn *txn, const unsigned char *id, size_t *len); void ndb_destroy(struct ndb *); // BUILDER From a324523b85527d14834189979f8cb8aea0924cd9 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Mon, 23 Oct 2023 10:29:13 +0800 Subject: [PATCH 69/76] ndb: new methods for profile fetched_at This adds a few methods to Ndb for reading and writing fetched_at stats. These are a way of tracking when we last tried to fetch profiles so that we don't need to keep fetching them. --- nostrdb/Ndb.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/nostrdb/Ndb.swift b/nostrdb/Ndb.swift index e55c543ecc..150b63faca 100644 --- a/nostrdb/Ndb.swift +++ b/nostrdb/Ndb.swift @@ -182,6 +182,25 @@ class Ndb { } } + func write_profile_last_fetched(pubkey: Pubkey, fetched_at: UInt64) { + let _ = pubkey.id.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> () in + guard let p = ptr.baseAddress else { return } + ndb_write_last_profile_fetch(ndb.ndb, p, fetched_at) + } + } + + func read_profile_last_fetched(txn: NdbTxn, pubkey: Pubkey) -> UInt64? { + return pubkey.id.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> UInt64? in + guard let p = ptr.baseAddress else { return nil } + let res = ndb_read_last_profile_fetch(&txn.txn, p) + if res == 0 { + return nil + } + + return res + } + } + func process_event(_ str: String) -> Bool { return str.withCString { cstr in return ndb_process_event(ndb.ndb, cstr, Int32(str.utf8.count)) != 0 From 139df33cb7ffa317ea0969638b13ba59cd168789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Mon, 23 Oct 2023 23:32:43 +0000 Subject: [PATCH 70/76] Rename ZapButton to NoteZapButton and ZapButtonView to ProfileZapLinkView (no-op) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a non-functional refactor to rename two views with similar names, to avoid confusion. Signed-off-by: Daniel D’Aquino Reviewed-by: William Casarin Signed-off-by: William Casarin --- damus.xcodeproj/project.pbxproj | 21 +++++++++---------- .../{ZapButton.swift => NoteZapButton.swift} | 6 +++--- damus/Views/ActionBar/EventActionBar.swift | 2 +- damus/Views/Profile/ProfileView.swift | 2 +- damus/Views/ProfileActionSheetView.swift | 2 +- ...tonView.swift => ProfileZapLinkView.swift} | 6 +++--- 6 files changed, 19 insertions(+), 20 deletions(-) rename damus/Components/{ZapButton.swift => NoteZapButton.swift} (98%) rename damus/Views/Zaps/{ZapButtonView.swift => ProfileZapLinkView.swift} (92%) diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index ad4dbd4e78..fc4fbafdb7 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -299,7 +299,7 @@ 4CB883A82975FC1800DC99E7 /* Zaps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883A72975FC1800DC99E7 /* Zaps.swift */; }; 4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883A9297612FF00DC99E7 /* ZapTests.swift */; }; 4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AD2976FA9300DC99E7 /* FormatTests.swift */; }; - 4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AF297705DD00DC99E7 /* ZapButton.swift */; }; + 4CB883B0297705DD00DC99E7 /* NoteZapButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AF297705DD00DC99E7 /* NoteZapButton.swift */; }; 4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883B5297730E400DC99E7 /* LNUrls.swift */; }; 4CB8FC232A41ABA800763C51 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8FC222A41ABA500763C51 /* AboutView.swift */; }; 4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */; }; @@ -434,12 +434,11 @@ D72A2D022AD9C136002AFF62 /* EventViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */; }; D72A2D052AD9C1B5002AFF62 /* MockDamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D042AD9C1B5002AFF62 /* MockDamusState.swift */; }; D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */; }; - D723C38E2AB8D83400065664 /* ContentFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D723C38D2AB8D83400065664 /* ContentFilters.swift */; }; D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; }; D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; }; - D783A63F2AD4E53D00658DDA /* SuggestedHashtagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */; }; - D76874F32AE3632B00FB0F68 /* ZapButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76874F22AE3632B00FB0F68 /* ZapButtonView.swift */; }; + D76874F32AE3632B00FB0F68 /* ProfileZapLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */; }; D77BFA0B2AE3051200621634 /* ProfileActionSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */; }; + D783A63F2AD4E53D00658DDA /* SuggestedHashtagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */; }; D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */; }; D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC02AC4750B0080BA88 /* MentionView.swift */; }; D7870BC32AC47EBC0080BA88 /* EventLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */; }; @@ -996,7 +995,7 @@ 4CB883A72975FC1800DC99E7 /* Zaps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Zaps.swift; sourceTree = ""; }; 4CB883A9297612FF00DC99E7 /* ZapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapTests.swift; sourceTree = ""; }; 4CB883AD2976FA9300DC99E7 /* FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormatTests.swift; sourceTree = ""; }; - 4CB883AF297705DD00DC99E7 /* ZapButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButton.swift; sourceTree = ""; }; + 4CB883AF297705DD00DC99E7 /* NoteZapButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteZapButton.swift; sourceTree = ""; }; 4CB883B5297730E400DC99E7 /* LNUrls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrls.swift; sourceTree = ""; }; 4CB8FC222A41ABA500763C51 /* AboutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNameView.swift; sourceTree = ""; }; @@ -1138,9 +1137,9 @@ D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProfiles.swift; sourceTree = ""; }; D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = ""; }; D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = ""; }; - D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedHashtagsView.swift; sourceTree = ""; }; - D76874F22AE3632B00FB0F68 /* ZapButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButtonView.swift; sourceTree = ""; }; + D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZapLinkView.swift; sourceTree = ""; }; D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileActionSheetView.swift; sourceTree = ""; }; + D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedHashtagsView.swift; sourceTree = ""; }; D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContentViewTests.swift; sourceTree = ""; }; D7870BC02AC4750B0080BA88 /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = ""; }; D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventLoaderView.swift; sourceTree = ""; }; @@ -2091,7 +2090,7 @@ 5C513FB9297F72980072348F /* CustomPicker.swift */, 4CF0ABE22981BC7D00D66079 /* UserView.swift */, 7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */, - 4CB883AF297705DD00DC99E7 /* ZapButton.swift */, + 4CB883AF297705DD00DC99E7 /* NoteZapButton.swift */, 4C42812B298C848200DBF26F /* TranslateView.swift */, 7CFF6316299FEFE5005D382A /* SelectableText.swift */, 4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */, @@ -2242,7 +2241,7 @@ 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */, 4CA3FA0F29F593D000FDB3C3 /* ZapTypePicker.swift */, 4C73C5132A4437C10062CAC0 /* ZapUserView.swift */, - D76874F22AE3632B00FB0F68 /* ZapButtonView.swift */, + D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */, ); path = Zaps; sourceTree = ""; @@ -2896,7 +2895,7 @@ 4C5E54062A9671F800FF6E60 /* UserStatusSheet.swift in Sources */, F71694F42A6732B7001F4053 /* GradientFollowButton.swift in Sources */, 4C3AC7A728369BA200E1F516 /* SearchHomeView.swift in Sources */, - 4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */, + 4CB883B0297705DD00DC99E7 /* NoteZapButton.swift in Sources */, 4C363A922825FCF2006E126D /* ProfileUpdate.swift in Sources */, 4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */, 4C32B9502A9AD44700DC3548 /* FlatBufferBuilder.swift in Sources */, @@ -2967,7 +2966,7 @@ E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */, 4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */, 4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */, - D76874F32AE3632B00FB0F68 /* ZapButtonView.swift in Sources */, + D76874F32AE3632B00FB0F68 /* ProfileZapLinkView.swift in Sources */, D77BFA0B2AE3051200621634 /* ProfileActionSheetView.swift in Sources */, 4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */, 3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */, diff --git a/damus/Components/ZapButton.swift b/damus/Components/NoteZapButton.swift similarity index 98% rename from damus/Components/ZapButton.swift rename to damus/Components/NoteZapButton.swift index 5ad432bc55..84f464b1f4 100644 --- a/damus/Components/ZapButton.swift +++ b/damus/Components/NoteZapButton.swift @@ -1,5 +1,5 @@ // -// ZapButton.swift +// NoteZapButton.swift // damus // // Created by William Casarin on 2023-01-17. @@ -26,7 +26,7 @@ struct ZappingEvent { let target: ZapTarget } -struct ZapButton: View { +struct NoteZapButton: View { let damus_state: DamusState let target: ZapTarget let lnurl: String @@ -144,7 +144,7 @@ struct ZapButton_Previews: PreviewProvider { let pending_zap = PendingZap(amount_msat: 1000, target: ZapTarget.note(id: test_note.id, author: test_note.pubkey), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice))) let zaps = ZapsDataModel([.pending(pending_zap)]) - ZapButton(damus_state: test_damus_state, target: ZapTarget.note(id: test_note.id, author: test_note.pubkey), lnurl: "lnurl", zaps: zaps) + NoteZapButton(damus_state: test_damus_state, target: ZapTarget.note(id: test_note.id, author: test_note.pubkey), lnurl: "lnurl", zaps: zaps) } } diff --git a/damus/Views/ActionBar/EventActionBar.swift b/damus/Views/ActionBar/EventActionBar.swift index 3cb18a5a75..e0aa1ca1e4 100644 --- a/damus/Views/ActionBar/EventActionBar.swift +++ b/damus/Views/ActionBar/EventActionBar.swift @@ -84,7 +84,7 @@ struct EventActionBar: View { if let lnurl = self.lnurl { Spacer() - ZapButton(damus_state: damus_state, target: ZapTarget.note(id: event.id, author: event.pubkey), lnurl: lnurl, zaps: self.damus_state.events.get_cache_data(self.event.id).zaps_model) + NoteZapButton(damus_state: damus_state, target: ZapTarget.note(id: event.id, author: event.pubkey), lnurl: lnurl, zaps: self.damus_state.events.get_cache_data(self.event.id).zaps_model) } Spacer() diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift index 7e8966b477..ac9adbefdc 100644 --- a/damus/Views/Profile/ProfileView.swift +++ b/damus/Views/Profile/ProfileView.swift @@ -222,7 +222,7 @@ struct ProfileView: View { } func lnButton(unownedProfile: Profile?, record: ProfileRecord?) -> some View { - return ZapButtonView(unownedProfileRecord: record, profileModel: self.profile) { reactions_enabled, lud16, lnurl in + return ProfileZapLinkView(unownedProfileRecord: record, profileModel: self.profile) { reactions_enabled, lud16, lnurl in Image(reactions_enabled ? "zap.fill" : "zap") .foregroundColor(reactions_enabled ? .orange : Color.primary) .profile_button_style(scheme: colorScheme) diff --git a/damus/Views/ProfileActionSheetView.swift b/damus/Views/ProfileActionSheetView.swift index 029e5f5c3e..d9cc023ad4 100644 --- a/damus/Views/ProfileActionSheetView.swift +++ b/damus/Views/ProfileActionSheetView.swift @@ -61,7 +61,7 @@ struct ProfileActionSheetView: View { if let lnurl = self.profile_data()?.lnurl, lnurl != "" { return AnyView( VStack(alignment: .center, spacing: 10) { - ZapButtonView(damus_state: damus_state, pubkey: self.profile.pubkey, action: { dismiss() }) { reactions_enabled, lud16, lnurl in + ProfileZapLinkView(damus_state: damus_state, pubkey: self.profile.pubkey, action: { dismiss() }) { reactions_enabled, lud16, lnurl in Image(reactions_enabled ? "zap.fill" : "zap") .foregroundColor(reactions_enabled ? .orange : Color.primary) .profile_button_style(scheme: colorScheme) diff --git a/damus/Views/Zaps/ZapButtonView.swift b/damus/Views/Zaps/ProfileZapLinkView.swift similarity index 92% rename from damus/Views/Zaps/ZapButtonView.swift rename to damus/Views/Zaps/ProfileZapLinkView.swift index 07c91341af..89e480ee4a 100644 --- a/damus/Views/Zaps/ZapButtonView.swift +++ b/damus/Views/Zaps/ProfileZapLinkView.swift @@ -1,5 +1,5 @@ // -// ZapButtonView.swift +// ProfileZapLinkView.swift // damus // // Created by Daniel D’Aquino on 2023-10-20. @@ -7,7 +7,7 @@ import SwiftUI -struct ZapButtonView: View { +struct ProfileZapLinkView: View { typealias ContentViewFunction = (_ reactions_enabled: Bool, _ lud16: String?, _ lnurl: String?) -> Content typealias ActionFunction = () -> Void @@ -86,7 +86,7 @@ struct ZapButtonView: View { } #Preview { - ZapButtonView(pubkey: test_pubkey, reactions_enabled: true, lud16: make_test_profile().lud16, lnurl: "test@sendzaps.lol", label: { reactions_enabled, lud16, lnurl in + ProfileZapLinkView(pubkey: test_pubkey, reactions_enabled: true, lud16: make_test_profile().lud16, lnurl: "test@sendzaps.lol", label: { reactions_enabled, lud16, lnurl in Image("zap.fill") }) } From 692d29942b540e51a525b10c158738a8799ffe24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Mon, 23 Oct 2023 23:32:55 +0000 Subject: [PATCH 71/76] zaps: Implement single-tap zap on profile action sheet and fix zap fallthrough on default settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit implements a single-tap zap on the profile action sheet and fixes an issue where zapping would silently fail on default settings if the user had no lightning wallet installed in their system. Testing ------- Configurations: - iPhone 13 Mini (physical device) on iOS 17.0.2 with NWC wallet attached - iPhone 15 Pro (simulator) on iOS 17.0.1 with no lightning wallet nor NWC Damus: This commit Coverage: - Zapping using NWC connected wallet: PASS (Zaps and shows UX feedback of the completed action) - Zapping under default settings and no lightning wallet: PASS (Shows the wallet selector invoice view) - Long press on zap button brings custom zap view Regression testing ------------------ Preconditions: iPhone 15 Pro (simulator) on iOS 17.0.1 with no lightning wallet nor NWC Coverage: - Zapping user on their full profile shows wallet selector. PASS - On-post invoice shows wallet selector. PASS Closes: https://github.com/damus-io/damus/issues/1634 Changelog-Changed: Zap button on profile action sheet now zaps with a single click, while a long press brings custom zap view Changelog-Fixed: Fixed an issue where zapping would silently fail on default settings if the user does not have a lightning wallet preinstalled on their device. Signed-off-by: Daniel D’Aquino Reviewed-by: William Casarin Signed-off-by: William Casarin --- damus/Components/InvoiceView.swift | 22 ++- damus/Components/NoteZapButton.swift | 13 ++ damus/ContentView.swift | 7 +- damus/Models/Wallet.swift | 2 +- .../Images/ImageContextMenuModifier.swift | 7 +- damus/Views/ProfileActionSheetView.swift | 155 ++++++++++++++++-- damus/Views/SelectWalletView.swift | 6 +- damus/Views/Zaps/CustomizeZapView.swift | 20 +-- 8 files changed, 195 insertions(+), 37 deletions(-) diff --git a/damus/Components/InvoiceView.swift b/damus/Components/InvoiceView.swift index a38f7bfce3..cc99f7410b 100644 --- a/damus/Components/InvoiceView.swift +++ b/damus/Components/InvoiceView.swift @@ -39,7 +39,12 @@ struct InvoiceView: View { if settings.show_wallet_selector { present_sheet(.select_wallet(invoice: invoice.string)) } else { - open_with_wallet(wallet: settings.default_wallet.model, invoice: invoice.string) + do { + try open_with_wallet(wallet: settings.default_wallet.model, invoice: invoice.string) + } + catch { + present_sheet(.select_wallet(invoice: invoice.string)) + } } } label: { RoundedRectangle(cornerRadius: 20, style: .circular) @@ -82,21 +87,26 @@ struct InvoiceView: View { } } -func open_with_wallet(wallet: Wallet.Model, invoice: String) { +enum OpenWalletError: Error { + case no_wallet_to_open + case store_link_invalid + case system_cannot_open_store_link +} + +func open_with_wallet(wallet: Wallet.Model, invoice: String) throws { if let url = URL(string: "\(wallet.link)\(invoice)"), UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url) } else { guard let store_link = wallet.appStoreLink else { - // TODO: do something here if we don't have an appstore link - return + throw OpenWalletError.no_wallet_to_open } guard let url = URL(string: store_link) else { - return + throw OpenWalletError.store_link_invalid } guard UIApplication.shared.canOpenURL(url) else { - return + throw OpenWalletError.system_cannot_open_store_link } UIApplication.shared.open(url) diff --git a/damus/Components/NoteZapButton.swift b/damus/Components/NoteZapButton.swift index 84f464b1f4..3feef66028 100644 --- a/damus/Components/NoteZapButton.swift +++ b/damus/Components/NoteZapButton.swift @@ -18,6 +18,19 @@ enum ZappingError { case bad_lnurl case canceled case send_failed + + func humanReadableMessage() -> String { + switch self { + case .fetching_invoice: + return NSLocalizedString("Error fetching lightning invoice", comment: "Message to display when there was an error fetching a lightning invoice while attempting to zap.") + case .bad_lnurl: + return NSLocalizedString("Invalid lightning address", comment: "Message to display when there was an error attempting to zap due to an invalid lightning address.") + case .canceled: + return NSLocalizedString("Zap attempt from connected wallet was canceled.", comment: "Message to display when a zap from the user's connected wallet was canceled.") + case .send_failed: + return NSLocalizedString("Zap attempt from connected wallet failed.", comment: "Message to display when sending a zap from the user's connected wallet failed.") + } + } } struct ZappingEvent { diff --git a/damus/ContentView.swift b/damus/ContentView.swift index 2ab1cd687c..05a2378166 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -448,7 +448,12 @@ struct ContentView: View { present_sheet(.select_wallet(invoice: inv)) } else { let wallet = damus_state!.settings.default_wallet.model - open_with_wallet(wallet: wallet, invoice: inv) + do { + try open_with_wallet(wallet: wallet, invoice: inv) + } + catch { + present_sheet(.select_wallet(invoice: inv)) + } } case .sent_from_nwc: break diff --git a/damus/Models/Wallet.swift b/damus/Models/Wallet.swift index 30682dfc86..a90b25f0dd 100644 --- a/damus/Models/Wallet.swift +++ b/damus/Models/Wallet.swift @@ -51,7 +51,7 @@ enum Wallet: String, CaseIterable, Identifiable, StringCodable { switch self { case .system_default_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: nil, image: "") case .strike: return .init(index: 0, tag: "strike", displayName: "Strike", link: "strike:", appStoreLink: "https://apps.apple.com/us/app/strike-bitcoin-payments/id1488724463", image: "strike") diff --git a/damus/Views/Images/ImageContextMenuModifier.swift b/damus/Views/Images/ImageContextMenuModifier.swift index 167fce0368..bf0093a77e 100644 --- a/damus/Views/Images/ImageContextMenuModifier.swift +++ b/damus/Views/Images/ImageContextMenuModifier.swift @@ -61,7 +61,12 @@ struct ImageContextMenuModifier: ViewModifier { no_link_found.toggle() } else { if qrCodeLink.contains("lnurl") { - open_with_wallet(wallet: settings.default_wallet.model, invoice: qrCodeLink) + do { + try open_with_wallet(wallet: settings.default_wallet.model, invoice: qrCodeLink) + } + catch { + present_sheet(.select_wallet(invoice: qrCodeLink)) + } } else if let _ = URL(string: qrCodeLink) { open_link_confirm.toggle() } diff --git a/damus/Views/ProfileActionSheetView.swift b/damus/Views/ProfileActionSheetView.swift index d9cc023ad4..3de8276e1e 100644 --- a/damus/Views/ProfileActionSheetView.swift +++ b/damus/Views/ProfileActionSheetView.swift @@ -59,20 +59,7 @@ struct ProfileActionSheetView: View { var zapButton: some View { if let lnurl = self.profile_data()?.lnurl, lnurl != "" { - return AnyView( - VStack(alignment: .center, spacing: 10) { - ProfileZapLinkView(damus_state: damus_state, pubkey: self.profile.pubkey, action: { dismiss() }) { reactions_enabled, lud16, lnurl in - Image(reactions_enabled ? "zap.fill" : "zap") - .foregroundColor(reactions_enabled ? .orange : Color.primary) - .profile_button_style(scheme: colorScheme) - } - .buttonStyle(NeutralCircleButtonStyle()) - - Text(NSLocalizedString("Zap", comment: "Button label that allows the user to zap (i.e. send a Bitcoin tip via the lightning network) the user shown on-screen")) - .foregroundStyle(.secondary) - .font(.caption) - } - ) + return AnyView(ProfileActionSheetZapButton(damus_state: damus_state, profile: profile, lnurl: lnurl)) } else { return AnyView(EmptyView()) @@ -142,6 +129,146 @@ struct ProfileActionSheetView: View { } } +fileprivate struct ProfileActionSheetZapButton: View { + enum ZappingState: Equatable { + case not_zapped + case zapping + case zap_success + case zap_failure(error: ZappingError) + + func error_message() -> String? { + switch self { + case .zap_failure(let error): + return error.humanReadableMessage() + default: + return nil + } + } + } + + let damus_state: DamusState + @StateObject var profile: ProfileModel + let lnurl: String + @State var zap_state: ZappingState = .not_zapped + @State var show_error_alert: Bool = false + + @Environment(\.colorScheme) var colorScheme + + func receive_zap(zap_ev: ZappingEvent) { + print("Received zap event") + guard zap_ev.target == ZapTarget.profile(self.profile.pubkey) else { + return + } + + switch zap_ev.type { + case .failed(let err): + zap_state = .zap_failure(error: err) + show_error_alert = true + break + case .got_zap_invoice(let inv): + if damus_state.settings.show_wallet_selector { + present_sheet(.select_wallet(invoice: inv)) + } else { + let wallet = damus_state.settings.default_wallet.model + do { + try open_with_wallet(wallet: wallet, invoice: inv) + } + catch { + present_sheet(.select_wallet(invoice: inv)) + } + } + break + case .sent_from_nwc: + zap_state = .zap_success + break + } + } + + var button_label: String { + switch zap_state { + case .not_zapped: + return NSLocalizedString("Zap", comment: "Button label that allows the user to zap (i.e. send a Bitcoin tip via the lightning network) the user shown on-screen") + case .zapping: + return NSLocalizedString("Zapping", comment: "Button label indicating that a zap action is in progress (i.e. the user is currently sending a Bitcoin tip via the lightning network to the user shown on-screen) ") + case .zap_success: + return NSLocalizedString("Zapped!", comment: "Button label indicating that a zap action was successful (i.e. the user is successfully sent a Bitcoin tip via the lightning network to the user shown on-screen) ") + case .zap_failure(_): + return NSLocalizedString("Zap failed", comment: "Button label indicating that a zap action was unsuccessful (i.e. the user was unable to send a Bitcoin tip via the lightning network to the user shown on-screen) ") + } + } + + var body: some View { + VStack(alignment: .center, spacing: 10) { + Button( + action: { + send_zap(damus_state: damus_state, target: .profile(self.profile.pubkey), lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type) + zap_state = .zapping + }, + label: { + switch zap_state { + case .not_zapped: + Image("zap") + .foregroundColor(Color.primary) + .profile_button_style(scheme: colorScheme) + case .zapping: + ProgressView() + .foregroundColor(Color.primary) + .profile_button_style(scheme: colorScheme) + case .zap_success: + Image("checkmark") + .foregroundColor(Color.green) + .profile_button_style(scheme: colorScheme) + case .zap_failure: + Image("close") + .foregroundColor(Color.red) + .profile_button_style(scheme: colorScheme) + } + + } + ) + .disabled({ + switch zap_state { + case .not_zapped: + return false + default: + return true + } + }()) + .buttonStyle(NeutralCircleButtonStyle()) + + Text(button_label) + .foregroundStyle(.secondary) + .font(.caption) + } + .onReceive(handle_notify(.zapping)) { zap_ev in + receive_zap(zap_ev: zap_ev) + } + .simultaneousGesture(LongPressGesture().onEnded {_ in + present_sheet(.zap(target: .profile(self.profile.pubkey), lnurl: lnurl)) + }) + .alert(isPresented: $show_error_alert) { + Alert( + title: Text(NSLocalizedString("Zap failed", comment: "Title of an alert indicating that a zap action failed")), + message: Text(zap_state.error_message() ?? ""), + dismissButton: .default(Text(NSLocalizedString("OK", comment: "Button label to dismiss an error dialog"))) + ) + } + .onChange(of: zap_state) { new_zap_state in + switch new_zap_state { + case .zap_success, .zap_failure: + DispatchQueue.main.asyncAfter(deadline: .now() + 5) { + withAnimation { + zap_state = .not_zapped + } + } + break + default: + break + } + } + } +} + struct InnerHeightPreferenceKey: PreferenceKey { static var defaultValue: CGFloat = .zero static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { diff --git a/damus/Views/SelectWalletView.swift b/damus/Views/SelectWalletView.swift index 6243390413..310e63bec7 100644 --- a/damus/Views/SelectWalletView.swift +++ b/damus/Views/SelectWalletView.swift @@ -38,7 +38,8 @@ struct SelectWalletView: View { Section(NSLocalizedString("Select a Lightning wallet", comment: "Title of section for selecting a Lightning wallet to pay a Lightning invoice.")) { List{ Button() { - open_with_wallet(wallet: default_wallet.model, invoice: invoice) + // TODO: Handle cases where wallet cannot be opened by the system + try? open_with_wallet(wallet: default_wallet.model, invoice: invoice) } label: { HStack { Text("Default Wallet", comment: "Button to pay a Lightning invoice with the user's default Lightning wallet.").font(.body).foregroundColor(.blue) @@ -47,7 +48,8 @@ struct SelectWalletView: View { List($allWalletModels) { $wallet in if wallet.index >= 0 { Button() { - open_with_wallet(wallet: wallet, invoice: invoice) + // TODO: Handle cases where wallet cannot be opened by the system + try? open_with_wallet(wallet: wallet, invoice: invoice) } label: { HStack { Image(wallet.image).resizable().frame(width: 32.0, height: 32.0,alignment: .center).cornerRadius(5) diff --git a/damus/Views/Zaps/CustomizeZapView.swift b/damus/Views/Zaps/CustomizeZapView.swift index fb4cef3999..e7cce83c2b 100644 --- a/damus/Views/Zaps/CustomizeZapView.swift +++ b/damus/Views/Zaps/CustomizeZapView.swift @@ -194,16 +194,7 @@ struct CustomizeZapView: View { switch zap_ev.type { case .failed(let err): - switch err { - case .fetching_invoice: - model.error = NSLocalizedString("Error fetching lightning invoice", comment: "Message to display when there was an error fetching a lightning invoice while attempting to zap.") - case .bad_lnurl: - model.error = NSLocalizedString("Invalid lightning address", comment: "Message to display when there was an error attempting to zap due to an invalid lightning address.") - case .canceled: - model.error = NSLocalizedString("Zap attempt from connected wallet was canceled.", comment: "Message to display when a zap from the user's connected wallet was canceled.") - case .send_failed: - model.error = NSLocalizedString("Zap attempt from connected wallet failed.", comment: "Message to display when sending a zap from the user's connected wallet failed.") - } + model.error = err.humanReadableMessage() break case .got_zap_invoice(let inv): if state.settings.show_wallet_selector { @@ -212,8 +203,13 @@ struct CustomizeZapView: View { } else { end_editing() let wallet = state.settings.default_wallet.model - open_with_wallet(wallet: wallet, invoice: inv) - dismiss() + do { + try open_with_wallet(wallet: wallet, invoice: inv) + dismiss() + } + catch { + present_sheet(.select_wallet(invoice: inv)) + } } case .sent_from_nwc: dismiss() From 9969e70b5ff3187dd0a5f95d3661c0b7d3d6298f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Mon, 23 Oct 2023 23:33:02 +0000 Subject: [PATCH 72/76] ui: Add follow button to profile action sheet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Testing: Tested on iPhone 15 Pro simulator with iOS 17.0.1 Closes: https://github.com/damus-io/damus/issues/1636 Changelog-Added: Add follow button to profile action sheet Signed-off-by: Daniel D’Aquino Reviewed-by: William Casarin Signed-off-by: William Casarin --- damus/Views/ProfileActionSheetView.swift | 60 +++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/damus/Views/ProfileActionSheetView.swift b/damus/Views/ProfileActionSheetView.swift index 3de8276e1e..d892889441 100644 --- a/damus/Views/ProfileActionSheetView.swift +++ b/damus/Views/ProfileActionSheetView.swift @@ -37,6 +37,14 @@ struct ProfileActionSheetView: View { return self.profile_data()?.profile } + var followButton: some View { + return ProfileActionSheetFollowButton( + target: .pubkey(self.profile.pubkey), + follows_you: self.profile.follows(pubkey: damus_state.pubkey), + follow_state: damus_state.contacts.follow_state(profile.pubkey) + ) + } + var dmButton: some View { let dm_model = damus_state.dms.lookup_or_create(profile.pubkey) return VStack(alignment: .center, spacing: 10) { @@ -92,8 +100,9 @@ struct ProfileActionSheetView: View { } HStack(spacing: 20) { - self.dmButton + self.followButton self.zapButton + self.dmButton } .padding() @@ -129,6 +138,55 @@ struct ProfileActionSheetView: View { } } +fileprivate struct ProfileActionSheetFollowButton: View { + @Environment(\.colorScheme) var colorScheme + + let target: FollowTarget + let follows_you: Bool + @State var follow_state: FollowState + + var body: some View { + VStack(alignment: .center, spacing: 10) { + Button( + action: { + follow_state = perform_follow_btn_action(follow_state, target: target) + }, + label: { + switch follow_state { + case .unfollows: + Image("user-add-down") + .foregroundColor(Color.primary) + .profile_button_style(scheme: colorScheme) + default: + Image("user-added") + .foregroundColor(Color.green) + .profile_button_style(scheme: colorScheme) + } + + } + ) + .buttonStyle(NeutralCircleButtonStyle()) + + Text(verbatim: "\(follow_btn_txt(follow_state, follows_you: follows_you))") + .foregroundStyle(.secondary) + .font(.caption) + } + .onReceive(handle_notify(.followed)) { follow in + guard case .pubkey(let pk) = follow, + pk == target.pubkey else { return } + + self.follow_state = .follows + } + .onReceive(handle_notify(.unfollowed)) { unfollow in + guard case .pubkey(let pk) = unfollow, + pk == target.pubkey else { return } + + self.follow_state = .unfollows + } + } +} + + fileprivate struct ProfileActionSheetZapButton: View { enum ZappingState: Equatable { case not_zapped From bbccc27a26f0c72050442880e55235c4848c4bb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Mon, 23 Oct 2023 23:33:09 +0000 Subject: [PATCH 73/76] ui: Add setting that allows users to optionally disable profile action sheets. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tested on iOS 17.0.1 on an iPhone 15 Pro simulator. Closes: https://github.com/damus-io/damus/issues/1641 Changelog-Added: Add setting that allows users to optionally disable the new profile action sheet feature Signed-off-by: Daniel D’Aquino Reviewed-by: William Casarin Signed-off-by: William Casarin --- damus/Models/UserSettingsStore.swift | 3 +++ damus/Views/Events/EventProfile.swift | 2 +- damus/Views/Profile/MaybeAnonPfpView.swift | 2 +- damus/Views/ProfileActionSheetView.swift | 9 +++++++++ damus/Views/Settings/AppearanceSettingsView.swift | 9 +++++++++ 5 files changed, 23 insertions(+), 2 deletions(-) diff --git a/damus/Models/UserSettingsStore.swift b/damus/Models/UserSettingsStore.swift index 2bab8c1e0a..a9137addae 100644 --- a/damus/Models/UserSettingsStore.swift +++ b/damus/Models/UserSettingsStore.swift @@ -112,6 +112,9 @@ class UserSettingsStore: ObservableObject { @Setting(key: "hide_nsfw_tagged_content", default_value: false) var hide_nsfw_tagged_content: Bool + + @Setting(key: "show_profile_action_sheet_on_pfp_click", default_value: true) + var show_profile_action_sheet_on_pfp_click: Bool @Setting(key: "zap_vibration", default_value: true) var zap_vibration: Bool diff --git a/damus/Views/Events/EventProfile.swift b/damus/Views/Events/EventProfile.swift index 60525b1333..ac5130c740 100644 --- a/damus/Views/Events/EventProfile.swift +++ b/damus/Views/Events/EventProfile.swift @@ -39,7 +39,7 @@ struct EventProfile: View { HStack(alignment: .center, spacing: 10) { ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles, disable_animation: disable_animation, show_zappability: true) .onTapGesture { - notify(.present_sheet(Sheets.profile_action(pubkey))) + show_profile_action_sheet_if_enabled(damus_state: damus_state, pubkey: pubkey) } VStack(alignment: .leading, spacing: 0) { diff --git a/damus/Views/Profile/MaybeAnonPfpView.swift b/damus/Views/Profile/MaybeAnonPfpView.swift index 62c1239349..f5cf1249ee 100644 --- a/damus/Views/Profile/MaybeAnonPfpView.swift +++ b/damus/Views/Profile/MaybeAnonPfpView.swift @@ -30,7 +30,7 @@ struct MaybeAnonPfpView: View { } else { ProfilePicView(pubkey: pubkey, size: size, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation, show_zappability: true) .onTapGesture { - notify(.present_sheet(Sheets.profile_action(pubkey))) + show_profile_action_sheet_if_enabled(damus_state: state, pubkey: pubkey) } } } diff --git a/damus/Views/ProfileActionSheetView.swift b/damus/Views/ProfileActionSheetView.swift index d892889441..4ce08c1728 100644 --- a/damus/Views/ProfileActionSheetView.swift +++ b/damus/Views/ProfileActionSheetView.swift @@ -334,6 +334,15 @@ struct InnerHeightPreferenceKey: PreferenceKey { } } +func show_profile_action_sheet_if_enabled(damus_state: DamusState, pubkey: Pubkey) { + if damus_state.settings.show_profile_action_sheet_on_pfp_click { + notify(.present_sheet(Sheets.profile_action(pubkey))) + } + else { + damus_state.nav.push(route: Route.ProfileByKey(pubkey: pubkey)) + } +} + #Preview { ProfileActionSheetView(damus_state: test_damus_state, pubkey: test_pubkey) } diff --git a/damus/Views/Settings/AppearanceSettingsView.swift b/damus/Views/Settings/AppearanceSettingsView.swift index 0ee6311b37..e1add45c15 100644 --- a/damus/Views/Settings/AppearanceSettingsView.swift +++ b/damus/Views/Settings/AppearanceSettingsView.swift @@ -100,6 +100,15 @@ struct AppearanceSettingsView: View { Toggle(NSLocalizedString("Hide notes with #nsfw tags", comment: "Setting to hide notes with the #nsfw (not safe for work) tags"), isOn: $settings.hide_nsfw_tagged_content) .toggleStyle(.switch) } + + // MARK: - Profiles + Section( + header: Text(NSLocalizedString("Profiles", comment: "Section title for profile view configuration.")), + footer: Text(NSLocalizedString("Profile action sheets allow you to follow, zap, or DM profiles more quickly without having to view their full profile", comment: "Section footer clarifying what the profile action sheet feature does")) + ) { + Toggle(NSLocalizedString("Show profile action sheets", comment: "Setting to show profile action sheets when clicking on a user's profile picture"), isOn: $settings.show_profile_action_sheet_on_pfp_click) + .toggleStyle(.switch) + } } From 76508dbbfd7277a3ca0b41cbf1d3dd0fef6ad218 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Mon, 23 Oct 2023 10:31:47 +0800 Subject: [PATCH 74/76] perf: don't continuously attempt to fetch old profiles Changelog-Changed: Save bandwidth by only fetching new profiles after a certain amount of time --- damus/Models/EventsModel.swift | 3 +- damus/Models/FollowersModel.swift | 7 ++-- damus/Models/FollowingModel.swift | 8 ++-- damus/Models/HomeModel.swift | 7 ++-- damus/Models/ProfileModel.swift | 3 +- damus/Models/SearchHomeModel.swift | 67 ++++++++++++++++++------------ damus/Models/SearchModel.swift | 3 +- damus/Models/ThreadModel.swift | 3 +- damus/Models/ZapsModel.swift | 3 +- damus/Nostr/Profiles.swift | 23 ++++++++-- damus/Util/LNUrls.swift | 4 +- damus/Views/FollowingView.swift | 3 +- 12 files changed, 86 insertions(+), 48 deletions(-) diff --git a/damus/Models/EventsModel.swift b/damus/Models/EventsModel.swift index 96248b8640..7c045e916d 100644 --- a/damus/Models/EventsModel.swift +++ b/damus/Models/EventsModel.swift @@ -64,7 +64,8 @@ class EventsModel: ObservableObject { case .ok: break case .eose: - load_profiles(profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state) + let txn = NdbTxn(ndb: self.state.ndb) + load_profiles(profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state, txn: txn) } } } diff --git a/damus/Models/FollowersModel.swift b/damus/Models/FollowersModel.swift index 4faee3f03f..1f5ca6caeb 100644 --- a/damus/Models/FollowersModel.swift +++ b/damus/Models/FollowersModel.swift @@ -53,8 +53,8 @@ class FollowersModel: ObservableObject { has_contact.insert(ev.pubkey) } - func load_profiles(relay_id: String) { - let authors = find_profiles_to_fetch_from_keys(profiles: damus_state.profiles, pks: contacts ?? []) + func load_profiles(relay_id: String, txn: NdbTxn) { + let authors = find_profiles_to_fetch_from_keys(profiles: damus_state.profiles, pks: contacts ?? [], txn: txn) if authors.isEmpty { return } @@ -83,7 +83,8 @@ class FollowersModel: ObservableObject { case .eose(let sub_id): if sub_id == self.sub_id { - load_profiles(relay_id: relay_id) + let txn = NdbTxn(ndb: self.damus_state.ndb) + load_profiles(relay_id: relay_id, txn: txn) } else if sub_id == self.profiles_id { damus_state.pool.unsubscribe(sub_id: profiles_id, to: [relay_id]) } diff --git a/damus/Models/FollowingModel.swift b/damus/Models/FollowingModel.swift index 45f84fe1ef..87c67074e8 100644 --- a/damus/Models/FollowingModel.swift +++ b/damus/Models/FollowingModel.swift @@ -22,11 +22,11 @@ class FollowingModel { self.hashtags = hashtags } - func get_filter() -> NostrFilter { + func get_filter(txn: NdbTxn) -> NostrFilter { var f = NostrFilter(kinds: [.metadata]) f.authors = self.contacts.reduce(into: Array()) { acc, pk in // don't fetch profiles we already have - if damus_state.profiles.has_fresh_profile(id: pk) { + if damus_state.profiles.has_fresh_profile(id: pk, txn: txn) { return } acc.append(pk) @@ -34,8 +34,8 @@ class FollowingModel { return f } - func subscribe() { - let filter = get_filter() + func subscribe(txn: NdbTxn) { + let filter = get_filter(txn: txn) if (filter.authors?.count ?? 0) == 0 { needs_sub = false return diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift index 4ee4d752b6..fd0a16b6c8 100644 --- a/damus/Models/HomeModel.swift +++ b/damus/Models/HomeModel.swift @@ -430,14 +430,15 @@ class HomeModel { case .eose(let sub_id): + let txn = NdbTxn(ndb: damus_state.ndb) if sub_id == dms_subid { var dms = dms.dms.flatMap { $0.events } dms.append(contentsOf: incoming_dms) - load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(dms), damus_state: damus_state) + load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(dms), damus_state: damus_state, txn: txn) } else if sub_id == notifications_subid { - load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_keys(notifications.uniq_pubkeys()), damus_state: damus_state) + load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_keys(notifications.uniq_pubkeys()), damus_state: damus_state, txn: txn) } else if sub_id == home_subid { - load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus_state) + load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus_state, txn: txn) } self.loading = false diff --git a/damus/Models/ProfileModel.swift b/damus/Models/ProfileModel.swift index 71229d3173..3f40602e63 100644 --- a/damus/Models/ProfileModel.swift +++ b/damus/Models/ProfileModel.swift @@ -123,8 +123,9 @@ class ProfileModel: ObservableObject, Equatable { break //notify(.notice, notice) case .eose: + let txn = NdbTxn(ndb: damus.ndb) if resp.subid == sub_id { - load_profiles(profiles_subid: prof_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus) + load_profiles(profiles_subid: prof_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus, txn: txn) } progress += 1 break diff --git a/damus/Models/SearchHomeModel.swift b/damus/Models/SearchHomeModel.swift index 1fd3d3729a..fdfa675193 100644 --- a/damus/Models/SearchHomeModel.swift +++ b/damus/Models/SearchHomeModel.swift @@ -83,38 +83,38 @@ class SearchHomeModel: ObservableObject { // global events are not realtime unsubscribe(to: relay_id) - load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.all_events), damus_state: damus_state) + let txn = NdbTxn(ndb: damus_state.ndb) + load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.all_events), damus_state: damus_state, txn: txn) } - - + break } } } -func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: EventCache) -> [Pubkey] { +func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: EventCache, txn: NdbTxn) -> [Pubkey] { switch load { case .from_events(let events): - return find_profiles_to_fetch_from_events(profiles: profiles, events: events, cache: cache) + return find_profiles_to_fetch_from_events(profiles: profiles, events: events, cache: cache, txn: txn) case .from_keys(let pks): - return find_profiles_to_fetch_from_keys(profiles: profiles, pks: pks) + return find_profiles_to_fetch_from_keys(profiles: profiles, pks: pks, txn: txn) } } -func find_profiles_to_fetch_from_keys(profiles: Profiles, pks: [Pubkey]) -> [Pubkey] { - Array(Set(pks.filter { pk in !profiles.has_fresh_profile(id: pk) })) +func find_profiles_to_fetch_from_keys(profiles: Profiles, pks: [Pubkey], txn: NdbTxn) -> [Pubkey] { + Array(Set(pks.filter { pk in !profiles.has_fresh_profile(id: pk, txn: txn) })) } -func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent], cache: EventCache) -> [Pubkey] { +func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent], cache: EventCache, txn: NdbTxn) -> [Pubkey] { var pubkeys = Set() for ev in events { // lookup profiles from boosted events - if ev.known_kind == .boost, let bev = ev.get_inner_event(cache: cache), !profiles.has_fresh_profile(id: bev.pubkey) { + if ev.known_kind == .boost, let bev = ev.get_inner_event(cache: cache), !profiles.has_fresh_profile(id: bev.pubkey, txn: txn) { pubkeys.insert(bev.pubkey) } - if !profiles.has_fresh_profile(id: ev.pubkey) { + if !profiles.has_fresh_profile(id: ev.pubkey, txn: txn) { pubkeys.insert(ev.pubkey) } } @@ -127,27 +127,42 @@ enum PubkeysToLoad { case from_keys([Pubkey]) } -func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad, damus_state: DamusState) { - let authors = find_profiles_to_fetch(profiles: damus_state.profiles, load: load, cache: damus_state.events) +func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad, damus_state: DamusState, txn: NdbTxn) { + let authors = find_profiles_to_fetch(profiles: damus_state.profiles, load: load, cache: damus_state.events, txn: txn) + guard !authors.isEmpty else { return } - print("loading \(authors.count) profiles from \(relay_id)") - - let filter = NostrFilter(kinds: [.metadata], - authors: authors) - - damus_state.pool.subscribe_to(sub_id: profiles_subid, filters: [filter], to: [relay_id]) { sub_id, conn_ev in - guard case .nostr_event(let ev) = conn_ev, - case .eose = ev, - sub_id == profiles_subid - else { - return + print("load_profiles: requesting \(authors.count) profiles from \(relay_id)") + + let filter = NostrFilter(kinds: [.metadata], authors: authors) + + damus_state.pool.subscribe_to(sub_id: profiles_subid, filters: [filter], to: [relay_id]) { rid, conn_ev in + + let now = UInt64(Date.now.timeIntervalSince1970) + switch conn_ev { + case .ws_event: + break + case .nostr_event(let ev): + guard ev.subid == profiles_subid, rid == relay_id else { return } + + switch ev { + case .event(_, let ev): + if ev.known_kind == .metadata { + damus_state.ndb.write_profile_last_fetched(pubkey: ev.pubkey, fetched_at: now) + } + case .eose: + print("load_profiles: done loading \(authors.count) profiles from \(relay_id)") + damus_state.pool.unsubscribe(sub_id: profiles_subid, to: [relay_id]) + case .ok: + break + case .notice: + break + } } - print("done loading \(authors.count) profiles from \(relay_id)") - damus_state.pool.unsubscribe(sub_id: profiles_subid, to: [relay_id]) + } } diff --git a/damus/Models/SearchModel.swift b/damus/Models/SearchModel.swift index a80eb5558a..0d945ca397 100644 --- a/damus/Models/SearchModel.swift +++ b/damus/Models/SearchModel.swift @@ -80,7 +80,8 @@ class SearchModel: ObservableObject { self.loading = false if sub_id == self.sub_id { - load_profiles(profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(self.events.all_events), damus_state: state) + let txn = NdbTxn(ndb: state.ndb) + load_profiles(profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(self.events.all_events), damus_state: state, txn: txn) } } } diff --git a/damus/Models/ThreadModel.swift b/damus/Models/ThreadModel.swift index 9c44f32983..6b25bb2bc1 100644 --- a/damus/Models/ThreadModel.swift +++ b/damus/Models/ThreadModel.swift @@ -120,7 +120,8 @@ class ThreadModel: ObservableObject { } if sub_id == self.base_subid { - load_profiles(profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(Array(event_map)), damus_state: damus_state) + let txn = NdbTxn(ndb: damus_state.ndb) + load_profiles(profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(Array(event_map)), damus_state: damus_state, txn: txn) } } diff --git a/damus/Models/ZapsModel.swift b/damus/Models/ZapsModel.swift index 3a9aeb34c0..9acc4778f0 100644 --- a/damus/Models/ZapsModel.swift +++ b/damus/Models/ZapsModel.swift @@ -55,7 +55,8 @@ class ZapsModel: ObservableObject { break case .eose: let events = state.events.lookup_zaps(target: target).map { $0.request.ev } - load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state) + let txn = NdbTxn(ndb: state.ndb) + load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state, txn: txn) case .event(_, let ev): guard ev.kind == 9735, let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey), diff --git a/damus/Nostr/Profiles.swift b/damus/Nostr/Profiles.swift index 10dec04cbb..ad9a5ddeb2 100644 --- a/damus/Nostr/Profiles.swift +++ b/damus/Nostr/Profiles.swift @@ -30,7 +30,7 @@ class ProfileData { class Profiles { private var ndb: Ndb - static let db_freshness_threshold: TimeInterval = 24 * 60 * 60 + static let db_freshness_threshold: TimeInterval = 24 * 60 * 8 @MainActor private var profiles: [Pubkey: ProfileData] = [:] @@ -93,9 +93,24 @@ class Profiles { return ndb.lookup_profile_key(pubkey) } - func has_fresh_profile(id: Pubkey) -> Bool { - guard let recv = lookup_with_timestamp(id).unsafeUnownedValue?.receivedAt else { return false } - return Date.now.timeIntervalSince(Date(timeIntervalSince1970: Double(recv))) < Profiles.db_freshness_threshold + func has_fresh_profile(id: Pubkey, txn: NdbTxn) -> Bool { + guard let fetched_at = ndb.read_profile_last_fetched(txn: txn, pubkey: id) + else { + return false + } + + // In situations where a batch of profiles was fetched all at once, + // this will reduce the herding of the profile requests + let fuzz = Double.random(in: -60...60) + let threshold = Profiles.db_freshness_threshold + fuzz + let fetch_date = Date(timeIntervalSince1970: Double(fetched_at)) + + let since = Date.now.timeIntervalSince(fetch_date) + let fresh = since < threshold + + //print("fresh = \(fresh): fetch_date \(since) < threshold \(threshold) \(id)") + + return fresh } } diff --git a/damus/Util/LNUrls.swift b/damus/Util/LNUrls.swift index c4614c338d..9d00b1a195 100644 --- a/damus/Util/LNUrls.swift +++ b/damus/Util/LNUrls.swift @@ -29,10 +29,10 @@ class LNUrls { guard tries < 5 else { return nil } self.endpoints[pubkey] = .failed(tries: tries + 1) case .fetched(let pr): - print("lnurls.lookup_or_fetch fetched \(lnurl)") + //print("lnurls.lookup_or_fetch fetched \(lnurl)") return pr case .fetching(let task): - print("lnurls.lookup_or_fetch already fetching \(lnurl)") + //print("lnurls.lookup_or_fetch already fetching \(lnurl)") return await task.value case .not_fetched: print("lnurls.lookup_or_fetch not fetched \(lnurl)") diff --git a/damus/Views/FollowingView.swift b/damus/Views/FollowingView.swift index 045daa0cd1..16dab49c16 100644 --- a/damus/Views/FollowingView.swift +++ b/damus/Views/FollowingView.swift @@ -151,7 +151,8 @@ struct FollowingView: View { } .tabViewStyle(.page(indexDisplayMode: .never)) .onAppear { - following.subscribe() + let txn = NdbTxn(ndb: self.damus_state.ndb) + following.subscribe(txn: txn) } .onDisappear { following.unsubscribe() From 4389cc2128769b297207fc6de6fe0ebb2e96f973 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Mon, 23 Oct 2023 11:21:37 +0800 Subject: [PATCH 75/76] load_profiles: add context for debugging This is useful to see where the load_profiles request is coming from We may need to switch to a central dispatch for profile loading, I suspect there is a lot of redundancy between requests. --- damus/Models/EventsModel.swift | 2 +- damus/Models/HomeModel.swift | 6 +++--- damus/Models/ProfileModel.swift | 2 +- damus/Models/SearchHomeModel.swift | 8 ++++---- damus/Models/SearchModel.swift | 2 +- damus/Models/ThreadModel.swift | 2 +- damus/Models/ZapsModel.swift | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/damus/Models/EventsModel.swift b/damus/Models/EventsModel.swift index 7c045e916d..25a61badb6 100644 --- a/damus/Models/EventsModel.swift +++ b/damus/Models/EventsModel.swift @@ -65,7 +65,7 @@ class EventsModel: ObservableObject { break case .eose: let txn = NdbTxn(ndb: self.state.ndb) - load_profiles(profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state, txn: txn) + load_profiles(context: "events_model", profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state, txn: txn) } } } diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift index fd0a16b6c8..266e853bf3 100644 --- a/damus/Models/HomeModel.swift +++ b/damus/Models/HomeModel.swift @@ -434,11 +434,11 @@ class HomeModel { if sub_id == dms_subid { var dms = dms.dms.flatMap { $0.events } dms.append(contentsOf: incoming_dms) - load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(dms), damus_state: damus_state, txn: txn) + load_profiles(context: "dms", profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(dms), damus_state: damus_state, txn: txn) } else if sub_id == notifications_subid { - load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_keys(notifications.uniq_pubkeys()), damus_state: damus_state, txn: txn) + load_profiles(context: "notifications", profiles_subid: profiles_subid, relay_id: relay_id, load: .from_keys(notifications.uniq_pubkeys()), damus_state: damus_state, txn: txn) } else if sub_id == home_subid { - load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus_state, txn: txn) + load_profiles(context: "home", profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus_state, txn: txn) } self.loading = false diff --git a/damus/Models/ProfileModel.swift b/damus/Models/ProfileModel.swift index 3f40602e63..15941596bb 100644 --- a/damus/Models/ProfileModel.swift +++ b/damus/Models/ProfileModel.swift @@ -125,7 +125,7 @@ class ProfileModel: ObservableObject, Equatable { case .eose: let txn = NdbTxn(ndb: damus.ndb) if resp.subid == sub_id { - load_profiles(profiles_subid: prof_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus, txn: txn) + load_profiles(context: "profile", profiles_subid: prof_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus, txn: txn) } progress += 1 break diff --git a/damus/Models/SearchHomeModel.swift b/damus/Models/SearchHomeModel.swift index fdfa675193..ff78035afa 100644 --- a/damus/Models/SearchHomeModel.swift +++ b/damus/Models/SearchHomeModel.swift @@ -84,7 +84,7 @@ class SearchHomeModel: ObservableObject { unsubscribe(to: relay_id) let txn = NdbTxn(ndb: damus_state.ndb) - load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.all_events), damus_state: damus_state, txn: txn) + load_profiles(context: "universe", profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.all_events), damus_state: damus_state, txn: txn) } break @@ -127,14 +127,14 @@ enum PubkeysToLoad { case from_keys([Pubkey]) } -func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad, damus_state: DamusState, txn: NdbTxn) { +func load_profiles(context: String, profiles_subid: String, relay_id: String, load: PubkeysToLoad, damus_state: DamusState, txn: NdbTxn) { let authors = find_profiles_to_fetch(profiles: damus_state.profiles, load: load, cache: damus_state.events, txn: txn) guard !authors.isEmpty else { return } - print("load_profiles: requesting \(authors.count) profiles from \(relay_id)") + print("load_profiles[\(context)]: requesting \(authors.count) profiles from \(relay_id)") let filter = NostrFilter(kinds: [.metadata], authors: authors) @@ -153,7 +153,7 @@ func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToL damus_state.ndb.write_profile_last_fetched(pubkey: ev.pubkey, fetched_at: now) } case .eose: - print("load_profiles: done loading \(authors.count) profiles from \(relay_id)") + print("load_profiles[\(context)]: done loading \(authors.count) profiles from \(relay_id)") damus_state.pool.unsubscribe(sub_id: profiles_subid, to: [relay_id]) case .ok: break diff --git a/damus/Models/SearchModel.swift b/damus/Models/SearchModel.swift index 0d945ca397..520ea7cf13 100644 --- a/damus/Models/SearchModel.swift +++ b/damus/Models/SearchModel.swift @@ -81,7 +81,7 @@ class SearchModel: ObservableObject { if sub_id == self.sub_id { let txn = NdbTxn(ndb: state.ndb) - load_profiles(profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(self.events.all_events), damus_state: state, txn: txn) + load_profiles(context: "search", profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(self.events.all_events), damus_state: state, txn: txn) } } } diff --git a/damus/Models/ThreadModel.swift b/damus/Models/ThreadModel.swift index 6b25bb2bc1..760b5010f1 100644 --- a/damus/Models/ThreadModel.swift +++ b/damus/Models/ThreadModel.swift @@ -121,7 +121,7 @@ class ThreadModel: ObservableObject { if sub_id == self.base_subid { let txn = NdbTxn(ndb: damus_state.ndb) - load_profiles(profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(Array(event_map)), damus_state: damus_state, txn: txn) + load_profiles(context: "thread", profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(Array(event_map)), damus_state: damus_state, txn: txn) } } diff --git a/damus/Models/ZapsModel.swift b/damus/Models/ZapsModel.swift index 9acc4778f0..2b1cb9fd3c 100644 --- a/damus/Models/ZapsModel.swift +++ b/damus/Models/ZapsModel.swift @@ -56,7 +56,7 @@ class ZapsModel: ObservableObject { case .eose: let events = state.events.lookup_zaps(target: target).map { $0.request.ev } let txn = NdbTxn(ndb: state.ndb) - load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state, txn: txn) + load_profiles(context: "zaps_model", profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state, txn: txn) case .event(_, let ev): guard ev.kind == 9735, let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey), From 7710839261bcdac27028755f2d7c07329b2690ff Mon Sep 17 00:00:00 2001 From: William Casarin Date: Fri, 27 Oct 2023 09:02:05 +0800 Subject: [PATCH 76/76] noting: users are now notified when you quote repost them Changelog-Changed: Users are now notified when you quote repost them --- damus/Views/PostView.swift | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/damus/Views/PostView.swift b/damus/Views/PostView.swift index 451007e9a4..c276b7cf8a 100644 --- a/damus/Views/PostView.swift +++ b/damus/Views/PostView.swift @@ -103,7 +103,7 @@ struct PostView: View { } return true } - let new_post = build_post(post: self.post, action: action, uploadedMedias: uploadedMedias, references: refs) + let new_post = build_post(state: damus_state, post: self.post, action: action, uploadedMedias: uploadedMedias, references: refs) notify(.post(.post(new_post))) @@ -631,7 +631,7 @@ func load_draft_for_post(drafts: Drafts, action: PostAction) -> DraftArtifacts? } -func build_post(post: NSMutableAttributedString, action: PostAction, uploadedMedias: [UploadedMedia], references: [RefId]) -> NostrPost { +func build_post(state: DamusState, post: NSMutableAttributedString, action: PostAction, uploadedMedias: [UploadedMedia], references: [RefId]) -> NostrPost { post.enumerateAttributes(in: NSRange(location: 0, length: post.length), options: []) { attributes, range, stop in if let link = attributes[.link] as? String { let normalized_link: String @@ -654,7 +654,7 @@ func build_post(post: NSMutableAttributedString, action: PostAction, uploadedMed let imagesString = uploadedMedias.map { $0.uploadedURL.absoluteString }.joined(separator: " ") - let img_meta_tags = uploadedMedias.compactMap { $0.metadata?.to_tag() } + var tags = uploadedMedias.compactMap { $0.metadata?.to_tag() } if !imagesString.isEmpty { content.append(" " + imagesString + " ") @@ -662,7 +662,12 @@ func build_post(post: NSMutableAttributedString, action: PostAction, uploadedMed if case .quoting(let ev) = action { content.append(" nostr:" + bech32_note_id(ev.id)) + + if let quoted_ev = state.events.lookup(ev.id) { + tags.append(["p", quoted_ev.pubkey.hex()]) + } } - return NostrPost(content: content, references: references, kind: .text, tags: img_meta_tags) + return NostrPost(content: content, references: references, kind: .text, tags: tags) } +