Compare commits

..

138 Commits

Author SHA1 Message Date
tyiu a4327a6a7a WIP translations CI 2023-03-02 20:19:32 +13:00
William Casarin 502c4daf6f v1.1.0-10 changelog 2023-03-01 10:03:10 -08:00
William Casarin ffe2c7284a v1.1.0-10 2023-03-01 10:02:30 -08:00
OlegAba 6b1f57d6d0 Truncate long notes (#715)
Changelog-Added: Truncate large posts and add a show more button
2023-03-01 09:57:39 -08:00
William Casarin 77f5268336 Private Zaps
This adds private zaps, which have messages and authors encrypted to
the target. Keys are deterministically generated so that both the
receiver and sender can decrypt.

Changelog-Added: Private Zaps
2023-03-01 09:56:25 -08:00
tyiu c72c0079cc Translations (#701)
* Apply translations in fr_FR

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'fr_FR' language.

* Apply translations in fr_FR

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'fr_FR' language.

* Apply translations in fr_FR

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'fr_FR' language.

* Apply translations in fr_FR

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'fr_FR' language.

* Apply translations in fr_FR

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'fr_FR' language.

* Apply translations in fr_FR

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'fr_FR' language.

* Apply translations in fr_FR

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'fr_FR' language.

* Apply translations in fr_FR

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'fr_FR' language.

* Apply translations in fr_FR

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'fr_FR' language.

* Apply translations in fr_FR

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'fr_FR' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.

* Apply translations in de

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.

* Apply translations in de

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'de' language.

* Apply translations in nl

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'nl' language.

* Apply translations in nl

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ar' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ar' language.

* Apply translations in cs

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'cs' language.

* Apply translations in cs

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'cs' language.

* Apply translations in cs

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'cs' language.

* Apply translations in el_GR

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'el_GR' language.

* Apply translations in el_GR

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'el_GR' language.

* Apply translations in el_GR

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'el_GR' language.

* Apply translations in el_GR

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'el_GR' language.

* Apply translations in el_GR

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.

* Apply translations in el_GR

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.

* Apply translations in el_GR

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.

* Apply translations in fr_FR

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'fr_FR' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ar

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.

* Apply translations in de

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'de' language.

* Apply translations in cs

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'cs' language.

* Apply translations in it_IT

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'it_IT' language.

* Apply translations in de

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.

* Apply translations in de

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.

* Apply translations in de

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.

* Apply translations in de

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in lv_LV

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'lv_LV' language.

* Apply translations in lv_LV

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'lv_LV' language.

* Apply translations in lv_LV

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'lv_LV' language.

* Apply translations in lv_LV

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'lv_LV' language.

* Apply translations in lv_LV

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'lv_LV' language.

* Apply translations in lv_LV

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'lv_LV' language.

* Apply translations in lv_LV

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'lv_LV' language.

* Apply translations in lv_LV

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'lv_LV' language.

* Apply translations in lv_LV

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'lv_LV' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2023-03-01 07:42:13 -08:00
William Casarin 5ab1d6294c Fix default zap amount setting not getting updated
Changelog-Fixed: Fix default zap amount setting not getting updated
2023-02-27 11:08:03 -08:00
William Casarin 2f90f2d4b7 Fix issue where keyboard covers custom zap comment
Changelog-Fixed: Fix issue where keyboard covers custom zap comment
2023-02-27 11:03:09 -08:00
William Casarin 1288732e5d v1.1.0-9 changelog 2023-02-26 16:01:21 -08:00
William Casarin 4a6c6a65ab v1.1.0-9 2023-02-26 15:59:55 -08:00
William Casarin 0f29d67e1f ensure blocked users do not show in notifications 2023-02-26 15:56:31 -08:00
William Casarin 9fd2f51971 Merge remote-tracking branch 'tyiu/tyiu/translations' 2023-02-26 15:48:55 -08:00
William Casarin 386bae64ca scroll coordinate space 2023-02-26 15:46:17 -08:00
William Casarin 4b5c217213 Add scroll queue detection in notification view
This will stop injecting events into the timeline if you're scrolling
2023-02-26 14:14:25 -08:00
tyiu 240fda2429 Merge branch 'tyiu/notifications' into tyiu/translations 2023-02-27 10:58:10 +13:00
tyiu bacd9b3c38 Add strings for event grouped notifications 2023-02-27 10:47:05 +13:00
tyiu 0152286859 Fix missing comments on new strings 2023-02-27 10:43:00 +13:00
transifex-integration[bot] 06e9a1b392 Apply translations in ar
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.
2023-02-27 10:27:19 +13:00
transifex-integration[bot] 483730af18 Apply translations in pl_PL
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-02-27 10:27:19 +13:00
transifex-integration[bot] 23229015a6 Apply translations in de
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-02-27 10:27:19 +13:00
transifex-integration[bot] 7ab95583df Apply translations in de
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-02-27 10:27:19 +13:00
transifex-integration[bot] b7a48a24e9 Apply translations in de
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-02-27 10:27:18 +13:00
tyiu 04028d9cff Fix wording in SaveKeysView to be more mobile-friendly 2023-02-27 10:27:18 +13:00
tyiu 6918fb46cf Fix localization bug on RelayFilterView 2023-02-27 10:27:18 +13:00
transifex-integration[bot] 2b854ef9b7 Apply translations in ar
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.
2023-02-27 10:27:18 +13:00
transifex-integration[bot] 5eb61f1ac1 Apply translations in pl_PL
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-02-27 10:27:18 +13:00
transifex-integration[bot] c3bbf7aa8f Apply translations in pl_PL
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-02-27 10:27:18 +13:00
transifex-integration[bot] 1e52d958ac Apply translations in pl_PL
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-02-27 10:27:18 +13:00
transifex-integration[bot] 5252e5f5bb Apply translations in pl_PL
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-02-27 10:27:17 +13:00
transifex-integration[bot] 71d5625f04 Apply translations in pl_PL
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-02-27 10:27:17 +13:00
transifex-integration[bot] 990e783c30 Apply translations in pl_PL
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-02-27 10:27:17 +13:00
transifex-integration[bot] 3602189133 Apply translations in pl_PL
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-02-27 10:27:17 +13:00
transifex-integration[bot] 3ca9acdf34 Apply translations in pl_PL
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-02-27 10:27:17 +13:00
transifex-integration[bot] 2036d5843b Apply translations in pl_PL
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-02-27 10:27:17 +13:00
transifex-integration[bot] 2d3bd11d56 Apply translations in pl_PL
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-02-27 10:27:17 +13:00
transifex-integration[bot] a715987e71 Apply translations in pl_PL
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-02-27 10:27:16 +13:00
transifex-integration[bot] 0303031445 Apply translations in uk
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'uk' language.
2023-02-27 10:27:16 +13:00
transifex-integration[bot] d6ae9a5d79 Apply translations in es_419
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-02-27 10:27:16 +13:00
transifex-integration[bot] 356bd06e6a Apply translations in pl_PL
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-02-27 10:27:16 +13:00
transifex-integration[bot] e757bdca90 Apply translations in pl_PL
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-02-27 10:27:16 +13:00
transifex-integration[bot] ff1b4d724d Apply translations in ja
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-02-27 10:27:16 +13:00
transifex-integration[bot] b8614f055c Apply translations in el_GR
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.
2023-02-27 10:27:16 +13:00
transifex-integration[bot] 63ab151a5e Apply translations in nl
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-02-27 10:27:15 +13:00
transifex-integration[bot] fd9d4deb44 Apply translations in cs
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'cs' language.
2023-02-27 10:27:15 +13:00
transifex-integration[bot] a5c719673e Apply translations in zh_CN
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-02-27 10:27:15 +13:00
transifex-integration[bot] 2bf3c6718d Apply translations in ar
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.
2023-02-27 10:27:15 +13:00
transifex-integration[bot] 3f3e59488a Apply translations in fa
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'fa' language.
2023-02-27 10:27:15 +13:00
transifex-integration[bot] b1b4b5b6c9 Apply translations in fa
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'fa' language.
2023-02-27 10:27:15 +13:00
transifex-integration[bot] 7455665672 Apply translations in el_GR
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.
2023-02-27 10:27:15 +13:00
transifex-integration[bot] 9f22234926 Apply translations in cs
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'cs' language.
2023-02-27 10:27:15 +13:00
transifex-integration[bot] 21a8a4e96f Apply translations in ar
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.
2023-02-27 10:27:14 +13:00
transifex-integration[bot] c635f3d77a Apply translations in ar
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.
2023-02-27 10:27:14 +13:00
transifex-integration[bot] 2e9a4388b9 Apply translations in ar
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.
2023-02-27 10:27:14 +13:00
transifex-integration[bot] 4a1949eeb8 Apply translations in ar
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.
2023-02-27 10:27:14 +13:00
transifex-integration[bot] b4fcb58bcb Apply translations in nl
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-02-27 10:27:14 +13:00
transifex-integration[bot] 7d852eb33b Apply translations in nl
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-02-27 10:27:14 +13:00
transifex-integration[bot] a448f610c0 Apply translations in nl
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-02-27 10:27:14 +13:00
transifex-integration[bot] 8cc561b8c6 Apply translations in ja
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-02-27 10:27:13 +13:00
transifex-integration[bot] 01630d0a4c Apply translations in zh_CN
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-02-27 10:27:13 +13:00
tyiu 16156f4d9a Fix number formatting for Arabic and other languages 2023-02-27 10:27:13 +13:00
tyiu f840fe9c80 Update source English strings per feedback from translators 2023-02-27 10:27:13 +13:00
tyiu 77bcd1b715 Fix localization and add tests for EventGroupView 2023-02-27 10:20:50 +13:00
Swift 75fd8de456 Fix post text in darkmode
Closes: #693
2023-02-26 12:21:04 -08:00
William Casarin 71f7ea47df Customized Zaps
Changelog-Added: Customized zaps
2023-02-26 11:53:29 -08:00
William Casarin 64b1a57918 Notifications
Changelog-Added: Add new Notifications View
2023-02-25 11:33:25 -08:00
William Casarin e4dd585754 Fix text color in dark mode on post view 2023-02-24 15:11:09 -08:00
Swift 436d20dfbd Rich tagging
Changelog-Changed: No more inline npubs when tagging users
Closes: #691
2023-02-24 13:35:33 -08:00
OlegAba 810b3e1fa5 Fix mention rounded border
Closes: #670
2023-02-24 13:04:36 -08:00
OlegAba 75fb0d19e2 Fix ImageCarousel corner radius and context menu 2023-02-24 13:04:13 -08:00
OlegAba a2749eaaaa Fix event dividers 2023-02-24 13:04:11 -08:00
OlegAba 83c9289345 Lazy loading of thread child events
Closes: #679
2023-02-24 12:49:26 -08:00
Joel Klabo 4c3a83772e Update Alignment of Side Menu Labels
Changelog-Fixed: Fix alignment of side menu labels
Closes: #688
2023-02-24 12:45:47 -08:00
tyiu 5cd4c2d75e Fix localization issues, add tests, import translations, and add zh-CN and zh-TW
Closes: #689
2023-02-24 12:44:54 -08:00
Joel Klabo 85e797a054 Embed in ScrollView 2023-02-24 12:42:49 -08:00
Joel Klabo 9f52e2c246 Fix Identical Participants in ParticipantView
Changelog-Fixed: Fix duplicated participants in reply-to view
Closes: #685
2023-02-24 12:42:18 -08:00
Joel Klabo 8b9958a4ad Add Bookmarking (Local to device)
Changelog-Added: Bookmarking
Closes: #649
2023-02-21 12:33:21 -08:00
William Casarin 87a0bdac94 Load missing profiles in Zaps view
Changelog-Fixed: Load missing profiles in Zaps view
2023-02-21 10:08:15 -08:00
William Casarin 37b964c296 Fix issue where CPU is continuously pegged when scrolling
Since should_queue is continuosly getting set, this is causing the
InnerTimelineView to continuously rerender, pegging the CPU to 100%

This change only updates the var if it changes from the previous value.
2023-02-21 05:07:27 -08:00
William Casarin b1a2b47116 Eliminate popping when scrolling
This commit makes a few changes:

- Link preview views are no longer cached, only the metadata. This fixes
  a memory leak when preview videos. It will keep playing the video
  forever eventually leading to a crash. This is fixed!

- Cache the intrinsic height of previews, when loading notes it looks
  for the cached height so that things don't pop-in after the fact

- Note artifacts and previews are set in the constructor instead of
  onAppear, this prevents the size from changing and popping after it
  has been loaded into the lazyvstack

Changelog-Fixed: Fix memory leak with inline videos
Changelog-Fixed: Eliminate popping when scrolling
2023-02-21 04:36:01 -08:00
William Casarin af6f88ab17 Fix moving post button to quell jack's OCD 2023-02-20 15:04:06 -08:00
William Casarin 647495dbc0 Fix bug where feed sometimes gets reset to realtime when scrolling 2023-02-20 14:58:10 -08:00
William Casarin 826fd1ef33 More consistent scrolling to top behavior 2023-02-20 14:26:45 -08:00
William Casarin 54dd2035a1 Always flush events when switching timelines 2023-02-20 14:21:21 -08:00
William Casarin 587819c8eb Always switch to realtime mode on scroll-to-top, remove realtime indicator 2023-02-20 13:51:54 -08:00
William Casarin 8954c1c245 Remove load more popup 2023-02-20 13:48:36 -08:00
William Casarin 19a421604c Remove all localization from formatting strings
until we have test converage
2023-02-20 12:44:43 -08:00
William Casarin 68b57d8b99 Fix localization crash 2023-02-20 12:40:34 -08:00
William Casarin f3056653db v1.1.0-3 changelog 2023-02-20 11:36:59 -08:00
William Casarin 6196279d2b v1.1.0-3 2023-02-20 11:35:44 -08:00
William Casarin f213420b41 Merge remote-tracking branch 'tyiu/tyiu/translations' 2023-02-20 11:34:28 -08:00
William Casarin b4140dc5f2 Add a "load more" button instead of always inserting events in timelines
Changelog-Added: Add a "load more" button instead of always inserting events in timelines
2023-02-20 11:12:31 -08:00
tyiu 1b27e9041f Fix localization issues 2023-02-19 13:17:53 -05:00
William Casarin 795577a0a1 Rename Global feed to Universe
Changelog-Changed: Rename global feed to universe
2023-02-19 09:53:24 -08:00
William Casarin d5c45dc8ba Fix rare markdown crash 2023-02-19 09:20:09 -08:00
Bryan Montz 603a5a1814 Refinements to RelayConnection plus tests for creating requests
Closes: #644
2023-02-19 08:36:03 -08:00
tyiu 06a1a9aba6 Fix Zaps string pluralization bug
Closes: #646
2023-02-19 08:31:43 -08:00
Bryan Montz ff1815cce0 refactor similar RepostsModel and ReactionsModel into one parent class
Closes: #650
2023-02-19 08:26:30 -08:00
OlegAba 0bdec912f8 Restrict dynamic font type - max size 2023-02-18 09:34:50 -08:00
William Casarin 6d8312fa57 Merge remote-tracking branch 'tyiu/tyiu/translations' 2023-02-18 09:33:54 -08:00
Bryan Montz f6d56179eb Image and color asset clean-up
Closes: #643
2023-02-18 09:29:20 -08:00
Bryan Montz 193e922c9c code clean-up: @discardableResult, unused params, simplify getting specific relays from pool
Closes: #635
2023-02-18 09:22:09 -08:00
OlegAba a1a89dc98e Add selectable text feature
Changelog-Added: Added the ability to select text on posts
Closes: #639
2023-02-18 08:59:47 -08:00
ericholguin 3e764e75e4 Post view improvements
Changelog-Changed: Improve look of post view
Closes: #561
2023-02-17 10:30:46 -08:00
William Casarin 7c563cb0ae Revert "Add menu ellipsis button to notes"
This reverts commit 390c9162ae.
2023-02-17 10:24:47 -08:00
tyiu a328b0d1a8 Import translations 2023-02-17 09:29:56 -05:00
OlegAba 5018b9aa1e Added a 20MB content length limit for all image files
Changelog-Changed: Added a 20MB content length limit for all image files
Closes: #335
2023-02-16 12:19:18 -08:00
middlingphys 1f6657e471 Remove trailing slash when adding a relay
Changelog-Fixed: Remove trailing slash when adding a relay
Closes: #562
2023-02-16 12:17:04 -08:00
ericholguin 062b5dc040 Added Posts or Post & Replies selector to Profile
Changelog-Added: Added Posts or Post & Replies selector to Profile
Closes: #496
2023-02-16 08:48:23 -08:00
ericholguin 390c9162ae Add menu ellipsis button to notes
Changelog-Changed: Switch from long-press to ... on events for context menu
Closes: #568
2023-02-16 08:37:54 -08:00
Bryan Montz 94f66adf8d Improve EventActionBar button spacing
Changelog-Changed: Improved EventActionBar button spacing
Closes: #576
2023-02-16 08:34:22 -08:00
OlegAba d547dade04 Use top anchor for scroll to top event
Changelog-Fixed: Scroll to top of events instead of the bottom
Closes: #570
2023-02-16 08:29:25 -08:00
OlegAba 94a67adff9 Fix padding 2023-02-16 08:29:21 -08:00
William Casarin 29f192c377 Merge remote-tracking branch 'tyiu/tyiu/translations' 2023-02-16 07:29:37 -08:00
tyiu 4e67c88607 Export and import translations 2023-02-16 10:27:00 -05:00
William Casarin 42200c347b Merge remote-tracking branch 'tyiu/tyiu/translations' 2023-02-16 07:22:01 -08:00
tyiu 36f05ccaed Export and import translations 2023-02-16 10:17:09 -05:00
tyiu 98a1b95d12 Use Text(verbatim:) to indicate non-translatable strings 2023-02-16 10:15:38 -05:00
Bryan Montz 4cdef502e9 profile: copy button polish
- Updated checkmark icon to SF Symbols
- Updated copy icon to one from SF Symbols

Changelog-Changed: Polished profile key copy buttons, added animation
Closes: #619
2023-02-16 06:13:59 -08:00
Joel Klabo ae2e70ba7d Format Large Numbers of Action Bar Actions
Changelog-Changed: Format large numbers of action bar actions
Closes: #626
2023-02-16 06:09:09 -08:00
Bryan Montz 1b4e54582f fixed tests 2023-02-16 06:07:04 -08:00
William Casarin 909148f0be add missing file 2023-02-15 19:15:19 -08:00
OlegAba c100c6db47 Merge remote-tracking branch 'oleg/custom-profile-navbar'
Changelog-Added: Improved profile navbar
2023-02-15 12:32:25 -08:00
Bryan Montz 8d3fb397f7 Improved blur on images, especially in dark mode
Changelog-Changed: Improved blur on images, especially in dark mode
Closes: #583
2023-02-15 11:19:09 -08:00
William Casarin f8742a609c Merge remote-tracking branch 'tyiu/tyiu/translations' 2023-02-15 11:17:42 -08:00
William Casarin d55d0d61ed perf: debounce incoming dms
This fixes perf issues on startup if you have lots of dms

Changelog-Fixed: Fix lag on startup when you have lots of DMs
Changelog-Fixed: Fix an issues where dm notifications appear without any new events
2023-02-15 11:14:13 -08:00
William Casarin cf90480501 perf: decode large events in background threads
should help with hitches a bit
2023-02-15 11:14:13 -08:00
OlegAba f0075904c2 Fix frequent KFImage hang
Changelog-Fixed: Fix some hangs when scrolling by images
Closes: #614
2023-02-15 11:13:04 -08:00
tyiu a41acc12e7 Import translations 2023-02-15 12:50:57 -05:00
William Casarin 1e22984d52 Merge remote-tracking branch 'tyiu/tyiu/translations' 2023-02-15 08:49:45 -08:00
tyiu 9080e4efae Force default zap amount text field to accept only numbers
Changelog-Fixed: Force default zap amount text field to accept only numbers
Closes: #612
2023-02-15 08:47:22 -08:00
tyiu 6488634eda Import translations 2023-02-15 10:54:45 -05:00
tyiu 355cd1283c Wrap non-translatable strings so that they do not get exported 2023-02-15 10:44:44 -05:00
William Casarin 6ed9c408f9 v1.1.0-2 changelog 2023-02-14 10:17:08 -08:00
William Casarin 5f52e6f62f v1.1.0-2 2023-02-14 10:15:40 -08:00
OlegAba 2366089896 Fix vertical spacing bug 2023-02-10 16:52:34 -05:00
OlegAba 9a95967a81 Refactor pfp image view to use zoomable scroll view 2023-02-10 01:03:55 -05:00
OlegAba 504108da75 Add custom profile navbar 2023-02-09 18:24:16 -05:00
OlegAba d43a2ff92d Move safeAreaInset ref to Theme 2023-02-09 18:22:48 -05:00
243 changed files with 8259 additions and 24861 deletions
@@ -0,0 +1,24 @@
name: Export Source Translations
on:
push:
branches:
- master
jobs:
export-source-translations:
name: Update translations branch
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Run export script
run: |
sh devtools/export-source-translation.sh
- uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: Update Translations 🤖
branch: translations
create_branch: true
push_options: '--force'
if: env.GIT_DIFF
+92
View File
@@ -1,3 +1,94 @@
## [1.1.0-10] - 2023-03-01
### Added
- Truncate large posts and add a show more button (OlegAba)
- Private Zaps (William Casarin)
### Fixed
- Fix default zap amount setting not getting updated (William Casarin)
- Fix issue where keyboard covers custom zap comment (William Casarin)
[1.1.0-10]: https://github.com/damus-io/damus/releases/tag/v1.1.0-10
## [1.1.0-9] - 2023-02-26
### Added
- Customized zaps (William Casarin)
- Add new Notifications View (William Casarin)
- Bookmarking (Joel Klabo)
### Changed
- No more inline npubs when tagging users (Swift)
### Fixed
- Fix alignment of side menu labels (Joel Klabo)
- Fix duplicated participants in reply-to view (Joel Klabo)
- Load missing profiles in Zaps view (William Casarin)
- Fix memory leak with inline videos (William Casarin)
- Eliminate popping when scrolling (William Casarin)
[1.1.0-9]: https://github.com/damus-io/damus/releases/tag/v1.1.0-9
## [1.1.0-3] - 2023-02-20
### Added
- Add a "load more" button instead of always inserting events in timelines (William Casarin)
- Added the ability to select text on posts (OlegAba)
- Added Posts or Post & Replies selector to Profile (ericholguin)
- Improved profile navbar (OlegAba)
### Changed
- Rename global feed to universe (William Casarin)
- Improve look of post view (ericholguin)
- Added a 20MB content length limit for all image files (OlegAba)
- Improved EventActionBar button spacing (Bryan Montz)
- Polished profile key copy buttons, added animation (Bryan Montz)
- Format large numbers of action bar actions (Joel Klabo)
- Improved blur on images, especially in dark mode (Bryan Montz)
### Fixed
- Remove trailing slash when adding a relay (middlingphys)
- Scroll to top of events instead of the bottom (OlegAba)
- Fix lag on startup when you have lots of DMs (William Casarin)
- Fix an issues where dm notifications appear without any new events (William Casarin)
- Fix some hangs when scrolling by images (OlegAba)
- Force default zap amount text field to accept only numbers (Terry Yiu)
[1.1.0-3]: https://github.com/damus-io/damus/releases/tag/v1.1.0-3
## [1.1.0-2] - 2023-02-14
### Added
- Save drafts to posts, replies and DMs (Terry Yiu)
### Fixed
- Ensure stats get updated in realtime on action bars (William Casarin)
- Fix reposts not getting counted properly (William Casarin)
- Fix a bug where zaps on other people's posts weren't showing (William Casarin)
- Fix punctuation getting included in some urls (Gert Goet)
- Improve language detection (Terry Yiu)
- Fix some animated image crashes (William Casarin)
[1.1.0-2]: https://github.com/damus-io/damus/releases/tag/v1.1.0-2
## [1.0.0-15] - 2023-02-10
### Added
@@ -559,3 +650,4 @@
[0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2
-18
View File
@@ -108,24 +108,6 @@ All user-facing strings must have a comment in order to provide context to trans
[transifex]: https://explore.transifex.com/damus/damus-ios/
#### Export Source Translations
If user-facing strings have been added or changed, please export them for translation as part of your pull request or commit by running:
```zsh
./devtools/export-source-translation.sh
```
This command will export source translations to `translations/en-US.xcloc/Localized Contents/en-US.xliff`, which the Transifex integration will read from the `master` branch and allow translators to translate those strings.
#### Import Translations
Once 100% of strings have been translated for a given locale, Transifex will open up a pull request with the `translations/<locale>.xliff` file changed. Currently, it must be manually imported into the project before merging the pull request by running:
```zsh
./devtools/import-translation.sh <locale_code_in_snake_case>
```
### Awards
There may be nostr badges awarded for contributors in the future... :)
+163 -6
View File
@@ -11,6 +11,11 @@
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */; };
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAEC294FCCFC00EE4006 /* Constants.swift */; };
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; };
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */; };
3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */; };
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */; };
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */; };
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */; };
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; };
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
@@ -42,6 +47,12 @@
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */; };
4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8B28398BC6008A31F1 /* Keys.swift */; };
4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */; };
4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */; };
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */; };
4C30AC7429A5680900E2BD5A /* EventGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7329A5680900E2BD5A /* EventGroupView.swift */; };
4C30AC7629A5770900E2BD5A /* NotificationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7529A5770900E2BD5A /* NotificationItemView.swift */; };
4C30AC7829A577AB00E2BD5A /* EventCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7729A577AB00E2BD5A /* EventCache.swift */; };
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7F29A6A53F00E2BD5A /* ProfilePicturesView.swift */; };
4C363A8428233689006E126D /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8328233689006E126D /* Parser.swift */; };
4C363A8828236948006E126D /* BlocksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8728236948006E126D /* BlocksView.swift */; };
4C363A8A28236B57006E126D /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8928236B57006E126D /* MentionView.swift */; };
@@ -92,6 +103,9 @@
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */; };
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C42812B298C848200DBF26F /* TranslateView.swift */; };
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C477C9D282C3A4800033AA3 /* TipCounter.swift */; };
4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0629A540BA003E4487 /* NotificationsModel.swift */; };
4C54AA0A29A55429003E4487 /* EventGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0929A55429003E4487 /* EventGroup.swift */; };
4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0B29A5543C003E4487 /* ZapGroup.swift */; };
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */; };
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */; };
4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5F9113283D694D0052CD1C /* FollowTarget.swift */; };
@@ -121,6 +135,8 @@
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */; };
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C987B56283FD07F0042CE38 /* FollowersModel.swift */; };
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */; };
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */; };
4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */; };
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; };
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AC298851D000060CEA /* AccountDeletion.swift */; };
4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */; };
@@ -155,6 +171,9 @@
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */; };
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.swift */; };
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; };
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */; };
4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */; };
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */; };
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F8CC281352B30009DFBB /* Notifications.swift */; };
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */; };
4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */; };
@@ -198,6 +217,7 @@
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */; };
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABF52985CD5500D66079 /* UserSearch.swift */; };
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; };
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfileZoomView.swift */; };
@@ -205,15 +225,19 @@
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; };
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FBD06E296255C400D9D3B2 /* Theme.swift */; };
6C7DE41F2955169800E66263 /* Vault in Frameworks */ = {isa = PBXBuildFile; productRef = 6C7DE41E2955169800E66263 /* Vault */; };
7C45AE71297353390031D7BC /* KFImageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C45AE70297353390031D7BC /* KFImageModel.swift */; };
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C60CAEE298471A1009C80D6 /* CoreSVG.swift */; };
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */; };
7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */; };
7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CFF6316299FEFE5005D382A /* SelectableText.swift */; };
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; };
9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C83F89229A937B900136C08 /* TextViewWrapper.swift */; };
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; };
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; };
E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */; };
F75BA12D29A1855400E10810 /* BookmarksManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75BA12C29A1855400E10810 /* BookmarksManager.swift */; };
F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75BA12E29A18EF500E10810 /* BookmarksView.swift */; };
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E91298B0F0700AB113A /* RelayDetailView.swift */; };
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */; };
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */; };
@@ -249,6 +273,21 @@
3A25EF142992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A25EF152992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "el-GR"; path = "el-GR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A2B8B0A296A8982009CC16D /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "en-US"; path = "en-US.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyDescriptionTests.swift; sourceTree = "<group>"; };
3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailBarTests.swift; sourceTree = "<group>"; };
3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationUtil.swift; sourceTree = "<group>"; };
3A3040F229A91366008A0F29 /* ProfileViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewTests.swift; sourceTree = "<group>"; };
3A3040F929A91ED6008A0F29 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A3040FA29A91EFC008A0F29 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A3040FB29A91F03008A0F29 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-HK"; path = "zh-HK.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A3040FC29A91F31008A0F29 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-TW"; path = "zh-TW.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A3040FD29A91F31008A0F29 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-TW"; path = "zh-TW.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A3040FE29A91F31008A0F29 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-TW"; path = "zh-TW.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A3040FF29AB02D1008A0F29 /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-US"; path = "en-US.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGroupViewTests.swift; sourceTree = "<group>"; };
3A41E559299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A4F3320297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A4F3321297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A4F3322297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-FR"; path = "fr-FR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -259,6 +298,12 @@
3A66D927299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A66D928299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
3A66D929299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A827A18299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A827A19299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
3A827A1A299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A8624D9299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A8624DA299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
3A8624DB299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A929C20297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A929C21297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A929C22297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "it-IT"; path = "it-IT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -312,6 +357,12 @@
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePictureSelector.swift; sourceTree = "<group>"; };
4C285C8B28398BC6008A31F1 /* Keys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keys.swift; sourceTree = "<group>"; };
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveKeysView.swift; sourceTree = "<group>"; };
4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; };
4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
4C30AC7329A5680900E2BD5A /* EventGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGroupView.swift; sourceTree = "<group>"; };
4C30AC7529A5770900E2BD5A /* NotificationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemView.swift; sourceTree = "<group>"; };
4C30AC7729A577AB00E2BD5A /* EventCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventCache.swift; sourceTree = "<group>"; };
4C30AC7F29A6A53F00E2BD5A /* ProfilePicturesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePicturesView.swift; sourceTree = "<group>"; };
4C363A8328233689006E126D /* Parser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = "<group>"; };
4C363A8728236948006E126D /* BlocksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlocksView.swift; sourceTree = "<group>"; };
4C363A8928236B57006E126D /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = "<group>"; };
@@ -392,6 +443,9 @@
4C42812B298C848200DBF26F /* TranslateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslateView.swift; sourceTree = "<group>"; };
4C477C9D282C3A4800033AA3 /* TipCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipCounter.swift; sourceTree = "<group>"; };
4C4A3A5A288A1B2200453788 /* damus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = damus.entitlements; sourceTree = "<group>"; };
4C54AA0629A540BA003E4487 /* NotificationsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsModel.swift; sourceTree = "<group>"; };
4C54AA0929A55429003E4487 /* EventGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGroup.swift; sourceTree = "<group>"; };
4C54AA0B29A5543C003E4487 /* ZapGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapGroup.swift; sourceTree = "<group>"; };
4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHomeModel.swift; sourceTree = "<group>"; };
4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsView.swift; sourceTree = "<group>"; };
4C5F9113283D694D0052CD1C /* FollowTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowTarget.swift; sourceTree = "<group>"; };
@@ -421,6 +475,8 @@
4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Tests.swift; sourceTree = "<group>"; };
4C987B56283FD07F0042CE38 /* FollowersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersModel.swift; sourceTree = "<group>"; };
4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatroomMetadata.swift; sourceTree = "<group>"; };
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapView.swift; sourceTree = "<group>"; };
4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeAnonPfpView.swift; sourceTree = "<group>"; };
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
4CAAD8AC298851D000060CEA /* AccountDeletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletion.swift; sourceTree = "<group>"; };
4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConfigView.swift; sourceTree = "<group>"; };
@@ -455,6 +511,9 @@
4CC7AAF7297F1CEE00430951 /* EventProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfile.swift; sourceTree = "<group>"; };
4CC7AAF9297F64AC00430951 /* EventMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMenu.swift; sourceTree = "<group>"; };
4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; };
4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventHolder.swift; sourceTree = "<group>"; };
4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreButton.swift; sourceTree = "<group>"; };
4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InnerTimelineView.swift; sourceTree = "<group>"; };
4CE4F8CC281352B30009DFBB /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigView.swift; sourceTree = "<group>"; };
4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldAlert.swift; sourceTree = "<group>"; };
@@ -501,21 +560,26 @@
4CF0ABEF29857E9200D66079 /* Bech32Object.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Object.swift; sourceTree = "<group>"; };
4CF0ABF52985CD5500D66079 /* UserSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSearch.swift; sourceTree = "<group>"; };
4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; };
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; };
5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; };
5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
6439E013296790CF0020672B /* ProfileZoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZoomView.swift; sourceTree = "<group>"; };
643EA5C7296B764E005081BB /* RelayFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterView.swift; sourceTree = "<group>"; };
647D9A8C2968520300A295DE /* SideMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = "<group>"; };
64FBD06E296255C400D9D3B2 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
7C45AE70297353390031D7BC /* KFImageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFImageModel.swift; sourceTree = "<group>"; };
7C60CAEE298471A1009C80D6 /* CoreSVG.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreSVG.swift; sourceTree = "<group>"; };
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomableScrollView.swift; sourceTree = "<group>"; };
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KFOptionSetter+.swift"; sourceTree = "<group>"; };
7CFF6316299FEFE5005D382A /* SelectableText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableText.swift; sourceTree = "<group>"; };
9609F057296E220800069BF3 /* BannerImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerImageView.swift; sourceTree = "<group>"; };
9C83F89229A937B900136C08 /* TextViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewWrapper.swift; sourceTree = "<group>"; };
BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; };
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; };
DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownTests.swift; sourceTree = "<group>"; };
E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = "<group>"; };
E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadV2View.swift; sourceTree = "<group>"; };
F75BA12C29A1855400E10810 /* BookmarksManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksManager.swift; sourceTree = "<group>"; };
F75BA12E29A18EF500E10810 /* BookmarksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksView.swift; sourceTree = "<group>"; };
F7908E91298B0F0700AB113A /* RelayDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayDetailView.swift; sourceTree = "<group>"; };
F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIPURLBuilder.swift; sourceTree = "<group>"; };
F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToDismiss.swift; sourceTree = "<group>"; };
@@ -624,6 +688,7 @@
4C0A3F8D280F63FF000448DE /* Models */ = {
isa = PBXGroup;
children = (
4C54AA0829A55416003E4487 /* Notifications */,
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */,
4C0A3F8E280F640A000448DE /* ThreadModel.swift */,
4C0A3F92280F66F5000448DE /* ReplyMap.swift */,
@@ -644,6 +709,7 @@
4C63334F283D40E500B1C9C3 /* HomeModel.swift */,
4C633351283D419F00B1C9C3 /* SignalModel.swift */,
4C5F9113283D694D0052CD1C /* FollowTarget.swift */,
F75BA12C29A1855400E10810 /* BookmarksManager.swift */,
4C5F9115283D855D0052CD1C /* EventsModel.swift */,
4C5F9117283D88E40052CD1C /* FollowingModel.swift */,
4C987B56283FD07F0042CE38 /* FollowersModel.swift */,
@@ -655,7 +721,6 @@
BA693073295D649800ADDB87 /* UserSettingsStore.swift */,
4FE60CDC295E1C5E00105A1F /* Wallet.swift */,
4CB88392296F798300DC99E7 /* ReactionsModel.swift */,
7C45AE70297353390031D7BC /* KFImageModel.swift */,
4CF0ABD32980996B00D66079 /* Report.swift */,
4CF0ABDD2981A69500D66079 /* MutelistModel.swift */,
3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */,
@@ -663,13 +728,36 @@
3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */,
4CE8795A2996C47A00F758CC /* ZapsModel.swift */,
3AA59D1C2999B0400061C48E /* DraftsModel.swift */,
4C54AA0629A540BA003E4487 /* NotificationsModel.swift */,
);
path = Models;
sourceTree = "<group>";
};
4C30AC7029A5676F00E2BD5A /* Notifications */ = {
isa = PBXGroup;
children = (
4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */,
4C30AC7329A5680900E2BD5A /* EventGroupView.swift */,
4C30AC7529A5770900E2BD5A /* NotificationItemView.swift */,
4C30AC7F29A6A53F00E2BD5A /* ProfilePicturesView.swift */,
);
path = Notifications;
sourceTree = "<group>";
};
4C54AA0829A55416003E4487 /* Notifications */ = {
isa = PBXGroup;
children = (
4C54AA0929A55429003E4487 /* EventGroup.swift */,
4C54AA0B29A5543C003E4487 /* ZapGroup.swift */,
);
path = Notifications;
sourceTree = "<group>";
};
4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup;
children = (
4C30AC7029A5676F00E2BD5A /* Notifications */,
4CE0E2B029A3DF4700DB4CA2 /* Timeline */,
4CE879562996C44A00F758CC /* Zaps */,
4CB9D4A52992D01900A9A7E4 /* Profile */,
4CAAD8AE29888A9B00060CEA /* Relays */,
@@ -681,6 +769,7 @@
4CB88387296AF97C00DC99E7 /* ActionBar */,
4CE4F9E228528C5200C00DD9 /* AddRelayView.swift */,
4C363A8728236948006E126D /* BlocksView.swift */,
F75BA12E29A18EF500E10810 /* BookmarksView.swift */,
4C285C8128385570008A31F1 /* CarouselView.swift */,
4C0A3F8B280F5FCA000448DE /* ChatroomView.swift */,
4C0A3F90280F6528000448DE /* ChatView.swift */,
@@ -700,6 +789,7 @@
4C363A8D28236FE4006E126D /* NoteContentView.swift */,
4C75EFAC28049CFB0006080F /* PostButton.swift */,
4C75EFA327FA577B0006080F /* PostView.swift */,
9C83F89229A937B900136C08 /* TextViewWrapper.swift */,
4CEE2AF8280B2EAC00AB5EEF /* PowView.swift */,
4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */,
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */,
@@ -780,6 +870,11 @@
4CB883A72975FC1800DC99E7 /* Zaps.swift */,
4CB883B5297730E400DC99E7 /* LNUrls.swift */,
3AB72AB8298ECF30004BB58C /* Translator.swift */,
4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */,
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */,
4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */,
3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */,
4C30AC7729A577AB00E2BD5A /* EventCache.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -821,6 +916,7 @@
children = (
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */,
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */,
4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */,
);
path = Profile;
sourceTree = "<group>";
@@ -842,6 +938,15 @@
path = Events;
sourceTree = "<group>";
};
4CE0E2B029A3DF4700DB4CA2 /* Timeline */ = {
isa = PBXGroup;
children = (
4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */,
4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */,
);
path = Timeline;
sourceTree = "<group>";
};
4CE4F9DF285287A000C00DD9 /* Components */ = {
isa = PBXGroup;
children = (
@@ -860,6 +965,7 @@
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */,
4CB883AF297705DD00DC99E7 /* ZapButton.swift */,
4C42812B298C848200DBF26F /* TranslateView.swift */,
7CFF6316299FEFE5005D382A /* SelectableText.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -919,6 +1025,7 @@
4CE6DEF627F7A08200C66700 /* damusTests */ = {
isa = PBXGroup;
children = (
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */,
DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */,
4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */,
4C363A9F2828A8DD006E126D /* LikeTests.swift */,
@@ -930,6 +1037,10 @@
4CF0ABDB2981A19E00D66079 /* ListTests.swift */,
4CB883A9297612FF00DC99E7 /* ZapTests.swift */,
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */,
3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */,
3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */,
3A3040F229A91366008A0F29 /* ProfileViewTests.swift */,
3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */,
);
path = damusTests;
sourceTree = "<group>";
@@ -964,6 +1075,7 @@
isa = PBXGroup;
children = (
4CE879572996C45300F758CC /* ZapsView.swift */,
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */,
);
path = Zaps;
sourceTree = "<group>";
@@ -1116,6 +1228,11 @@
"zh-CN",
"el-GR",
ja,
id,
cs,
ru,
"zh-HK",
"zh-TW",
);
mainGroup = 4CE6DEDA27F7A08100C66700;
packageReferences = (
@@ -1172,6 +1289,7 @@
4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */,
4C363A8A28236B57006E126D /* MentionView.swift in Sources */,
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */,
4C30AC7829A577AB00E2BD5A /* EventCache.swift in Sources */,
4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */,
4C216F34286F5ACD00040376 /* DMView.swift in Sources */,
4C3EA64428FF558100C48A62 /* sha256.c in Sources */,
@@ -1184,6 +1302,7 @@
4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */,
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */,
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */,
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */,
4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */,
@@ -1195,8 +1314,10 @@
4C363AA228296A7E006E126D /* SearchView.swift in Sources */,
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */,
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */,
4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */,
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */,
F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */,
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */,
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */,
4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */,
@@ -1210,6 +1331,8 @@
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */,
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */,
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */,
4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */,
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */,
4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */,
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
@@ -1223,6 +1346,7 @@
4C649844285A952100EAE2B3 /* LocalUserConfig.swift in Sources */,
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
4C30AC7629A5770900E2BD5A /* NotificationItemView.swift in Sources */,
4C363A8428233689006E126D /* Parser.swift in Sources */,
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */,
4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */,
@@ -1234,6 +1358,7 @@
E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */,
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */,
4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */,
9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */,
4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */,
4CB883A82975FC1800DC99E7 /* Zaps.swift in Sources */,
4C75EFB128049D510006080F /* NostrResponse.swift in Sources */,
@@ -1242,11 +1367,12 @@
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */,
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */,
4C285C8228385570008A31F1 /* CarouselView.swift in Sources */,
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */,
F75BA12D29A1855400E10810 /* BookmarksManager.swift in Sources */,
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */,
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */,
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */,
7C45AE71297353390031D7BC /* KFImageModel.swift in Sources */,
4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */,
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
4CC7AAF2297F129C00430951 /* EmbeddedEventView.swift in Sources */,
@@ -1254,6 +1380,7 @@
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */,
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */,
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */,
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */,
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */,
@@ -1270,15 +1397,19 @@
4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */,
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */,
4CE879582996C45300F758CC /* ZapsView.swift in Sources */,
4C30AC7429A5680900E2BD5A /* EventGroupView.swift in Sources */,
4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */,
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */,
4C363A94282704FA006E126D /* Post.swift in Sources */,
4C216F32286E388800040376 /* DMChatView.swift in Sources */,
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */,
4C54AA0A29A55429003E4487 /* EventGroup.swift in Sources */,
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */,
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */,
@@ -1299,6 +1430,7 @@
4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */,
4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */,
4CF0ABD82981980C00D66079 /* Lists.swift in Sources */,
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */,
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */,
@@ -1327,6 +1459,8 @@
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */,
4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */,
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */,
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
@@ -1342,6 +1476,7 @@
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
4C06670B28FDE64700038D2A /* damus.c in Sources */,
4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */,
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */,
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */,
@@ -1350,6 +1485,7 @@
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */,
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */,
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */,
4C75EFB528049D790006080F /* Relay.swift in Sources */,
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */,
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
@@ -1365,8 +1501,11 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */,
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */,
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */,
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */,
3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */,
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */,
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */,
@@ -1374,7 +1513,9 @@
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */,
4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */,
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */,
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */,
4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */,
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */,
4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1421,6 +1562,11 @@
3A5CAE1F298DC0DB00B5334F /* zh-CN */,
3A25EF152992DA5D008ABE69 /* el-GR */,
3A66D929299472FA008B44F4 /* ja */,
3A41E55B299D52BE001FA465 /* id */,
3A8624DB299E82BE00BD8BE9 /* cs */,
3A827A1A299FC69D00C4D171 /* ru */,
3A3040FB29A91F03008A0F29 /* zh-HK */,
3A3040FD29A91F31008A0F29 /* zh-TW */,
);
name = Localizable.stringsdict;
sourceTree = "<group>";
@@ -1441,6 +1587,11 @@
3A5CAE1D298DC0DB00B5334F /* zh-CN */,
3A25EF132992DA5D008ABE69 /* el-GR */,
3A66D927299472FA008B44F4 /* ja */,
3A41E559299D52BE001FA465 /* id */,
3A8624D9299E82BE00BD8BE9 /* cs */,
3A827A18299FC69D00C4D171 /* ru */,
3A3040F929A91ED6008A0F29 /* zh-HK */,
3A3040FC29A91F31008A0F29 /* zh-TW */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
@@ -1461,6 +1612,12 @@
3A5CAE1E298DC0DB00B5334F /* zh-CN */,
3A25EF142992DA5D008ABE69 /* el-GR */,
3A66D928299472FA008B44F4 /* ja */,
3A41E55A299D52BE001FA465 /* id */,
3A8624DA299E82BE00BD8BE9 /* cs */,
3A827A19299FC69D00C4D171 /* ru */,
3A3040FA29A91EFC008A0F29 /* zh-HK */,
3A3040FE29A91F31008A0F29 /* zh-TW */,
3A3040FF29AB02D1008A0F29 /* en-US */,
);
name = Localizable.strings;
sourceTree = "<group>";
@@ -1596,7 +1753,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 10;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -1638,7 +1795,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 10;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xC5",
"green" : "0x43",
"red" : "0xCC"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x00",
"green" : "0x00",
"red" : "0x00"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0x4D",
"red" : "0x4B"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x1E",
"green" : "0x1C",
"red" : "0x1C"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x4F",
"green" : "0xC3",
"red" : "0x66"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xF4",
"green" : "0xEE",
"red" : "0xEE"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x5F",
"green" : "0x5F",
"red" : "0x5F"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xC5",
"green" : "0x43",
"red" : "0xCC"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -11,24 +11,6 @@
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFF"
}
},
"idiom" : "universal"
}
],
"info" : {
@@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "ic-copy.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 B

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

Before

Width:  |  Height:  |  Size: 400 B

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

Before

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 B

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

Before

Width:  |  Height:  |  Size: 950 B

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

Before

Width:  |  Height:  |  Size: 252 B

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

Before

Width:  |  Height:  |  Size: 458 B

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

Before

Width:  |  Height:  |  Size: 671 B

+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "lnlink.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "damus-nobg.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "muun.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "phoenix.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "river.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "strike.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "undercover.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "walletofsatoshi.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "zebedee.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+1 -10
View File
@@ -2,16 +2,7 @@
"images" : [
{
"filename" : "zeus.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"idiom" : "universal"
}
],
"info" : {
+13 -46
View File
@@ -66,20 +66,11 @@ struct ImageContextMenuModifier: ViewModifier {
private struct ImageContainerView: View {
@ObservedObject var imageModel: KFImageModel
let url: URL?
@State private var image: UIImage?
@State private var showShareSheet = false
init(url: URL?) {
self.imageModel = KFImageModel(
url: url,
fallbackUrl: nil,
maxByteSize: 2000000, // 2 MB
downsampleSize: CGSize(width: 400, height: 400)
)
}
private struct ImageHandler: ImageModifier {
@Binding var handler: UIImage?
@@ -91,30 +82,17 @@ private struct ImageContainerView: View {
var body: some View {
KFAnimatedImage(imageModel.url)
.callbackQueue(.dispatch(.global(qos: .background)))
.processingQueue(.dispatch(.global(qos: .background)))
.cacheOriginalImage()
KFAnimatedImage(url)
.imageContext(.note)
.configure { view in
view.framePreloadCount = 1
view.framePreloadCount = 3
}
.scaleFactor(UIScreen.main.scale)
.loadDiskFileSynchronously()
.fade(duration: 0.1)
.imageModifier(ImageHandler(handler: $image))
.onFailure { _ in
imageModel.downloadFailed()
}
.id(imageModel.refreshID)
.clipped()
.modifier(ImageContextMenuModifier(url: imageModel.url, image: image, showShareSheet: $showShareSheet))
.modifier(ImageContextMenuModifier(url: url, image: image, showShareSheet: $showShareSheet))
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: [imageModel.url])
ShareSheet(activityItems: [url])
}
// TODO: Update ImageCarousel with serializer and processor
// .serialize(by: imageModel.serializer)
// .setProcessor(imageModel.processor)
}
}
@@ -127,14 +105,6 @@ struct ImageView: View {
@State private var selectedIndex = 0
@State var showMenu = true
var safeAreaInsets: UIEdgeInsets? {
return UIApplication
.shared
.connectedScenes
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
.first { $0.isKeyWindow }?.safeAreaInsets
}
var navBarView: some View {
VStack {
HStack {
@@ -180,8 +150,8 @@ struct ImageView: View {
ZoomableScrollView {
ImageContainerView(url: urls[index])
.aspectRatio(contentMode: .fit)
.padding(.top, safeAreaInsets?.top)
.padding(.bottom, safeAreaInsets?.bottom)
.padding(.top, Theme.safeAreaInsets?.top)
.padding(.bottom, Theme.safeAreaInsets?.bottom)
}
.modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
presentationMode.wrappedValue.dismiss()
@@ -210,7 +180,7 @@ struct ImageView: View {
}
}
.animation(.easeInOut, value: showMenu)
.padding(.bottom, safeAreaInsets?.bottom)
.padding(.bottom, Theme.safeAreaInsets?.bottom)
)
}
}
@@ -229,16 +199,13 @@ struct ImageCarousel: View {
.foregroundColor(Color.clear)
.overlay {
KFAnimatedImage(url)
.callbackQueue(.dispatch(.global(qos: .background)))
.processingQueue(.dispatch(.global(qos: .background)))
.cacheOriginalImage()
.loadDiskFileSynchronously()
.scaleFactor(UIScreen.main.scale)
.fade(duration: 0.1)
.imageContext(.note)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
}
.aspectRatio(contentMode: .fit)
.cornerRadius(10)
.tabItem {
Text(url.absoluteString)
}
@@ -251,11 +218,11 @@ struct ImageCarousel: View {
}
}
}
.cornerRadius(10)
.fullScreenCover(isPresented: $open_sheet) {
ImageView(urls: urls)
}
.frame(height: 200)
.clipped()
.onTapGesture {
open_sheet = true
}
+96
View File
@@ -0,0 +1,96 @@
//
// SelectableText.swift
// damus
//
// Created by Oleg Abalonski on 2/16/23.
//
import UIKit
import SwiftUI
struct SelectableText: View {
let attributedString: AttributedString
@State private var selectedTextHeight: CGFloat = .zero
@State private var selectedTextWidth: CGFloat = .zero
var body: some View {
GeometryReader { geo in
TextViewRepresentable(
attributedString: attributedString,
textColor: UIColor.label,
font: UIFont.preferredFont(forTextStyle: .title2),
fixedWidth: selectedTextWidth,
height: $selectedTextHeight
)
.onAppear {
self.selectedTextWidth = geo.size.width
}
.onChange(of: geo.size) { newSize in
self.selectedTextWidth = newSize.width
}
}
.frame(height: selectedTextHeight)
}
}
fileprivate struct TextViewRepresentable: UIViewRepresentable {
let attributedString: AttributedString
let textColor: UIColor
let font: UIFont
let fixedWidth: CGFloat
@Binding var height: CGFloat
func makeUIView(context: UIViewRepresentableContext<Self>) -> UITextView {
let view = UITextView()
view.isEditable = false
view.dataDetectorTypes = .all
view.isSelectable = true
view.textContainer.lineFragmentPadding = 0
view.textContainerInset = .zero
return view
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<Self>) {
let mutableAttributedString = createNSAttributedString()
uiView.attributedText = mutableAttributedString
let newHeight = mutableAttributedString.height(containerWidth: fixedWidth)
DispatchQueue.main.async {
height = newHeight
}
}
func createNSAttributedString() -> NSMutableAttributedString {
let mutableAttributedString = NSMutableAttributedString(attributedString)
let myAttribute = [
NSAttributedString.Key.font: font,
NSAttributedString.Key.foregroundColor: textColor
]
mutableAttributedString.addAttributes(
myAttribute,
range: NSRange.init(location: 0, length: mutableAttributedString.length)
)
return mutableAttributedString
}
}
fileprivate extension NSAttributedString {
func height(containerWidth: CGFloat) -> CGFloat {
let rect = self.boundingRect(
with: CGSize.init(width: containerWidth, height: CGFloat.greatestFiniteMagnitude),
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil
)
return ceil(rect.size.height)
}
}
+2 -5
View File
@@ -11,7 +11,6 @@ import NaturalLanguage
struct TranslateView: View {
let damus_state: DamusState
let event: NostrEvent
let size: EventViewKind
@State var checkingTranslationStatus: Bool = false
@State var currentLanguage: String = "en"
@@ -34,9 +33,7 @@ struct TranslateView: View {
}
.translate_button_style()
Text(artifacts.content)
.font(eventviewsize_to_font(size))
.fixedSize(horizontal: false, vertical: true)
SelectableText(attributedString: artifacts.content)
}
}
@@ -143,6 +140,6 @@ struct TranslateView: View {
struct TranslateView_Previews: PreviewProvider {
static var previews: some View {
let ds = test_damus_state()
TranslateView(damus_state: ds, event: test_event, size: .selected)
TranslateView(damus_state: ds, event: test_event)
}
}
+1 -5
View File
@@ -12,11 +12,7 @@ struct UserView: View {
let pubkey: String
var body: some View {
let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state)
let followers = FollowersModel(damus_state: damus_state, target: pubkey)
let pv = ProfileView(damus_state: damus_state, profile: pmodel, followers: followers)
NavigationLink(destination: pv) {
NavigationLink(destination: ProfileView(damus_state: damus_state, pubkey: pubkey)) {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
VStack(alignment: .leading) {
+126 -67
View File
@@ -7,6 +7,22 @@
import SwiftUI
enum ZappingEventType {
case failed(ZappingError)
case got_zap_invoice(String)
}
enum ZappingError {
case fetching_invoice
case bad_lnurl
}
struct ZappingEvent {
let is_custom: Bool
let type: ZappingEventType
let event: NostrEvent
}
struct ZapButton: View {
let damus_state: DamusState
let event: NostrEvent
@@ -19,61 +35,8 @@ struct ZapButton: View {
@State var slider_value: Double = 0.0
@State var slider_visible: Bool = false
@State var showing_select_wallet: Bool = false
func send_zap() {
guard let privkey = damus_state.keypair.privkey else {
return
}
// Only take the first 10 because reasons
let relays = Array(damus_state.pool.descriptors.prefix(10))
let target = ZapTarget.note(id: event.id, author: event.pubkey)
// TODO: gather comment?
let content = ""
let zapreq = make_zap_request_event(pubkey: damus_state.pubkey, privkey: privkey, content: content, relays: relays, target: target)
zapping = true
Task {
var mpayreq = damus_state.lnurls.lookup(target.pubkey)
if mpayreq == nil {
mpayreq = await fetch_static_payreq(lnurl)
}
guard let payreq = mpayreq else {
// TODO: show error
DispatchQueue.main.async {
zapping = false
}
return
}
DispatchQueue.main.async {
damus_state.lnurls.endpoints[target.pubkey] = payreq
}
let zap_amount = get_default_zap_amount(pubkey: damus_state.pubkey) ?? 1000
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount) else {
DispatchQueue.main.async {
zapping = false
}
return
}
DispatchQueue.main.async {
zapping = false
if should_show_wallet_selector(damus_state.pubkey) {
self.invoice = inv
self.showing_select_wallet = true
} else {
open_with_wallet(wallet: get_default_wallet(damus_state.pubkey).model, invoice: inv)
}
}
}
//damus_state.pool.send(.event(zapreq))
}
@State var showing_zap_customizer: Bool = false
@State var is_charging: Bool = false
var zap_img: String {
if bar.zapped {
@@ -92,6 +55,10 @@ struct ZapButton: View {
return Color.orange
}
if is_charging {
return Color.yellow
}
if !zapping {
return nil
}
@@ -100,24 +67,63 @@ struct ZapButton: View {
}
var body: some View {
ZStack {
EventActionButton(img: zap_img, col: zap_color) {
if bar.zapped {
//notify(.delete, bar.our_tip)
} else if !zapping {
send_zap()
HStack(spacing: 4) {
Image(systemName: zap_img)
.foregroundColor(zap_color == nil ? Color.gray : zap_color!)
.font(.footnote.weight(.medium))
.onTapGesture {
if bar.zapped {
//notify(.delete, bar.our_tip)
} else if !zapping {
self.showing_zap_customizer = true
//send_zap(damus_state: damus_state, event: event, lnurl: lnurl, is_custom: false)
//self.zapping = true
}
}
.onLongPressGesture(minimumDuration: 0, pressing: { is_charing in
self.is_charging = is_charging
}, perform: {
self.showing_zap_customizer = true
})
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
if bar.zap_total > 0 {
Text(verbatim: format_msats_abbrev(bar.zap_total))
.font(.footnote)
.foregroundColor(bar.zapped ? Color.orange : Color.gray)
}
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
Text("\(bar.zap_total > 0 ? "\(format_msats_abbrev(bar.zap_total))" : "")")
.offset(x: 22)
.font(.footnote)
.foregroundColor(bar.zapped ? Color.orange : Color.gray)
}
.sheet(isPresented: $showing_zap_customizer) {
CustomizeZapView(state: damus_state, event: event, lnurl: lnurl)
}
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: invoice)
}
.onReceive(handle_notify(.zapping)) { notif in
let zap_ev = notif.object as! ZappingEvent
guard zap_ev.event.id == self.event.id else {
return
}
guard !zap_ev.is_custom else {
return
}
switch zap_ev.type {
case .failed:
break
case .got_zap_invoice(let inv):
if should_show_wallet_selector(damus_state.pubkey) {
self.invoice = inv
self.showing_select_wallet = true
} else {
open_with_wallet(wallet: get_default_wallet(damus_state.pubkey).model, invoice: inv)
}
}
self.zapping = false
}
}
}
@@ -129,3 +135,56 @@ struct ZapButton_Previews: PreviewProvider {
}
}
func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) {
guard let keypair = damus_state.keypair.to_full() else {
return
}
// Only take the first 10 because reasons
let relays = Array(damus_state.pool.descriptors.prefix(10))
let target = ZapTarget.note(id: event.id, author: event.pubkey)
let content = comment ?? ""
let zapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type)
Task {
var mpayreq = damus_state.lnurls.lookup(target.pubkey)
if mpayreq == nil {
mpayreq = await fetch_static_payreq(lnurl)
}
guard let payreq = mpayreq else {
// TODO: show error
DispatchQueue.main.async {
let typ = ZappingEventType.failed(.bad_lnurl)
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
notify(.zapping, ev)
}
return
}
DispatchQueue.main.async {
damus_state.lnurls.endpoints[target.pubkey] = payreq
}
let zap_amount = amount_sats ?? get_default_zap_amount(pubkey: damus_state.pubkey) ?? 1000
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount, zap_type: zap_type, comment: comment) else {
DispatchQueue.main.async {
let typ = ZappingEventType.failed(.fetching_invoice)
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
notify(.zapping, ev)
}
return
}
DispatchQueue.main.async {
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), event: event)
notify(.zapping, ev)
}
}
return
}
+10 -9
View File
@@ -92,7 +92,7 @@ struct ContentView: View {
@State var filter_state : FilterState = .posts_and_replies
@State private var isSideBarOpened = false
@StateObject var home: HomeModel = HomeModel()
// connect retry timer
let timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect()
@@ -136,7 +136,7 @@ struct ContentView: View {
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
ZStack {
if let damus = self.damus_state {
TimelineView(events: $home.events, loading: $home.loading, damus: damus, show_friend_icon: false, filter: filter)
TimelineView(events: home.events, loading: $home.loading, damus: damus, show_friend_icon: false, filter: filter)
}
}
}
@@ -163,10 +163,10 @@ struct ContentView: View {
Text("Notifications", comment: "Toolbar label for Notifications view.")
.bold()
case .search:
Text("Global", comment: "Toolbar label for Global view where posts from all connected relay servers appear.")
Text("Universe 🛸", comment: "Toolbar label for the universal view where posts from all connected relay servers appear.")
.bold()
case .none:
Text("", comment: "Toolbar label for unknown views. This label would be displayed only if a new timeline view is added but a toolbar label was not explicitly assigned to it yet.")
Text(verbatim: "")
}
}
}
@@ -192,7 +192,7 @@ struct ContentView: View {
case .notifications:
VStack(spacing: 0) {
Divider()
TimelineView(events: $home.notifications, loading: $home.loading, damus: damus, show_friend_icon: true, filter: { _ in true })
NotificationsView(state: damus, notifications: home.notifications)
}
case .dms:
DirectMessagesView(damus_state: damus_state!)
@@ -202,7 +202,7 @@ struct ContentView: View {
EmptyView()
}
}
.navigationBarTitle(selected_timeline == .home ? NSLocalizedString("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.") : NSLocalizedString("Global", comment: "Navigation bar title for Global view where posts from all connected relay servers appear."), displayMode: .inline)
.navigationBarTitle(selected_timeline == .home ? NSLocalizedString("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.") : NSLocalizedString("Universe 🛸", comment: "Navigation bar title for universal view where posts from all connected relay servers appear."), displayMode: .inline)
.toolbar {
ToolbarItem(placement: .principal) {
timelineNavItem
@@ -295,7 +295,7 @@ struct ContentView: View {
self.active_sheet = .filter
}) {
// checklist, checklist.checked, lisdt.bullet, list.bullet.circle, line.3.horizontal.decrease..., line.3.horizontail.decrease
Label("Filter", systemImage: "line.3.horizontal.decrease")
Label(NSLocalizedString("Filter", comment: "Button label text for filtering relay servers."), systemImage: "line.3.horizontal.decrease")
.foregroundColor(.gray)
//.contentShape(Rectangle())
}
@@ -328,7 +328,7 @@ struct ContentView: View {
PostView(replying_to: nil, references: [], damus_state: damus_state!)
case .reply(let event):
ReplyView(replying_to: event, damus: damus_state!)
case .event(let event):
case .event:
EventDetailView()
case .filter:
let timeline = selected_timeline ?? .home
@@ -615,7 +615,8 @@ struct ContentView: View {
settings: UserSettingsStore(),
relay_filters: relay_filters,
relay_metadata: metadatas,
drafts: Drafts()
drafts: Drafts(),
events: EventCache()
)
home.damus_state = self.damus_state!
+50
View File
@@ -0,0 +1,50 @@
//
// BookmarksManager.swift
// damus
//
// Created by Joel Klabo on 2/18/23.
//
import Foundation
class BookmarksManager {
private let userDefaults = UserDefaults.standard
private let pubkey: String
init(pubkey: String) {
self.pubkey = pubkey
}
var bookmarks: [String] {
get {
return userDefaults.stringArray(forKey: storageKey()) ?? []
}
set {
let uniqueBookmarks = Array(Set(newValue))
if uniqueBookmarks != bookmarks {
userDefaults.set(uniqueBookmarks, forKey: storageKey())
}
}
}
func isBookmarked(_ string: String) -> Bool {
return bookmarks.contains(string)
}
func updateBookmark(_ string: String) {
if isBookmarked(string) {
bookmarks = bookmarks.filter { $0 != string }
} else {
bookmarks.append(string)
}
}
func clearAll() {
bookmarks = []
}
private func storageKey() -> String {
pk_setting_key(pubkey, key: "bookmarks")
}
}
+2 -2
View File
@@ -24,6 +24,7 @@ struct DamusState {
let relay_filters: RelayFilters
let relay_metadata: RelayMetadatas
let drafts: Drafts
let events: EventCache
var pubkey: String {
return keypair.pubkey
@@ -32,9 +33,8 @@ struct DamusState {
var is_privkey_user: Bool {
keypair.privkey != nil
}
static var empty: DamusState {
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts())
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache())
}
}
+2 -2
View File
@@ -8,6 +8,6 @@
import Foundation
class Drafts: ObservableObject {
@Published var post: String = ""
@Published var replies: [NostrEvent: String] = [:]
@Published var post: NSMutableAttributedString = NSMutableAttributedString(string: "")
@Published var replies: [NostrEvent: NSMutableAttributedString] = [:]
}
+56 -2
View File
@@ -9,9 +9,63 @@ import Foundation
class EventsModel: ObservableObject {
var has_event: Set<String> = Set()
let state: DamusState
let target: String
let kind: NostrKind
let sub_id = UUID().uuidString
let profiles_id = UUID().uuidString
@Published var events: [NostrEvent] = []
init() {
init(state: DamusState, target: String, kind: NostrKind) {
self.state = state
self.target = target
self.kind = kind
}
private func get_filter() -> NostrFilter {
var filter = NostrFilter.filter_kinds([kind.rawValue])
filter.referenced_ids = [target]
filter.limit = 500
return filter
}
func subscribe() {
state.pool.subscribe(sub_id: sub_id,
filters: [get_filter()],
handler: handle_nostr_event)
}
func unsubscribe() {
state.pool.unsubscribe(sub_id: sub_id)
}
private func handle_event(relay_id: String, ev: NostrEvent) {
guard ev.kind == kind.rawValue else {
return
}
guard last_etag(tags: ev.tags) == target else {
return
}
if insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { a, b in a.created_at < b.created_at } ) {
objectWillChange.send()
}
}
func handle_nostr_event(relay_id: String, ev: NostrConnectionEvent) {
guard case .nostr_event(let nev) = ev else {
return
}
switch nev {
case .event(_, let ev):
handle_event(relay_id: relay_id, ev: ev)
case .notice(_):
break
case .eose(_):
load_profiles(profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state)
}
}
}
+96 -41
View File
@@ -38,6 +38,9 @@ class HomeModel: ObservableObject {
var channels: [String: NostrEvent] = [:]
var last_event_of_kind: [String: [Int: NostrEvent]] = [:]
var done_init: Bool = false
var incoming_dms: [NostrEvent] = []
let dm_debouncer = Debouncer(interval: 0.5)
var should_debounce_dms = true
let home_subid = UUID().description
let contacts_subid = UUID().description
@@ -47,25 +50,33 @@ class HomeModel: ObservableObject {
let profiles_subid = UUID().description
@Published var new_events: NewEventsBits = NewEventsBits()
@Published var notifications: [NostrEvent] = []
@Published var notifications = NotificationsModel()
@Published var dms: DirectMessagesModel
@Published var events: [NostrEvent] = []
@Published var events = EventHolder()
@Published var loading: Bool = false
@Published var signal: SignalModel = SignalModel()
init() {
self.damus_state = DamusState.empty
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
self.dms = DirectMessagesModel(our_pubkey: "")
}
init(damus_state: DamusState) {
self.damus_state = damus_state
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
self.setup_debouncer()
}
var pool: RelayPool {
return damus_state.pool
}
func setup_debouncer() {
// turn off debouncer after initial load
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
self.should_debounce_dms = false
}
}
func has_sub_id_event(sub_id: String, ev_id: String) -> Bool {
if !has_event.keys.contains(sub_id) {
@@ -114,21 +125,23 @@ class HomeModel: ObservableObject {
handle_channel_meta(ev)
case .zap:
handle_zap_event(ev)
case .zap_request:
break
}
}
func handle_zap_event_with_zapper(_ ev: NostrEvent, our_pubkey: String, zapper: String) {
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper) else {
func handle_zap_event_with_zapper(_ ev: NostrEvent, our_keypair: Keypair, zapper: String) {
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
return
}
damus_state.zaps.add_zap(zap: zap)
guard zap.target.pubkey == our_pubkey else {
guard zap.target.pubkey == our_keypair.pubkey else {
return
}
if !insert_uniq_sorted_event(events: &notifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) {
if !notifications.insert_zap(zap) {
return
}
@@ -142,8 +155,9 @@ class HomeModel: ObservableObject {
return
}
let our_keypair = damus_state.keypair
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) {
handle_zap_event_with_zapper(ev, our_pubkey: damus_state.pubkey, zapper: local_zapper)
handle_zap_event_with_zapper(ev, our_keypair: our_keypair, zapper: local_zapper)
return
}
@@ -162,7 +176,7 @@ class HomeModel: ObservableObject {
DispatchQueue.main.async {
self.damus_state.profiles.zappers[ptag] = zapper
self.handle_zap_event_with_zapper(ev, our_pubkey: self.damus_state.pubkey, zapper: zapper)
self.handle_zap_event_with_zapper(ev, our_keypair: our_keypair, zapper: zapper)
}
}
@@ -180,9 +194,9 @@ class HomeModel: ObservableObject {
}
func filter_muted() {
self.events = events.filter { !damus_state.contacts.is_muted($0.pubkey) }
events.filter { !damus_state.contacts.is_muted($0.pubkey) }
self.dms.dms = dms.dms.filter { !damus_state.contacts.is_muted($0.0) }
self.notifications = notifications.filter { !damus_state.contacts.is_muted($0.pubkey) }
notifications.filter { !damus_state.contacts.is_muted($0.pubkey) }
}
func handle_delete_event(_ ev: NostrEvent) {
@@ -214,7 +228,7 @@ class HomeModel: ObservableObject {
guard inner_ev.is_valid else {
return
}
if inner_ev.is_textlike {
handle_text_event(sub_id: sub_id, ev)
}
@@ -240,12 +254,11 @@ class HomeModel: ObservableObject {
return
}
// CHECK SIGS ON THESE
switch damus_state.likes.add_event(ev, target: e.ref_id) {
case .already_counted:
break
case .success(let n):
handle_notification(ev: ev)
let liked = Counted(event: ev, id: e.ref_id, total: n)
notify(.liked, liked)
notify(.update_stats, e.ref_id)
@@ -303,10 +316,11 @@ class HomeModel: ObservableObject {
case .eose(let sub_id):
if sub_id == dms_subid {
let dms = dms.dms.flatMap { $0.1.events }
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: dms, damus_state: damus_state)
var dms = dms.dms.flatMap { $0.1.events }
dms.append(contentsOf: incoming_dms)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(dms), damus_state: damus_state)
} else if sub_id == notifications_subid {
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: notifications, damus_state: damus_state)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_keys(notifications.uniq_pubkeys()), damus_state: damus_state)
}
self.loading = false
@@ -359,7 +373,6 @@ class HomeModel: ObservableObject {
// TODO: separate likes?
var home_filter = NostrFilter.filter_kinds([
NostrKind.text.rawValue,
NostrKind.chat.rawValue,
NostrKind.like.rawValue,
NostrKind.boost.rawValue,
])
@@ -369,7 +382,6 @@ class HomeModel: ObservableObject {
var notifications_filter = NostrFilter.filter_kinds([
NostrKind.text.rawValue,
NostrKind.chat.rawValue,
NostrKind.like.rawValue,
NostrKind.boost.rawValue,
NostrKind.zap.rawValue,
@@ -445,10 +457,19 @@ class HomeModel: ObservableObject {
return
}
if !insert_uniq_sorted_event(events: &notifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) {
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
return
}
damus_state.events.insert(ev)
if let inner_ev = ev.inner_event {
damus_state.events.insert(inner_ev)
}
if !notifications.insert_event(ev) {
return
}
handle_last_event(ev: ev, timeline: .notifications)
}
@@ -458,29 +479,47 @@ class HomeModel: ObservableObject {
}
}
func insert_home_event(_ ev: NostrEvent) -> Bool {
let ok = insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at })
if ok {
func insert_home_event(_ ev: NostrEvent) {
if events.insert(ev) {
handle_last_event(ev: ev, timeline: .home)
}
return ok
}
func handle_text_event(sub_id: String, _ ev: NostrEvent) {
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
return
}
damus_state.events.insert(ev)
if sub_id == home_subid {
let _ = insert_home_event(ev)
insert_home_event(ev)
} else if sub_id == notifications_subid {
handle_notification(ev: ev)
}
}
func handle_dm(_ ev: NostrEvent) {
if let notifs = handle_incoming_dm(contacts: damus_state.contacts, prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, ev: ev) {
self.new_events = notifs
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
return
}
if !should_debounce_dms {
self.incoming_dms.append(ev)
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
self.new_events = notifs
}
self.incoming_dms = []
return
}
incoming_dms.append(ev)
dm_debouncer.debounce {
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
self.new_events = notifs
}
self.incoming_dms = []
}
}
}
@@ -626,14 +665,14 @@ func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEve
// load pfps asap
let picture = tprof.profile.picture ?? robohash(ev.pubkey)
if let _ = URL(string: picture) {
if URL(string: picture) != nil {
DispatchQueue.main.async {
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
}
}
let banner = tprof.profile.banner ?? ""
if let _ = URL(string: banner) {
if URL(string: banner) != nil {
DispatchQueue.main.async {
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
}
@@ -761,14 +800,11 @@ func fetch_relay_metadata(relay_id: String) async throws -> RelayMetadata? {
func process_relay_metadata() {
}
func handle_incoming_dm(contacts: Contacts, prev_events: NewEventsBits, dms: DirectMessagesModel, our_pubkey: String, ev: NostrEvent) -> NewEventsBits? {
// hide blocked users
guard should_show_event(contacts: contacts, ev: ev) else {
return prev_events
}
@discardableResult
func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesModel, prev_events: NewEventsBits) -> (Bool, NewEventsBits?) {
var inserted = false
var found = false
let ours = ev.pubkey == our_pubkey
var i = 0
@@ -795,15 +831,34 @@ func handle_incoming_dm(contacts: Contacts, prev_events: NewEventsBits, dms: Dir
}
if !found {
inserted = true
let model = DirectMessageModel(events: [ev], our_pubkey: our_pubkey)
dms.dms.append((the_pk, model))
inserted = true
}
var new_bits: NewEventsBits? = nil
if inserted {
new_bits = handle_last_events(new_events: prev_events, ev: ev, timeline: .dms, shouldNotify: !ours)
}
return (inserted, new_bits)
}
@discardableResult
func handle_incoming_dms(prev_events: NewEventsBits, dms: DirectMessagesModel, our_pubkey: String, evs: [NostrEvent]) -> NewEventsBits? {
var inserted = false
var new_events: NewEventsBits? = nil
for ev in evs {
let res = handle_incoming_dm(ev: ev, our_pubkey: our_pubkey, dms: dms, prev_events: prev_events)
inserted = res.0 || inserted
if let new = res.1 {
new_events = new
}
}
if inserted {
new_events = handle_last_events(new_events: prev_events, ev: ev, timeline: .dms, shouldNotify: !ours)
dms.dms = dms.dms.filter({ $0.1.events.count > 0 }).sorted { a, b in
return a.1.events.last!.created_at > b.1.events.last!.created_at
}
-114
View File
@@ -1,114 +0,0 @@
//
// KFImageModel.swift
// damus
//
// Created by Oleg Abalonski on 1/11/23.
//
import UIKit
import Kingfisher
class KFImageModel: ObservableObject {
let url: URL?
let fallbackUrl: URL?
let processor: ImageProcessor
let serializer: CacheSerializer
@Published var refreshID = ""
init(url: URL?, fallbackUrl: URL?, maxByteSize: Int, downsampleSize: CGSize) {
self.url = url
self.fallbackUrl = fallbackUrl
self.processor = CustomImageProcessor(maxSize: maxByteSize, downsampleSize: downsampleSize)
self.serializer = CustomCacheSerializer(maxSize: maxByteSize, downsampleSize: downsampleSize)
}
func refresh() -> Void {
DispatchQueue.main.async {
self.refreshID = UUID().uuidString
}
}
func cache(_ image: UIImage, forKey key: String) -> Void {
KingfisherManager.shared.cache.store(image, forKey: key, processorIdentifier: processor.identifier) { _ in
self.refresh()
}
}
func downloadFailed() -> Void {
guard let url = url, let fallbackUrl = fallbackUrl else { return }
DispatchQueue.global(qos: .background).async {
KingfisherManager.shared.downloader.downloadImage(with: fallbackUrl) { result in
var fallbackImage: UIImage {
switch result {
case .success(let imageLoadingResult):
return imageLoadingResult.image
case .failure(let error):
print(error)
return UIImage()
}
}
self.cache(fallbackImage, forKey: url.absoluteString)
}
}
}
}
struct CustomImageProcessor: ImageProcessor {
let maxSize: Int
let downsampleSize: CGSize
let identifier = "com.damus.customimageprocessor"
func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
switch item {
case .image(_):
// This case will never run
return DefaultImageProcessor.default.process(item: item, options: options)
case .data(let data):
// Handle large image size
if data.count > maxSize {
return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
}
// Handle SVG image
if let dataString = String(data: data, encoding: .utf8),
let svg = SVG(dataString) {
let render = UIGraphicsImageRenderer(size: svg.size)
let image = render.image { context in
svg.draw(in: context.cgContext)
}
return image.kf.scaled(to: options.scaleFactor)
}
return DefaultImageProcessor.default.process(item: item, options: options)
}
}
}
struct CustomCacheSerializer: CacheSerializer {
let maxSize: Int
let downsampleSize: CGSize
func data(with image: Kingfisher.KFCrossPlatformImage, original: Data?) -> Data? {
return DefaultCacheSerializer.default.data(with: image, original: original)
}
func image(with data: Data, options: Kingfisher.KingfisherParsedOptionsInfo) -> Kingfisher.KFCrossPlatformImage? {
if data.count > maxSize {
return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
}
return DefaultCacheSerializer.default.image(with: data, options: options)
}
}
+30 -2
View File
@@ -211,6 +211,32 @@ enum Amount: Equatable {
}
}
func format_actions_abbrev(_ actions: Int) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.positiveSuffix = "m"
formatter.positivePrefix = ""
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 3
formatter.roundingMode = .down
formatter.roundingIncrement = 0.1
formatter.multiplier = 1
if actions >= 1_000_000 {
formatter.positiveSuffix = "m"
formatter.multiplier = 0.000001
} else if actions >= 1000 {
formatter.positiveSuffix = "k"
formatter.multiplier = 0.001
} else {
return "\(actions)"
}
let actions = NSNumber(value: actions)
return formatter.string(from: actions) ?? "\(actions)"
}
func format_msats_abbrev(_ msats: Int64) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
@@ -237,17 +263,19 @@ func format_msats_abbrev(_ msats: Int64) -> String {
return formatter.string(from: sats) ?? sats.stringValue
}
func format_msats(_ msat: Int64) -> String {
func format_msats(_ msat: Int64, locale: Locale = Locale.current) -> String {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
numberFormatter.minimumFractionDigits = 0
numberFormatter.maximumFractionDigits = 3
numberFormatter.roundingMode = .down
numberFormatter.locale = locale
let sats = NSNumber(value: (Double(msat) / 1000.0))
let formattedSats = numberFormatter.string(from: sats) ?? sats.stringValue
return String(format: NSLocalizedString("sats_count", comment: "Amount of sats."), sats.decimalValue as NSDecimalNumber, formattedSats)
let bundle = bundleForLocale(locale: locale)
return String(format: bundle.localizedString(forKey: "sats_count", value: nil, table: nil), locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats)
}
func convert_invoice_block(_ b: invoice_block) -> Block? {
@@ -0,0 +1,32 @@
//
// ReactionGroup.swift
// damus
//
// Created by William Casarin on 2023-02-21.
//
import Foundation
class EventGroup {
var events: [NostrEvent]
var last_event_at: Int64 {
guard let first = self.events.first else {
return 0
}
return first.created_at
}
init() {
self.events = []
}
init(events: [NostrEvent]) {
self.events = events
}
func insert(_ ev: NostrEvent) -> Bool {
return insert_uniq_sorted_event_created(events: &events, new_ev: ev)
}
}
+59
View File
@@ -0,0 +1,59 @@
//
// ZapGroup.swift
// damus
//
// Created by William Casarin on 2023-02-21.
//
import Foundation
class ZapGroup {
var zaps: [Zap]
var msat_total: Int64
var zappers: Set<String>
var last_event_at: Int64 {
guard let first = zaps.first else {
return 0
}
return first.event.created_at
}
func zap_requests() -> [NostrEvent] {
zaps.map { z in
if let priv = z.private_request {
return priv
} else {
return z.request.ev
}
}
}
init(zaps: [Zap]) {
self.zaps = zaps
self.msat_total = 0
self.zappers = Set()
}
init() {
self.zaps = []
self.msat_total = 0
self.zappers = Set()
}
func insert(_ zap: Zap) -> Bool {
if !insert_uniq_sorted_zap_by_created(zaps: &zaps, new_zap: zap) {
return false
}
msat_total += zap.invoice.amount
if !zappers.contains(zap.request.ev.pubkey) {
zappers.insert(zap.request.ev.pubkey)
}
return true
}
}
+298
View File
@@ -0,0 +1,298 @@
//
// NotificationsModel.swift
// damus
//
// Created by William Casarin on 2023-02-21.
//
import Foundation
enum NotificationItem {
case repost(String, EventGroup)
case reaction(String, EventGroup)
case profile_zap(ZapGroup)
case event_zap(String, ZapGroup)
case reply(NostrEvent)
var id: String {
switch self {
case .repost(let evid, _):
return "repost_" + evid
case .reaction(let evid, _):
return "reaction_" + evid
case .profile_zap:
return "profile_zap"
case .event_zap(let evid, _):
return "event_zap_" + evid
case .reply(let ev):
return "reply_" + ev.id
}
}
var last_event_at: Int64 {
switch self {
case .reaction(_, let evgrp):
return evgrp.last_event_at
case .repost(_, let evgrp):
return evgrp.last_event_at
case .profile_zap(let zapgrp):
return zapgrp.last_event_at
case .event_zap(_, let zapgrp):
return zapgrp.last_event_at
case .reply(let reply):
return reply.created_at
}
}
}
class NotificationsModel: ObservableObject, ScrollQueue {
var incoming_zaps: [Zap]
var incoming_events: [NostrEvent]
var should_queue: Bool
// mappings from events to
var zaps: [String: ZapGroup]
var profile_zaps: ZapGroup
var reactions: [String: EventGroup]
var reposts: [String: EventGroup]
var replies: [NostrEvent]
var has_reply: Set<String>
@Published var notifications: [NotificationItem]
init() {
self.zaps = [:]
self.reactions = [:]
self.reposts = [:]
self.replies = []
self.has_reply = Set()
self.should_queue = true
self.incoming_zaps = []
self.incoming_events = []
self.profile_zaps = ZapGroup()
self.notifications = []
}
func set_should_queue(_ val: Bool) {
self.should_queue = val
}
func uniq_pubkeys() -> [String] {
var pks = Set<String>()
for ev in incoming_events {
pks.insert(ev.pubkey)
}
for grp in reposts {
for ev in grp.value.events {
pks.insert(ev.pubkey)
}
}
for ev in replies {
pks.insert(ev.pubkey)
}
for zap in incoming_zaps {
pks.insert(zap.request.ev.pubkey)
}
return Array(pks)
}
func build_notifications() -> [NotificationItem] {
var notifs: [NotificationItem] = []
for el in zaps {
let evid = el.key
let zapgrp = el.value
let notif: NotificationItem = .event_zap(evid, zapgrp)
notifs.append(notif)
}
if !profile_zaps.zaps.isEmpty {
notifs.append(.profile_zap(profile_zaps))
}
for el in reposts {
let evid = el.key
let evgrp = el.value
notifs.append(.repost(evid, evgrp))
}
for el in reactions {
let evid = el.key
let evgrp = el.value
notifs.append(.reaction(evid, evgrp))
}
for reply in replies {
notifs.append(.reply(reply))
}
notifs.sort { $0.last_event_at > $1.last_event_at }
return notifs
}
private func insert_repost(_ ev: NostrEvent) -> Bool {
guard let reposted_ev = ev.inner_event else {
return false
}
let id = reposted_ev.id
if let evgrp = self.reposts[id] {
return evgrp.insert(ev)
} else {
let evgrp = EventGroup()
self.reposts[id] = evgrp
return evgrp.insert(ev)
}
}
private func insert_text(_ ev: NostrEvent) -> Bool {
guard !has_reply.contains(ev.id) else {
return false
}
has_reply.insert(ev.id)
replies.append(ev)
return true
}
private func insert_reaction(_ ev: NostrEvent) -> Bool {
guard let ref_id = ev.referenced_ids.last else {
return false
}
let id = ref_id.id
if let evgrp = self.reactions[id] {
return evgrp.insert(ev)
} else {
let evgrp = EventGroup()
self.reactions[id] = evgrp
return evgrp.insert(ev)
}
}
private func insert_event_immediate(_ ev: NostrEvent) -> Bool {
if ev.known_kind == .boost {
return insert_repost(ev)
} else if ev.known_kind == .like {
return insert_reaction(ev)
} else if ev.known_kind == .text {
return insert_text(ev)
}
return false
}
private func insert_zap_immediate(_ zap: Zap) -> Bool {
switch zap.target {
case .note(let notezt):
let id = notezt.note_id
if let zapgrp = self.zaps[notezt.note_id] {
return zapgrp.insert(zap)
} else {
let zapgrp = ZapGroup()
self.zaps[id] = zapgrp
return zapgrp.insert(zap)
}
case .profile:
return profile_zaps.insert(zap)
}
}
func insert_event(_ ev: NostrEvent) -> Bool {
if should_queue {
return insert_uniq_sorted_event_created(events: &incoming_events, new_ev: ev)
}
if insert_event_immediate(ev) {
self.notifications = build_notifications()
return true
}
return false
}
func insert_zap(_ zap: Zap) -> Bool {
if should_queue {
return insert_uniq_sorted_zap_by_created(zaps: &incoming_zaps, new_zap: zap)
}
if insert_zap_immediate(zap) {
self.notifications = build_notifications()
return true
}
return false
}
func filter(_ isIncluded: (NostrEvent) -> Bool) {
var changed = false
var count = 0
count = incoming_events.count
incoming_events = incoming_events.filter(isIncluded)
changed = changed || incoming_events.count != count
count = profile_zaps.zaps.count
profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request.ev) }
changed = changed || profile_zaps.zaps.count != count
for el in reactions {
count = el.value.events.count
el.value.events = el.value.events.filter(isIncluded)
changed = changed || el.value.events.count != count
}
for el in reposts {
count = el.value.events.count
el.value.events = el.value.events.filter(isIncluded)
changed = changed || el.value.events.count != count
}
for el in zaps {
count = el.value.zaps.count
el.value.zaps = el.value.zaps.filter {
isIncluded($0.request.ev)
}
changed = changed || el.value.zaps.count != count
}
count = replies.count
replies = replies.filter(isIncluded)
changed = changed || replies.count != count
if changed {
self.notifications = build_notifications()
}
}
func flush() -> Bool {
var inserted = false
for zap in incoming_zaps {
inserted = insert_zap_immediate(zap) || inserted
}
for event in incoming_events {
inserted = insert_event_immediate(event) || inserted
}
if inserted {
self.notifications = build_notifications()
}
return inserted
}
}
+4 -2
View File
@@ -8,7 +8,7 @@
import Foundation
class ProfileModel: ObservableObject, Equatable {
@Published var events: [NostrEvent] = []
var events: EventHolder = EventHolder()
@Published var contacts: NostrEvent? = nil
@Published var following: Int = 0
@Published var relays: [String: RelayInfo]? = nil
@@ -111,7 +111,9 @@ class ProfileModel: ObservableObject, Equatable {
return
}
if ev.is_textlike || ev.known_kind == .boost {
let _ = insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at})
if self.events.insert(ev) {
self.objectWillChange.send()
}
} else if ev.known_kind == .contacts {
handle_profile_contact_event(ev)
} else if ev.known_kind == .metadata {
+3 -65
View File
@@ -8,71 +8,9 @@
import Foundation
class ReactionsModel: ObservableObject {
let state: DamusState
let target: String
let sub_id: String
let profiles_id: String
final class ReactionsModel: EventsModel {
@Published var reactions: [NostrEvent]
init (state: DamusState, target: String) {
self.state = state
self.target = target
self.sub_id = UUID().description
self.profiles_id = UUID().description
self.reactions = []
}
func get_filter() -> NostrFilter {
var filter = NostrFilter.filter_kinds([7])
filter.referenced_ids = [target]
filter.limit = 500
return filter
}
func subscribe() {
let filter = get_filter()
let filters = [filter]
self.state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_nostr_event)
}
func unsubscribe() {
self.state.pool.unsubscribe(sub_id: sub_id)
}
func handle_event(relay_id: String, ev: NostrEvent) {
guard ev.kind == 7 else {
return
}
guard let reacted_to = last_etag(tags: ev.tags) else {
return
}
guard reacted_to == self.target else {
return
}
if insert_uniq_sorted_event(events: &self.reactions, new_ev: ev, cmp: { a, b in a.created_at < b.created_at } ) {
objectWillChange.send()
}
}
func handle_nostr_event(relay_id: String, ev: NostrConnectionEvent) {
guard case .nostr_event(let nev) = ev else {
return
}
switch nev {
case .event(_, let ev):
handle_event(relay_id: relay_id, ev: ev)
case .notice(_):
break
case .eose(_):
load_profiles(profiles_subid: profiles_id, relay_id: relay_id, events: reactions, damus_state: state)
break
}
init(state: DamusState, target: String) {
super.init(state: state, target: target, kind: .like)
}
}
+4 -66
View File
@@ -7,71 +7,9 @@
import Foundation
class RepostsModel: ObservableObject {
let state: DamusState
let target: String
let sub_id: String
let profiles_id: String
@Published var reposts: [NostrEvent]
init (state: DamusState, target: String) {
self.state = state
self.target = target
self.sub_id = UUID().description
self.profiles_id = UUID().description
self.reposts = []
}
func get_filter() -> NostrFilter {
var filter = NostrFilter.filter_kinds([NostrKind.boost.rawValue])
filter.referenced_ids = [target]
filter.limit = 500
return filter
}
func subscribe() {
let filter = get_filter()
let filters = [filter]
self.state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_nostr_event)
}
func unsubscribe() {
self.state.pool.unsubscribe(sub_id: sub_id)
}
func handle_event(relay_id: String, ev: NostrEvent) {
guard ev.kind == NostrKind.boost.rawValue else {
return
}
guard let reposted_event = last_etag(tags: ev.tags) else {
return
}
guard reposted_event == self.target else {
return
}
if insert_uniq_sorted_event(events: &self.reposts, new_ev: ev, cmp: { a, b in a.created_at < b.created_at } ) {
objectWillChange.send()
}
}
func handle_nostr_event(relay_id: String, ev: NostrConnectionEvent) {
guard case .nostr_event(let nev) = ev else {
return
}
switch nev {
case .event(_, let ev):
handle_event(relay_id: relay_id, ev: ev)
case .notice(_):
break
case .eose(_):
load_profiles(profiles_subid: profiles_id, relay_id: relay_id, events: reposts, damus_state: state)
break
}
final class RepostsModel: EventsModel {
init(state: DamusState, target: String) {
super.init(state: state, target: target, kind: .boost)
}
}
+37 -8
View File
@@ -10,7 +10,7 @@ import Foundation
/// The data model for the SearchHome view, typically something global-like
class SearchHomeModel: ObservableObject {
@Published var events: [NostrEvent] = []
var events: EventHolder = EventHolder()
@Published var loading: Bool = false
var seen_pubkey: Set<String> = Set()
@@ -31,7 +31,8 @@ class SearchHomeModel: ObservableObject {
}
func filter_muted() {
events = events.filter { should_show_event(contacts: damus_state.contacts, ev: $0) }
events.filter { should_show_event(contacts: damus_state.contacts, ev: $0) }
self.objectWillChange.send()
}
func subscribe() {
@@ -61,8 +62,8 @@ class SearchHomeModel: ObservableObject {
}
seen_pubkey.insert(ev.pubkey)
let _ = insert_uniq_sorted_event(events: &events, new_ev: ev) {
$0.created_at > $1.created_at
if self.events.insert(ev) {
self.objectWillChange.send()
}
}
case .notice(let msg):
@@ -75,7 +76,7 @@ class SearchHomeModel: ObservableObject {
// global events are not realtime
unsubscribe(to: relay_id)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: events, damus_state: damus_state)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.all_events), damus_state: damus_state)
}
@@ -97,8 +98,31 @@ func find_profiles_to_fetch_pk(profiles: Profiles, event_pubkeys: [String]) -> [
return Array(pubkeys)
}
func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad) -> [String] {
switch load {
case .from_events(let events):
return find_profiles_to_fetch_from_events(profiles: profiles, events: events)
case .from_keys(let pks):
return find_profiles_to_fetch_from_keys(profiles: profiles, pks: pks)
}
}
func find_profiles_to_fetch_from_keys(profiles: Profiles, pks: [String]) -> [String] {
var pubkeys = Set<String>()
func find_profiles_to_fetch(profiles: Profiles, events: [NostrEvent]) -> [String] {
for pk in pks {
if profiles.lookup(id: pk) != nil {
continue
}
pubkeys.insert(pk)
}
return Array(pubkeys)
}
func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent]) -> [String] {
var pubkeys = Set<String>()
for ev in events {
@@ -112,9 +136,14 @@ func find_profiles_to_fetch(profiles: Profiles, events: [NostrEvent]) -> [String
return Array(pubkeys)
}
func load_profiles(profiles_subid: String, relay_id: String, events: [NostrEvent], damus_state: DamusState) {
enum PubkeysToLoad {
case from_events([NostrEvent])
case from_keys([String])
}
func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad, damus_state: DamusState) {
var filter = NostrFilter.filter_profiles
let authors = find_profiles_to_fetch(profiles: damus_state.profiles, events: events)
let authors = find_profiles_to_fetch(profiles: damus_state.profiles, load: load)
filter.authors = authors
guard !authors.isEmpty else {
+4 -3
View File
@@ -9,7 +9,7 @@ import Foundation
class SearchModel: ObservableObject {
@Published var events: [NostrEvent] = []
var events: EventHolder = EventHolder()
@Published var loading: Bool = false
@Published var channel_name: String? = nil
@@ -26,7 +26,8 @@ class SearchModel: ObservableObject {
}
func filter_muted() {
self.events = self.events.filter { should_show_event(contacts: contacts, ev: $0) }
self.events.filter { should_show_event(contacts: contacts, ev: $0) }
self.objectWillChange.send()
}
func subscribe() {
@@ -57,7 +58,7 @@ class SearchModel: ObservableObject {
return
}
if insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at } ) {
if self.events.insert(ev) {
objectWillChange.send()
}
}
+1 -1
View File
@@ -207,7 +207,7 @@ class ThreadModel: ObservableObject {
}
if sub_id == self.base_subid {
load_profiles(profiles_subid: self.profiles_subid, relay_id: relay_id, events: events, damus_state: damus_state)
load_profiles(profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: damus_state)
}
}
+28 -22
View File
@@ -13,6 +13,7 @@ class ZapsModel: ObservableObject {
var zaps: [Zap]
let zaps_subid = UUID().description
let profiles_subid = UUID().description
init(state: DamusState, target: ZapTarget) {
self.state = state
@@ -44,34 +45,39 @@ class ZapsModel: ObservableObject {
return
}
guard case .event(_, let ev) = resp else {
return
}
guard ev.kind == 9735 else {
return
}
if let zap = state.zaps.zaps[ev.id] {
if insert_uniq_sorted_zap(zaps: &zaps, new_zap: zap) {
objectWillChange.send()
}
} else {
guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else {
switch resp {
case .notice:
break
case .eose:
let events = self.zaps.map { $0.request.ev }
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state)
case .event(_, let ev):
guard ev.kind == 9735 else {
return
}
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper) else {
return
}
state.zaps.add_zap(zap: zap)
if insert_uniq_sorted_zap(zaps: &zaps, new_zap: zap) {
objectWillChange.send()
if let zap = state.zaps.zaps[ev.id] {
if insert_uniq_sorted_zap_by_amount(zaps: &zaps, new_zap: zap) {
objectWillChange.send()
}
} else {
guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else {
return
}
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: state.keypair.privkey) else {
return
}
state.zaps.add_zap(zap: zap)
if insert_uniq_sorted_zap_by_amount(zaps: &zaps, new_zap: zap) {
objectWillChange.send()
}
}
}
}
}
+3
View File
@@ -141,6 +141,9 @@ struct Profile: Codable {
}
static func displayName(profile: Profile?, pubkey: String) -> String {
if pubkey == "anon" {
return "Anonymous"
}
let pk = bech32_nopre_pubkey(pubkey) ?? pubkey
return profile?.name ?? abbrev_pubkey(pk)
}
+154 -23
View File
@@ -157,7 +157,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
pubkey = refkey.ref_id
}
let dec = decrypt_dm(key, pubkey: pubkey, content: self.content)
let dec = decrypt_dm(key, pubkey: pubkey, content: self.content, encoding: .base64)
self.decrypted_content = dec
return dec
@@ -168,6 +168,9 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
return decrypted(privkey: privkey) ?? "*failed to decrypt content*"
}
return content
/*
switch validity {
case .ok:
return content
@@ -176,6 +179,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
case .bad_sig:
return content + "\n\n*WARNING: invalid signature, could be forged!*"
}
*/
}
var description: String {
@@ -278,21 +282,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
return (self.flags & 1) != 0
}
init(content: String, pubkey: String, kind: Int = 1, tags: [[String]] = [], createdAt: Int64 = Int64(Date().timeIntervalSince1970)) {
self.id = ""
self.sig = ""
self.content = content
self.pubkey = pubkey
self.kind = kind
self.tags = tags
self.created_at = createdAt
}
/// Intiialization statement used to specificy ID
///
/// This is mainly used for contant and testing data
init(id: String, content: String, pubkey: String, kind: Int = 1, tags: [[String]] = []) {
init(id: String = "", content: String, pubkey: String, kind: Int = 1, tags: [[String]] = [], createdAt: Int64 = Int64(Date().timeIntervalSince1970)) {
self.id = id
self.sig = ""
@@ -300,7 +290,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
self.pubkey = pubkey
self.kind = kind
self.tags = tags
self.created_at = Int64(Date().timeIntervalSince1970)
self.created_at = createdAt
}
init(from: NostrEvent, content: String? = nil) {
@@ -587,14 +577,115 @@ func zap_target_to_tags(_ target: ZapTarget) -> [[String]] {
}
}
func make_zap_request_event(pubkey: String, privkey: String, content: String, relays: [RelayDescriptor], target: ZapTarget) -> NostrEvent {
func make_private_zap_request_event(identity: FullKeypair, enc_key: FullKeypair, target: ZapTarget, message: String) -> String? {
// target tags must be the same as zap request target tags
let tags = zap_target_to_tags(target)
let note = NostrEvent(content: message, pubkey: identity.pubkey, kind: 9733, tags: tags)
note.id = calculate_event_id(ev: note)
note.sig = sign_event(privkey: identity.privkey, ev: note)
guard let note_json = encode_json(note) else {
return nil
}
return encrypt_message(message: note_json, privkey: enc_key.privkey, to_pk: target.pubkey, encoding: .bech32)
}
func decrypt_private_zap(our_privkey: String, zapreq: NostrEvent, target: ZapTarget) -> NostrEvent? {
guard let anon_tag = zapreq.tags.first(where: { t in t.count >= 2 && t[0] == "anon" }) else {
return nil
}
let enc_note = anon_tag[1]
var note = decrypt_note(our_privkey: our_privkey, their_pubkey: zapreq.pubkey, enc_note: enc_note, encoding: .bech32)
// check to see if the private note was from us
if note == nil {
guard let our_private_keypair = generate_private_keypair(our_privkey: our_privkey, id: target.id, created_at: zapreq.created_at) else{
return nil
}
// use our private keypair and their pubkey to get the shared secret
note = decrypt_note(our_privkey: our_private_keypair.privkey, their_pubkey: target.pubkey, enc_note: enc_note, encoding: .bech32)
}
guard let note else {
return nil
}
guard note.kind == 9733 else {
return nil
}
let zr_etag = zapreq.referenced_ids.first
let note_etag = note.referenced_ids.first
guard zr_etag == note_etag else {
return nil
}
let zr_ptag = zapreq.referenced_pubkeys.first
let note_ptag = note.referenced_pubkeys.first
guard let zr_ptag, let note_ptag, zr_ptag == note_ptag else {
return nil
}
guard validate_event(ev: note) == .ok else {
return nil
}
return note
}
func generate_private_keypair(our_privkey: String, id: String, created_at: Int64) -> FullKeypair? {
let to_hash = our_privkey + id + String(created_at)
guard let dat = to_hash.data(using: .utf8) else {
return nil
}
let privkey_bytes = sha256(dat)
let privkey = hex_encode(privkey_bytes)
guard let pubkey = privkey_to_pubkey(privkey: privkey) else {
return nil
}
return FullKeypair(pubkey: pubkey, privkey: privkey)
}
func make_zap_request_event(keypair: FullKeypair, content: String, relays: [RelayDescriptor], target: ZapTarget, zap_type: ZapType) -> NostrEvent? {
var tags = zap_target_to_tags(target)
var relay_tag = ["relays"]
relay_tag.append(contentsOf: relays.map { $0.url.absoluteString })
tags.append(relay_tag)
let ev = NostrEvent(content: content, pubkey: pubkey, kind: 9734, tags: tags)
var kp = keypair
let now = Int64(Date().timeIntervalSince1970)
var message = content
switch zap_type {
case .pub:
break
case .non_zap:
break
case .anon:
tags.append(["anon"])
kp = generate_new_keypair().to_full()!
case .priv:
guard let priv_kp = generate_private_keypair(our_privkey: keypair.privkey, id: target.id, created_at: now) else {
return nil
}
kp = priv_kp
guard let privreq = make_private_zap_request_event(identity: keypair, enc_key: kp, target: target, message: message) else {
return nil
}
tags.append(["anon", privreq])
message = ""
}
let ev = NostrEvent(content: message, pubkey: kp.pubkey, kind: 9734, tags: tags, createdAt: now)
ev.id = calculate_event_id(ev: ev)
ev.sig = sign_event(privkey: privkey, ev: ev)
ev.sig = sign_event(privkey: kp.privkey, ev: ev)
return ev
}
@@ -624,14 +715,14 @@ func event_to_json(ev: NostrEvent) -> String {
return str
}
func decrypt_dm(_ privkey: String?, pubkey: String, content: String) -> String? {
func decrypt_dm(_ privkey: String?, pubkey: String, content: String, encoding: EncEncoding) -> String? {
guard let privkey = privkey else {
return nil
}
guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: pubkey) else {
return nil
}
guard let dat = decode_dm_base64(content) else {
guard let dat = (encoding == .base64 ? decode_dm_base64(content) : decode_dm_bech32(content)) else {
return nil
}
guard let dat = aes_decrypt(data: dat.content, iv: dat.iv, shared_sec: shared_sec) else {
@@ -640,6 +731,13 @@ func decrypt_dm(_ privkey: String?, pubkey: String, content: String) -> String?
return String(data: dat, encoding: .utf8)
}
func decrypt_note(our_privkey: String, their_pubkey: String, enc_note: String, encoding: EncEncoding) -> NostrEvent? {
guard let dec = decrypt_dm(our_privkey, pubkey: their_pubkey, content: enc_note, encoding: encoding) else {
return nil
}
return decode_nostr_event_json(json: dec)
}
func get_shared_secret(privkey: String, pubkey: String) -> [UInt8]? {
guard let privkey_bytes = try? privkey.bytes else {
@@ -685,6 +783,39 @@ struct DirectMessageBase64 {
let iv: [UInt8]
}
func encode_dm_bech32(content: [UInt8], iv: [UInt8]) -> String {
let content_bech32 = bech32_encode(hrp: "pzap", content)
let iv_bech32 = bech32_encode(hrp: "iv", iv)
return content_bech32 + "_" + iv_bech32
}
func decode_dm_bech32(_ all: String) -> DirectMessageBase64? {
let parts = all.split(separator: "_")
guard parts.count == 2 else {
return nil
}
let content_bech32 = String(parts[0])
let iv_bech32 = String(parts[1])
guard let content_tup = try? bech32_decode(content_bech32) else {
return nil
}
guard let iv_tup = try? bech32_decode(iv_bech32) else {
return nil
}
guard content_tup.hrp == "pzap" else {
return nil
}
guard iv_tup.hrp == "iv" else {
return nil
}
return DirectMessageBase64(content: content_tup.data.bytes, iv: iv_tup.data.bytes)
}
func encode_dm_base64(content: [UInt8], iv: [UInt8]) -> String {
let content_b64 = base64_encode(content)
let iv_b64 = base64_encode(iv)
@@ -849,7 +980,7 @@ func first_eref_mention(ev: NostrEvent, privkey: String?) -> Mention? {
extension [ReferencedId] {
var pRefs: [ReferencedId] {
get {
self.filter { ref in
Set(self).filter { ref in
ref.key == "p"
}
}
+1
View File
@@ -21,4 +21,5 @@ enum NostrKind: Int {
case chat = 42
case list = 30000
case zap = 9735
case zap_request = 9734
}
+1 -1
View File
@@ -72,7 +72,7 @@ func char_to_hex(_ c: UInt8) -> UInt8?
return nil;
}
@discardableResult
func hex_decode(_ str: String) -> [UInt8]?
{
if str.count == 0 {
+40 -37
View File
@@ -13,42 +13,41 @@ enum NostrConnectionEvent {
case nostr_event(NostrResponse)
}
class RelayConnection: WebSocketDelegate {
var isConnected: Bool = false
var isConnecting: Bool = false
var isReconnecting: Bool = false
var last_connection_attempt: Double = 0
var socket: WebSocket
var handleEvent: (NostrConnectionEvent) -> ()
let url: URL
final class RelayConnection: WebSocketDelegate {
private(set) var isConnected = false
private(set) var isConnecting = false
private(set) var isReconnecting = false
private(set) var last_connection_attempt: TimeInterval = 0
private lazy var socket = {
let req = URLRequest(url: url)
let socket = WebSocket(request: req, compressionHandler: .none)
socket.delegate = self
return socket
}()
private var handleEvent: (NostrConnectionEvent) -> ()
private let url: URL
init(url: URL, handleEvent: @escaping (NostrConnectionEvent) -> ()) {
self.url = url
self.handleEvent = handleEvent
// just init, we don't actually use this one
self.socket = make_websocket(url: url)
}
func reconnect() {
if self.isConnected {
self.isReconnecting = true
self.disconnect()
if isConnected {
isReconnecting = true
disconnect()
} else {
// we're already disconnected, so just connect
self.connect(force: true)
connect(force: true)
}
}
func connect(force: Bool = false){
if !force && (self.isConnected || self.isConnecting) {
func connect(force: Bool = false) {
if !force && (isConnected || isConnecting) {
return
}
var req = URLRequest(url: self.url)
req.timeoutInterval = 5
socket = make_websocket(url: url)
socket.delegate = self
isConnecting = true
last_connection_attempt = Date().timeIntervalSince1970
socket.connect()
@@ -68,7 +67,9 @@ class RelayConnection: WebSocketDelegate {
socket.write(string: req)
}
// MARK: - WebSocketDelegate
func didReceive(event: WebSocketEvent, client: WebSocket) {
switch event {
case .connected:
@@ -83,15 +84,25 @@ class RelayConnection: WebSocketDelegate {
self.connect()
}
case .cancelled: fallthrough
case .error:
case .cancelled, .error:
self.isConnecting = false
self.isConnected = false
case .text(let txt):
if let ev = decode_nostr_event(txt: txt) {
handleEvent(.nostr_event(ev))
return
if txt.count > 2000 {
DispatchQueue.global(qos: .default).async {
if let ev = decode_nostr_event(txt: txt) {
DispatchQueue.main.async {
self.handleEvent(.nostr_event(ev))
}
return
}
}
} else {
if let ev = decode_nostr_event(txt: txt) {
handleEvent(.nostr_event(ev))
return
}
}
print("decode failed for \(txt)")
@@ -103,7 +114,6 @@ class RelayConnection: WebSocketDelegate {
handleEvent(.ws_event(event))
}
}
func make_nostr_req(_ req: NostrRequest) -> String? {
@@ -127,7 +137,7 @@ func make_nostr_push_event(ev: NostrEvent) -> String? {
}
func make_nostr_unsubscribe_req(_ sub_id: String) -> String? {
return "[\"CLOSE\",\"\(sub_id)\"]"
"[\"CLOSE\",\"\(sub_id)\"]"
}
func make_nostr_subscription_req(_ filters: [NostrFilter], sub_id: String) -> String? {
@@ -144,10 +154,3 @@ func make_nostr_subscription_req(_ filters: [NostrFilter], sub_id: String) -> St
req += "]"
return req
}
func make_websocket(url: URL) -> WebSocket {
let req = URLRequest(url: url)
//req.setValue("chat,superchat", forHTTPHeaderField: "Sec-WebSocket-Protocol")
return WebSocket(request: req, compressionHandler: .none)
}
+4 -18
View File
@@ -195,27 +195,13 @@ class RelayPool {
relay.connection.send(req)
}
}
func get_relays(_ ids: [String]) -> [Relay] {
var relays: [Relay] = []
for id in ids {
if let relay = get_relay(id) {
relays.append(relay)
}
}
return relays
relays.filter { ids.contains($0.id) }
}
func get_relay(_ id: String) -> Relay? {
for relay in relays {
if relay.id == id {
return relay
}
}
return nil
relays.first(where: { $0.id == id })
}
func record_last_pong(relay_id: String, event: NostrConnectionEvent) {
+1
View File
@@ -143,6 +143,7 @@ func eightToFiveBits(_ input: [UInt8]) -> [UInt8] {
}
/// Decode Bech32 string
@discardableResult
public func bech32_decode(_ str: String) throws -> (hrp: String, data: Data)? {
guard let strBytes = str.data(using: .utf8) else {
throw Bech32Error.nonUTF8String
+27
View File
@@ -0,0 +1,27 @@
//
// Debouncer.swift
// damus
//
// Created by William Casarin on 2023-02-15.
//
import Foundation
class Debouncer {
private let queue = DispatchQueue.main
private var workItem: DispatchWorkItem?
private var interval: TimeInterval
init(interval: TimeInterval) {
self.interval = interval
}
func debounce(action: @escaping () -> Void) {
// Cancel the previous work item if it hasn't yet executed
workItem?.cancel()
// Create a new work item with a delay
workItem = DispatchWorkItem { action() }
queue.asyncAfter(deadline: .now() + interval, execute: workItem!)
}
}
+27
View File
@@ -0,0 +1,27 @@
//
// EventCache.swift
// damus
//
// Created by William Casarin on 2023-02-21.
//
import Foundation
class EventCache {
private var events: [String: NostrEvent]
func lookup(_ evid: String) -> NostrEvent? {
return events[evid]
}
func insert(_ ev: NostrEvent) {
guard events[ev.id] == nil else {
return
}
events[ev.id] = ev
}
init() {
self.events = [:]
}
}
+103
View File
@@ -0,0 +1,103 @@
//
// EventHolder.swift
// damus
//
// Created by William Casarin on 2023-02-19.
//
import Foundation
/// Used for holding back events until they're ready to be displayed
class EventHolder: ObservableObject, ScrollQueue {
private var has_event: Set<String>
@Published var events: [NostrEvent]
@Published var incoming: [NostrEvent]
var should_queue: Bool
func set_should_queue(_ val: Bool) {
self.should_queue = val
}
var queued: Int {
return incoming.count
}
var has_incoming: Bool {
return queued > 0
}
var all_events: [NostrEvent] {
events + incoming
}
init() {
self.should_queue = false
self.events = []
self.incoming = []
self.has_event = Set()
}
init(events: [NostrEvent], incoming: [NostrEvent]) {
self.should_queue = false
self.events = events
self.incoming = incoming
self.has_event = Set()
}
func filter(_ isIncluded: (NostrEvent) -> Bool) {
self.events = self.events.filter(isIncluded)
self.incoming = self.incoming.filter(isIncluded)
}
func insert(_ ev: NostrEvent) -> Bool {
if should_queue {
return insert_queued(ev)
} else {
return insert_immediate(ev)
}
}
private func insert_immediate(_ ev: NostrEvent) -> Bool {
if has_event.contains(ev.id) {
return false
}
has_event.insert(ev.id)
if insert_uniq_sorted_event_created(events: &self.events, new_ev: ev) {
return true
}
return false
}
private func insert_queued(_ ev: NostrEvent) -> Bool {
if has_event.contains(ev.id) {
return false
}
has_event.insert(ev.id)
incoming.append(ev)
return true
}
func flush() {
guard !incoming.isEmpty else {
return
}
var changed = false
for event in incoming {
if insert_uniq_sorted_event_created(events: &events, new_ev: event) {
changed = true
}
}
if changed {
self.objectWillChange.send()
}
self.incoming = []
}
}
+23 -2
View File
@@ -38,7 +38,7 @@ func insert_uniq_by_pubkey(events: inout [NostrEvent], new_ev: NostrEvent, cmp:
return true
}
func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool {
func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap, cmp: (Zap, Zap) -> Bool) -> Bool {
var i: Int = 0
for zap in zaps {
@@ -47,7 +47,7 @@ func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool {
return false
}
if new_zap.invoice.amount > zap.invoice.amount {
if cmp(new_zap, zap) {
zaps.insert(new_zap, at: i)
return true
}
@@ -58,6 +58,27 @@ func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool {
return true
}
@discardableResult
func insert_uniq_sorted_zap_by_created(zaps: inout [Zap], new_zap: Zap) -> Bool {
return insert_uniq_sorted_zap(zaps: &zaps, new_zap: new_zap) { (a, b) in
a.event.created_at > b.event.created_at
}
}
@discardableResult
func insert_uniq_sorted_zap_by_amount(zaps: inout [Zap], new_zap: Zap) -> Bool {
return insert_uniq_sorted_zap(zaps: &zaps, new_zap: new_zap) { (a, b) in
a.invoice.amount > b.invoice.amount
}
}
func insert_uniq_sorted_event_created(events: inout [NostrEvent], new_ev: NostrEvent) -> Bool {
return insert_uniq_sorted_event(events: &events, new_ev: new_ev) {
$0.created_at > $1.created_at
}
}
@discardableResult
func insert_uniq_sorted_event(events: inout [NostrEvent], new_ev: NostrEvent, cmp: (NostrEvent, NostrEvent) -> Bool) -> Bool {
var i: Int = 0
+148
View File
@@ -0,0 +1,148 @@
//
// KFOptionSetter+.swift
// damus
//
// Created by Oleg Abalonski on 2/15/23.
//
import UIKit
import Kingfisher
extension KFOptionSetter {
func imageContext(_ imageContext: ImageContext) -> Self {
options.callbackQueue = .dispatch(.global(qos: .background))
options.processingQueue = .dispatch(.global(qos: .background))
options.downloader = CustomImageDownloader.shared
options.backgroundDecode = true
options.cacheOriginalImage = true
options.scaleFactor = UIScreen.main.scale
options.processor = CustomImageProcessor(
maxSize: imageContext.maxMebibyteSize(),
downsampleSize: imageContext.downsampleSize()
)
options.cacheSerializer = CustomCacheSerializer(
maxSize: imageContext.maxMebibyteSize(),
downsampleSize: imageContext.downsampleSize()
)
return self
}
func onFailure(fallbackUrl: URL?, cacheKey: String?) -> Self {
guard let url = fallbackUrl, let key = cacheKey else { return self }
let imageResource = ImageResource(downloadURL: url, cacheKey: key)
let source = imageResource.convertToSource()
options.alternativeSources = [source]
return self
}
}
let MAX_FILE_SIZE = 20_971_520 // 20MiB
enum ImageContext {
case pfp
case banner
case note
func maxMebibyteSize() -> Int {
switch self {
case .pfp:
return 5_242_880 // 5Mib
case .banner, .note:
return 20_971_520 // 20MiB
}
}
func downsampleSize() -> CGSize {
switch self {
case .pfp:
return CGSize(width: 200, height: 200)
case .banner:
return CGSize(width: 750, height: 250)
case .note:
return CGSize(width: 500, height: 500)
}
}
}
struct CustomImageProcessor: ImageProcessor {
let maxSize: Int
let downsampleSize: CGSize
let identifier = "com.damus.customimageprocessor"
func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
switch item {
case .image(_):
// This case will never run
return DefaultImageProcessor.default.process(item: item, options: options)
case .data(let data):
// Handle large image size
if data.count > maxSize {
return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
}
// Handle SVG image
if let dataString = String(data: data, encoding: .utf8),
let svg = SVG(dataString) {
let render = UIGraphicsImageRenderer(size: svg.size)
let image = render.image { context in
svg.draw(in: context.cgContext)
}
return image.kf.scaled(to: options.scaleFactor)
}
return DefaultImageProcessor.default.process(item: item, options: options)
}
}
}
struct CustomCacheSerializer: CacheSerializer {
let maxSize: Int
let downsampleSize: CGSize
func data(with image: Kingfisher.KFCrossPlatformImage, original: Data?) -> Data? {
return DefaultCacheSerializer.default.data(with: image, original: original)
}
func image(with data: Data, options: Kingfisher.KingfisherParsedOptionsInfo) -> Kingfisher.KFCrossPlatformImage? {
if data.count > maxSize {
return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
}
return DefaultCacheSerializer.default.image(with: data, options: options)
}
}
class CustomSessionDelegate: SessionDelegate {
override func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
let contentLength = response.expectedContentLength
// Content-Length header is optional (-1 when missing)
if (contentLength != -1 && contentLength > MAX_FILE_SIZE) {
return super.urlSession(session, dataTask: dataTask, didReceive: URLResponse(), completionHandler: completionHandler)
}
super.urlSession(session, dataTask: dataTask, didReceive: response, completionHandler: completionHandler)
}
}
class CustomImageDownloader: ImageDownloader {
static let shared = CustomImageDownloader(name: "shared")
override init(name: String) {
super.init(name: name)
sessionDelegate = CustomSessionDelegate()
}
}
+2
View File
@@ -9,8 +9,10 @@ import Foundation
struct LNUrlPayRequest: Decodable {
let allowsNostr: Bool?
let commentAllowed: Int?
let nostrPubkey: String?
let metadata: String?
let minSendable: Int64?
let maxSendable: Int64?
let status: String?
+10 -2
View File
@@ -10,10 +10,11 @@ import LinkPresentation
class CustomLinkView: LPLinkView {
override var intrinsicContentSize: CGSize { CGSize(width: 0, height: super.intrinsicContentSize.height) }
}
enum Metadata {
case linkmeta(LPLinkMetadata)
case linkmeta(CachedMetadata)
case url(URL)
}
@@ -26,12 +27,19 @@ struct LinkViewRepresentable: UIViewRepresentable {
func makeUIView(context: Context) -> CustomLinkView {
switch meta {
case .linkmeta(let linkmeta):
return CustomLinkView(metadata: linkmeta)
return CustomLinkView(metadata: linkmeta.meta)
case .url(let url):
return CustomLinkView(url: url)
}
}
func updateUIView(_ uiView: CustomLinkView, context: Context) {
switch meta {
case .linkmeta(let cached):
cached.intrinsic_height = uiView.intrinsicContentSize.height
case .url:
return
}
}
}
+17
View File
@@ -0,0 +1,17 @@
//
// LocalizationUtil.swift
// damus
//
// Created by Terry Yiu on 2/24/23.
//
import Foundation
func bundleForLocale(locale: Locale?) -> Bundle {
if locale == nil {
return Bundle.main
}
let path = Bundle.main.path(forResource: locale!.identifier, ofType: "lproj")
return path != nil ? (Bundle(path: path!) ?? Bundle.main) : Bundle.main
}
+5 -2
View File
@@ -8,8 +8,8 @@
import Foundation
public struct Markdown {
private let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
private var detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
/// Ensure the specified URL has a scheme by prepending "https://" if it's absent.
static func withScheme(_ url: any StringProtocol) -> any StringProtocol {
return url.contains("://") ? url : "https://" + url
@@ -31,6 +31,9 @@ public struct Markdown {
/// Process the input text and add markdown for any embedded URLs.
public func process(_ input: String) -> AttributedString {
guard let detector else {
return AttributedString(stringLiteral: input)
}
let matches = detector.matches(in: input, options: [], range: NSRange(location: 0, length: input.utf16.count))
var output = input
// Start with the last match, because replacing the first would invalidate all subsequent indices
+6
View File
@@ -101,6 +101,12 @@ extension Notification.Name {
static var update_stats: Notification.Name {
return Notification.Name("update_stats")
}
static var update_bookmarks: Notification.Name {
return Notification.Name("update_bookmarks")
}
static var zapping: Notification.Name {
return Notification.Name("zapping")
}
}
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {
+13 -3
View File
@@ -8,8 +8,18 @@
import Foundation
import LinkPresentation
class CachedMetadata {
let meta: LPLinkMetadata
var intrinsic_height: CGFloat?
init(meta: LPLinkMetadata) {
self.meta = meta
self.intrinsic_height = nil
}
}
enum Preview {
case value(LinkViewRepresentable)
case value(CachedMetadata)
case failed
}
@@ -20,12 +30,12 @@ class PreviewCache {
return previews[evid]
}
func store(evid: String, preview: LinkViewRepresentable?) {
func store(evid: String, preview: LPLinkMetadata?) {
switch preview {
case .none:
previews[evid] = .failed
case .some(let meta):
previews[evid] = .value(meta)
previews[evid] = .value(CachedMetadata(meta: meta))
}
}
+8
View File
@@ -25,4 +25,12 @@ class Theme {
UINavigationBar.appearance().tintColor = tintColor ?? titleColor ?? .black
}
static var safeAreaInsets: UIEdgeInsets? {
return UIApplication
.shared
.connectedScenes
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
.first { $0.isKeyWindow }?.safeAreaInsets
}
}
+2 -1
View File
@@ -50,5 +50,6 @@ public func time_ago_since(_ date: Date, _ calendar: Calendar = Calendar.current
return formatter.string(from: DateComponents(calendar: calendar, second: second))!
}
return NSLocalizedString("now", comment: "String indicating that a given timestamp just occurred")
let bundle = bundleForLocale(locale: calendar.locale ?? Locale.current)
return NSLocalizedString("now", bundle: bundle, comment: "String indicating that a given timestamp just occurred")
}
+27 -14
View File
@@ -7,12 +7,6 @@
import Foundation
enum ZapSource {
case author(String)
// TODO: anonymous
//case anonymous
}
public struct NoteZapTarget: Equatable {
public let note_id: String
public let author: String
@@ -55,8 +49,10 @@ struct Zap {
public let zapper: String /// zap authorizer
public let target: ZapTarget
public let request: ZapRequest
public let is_anon: Bool
public let private_request: NostrEvent?
public static func from_zap_event(zap_ev: NostrEvent, zapper: String) -> Zap? {
public static func from_zap_event(zap_ev: NostrEvent, zapper: String, our_privkey: String?) -> Zap? {
/// Make sure that we only create a zap event if it is authorized by the profile or event
guard zapper == zap_ev.pubkey else {
return nil
@@ -83,14 +79,26 @@ struct Zap {
guard let desc = get_zap_description(zap_ev, inv_desc: zap_invoice.description) else {
return nil
}
guard let zap_req = decode_nostr_event_json(desc) else {
return nil
}
guard validate_event(ev: zap_req) == .ok else {
return nil
}
guard let target = determine_zap_target(zap_req) else {
return nil
}
return Zap(event: zap_ev, invoice: zap_invoice, zapper: zapper, target: target, request: ZapRequest(ev: zap_req))
let private_request = our_privkey.flatMap {
decrypt_private_zap(our_privkey: $0, zapreq: zap_req, target: target)
}
let is_anon = private_request == nil && event_is_anonymous(ev: zap_req)
return Zap(event: zap_ev, invoice: zap_invoice, zapper: zapper, target: target, request: ZapRequest(ev: zap_req), is_anon: is_anon, private_request: private_request)
}
}
@@ -285,7 +293,7 @@ func fetch_static_payreq(_ lnurl: String) async -> LNUrlPayRequest? {
return endpoint
}
func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int) async -> String? {
func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent?, sats: Int, zap_type: ZapType, comment: String?) async -> String? {
guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else {
return nil
}
@@ -295,11 +303,16 @@ func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int)
var query = [URLQueryItem(name: "amount", value: "\(amount)")]
if zappable {
if let json = encode_json(zapreq) {
print("zapreq json: \(json)")
query.append(URLQueryItem(name: "nostr", value: json))
}
if let zapreq, zappable && zap_type != .non_zap {
let json = event_to_json(ev: zapreq)
print("zapreq json: \(json)")
query.append(URLQueryItem(name: "nostr", value: json))
}
// add a lud12 comment as well if we have it
if let comment, let limit = payreq.commentAllowed, limit != 0 {
let limited_comment = String(comment.prefix(limit))
query.append(URLQueryItem(name: "comment", value: limited_comment))
}
base_url.queryItems = query
+1 -3
View File
@@ -36,7 +36,7 @@ class Zaps {
if our_zaps[note_target.note_id] == nil {
our_zaps[note_target.note_id] = [zap]
} else {
let _ = insert_uniq_sorted_zap(zaps: &(our_zaps[note_target.note_id]!), new_zap: zap)
insert_uniq_sorted_zap_by_amount(zaps: &(our_zaps[note_target.note_id]!), new_zap: zap)
}
case .profile(_):
break
@@ -61,7 +61,5 @@ class Zaps {
event_totals[id] = event_totals[id]! + zap.invoice.amount
notify(.update_stats, zap.target.id)
return
}
}
+15 -21
View File
@@ -23,21 +23,19 @@ struct EventActionBar: View {
let event: NostrEvent
let test_lnurl: String?
let generator = UIImpactFeedbackGenerator(style: .medium)
let thread: ThreadV2?
// just used for previews
@State var sheet: ActionBarSheet? = nil
@State var confirm_boost: Bool = false
@State var show_share_sheet: Bool = false
@ObservedObject var bar: ActionBarModel
init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel? = nil, test_lnurl: String? = nil, thread: ThreadV2? = nil) {
init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel? = nil, test_lnurl: String? = nil) {
self.damus_state = damus_state
self.event = event
self.test_lnurl = test_lnurl
_bar = ObservedObject(wrappedValue: bar ?? make_actionbar_model(ev: event.id, damus: damus_state))
self.thread = thread
}
var lnurl: String? {
@@ -47,21 +45,13 @@ struct EventActionBar: View {
var body: some View {
HStack {
if damus_state.keypair.privkey != nil {
let self_replied = (thread != nil && thread!.childEvents.first { $0.pubkey == damus_state.pubkey } != nil)
EventActionButton(img: "bubble.left", col: self_replied ? Color.blue : nil) {
EventActionButton(img: "bubble.left", col: nil) {
notify(.reply, event)
}
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
if thread != nil && !thread!.childEvents.isEmpty {
Text("\(thread!.childEvents.count)")
.offset(x: -10)
.font(.footnote.weight(.medium))
.foregroundColor(self_replied ? Color.blue : Color.gray)
}
}
Spacer()
ZStack {
HStack(spacing: 4) {
EventActionButton(img: "arrow.2.squarepath", col: bar.boosted ? Color.green : nil) {
if bar.boosted {
@@ -71,14 +61,13 @@ struct EventActionBar: View {
}
}
.accessibilityLabel(NSLocalizedString("Boosts", comment: "Accessibility label for boosts button"))
Text("\(bar.boosts > 0 ? "\(bar.boosts)" : "")")
.offset(x: 18)
Text(verbatim: "\(bar.boosts > 0 ? "\(bar.boosts)" : "")")
.font(.footnote.weight(.medium))
.foregroundColor(bar.boosted ? Color.green : Color.gray)
}
Spacer()
ZStack {
HStack(spacing: 4) {
LikeButton(liked: bar.liked) {
if bar.liked {
notify(.delete, bar.our_like)
@@ -86,8 +75,7 @@ struct EventActionBar: View {
send_like()
}
}
Text("\(bar.likes > 0 ? "\(bar.likes)" : "")")
.offset(x: 22)
Text(verbatim: "\(bar.likes > 0 ? "\(bar.likes)" : "")")
.font(.footnote.weight(.medium))
.foregroundColor(bar.liked ? Color.accentColor : Color.gray)
@@ -168,9 +156,9 @@ struct EventActionBar: View {
func EventActionButton(img: String, col: Color?, action: @escaping () -> ()) -> some View {
Button(action: action) {
Label(NSLocalizedString("\u{00A0}", comment: "Non-breaking space character to fill in blank space next to event action button icons."), systemImage: img)
.font(.footnote.weight(.medium))
Image(systemName: img)
.foregroundColor(col == nil ? Color.gray : col!)
.font(.footnote.weight(.medium))
}
}
@@ -200,6 +188,8 @@ struct EventActionBar_Previews: PreviewProvider {
let likedbar = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, our_like: nil, our_boost: nil, our_zap: nil)
let likedbar_ours = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: nil, our_zap: nil)
let maxed_bar = ActionBarModel(likes: 999, boosts: 999, zaps: 999, zap_total: 99999999, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_zap: nil)
let extra_max_bar = ActionBarModel(likes: 9999, boosts: 9999, zaps: 9999, zap_total: 99999999, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_zap: nil)
let mega_max_bar = ActionBarModel(likes: 9999999, boosts: 99999, zaps: 9999, zap_total: 99999999, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_zap: nil)
let zapbar = ActionBarModel(likes: 0, boosts: 0, zaps: 5, zap_total: 10000000, our_like: nil, our_boost: nil, our_zap: nil)
VStack(spacing: 50) {
@@ -210,7 +200,11 @@ struct EventActionBar_Previews: PreviewProvider {
EventActionBar(damus_state: ds, event: ev, bar: likedbar_ours)
EventActionBar(damus_state: ds, event: ev, bar: maxed_bar)
EventActionBar(damus_state: ds, event: ev, bar: extra_max_bar)
EventActionBar(damus_state: ds, event: ev, bar: mega_max_bar)
EventActionBar(damus_state: ds, event: ev, bar: zapbar, test_lnurl: "lnurl")
}
.padding(20)
+21 -3
View File
@@ -26,14 +26,16 @@ struct EventDetailBar: View {
HStack {
if bar.boosts > 0 {
NavigationLink(destination: RepostsView(damus_state: state, model: RepostsModel(state: state, target: target))) {
Text("\(Text("\(bar.boosts)", comment: "Number of reposts.").font(.body.bold())) \(Text(String(format: NSLocalizedString("reposts_count", comment: "Part of a larger sentence to describe how many reposts there are."), bar.boosts)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.")
let noun = Text(verbatim: "\(repostsCountString(bar.boosts))").foregroundColor(.gray)
Text("\(Text("\(bar.boosts)").font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.")
}
.buttonStyle(PlainButtonStyle())
}
if bar.likes > 0 {
NavigationLink(destination: ReactionsView(damus_state: state, model: ReactionsModel(state: state, target: target))) {
Text("\(Text("\(bar.likes)", comment: "Number of reactions on a post.").font(.body.bold())) \(Text(String(format: NSLocalizedString("reactions_count", comment: "Part of a larger sentence to describe how many reactions there are on a post."), bar.likes)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.")
let noun = Text(verbatim: "\(reactionsCountString(bar.likes))").foregroundColor(.gray)
Text("\(Text("\(bar.likes)").font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.")
}
.buttonStyle(PlainButtonStyle())
}
@@ -41,7 +43,8 @@ struct EventDetailBar: View {
if bar.zaps > 0 {
let dst = ZapsView(state: state, target: .note(id: target, author: target_pk))
NavigationLink(destination: dst) {
Text("\(Text("\(bar.zaps)", comment: "Number of zap payments on a post.").font(.body.bold())) \(Text(String(format: NSLocalizedString("zaps_count", comment: "Part of a larger sentence to describe how many zap payments there are on a post."), bar.boosts)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.")
let noun = Text(verbatim: "\(zapsCountString(bar.zaps))").foregroundColor(.gray)
Text("\(Text("\(bar.zaps)").font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.")
}
.buttonStyle(PlainButtonStyle())
}
@@ -49,6 +52,21 @@ struct EventDetailBar: View {
}
}
func repostsCountString(_ count: Int, locale: Locale = Locale.current) -> String {
let bundle = bundleForLocale(locale: locale)
return String(format: bundle.localizedString(forKey: "reposts_count", value: nil, table: nil), locale: locale, count)
}
func reactionsCountString(_ count: Int, locale: Locale = Locale.current) -> String {
let bundle = bundleForLocale(locale: locale)
return String(format: bundle.localizedString(forKey: "reactions_count", value: nil, table: nil), locale: locale, count)
}
func zapsCountString(_ count: Int, locale: Locale = Locale.current) -> String {
let bundle = bundleForLocale(locale: locale)
return String(format: bundle.localizedString(forKey: "zaps_count", value: nil, table: nil), locale: locale, count)
}
struct EventDetailBar_Previews: PreviewProvider {
static var previews: some View {
EventDetailBar(state: test_damus_state(), target: "", target_pk: "")
+5 -22
View File
@@ -10,40 +10,23 @@ import Kingfisher
struct InnerBannerImageView: View {
let url: URL?
let defaultImage = UIImage(named: "profile-banner") ?? UIImage()
@ObservedObject var imageModel: KFImageModel
init(url: URL?) {
self.imageModel = KFImageModel(
url: url,
fallbackUrl: nil,
maxByteSize: 20_971_520, // 20 MiB
downsampleSize: CGSize(width: 750, height: 250)
)
}
var body: some View {
ZStack {
Color(uiColor: .systemBackground)
if (imageModel.url != nil) {
KFAnimatedImage(imageModel.url)
.callbackQueue(.dispatch(.global(qos: .background)))
.processingQueue(.dispatch(.global(qos: .background)))
.serialize(by: imageModel.serializer)
.setProcessor(imageModel.processor)
if (url != nil) {
KFAnimatedImage(url)
.imageContext(.banner)
.configure { view in
view.framePreloadCount = 1
view.framePreloadCount = 3
}
.placeholder { _ in
Color(uiColor: .secondarySystemBackground)
}
.scaleFactor(UIScreen.main.scale)
.loadDiskFileSynchronously()
.fade(duration: 0.1)
.onFailureImage(defaultImage)
.id(imageModel.refreshID)
} else {
Image(uiImage: defaultImage).resizable()
}
+69
View File
@@ -0,0 +1,69 @@
//
// BookmarksView.swift
// damus
//
// Created by Joel Klabo on 2/18/23.
//
import SwiftUI
struct BookmarksView: View {
let state: DamusState
private let noneFilter: (NostrEvent) -> Bool = { _ in true }
private let bookmarksTitle = NSLocalizedString("Bookmarks", comment: "Title of bookmarks view")
@State private var bookmarkEvents: [NostrEvent] = []
init(state: DamusState) {
self.state = state
}
var body: some View {
Group {
if bookmarkEvents.isEmpty {
VStack {
Image(systemName: "bookmark")
.resizable()
.scaledToFit()
.frame(width: 32.0, height: 32.0)
Text(NSLocalizedString("You have no bookmarks yet, add them in the context menu", comment: "Text indicating that there are no bookmarks to be viewed"))
}
.task {
updateBookmarks()
}
} else {
ScrollView {
InnerTimelineView(events: EventHolder(events: bookmarkEvents, incoming: []), damus: state, show_friend_icon: true, filter: noneFilter)
}
}
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(bookmarksTitle)
.toolbar {
if !bookmarkEvents.isEmpty {
Button(NSLocalizedString("Clear All", comment: "Button for clearing bookmarks data.")) {
BookmarksManager(pubkey: state.pubkey).clearAll()
bookmarkEvents = []
}
}
}
.onReceive(handle_notify(.update_bookmarks)) { _ in
updateBookmarks()
}
}
private func updateBookmarks() {
bookmarkEvents = BookmarksManager(pubkey: state.pubkey).bookmarks.compactMap { bookmark_json in
event_from_json(dat: bookmark_json)
}
}
}
/*
struct BookmarksView_Previews: PreviewProvider {
static var previews: some View {
BookmarksView()
}
}
*/
+5 -4
View File
@@ -63,7 +63,7 @@ struct ChatView: View {
}
var ReplyDescription: some View {
Text("\(reply_desc(profiles: damus_state.profiles, event: event))")
Text(verbatim: "\(reply_desc(profiles: damus_state.profiles, event: event))")
.font(.footnote)
.foregroundColor(.gray)
.frame(alignment: .leading)
@@ -89,12 +89,12 @@ struct ChatView: View {
ProfileName(pubkey: event.pubkey, profile: damus_state.profiles.lookup(id: event.pubkey), damus: damus_state, show_friend_confirmed: true)
.foregroundColor(colorScheme == .dark ? id_to_color(event.pubkey) : Color.black)
//.shadow(color: Color.black, radius: 2)
Text("\(format_relative_time(event.created_at))")
Text(verbatim: "\(format_relative_time(event.created_at))")
.foregroundColor(.gray)
}
}
if let ref_id = thread.replies.lookup(event.id) {
if let _ = thread.replies.lookup(event.id) {
if !is_reply_to_prev() {
/*
ReplyQuoteView(keypair: damus_state.keypair, quoter: event, event_id: ref_id, profiles: damus_state.profiles, previews: damus_state.previews)
@@ -112,8 +112,9 @@ struct ChatView: View {
NoteContentView(damus_state: damus_state,
event: event,
show_images: show_images,
size: .normal,
artifacts: .just_content(event.content),
size: .normal)
truncate: false)
if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey {
let bar = make_actionbar_model(ev: event.id, damus: damus_state)
+30 -12
View File
@@ -8,6 +8,7 @@ import AVFoundation
import Kingfisher
import SwiftUI
import LocalAuthentication
import Combine
struct ConfigView: View {
let state: DamusState
@@ -128,9 +129,16 @@ struct ConfigView: View {
}
}
Section(NSLocalizedString("Default Zap Amount in sats", comment: "Section title for zap configuration")) {
TextField("1000", text: $default_zap_amount)
TextField(String("1000"), text: $default_zap_amount)
.keyboardType(.numberPad)
.onReceive(Just(default_zap_amount)) { newValue in
if let parsed = handle_string_amount(new_value: newValue) {
self.default_zap_amount = String(parsed)
set_default_zap_amount(pubkey: self.state.pubkey, amount: parsed)
}
}
}
Section(NSLocalizedString("Translations", comment: "Section title for selecting the translation service.")) {
@@ -201,19 +209,13 @@ struct ConfigView: View {
}
}
let bundleShortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String
let bundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] as! String
Section(NSLocalizedString("Version", comment: "Section title for displaying the version number of the Damus app.")) {
Text("\(bundleShortVersion) (\(bundleVersion))", comment: "Text indicating which version of the Damus app is running. Should typically not need to be translated.")
if let bundleShortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"], let bundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] {
Section(NSLocalizedString("Version", comment: "Section title for displaying the version number of the Damus app.")) {
Text(verbatim: "\(bundleShortVersion) (\(bundleVersion))")
}
}
}
}
.onChange(of: default_zap_amount) { val in
guard let amt = Int(val) else {
return
}
set_default_zap_amount(pubkey: state.pubkey, amount: amt)
}
.navigationTitle(NSLocalizedString("Settings", comment: "Navigation title for Settings view."))
.navigationBarTitleDisplayMode(.large)
.alert(NSLocalizedString("Permanently Delete Account", comment: "Alert for deleting the users account."), isPresented: $confirm_delete_account) {
@@ -333,3 +335,19 @@ struct ConfigView_Previews: PreviewProvider {
}
}
}
func handle_string_amount(new_value: String) -> Int? {
let digits = Set("0123456789")
let filtered = new_value.filter { digits.contains($0) }
if filtered == "" {
return nil
}
guard let amt = Int(filtered) else {
return nil
}
return amt
}
+2 -2
View File
@@ -36,14 +36,14 @@ struct CreateAccountView: View {
HStack(alignment: .top) {
VStack {
Text(" ", comment: "Blank space to separate profile picture from profile editor form.")
Text(verbatim: " ")
.foregroundColor(.white)
}
VStack {
SignupForm {
FormLabel(NSLocalizedString("Username", comment: "Label to prompt username entry."))
HStack(spacing: 0.0) {
Text("@", comment: "Prefix character to username.")
Text(verbatim: "@")
.foregroundColor(.white)
.padding(.leading, -25.0)
+26 -10
View File
@@ -37,9 +37,7 @@ struct DMChatView: View {
var Header: some View {
let profile = damus_state.profiles.lookup(id: pubkey)
let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state)
let fmodel = FollowersModel(damus_state: damus_state, target: pubkey)
let profile_page = ProfileView(damus_state: damus_state, profile: pmodel, followers: fmodel)
let profile_page = ProfileView(damus_state: damus_state, pubkey: pubkey)
return NavigationLink(destination: profile_page) {
HStack {
ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles)
@@ -183,13 +181,12 @@ struct DMChatView_Previews: PreviewProvider {
}
}
enum EncEncoding {
case base64
case bech32
}
func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keypair, created_at: Int64? = nil) -> NostrEvent?
{
guard let privkey = keypair.privkey else {
return nil
}
func encrypt_message(message: String, privkey: String, to_pk: String, encoding: EncEncoding = .base64) -> String? {
let iv = random_bytes(count: 16).bytes
guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: to_pk) else {
return nil
@@ -198,7 +195,26 @@ func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keyp
guard let enc_message = aes_encrypt(data: utf8_message, iv: iv, shared_sec: shared_sec) else {
return nil
}
let enc_content = encode_dm_base64(content: enc_message.bytes, iv: iv)
switch encoding {
case .base64:
return encode_dm_base64(content: enc_message.bytes, iv: iv)
case .bech32:
return encode_dm_bech32(content: enc_message.bytes, iv: iv)
}
}
func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keypair, created_at: Int64? = nil) -> NostrEvent?
{
guard let privkey = keypair.privkey else {
return nil
}
guard let enc_content = encrypt_message(message: message, privkey: privkey, to_pk: to_pk) else {
return nil
}
let created = created_at ?? Int64(Date().timeIntervalSince1970)
let ev = NostrEvent(content: enc_content, pubkey: keypair.pubkey, kind: 4, tags: tags, createdAt: created)

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