Compare commits

..

233 Commits

Author SHA1 Message Date
tyiu fe9d6da027 Do not translate own notes if logged in with private key
Changelog-Fixed: Do not translate own notes if logged in with private key
2023-04-17 01:36:46 +02:00
William Casarin 02fc065005 Always check signatures on profile events
These contain sensitive data (lightning addresses) and it would be
really bad if these were forged.

Changelog-Changed: Always check signatures of profile events
2023-04-15 16:01:00 -07:00
Bartholomew Joyce 668b0a94df cleanup nostr bech32 entity parsing
Closes: #911
2023-04-15 15:04:13 -07:00
William Casarin ebba9d3004 Fix broken notifications 2023-04-15 13:42:10 -07:00
William Casarin 8733a34933 v1.4.3-1 changelog 2023-04-15 12:57:42 -07:00
William Casarin 0de6cfe344 Merge remote-tracking branch 'github/translations' 2023-04-15 12:54:34 -07:00
William Casarin a4d40dbfa6 Revert "Changelog-Added: Banner Image Upload"
I didn't mean to commit this yet

This reverts commit 95041600dc.
2023-04-15 12:43:44 -07:00
Swift 05d332eac3 Add deep links for local notifications
Allow notifications to be clickable

Changelog-Added: Add deep links for local notifications
Co-authored-by: William Casarin <jb55@jb55.com>
Closes: #880
2023-04-15 12:41:49 -07:00
William Casarin b5a3697d78 Refactor direct messages model
We can track the pubkey in the DirectMessageModel instead of having a
janky tuple.
2023-04-15 12:41:00 -07:00
William Casarin 247270f3d3 Introduce LocalNotification
This will be used for local notification data
2023-04-15 12:40:00 -07:00
William Casarin 9327068264 ContentView: open thread helper 2023-04-15 12:38:05 -07:00
transifex-integration[bot] c277c14bcd Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-15 18:48:00 +00:00
transifex-integration[bot] e688a691fc Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-15 18:47:38 +00:00
transifex-integration[bot] b470af8f1d Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-15 18:47:29 +00:00
transifex-integration[bot] efd1168217 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-15 18:46:02 +00:00
transifex-integration[bot] f7a3f9ab76 Apply translations in hu_HU
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'hu_HU' language.
2023-04-15 17:55:34 +00:00
Joel Klabo 95041600dc Changelog-Added: Banner Image Upload 2023-04-15 10:28:17 -07:00
William Casarin 8a8d2ebbc3 PostView: change wording on upload confirmation 2023-04-15 10:26:36 -07:00
William Casarin fad0a6b783 PostView: remove ! unwrap for mediaToUpload 2023-04-15 10:22:05 -07:00
Swift f5d7465368 Ask permission before uploading media
Changelog-Changed: Ask permission before uploading media
Closes: #886
2023-04-15 10:19:53 -07:00
OlegAba 8a785559c6 Fix tap area when mentioning users
Changelog-Fixed: Fix tap area when mentioning users
Closes: #895
2023-04-15 10:16:40 -07:00
William Casarin d4c8c15cc3 Revert "Revert "Fix tap area when mentioning users""
This reverts commit 735376b00f.
2023-04-15 09:59:55 -07:00
Ryan Calder 41a462871c Enable Mac Catalyst
Closes: #926
2023-04-15 09:47:21 -07:00
William Casarin 76a669acc2 Show DM message in local notification
Changelog-Changed: Show DM message in local notification
2023-04-15 09:46:13 -07:00
William Casarin 39236dc094 remove verbose logs 2023-04-15 09:44:17 -07:00
transifex-integration[bot] 5860125802 Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'cs' language.
2023-04-15 10:29:15 +00:00
transifex-integration[bot] ae96c3b707 Apply translations in el_GR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.
2023-04-15 09:50:43 +00:00
transifex-integration[bot] 136f6f37e8 Apply translations in el_GR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.
2023-04-15 09:49:12 +00:00
transifex-integration[bot] 21f84f722b Apply translations in el_GR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.
2023-04-15 09:49:04 +00:00
transifex-integration[bot] 47747379ee Apply translations in nl
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-04-15 09:48:02 +00:00
transifex-integration[bot] a1b95d40e6 Apply translations in el_GR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.
2023-04-15 09:43:47 +00:00
transifex-integration[bot] 0fc69d862a Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-15 09:36:45 +00:00
tyiu fb0330476d Export strings for translation 2023-04-15 01:32:07 +02:00
transifex-integration[bot] 4b978594fa Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-15 01:31:27 +02:00
William Casarin f0bbba7a33 Fix invalid DM author notifications
Changelog-Fixed: Fix invalid DM author notifications
2023-04-14 11:46:55 -07:00
William Casarin b5faae9d1c Make reposts use postbox 2023-04-14 11:46:55 -07:00
William Casarin a4d4954abd Fix relay signal indicator, properly show how many relays you are connected to
Changelog-Fixed: Fix relay signal indicator, properly show how many relays you are connected to
2023-04-14 11:46:55 -07:00
William Casarin 735376b00f Revert "Fix tap area when mentioning users"
This reverts commit a2cd51b6e7.
2023-04-14 11:36:45 -07:00
William Casarin 042e02d2e4 regression: unbreak dms 2023-04-14 10:33:50 -07:00
William Casarin 40468b1603 refactor: dms view init logic 2023-04-14 10:33:20 -07:00
William Casarin 8c19ec1532 small refactor to include_event 2023-04-14 09:54:54 -07:00
tyiu 1ac9620242 Add thread muting
Changelog-Added: Add thread muting
Closes: #893
2023-04-14 09:43:36 -07:00
Swift d5ecc9bce4 Preview media uploads when posting
Changelog-Added: Preview media uploads when posting
Closes: #894
2023-04-14 09:32:27 -07:00
William Casarin d82b69aac5 Merge remote-tracking branch 'github/translations' 2023-04-14 09:24:59 -07:00
William Casarin bad6ba3643 qrcode: don't default pubkey to "" 2023-04-14 09:23:14 -07:00
ericholguin 5c131e62d7 Add QR Code in profiles
Changelog-Added: Add QR Code in profiles
Closes: #918
2023-04-14 09:20:10 -07:00
William Casarin 29ab48287f start v1.4.3-1 build train 2023-04-14 09:20:10 -07:00
transifex-integration[bot] 5c854519db Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'cs' language.
2023-04-14 14:15:46 +00:00
transifex-integration[bot] 2cc04e24a3 Apply translations in el_GR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.
2023-04-14 11:51:53 +00:00
transifex-integration[bot] d24bea366d Apply translations in ar
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.
2023-04-14 03:20:41 +00:00
transifex-integration[bot] 85ce8cb93c Apply translations in nl
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-04-13 18:44:01 +00:00
tyiu 32bb8c365d Export strings for translation 2023-04-13 18:37:58 +02:00
transifex-integration[bot] d9285ab3ca Apply translations in nl
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-04-13 18:33:59 +02:00
tyiu 3cba771655 Export strings for translation 2023-04-13 18:33:58 +02:00
tyiu f6f2517fda Merge French variant translations into general French translations 2023-04-13 18:33:58 +02:00
transifex-integration[bot] 047325e6b2 Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'cs' language.
2023-04-13 18:33:58 +02:00
transifex-integration[bot] ba2108d659 Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'cs' language.
2023-04-13 18:33:58 +02:00
transifex-integration[bot] 863c7baa8b Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'cs' language.
2023-04-13 18:33:58 +02:00
transifex-integration[bot] 8a5e95e47a Apply translations in fr
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'fr' language.
2023-04-13 18:33:58 +02:00
transifex-integration[bot] de0997216d Apply translations in fr
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'fr' language.
2023-04-13 18:33:58 +02:00
transifex-integration[bot] cc64c82ec4 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'sv_SE' language.
2023-04-13 18:33:57 +02:00
transifex-integration[bot] e2ca02399b Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-13 18:33:57 +02:00
transifex-integration[bot] 5418f55cee Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-13 18:33:57 +02:00
transifex-integration[bot] eb65d473cd Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-13 18:33:57 +02:00
transifex-integration[bot] dd337c4805 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-13 18:33:57 +02:00
transifex-integration[bot] 2f6ed72f6d Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-13 18:33:57 +02:00
transifex-integration[bot] d71bb33408 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-13 18:33:57 +02:00
transifex-integration[bot] 72cfb2b071 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-13 18:33:57 +02:00
transifex-integration[bot] a67cb2df90 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-13 18:33:56 +02:00
William Casarin 23e9ce1455 v1.4.2 changelog 2023-04-13 09:27:57 -07:00
William Casarin 5f1132cbc8 v1.4.2-2 2023-04-12 20:11:08 -07:00
William Casarin 806c6257df A few more performance tweaks 2023-04-12 20:09:51 -07:00
William Casarin 18aafb086e Revert "Make tabs easier to click"
This reverts commit 2a2af056eb.
2023-04-12 19:49:52 -07:00
William Casarin fc534ea42d Avoid slow string byte counting functions 2023-04-12 19:26:28 -07:00
William Casarin 54c8958250 Fix a few more hitches 2023-04-12 18:22:16 -07:00
William Casarin e9f71ed07c Load images asyncronously from disk
Changelog-Fixed: Fix hitches caused by syncronous loading of cached images
2023-04-12 13:34:21 -07:00
William Casarin c719058487 Revert "Remove duplicate share sheet action"
This reverts commit 2b34e88a47.
2023-04-12 11:26:41 -07:00
Luis Cabrera ab853c406c Display follows in most recent to oldest
Changelog-Changed: Display follows in most recent to oldest
2023-04-11 15:30:32 -07:00
William Casarin 8a88824677 Merge remote-tracking branch 'github/translations' 2023-04-11 15:22:26 -07:00
William Casarin 0b3918710a Include #btc in custom #bitcoin hashtag
Changelog-Added: Include #btc in custom #bitcoin hashtag
2023-04-11 15:16:57 -07:00
William Casarin 1320ff6bec Fix tabs sometimes not switching
This is the dumbest code I've ever written

Changelog-Fixed: Fix tabs sometimes not switching
2023-04-11 15:05:03 -07:00
William Casarin 10cab37270 remove unneeded id thingies 2023-04-11 14:54:33 -07:00
William Casarin 179da97090 cleanup some dubious code 2023-04-11 14:21:48 -07:00
William Casarin 2b2d124495 Configurable notification dots
Changelog-Added: Make notification dots configurable
2023-04-11 13:58:04 -07:00
William Casarin 2a2af056eb Make tabs easier to click
Changelog-Changed: Make tabs easier to click
2023-04-11 13:58:04 -07:00
William Casarin f56edd5547 switch to v1.4.2 train 2023-04-11 13:00:03 -07:00
William Casarin b30d0c01db nip19: allow empty relays in nprofile and nevent 2023-04-11 12:45:45 -07:00
William Casarin e8d63768c1 Fix note mention test 2023-04-11 10:27:11 -07:00
William Casarin 1f648057d5 Add another bech32 test 2023-04-11 10:23:34 -07:00
Bartholomew Joyce ea14099b62 Improved parsing of bech32 entities 2023-04-11 10:23:34 -07:00
William Casarin f5942f5123 build 9 2023-04-11 10:23:34 -07:00
William Casarin cb8585e4f8 Fix naddr crash 2023-04-11 10:03:48 -07:00
transifex-integration[bot] 526689c742 Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-11 12:41:40 +00:00
transifex-integration[bot] 32a9856e2a Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-11 12:41:37 +00:00
transifex-integration[bot] 6082a2829f Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-11 12:41:16 +00:00
transifex-integration[bot] b3f6b451bf Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-11 12:41:14 +00:00
transifex-integration[bot] 91d51c5e76 Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-11 12:41:02 +00:00
transifex-integration[bot] 3262fe806a Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-11 12:40:53 +00:00
transifex-integration[bot] d04e9c9b5f Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-11 12:40:42 +00:00
transifex-integration[bot] 015eb5f9fe Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-11 12:40:36 +00:00
transifex-integration[bot] 4a525a7581 Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-11 12:40:27 +00:00
transifex-integration[bot] 72e14fc3a8 Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-11 12:33:08 +00:00
transifex-integration[bot] af275965ee Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-11 12:32:42 +00:00
transifex-integration[bot] 9f5913828a Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-11 12:32:30 +00:00
transifex-integration[bot] c7c21cdee7 Apply translations in hu_HU
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'hu_HU' language.
2023-04-11 09:01:19 +00:00
William Casarin 44a2c4ba7b v1.4.1-8 changelog 2023-04-10 16:13:43 -07:00
William Casarin a74aea9d12 v1.4.1-8 2023-04-10 16:12:26 -07:00
William Casarin 0866c70346 Fix tests 2023-04-10 16:09:43 -07:00
William Casarin 2b34e88a47 Remove duplicate share sheet action 2023-04-10 10:02:20 -07:00
William Casarin 2aa8d527b9 Don't leak mentions in DMs
Changelog-Fixed: Don't leak mentions in DMs
2023-04-10 09:31:26 -07:00
transifex-integration[bot] 88cbb55953 Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-10 15:26:20 +00:00
transifex-integration[bot] e738c6c1ca Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-10 15:26:08 +00:00
transifex-integration[bot] f4e0c8df5c Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ko' language.
2023-04-10 15:17:52 +00:00
transifex-integration[bot] 269269d056 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ko' language.
2023-04-10 15:14:51 +00:00
transifex-integration[bot] fd49539615 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ko' language.
2023-04-10 15:14:36 +00:00
transifex-integration[bot] dbb5c19002 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ko' language.
2023-04-10 15:14:28 +00:00
transifex-integration[bot] 6961113734 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ko' language.
2023-04-10 15:14:11 +00:00
transifex-integration[bot] 4642656ce2 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'ko' language.
2023-04-10 14:24:40 +00:00
transifex-integration[bot] 0c2132b122 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'ko' language.
2023-04-10 14:24:14 +00:00
transifex-integration[bot] a1311a940a Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'ko' language.
2023-04-10 14:22:27 +00:00
transifex-integration[bot] af18975240 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'ko' language.
2023-04-10 14:22:15 +00:00
William Casarin f500da03e8 nip27: handle nrelay a bit better 2023-04-09 22:45:38 -07:00
William Casarin 8a230861bf remove broken test 2023-04-09 22:45:21 -07:00
William Casarin 13354b0eb5 Refactor NIP19 implementation and add tests
Closes: #837
2023-04-09 22:03:51 -07:00
Bartholomew Joyce c6f4643b5a Add support for nostr: bech32 urls in posts and DMs (NIP19)
Changelog-Added: Add support for nostr: bech32 urls in posts and DMs (NIP19)
2023-04-09 22:03:27 -07:00
transifex-integration[bot] ee34b1c0a3 Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-10 01:12:21 +00:00
OlegAba a2cd51b6e7 Fix tap area when mentioning users
Changelog-Fixed: Fix tap area when mentioning users
Closes: #895
2023-04-09 16:17:57 -07:00
transifex-integration[bot] 831a409fe6 Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'pt_BR' language.
2023-04-09 14:48:54 +00:00
transifex-integration[bot] 11e22628bb Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'pt_BR' language.
2023-04-09 14:47:54 +00:00
transifex-integration[bot] 6598b5b4bb Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-08 20:05:16 -04:00
transifex-integration[bot] ca293ef29b Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-08 20:05:16 -04:00
transifex-integration[bot] 8051324d3e Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-08 20:05:15 -04:00
transifex-integration[bot] a11cf66088 Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-08 20:05:15 -04:00
transifex-integration[bot] a10142d3b9 Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-08 20:05:15 -04:00
transifex-integration[bot] 83fcd8600a Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-08 20:05:15 -04:00
transifex-integration[bot] 6283157ef4 Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-08 20:05:15 -04:00
transifex-integration[bot] 8b2f45da41 Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-08 20:05:15 -04:00
transifex-integration[bot] ed467a2f79 Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-08 20:05:15 -04:00
transifex-integration[bot] feace9b70d Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-08 20:05:15 -04:00
transifex-integration[bot] 62bccd6b60 Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-08 20:05:15 -04:00
transifex-integration[bot] 6f4d12cab4 Apply translations in zh_TW
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.
2023-04-08 20:05:14 -04:00
transifex-integration[bot] 436fe830c7 Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-08 20:05:14 -04:00
transifex-integration[bot] 69b6d54b0e Apply translations in zh_HK
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_HK' language.
2023-04-08 20:05:14 -04:00
transifex-integration[bot] 13522028ba Apply translations in zh_HK
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_HK' language.
2023-04-08 20:05:14 -04:00
transifex-integration[bot] 1459581ec9 Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-08 20:05:14 -04:00
transifex-integration[bot] 31aedd2a6e Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-08 20:05:14 -04:00
transifex-integration[bot] bb1dff13ce Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-08 20:05:14 -04:00
transifex-integration[bot] 3b8c884b30 Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-08 20:05:14 -04:00
transifex-integration[bot] 30299fafed Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-08 20:05:13 -04:00
William Casarin 46c208c9a5 v1.4.1-7 changelog 2023-04-07 12:15:34 -07:00
William Casarin 8e78bf9e1a v1.4.1-7 2023-04-07 12:14:57 -07:00
William Casarin 82fff4591c Add #zap and #zapathon custom hashtags
Changelog-Added: Add #zap and #zapathon custom hashtags
2023-04-07 12:05:54 -07:00
OlegAba b8226d674d Fix post view padding
Changelog-Fixed: Fix padding in post view
Closes: #879
2023-04-07 11:33:52 -07:00
tyiu 3f3892ba1d Add validation to prevent whitespaces be inputted on NIP-05 input field
Changelog-Changed: Add validation to prevent whitespaces be inputted on NIP-05 input field
Closes: #793
2023-04-07 11:22:38 -07:00
Bryan Montz 1b6224e665 Show most recently bookmarked notes at the top
Changelog-Fixed: Show most recently bookmarked notes at the top
Closes: #854
2023-04-07 11:21:42 -07:00
William Casarin 782f8d6a69 Change reply color from blue to purple. Blue is banned from Damus.
Changelog-Changed: Change reply color from blue to purple. Blue is banned from Damus.
2023-04-07 10:49:33 -07:00
Joel Klabo 9ad6af0e4f Fix Edit Profile Background Colors 2023-04-07 10:37:59 -07:00
William Casarin e16ea0f4dc Merge remote-tracking branch 'github/translations' 2023-04-07 10:15:53 -07:00
William Casarin 5b1808d8e7 Add custom #plebchain icon
Changelog-Added: Add custom #plebchain icon
2023-04-07 10:10:13 -07:00
transifex-integration[bot] d50b2deb26 Apply translations in nl
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-04-07 07:09:24 +00:00
transifex-integration[bot] 9de4730e17 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-07 06:14:30 +00:00
transifex-integration[bot] 382265dd39 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-07 06:13:13 +00:00
tyiu 6c7f8cdbe5 Fix localization issues and export strings for translation 2023-04-06 21:05:53 -04:00
William Casarin 36a92b3795 v1.4.1-6 changelog 2023-04-06 17:41:34 -07:00
William Casarin 6d4d218c28 v1.4.1-6 2023-04-06 17:39:41 -07:00
transifex-integration[bot] 8679da1275 Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-06 20:38:48 -04:00
transifex-integration[bot] 59dcbc130c Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-06 20:38:48 -04:00
transifex-integration[bot] badd3210e5 Apply translations in nl
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-04-06 20:38:48 -04:00
transifex-integration[bot] dd6cc3cf4f Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-06 20:38:48 -04:00
transifex-integration[bot] 36a0ca9c80 Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-06 20:38:48 -04:00
transifex-integration[bot] a7d3b19665 Apply translations in vi
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'vi' language.
2023-04-06 20:38:48 -04:00
transifex-integration[bot] 226e567987 Apply translations in vi
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'vi' language.
2023-04-06 20:38:48 -04:00
transifex-integration[bot] 898ffc0186 Apply translations in es_ES
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_ES' language.
2023-04-06 20:38:48 -04:00
transifex-integration[bot] 4d0f7b576b Apply translations in es_ES
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_ES' language.
2023-04-06 20:38:48 -04:00
transifex-integration[bot] 230d049384 Apply translations in es_ES
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_ES' language.
2023-04-06 20:38:47 -04:00
transifex-integration[bot] 774a4173b0 Apply translations in es_ES
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_ES' language.
2023-04-06 20:38:47 -04:00
transifex-integration[bot] f8b5d91720 Apply translations in es_ES
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_ES' language.
2023-04-06 20:38:47 -04:00
transifex-integration[bot] 0832c82ee8 Apply translations in es_ES
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_ES' language.
2023-04-06 20:38:46 -04:00
William Casarin 4786c6f0cb Don't show Translating... if we're not actually translating
Changelog-Fixed: Don't show Translating... if we're not actually translating
2023-04-06 17:33:01 -07:00
William Casarin 8d0aea22fd Custom hashtag offsets
This allows you to tweak the positioning a bit
2023-04-06 17:28:43 -07:00
William Casarin 3d27e49e70 Add custom #nostr and #coffeechain hashtags
Changelog-Added: Custom hashtags for #nostr and #coffeechain
2023-04-06 17:09:50 -07:00
William Casarin e9be227009 Add bitcoin icon to bitcoin hashtags
Changelog-Added: Add bitcoin icon to bitcoin hashtags
2023-04-06 16:04:16 -07:00
William Casarin 2e640db012 Make bitcoin hashtag orange 2023-04-06 12:10:39 -07:00
William Casarin eed16449fe oops, we're on build 5 2023-04-06 12:10:31 -07:00
William Casarin 3661d64450 regression: remove action bar from DM view 2023-04-06 12:10:17 -07:00
William Casarin 24c82293b3 Disable translations in DMs by default
There is an option to enable it now.

Changelog-Changed: Disable translations in DMs by default
2023-04-06 12:07:01 -07:00
William Casarin 7fb1bc48c4 rename NostrBuild to nostr.build
Closes: #883
2023-04-06 11:05:51 -07:00
William Casarin 2ae4a156da Make sure we only update translation cache on main thread 2023-04-06 10:39:01 -07:00
William Casarin f700dd799f v1.4.1-4 changelog 2023-04-06 10:31:52 -07:00
William Casarin 00548adc1f v1.4.1-4 2023-04-06 10:31:20 -07:00
William Casarin 7c08d4af45 perf: cache note artifacts 2023-04-06 10:27:15 -07:00
William Casarin c7bf1da797 perf: cache lnurl to avoid bech32 encoding on the main thread 2023-04-06 10:26:51 -07:00
William Casarin c5341ba337 Cache translations, fix translation popping
Completely refactor Translate View. Simplify bool logic into a state enum.

Changelog-Fixed: Fix translation text popping
Changelog-Added: Cache translations
2023-04-06 10:16:53 -07:00
William Casarin 95fb7bccf8 Merge remote-tracking branch 'github/translations' 2023-04-06 08:35:24 -07:00
William Casarin 6499738994 Fix auto-translations
Changelog-Fixed: Fix broken auto-translations
2023-04-06 08:27:27 -07:00
William Casarin 532eaa35bf pointless refactors 2023-04-06 08:27:27 -07:00
tyiu 9c1bb30f5f Localization fix and export translations 2023-04-06 10:47:51 -04:00
transifex-integration[bot] 4683b417b9 Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-06 10:42:42 -04:00
transifex-integration[bot] 2176b58215 Apply translations in zh_TW
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.
2023-04-06 10:42:42 -04:00
transifex-integration[bot] 3c7627bb9a Apply translations in zh_HK
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_HK' language.
2023-04-06 10:42:41 -04:00
transifex-integration[bot] 463ca5ce7b Apply translations in fr_FR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'fr_FR' language.
2023-04-06 10:42:41 -04:00
transifex-integration[bot] b6a0626890 Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-06 10:42:41 -04:00
transifex-integration[bot] dbfad189cb Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-06 10:42:41 -04:00
transifex-integration[bot] 8159213ef0 Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-06 10:42:41 -04:00
transifex-integration[bot] 684841c03a Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'cs' language.
2023-04-06 10:42:41 -04:00
transifex-integration[bot] e429bd7df9 Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'cs' language.
2023-04-06 10:42:41 -04:00
transifex-integration[bot] 45ebcf565a Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-06 10:42:41 -04:00
transifex-integration[bot] c031b08c9a Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-06 10:42:41 -04:00
transifex-integration[bot] d98503732e Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'de' language.
2023-04-06 10:42:40 -04:00
transifex-integration[bot] bfe0a39370 Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'de' language.
2023-04-06 10:42:40 -04:00
transifex-integration[bot] 36de599326 Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'de' language.
2023-04-06 10:42:40 -04:00
transifex-integration[bot] 3cbdfb7ddf Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'de' language.
2023-04-06 10:42:40 -04:00
transifex-integration[bot] ddd309634f Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'de' language.
2023-04-06 10:42:40 -04:00
transifex-integration[bot] 6bbf56f80c Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'de' language.
2023-04-06 10:42:40 -04:00
transifex-integration[bot] de1332d6ea Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'de' language.
2023-04-06 10:42:40 -04:00
transifex-integration[bot] 648f25ea7a Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-06 10:42:40 -04:00
transifex-integration[bot] b46dc04ae8 Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-06 10:42:40 -04:00
transifex-integration[bot] 8cabfaf90e Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-06 10:42:39 -04:00
transifex-integration[bot] 0b02956bb7 Apply translations in nl
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-04-06 10:42:39 -04:00
transifex-integration[bot] 4abea02d6e Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pt_BR' language.
2023-04-06 10:42:39 -04:00
transifex-integration[bot] 5d20c49f7d Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'pt_BR' language.
2023-04-06 10:42:39 -04:00
transifex-integration[bot] c11e0fa919 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-06 10:42:39 -04:00
transifex-integration[bot] 54bfeb04da Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-06 10:42:39 -04:00
transifex-integration[bot] 214eec9cb6 Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-06 10:42:39 -04:00
transifex-integration[bot] b48de9525e Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-06 10:42:39 -04:00
transifex-integration[bot] 690f3ec740 Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-06 10:42:39 -04:00
tyiu 6df4ffc1f0 Export translations 2023-04-06 10:42:38 -04:00
transifex-integration[bot] 83cead1fbb Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-06 10:42:38 -04:00
transifex-integration[bot] 478600cf0e Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-06 10:42:38 -04:00
transifex-integration[bot] 75002802fa Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-06 10:42:38 -04:00
transifex-integration[bot] faf072d432 Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-06 10:42:38 -04:00
transifex-integration[bot] e888da26a7 Apply translations in nl
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-04-06 10:42:38 -04:00
William Casarin b532dc48e1 Also trim whitespace before single note refs 2023-04-05 13:30:05 -07:00
128 changed files with 2995 additions and 1354 deletions
+111
View File
@@ -1,3 +1,114 @@
## [1.4.3-1] - 2023-04-15
### Added
- Add deep links for local notifications (Swift + Will)
- Add thread muting (Terry Yiu)
- Preview media uploads when posting (Swift)
- Add QR Code in profiles (ericholguin)
### Changed
- Ask permission before uploading media (Swift)
- Show DM message in local notification (William Casarin)
### Fixed
- Fix tap area when mentioning users (OlegAba)
- Fix invalid DM author notifications (William Casarin)
- Fix relay signal indicator, properly show how many relays you are connected to (William Casarin)
[1.4.3-1]: https://github.com/damus-io/damus/releases/tag/v1.4.3-1
## [1.4.2-2] - 2023-04-12
### Added
- Include #btc in custom #bitcoin hashtag (William Casarin)
- Make notification dots configurable (William Casarin)
### Changed
- Display follows in most recent to oldest (Luis Cabrera)
### Fixed
- Fix hitches caused by syncronous loading of cached images (William Casarin)
- Fix tabs sometimes not switching (William Casarin)
[1.4.2-2]: https://github.com/damus-io/damus/releases/tag/v1.4.2-2
## [1.4.1-8] - 2023-04-10
### Added
- Add support for nostr: bech32 urls in posts and DMs (NIP19) (Bartholomew Joyce)
### Fixed
- Don't leak mentions in DMs (William Casarin)
- Fix tap area when mentioning users (OlegAba)
[1.4.1-8]: https://github.com/damus-io/damus/releases/tag/v1.4.1-8
## [1.4.1-7] - 2023-04-07
### Added
- Add #zap and #zapathon custom hashtags (William Casarin)
- Add custom #plebchain icon (William Casarin)
### Changed
- Add validation to prevent whitespaces be inputted on NIP-05 input field (Terry Yiu)
- Change reply color from blue to purple. Blue is banned from Damus. (William Casarin)
### Fixed
- Fix padding in post view (OlegAba)
- Show most recently bookmarked notes at the top (Bryan Montz)
[1.4.1-7]: https://github.com/damus-io/damus/releases/tag/v1.4.1-7
## [1.4.1-6] - 2023-04-06
### Added
- Custom hashtags for #bitcoin, #nostr and #coffeechain (William Casarin)
### Changed
- Disable translations in DMs by default (William Casarin)
### Fixed
- Don't show Translating... if we're not actually translating (William Casarin)
[1.4.1-6]: https://github.com/damus-io/damus/releases/tag/v1.4.1-6
## [1.4.1-4] - 2023-04-06
### Added
- Cache translations (William Casarin)
### Fixed
- Fix translation text popping (William Casarin)
- Fix broken auto-translations (William Casarin)
- Fix extraneous padding on some image posts (William Casarin)
- Fix crash in relay list view (William Casarin)
[1.4.1-4]: https://github.com/damus-io/damus/releases/tag/v1.4.1-4
## [1.4.1-3] - 2023-04-05
### Added
+10 -3
View File
@@ -91,13 +91,12 @@ int bech32_encode(char *output, const char *hrp, const uint8_t *data, size_t dat
return 1;
}
bech32_encoding bech32_decode(char* hrp, uint8_t *data, size_t *data_len, const char *input, size_t max_input_len) {
bech32_encoding bech32_decode_len(char* hrp, uint8_t *data, size_t *data_len, const char *input, size_t input_len) {
uint32_t chk = 1;
size_t i;
size_t input_len = strlen(input);
size_t hrp_len;
int have_lower = 0, have_upper = 0;
if (input_len < 8 || input_len > max_input_len) {
if (input_len < 8) {
return BECH32_ENCODING_NONE;
}
*data_len = 0;
@@ -154,6 +153,14 @@ bech32_encoding bech32_decode(char* hrp, uint8_t *data, size_t *data_len, const
}
}
bech32_encoding bech32_decode(char* hrp, uint8_t *data, size_t *data_len, const char *input, size_t max_input_len) {
size_t len = strlen(input);
if (len > max_input_len) {
return BECH32_ENCODING_NONE;
}
return bech32_decode_len(hrp, data, data_len, input, len);
}
int bech32_convert_bits(uint8_t* out, size_t* outlen, int outbits, const uint8_t* in, size_t inlen, int inbits, int pad) {
uint32_t val = 0;
int bits = 0;
+8
View File
@@ -118,6 +118,14 @@ bech32_encoding bech32_decode(
size_t max_input_len
);
bech32_encoding bech32_decode_len(
char *hrp,
uint8_t *data,
size_t *data_len,
const char *input,
size_t input_len
);
/* Helper from bech32: translates inbits-bit bytes to outbits-bit bytes.
* @outlen is incremented as bytes are added.
* @pad is true if we're to pad, otherwise truncate last byte if necessary
+56
View File
@@ -0,0 +1,56 @@
//
// block.h
// damus
//
// Created by William Casarin on 2023-04-09.
//
#ifndef block_h
#define block_h
#include "nostr_bech32.h"
#include "str_block.h"
#define MAX_BLOCKS 1024
enum block_type {
BLOCK_HASHTAG = 1,
BLOCK_TEXT = 2,
BLOCK_MENTION_INDEX = 3,
BLOCK_MENTION_BECH32 = 4,
BLOCK_URL = 5,
BLOCK_INVOICE = 6,
};
typedef struct invoice_block {
struct str_block invstr;
union {
struct bolt11 *bolt11;
};
} invoice_block_t;
typedef struct mention_bech32_block {
struct str_block str;
struct nostr_bech32 bech32;
} mention_bech32_block_t;
typedef struct block {
enum block_type type;
union {
struct str_block str;
struct invoice_block invoice;
struct mention_bech32_block mention_bech32;
int mention_index;
} block;
} block_t;
typedef struct blocks {
int num_blocks;
struct block *blocks;
} blocks_t;
void blocks_init(struct blocks *blocks);
void blocks_free(struct blocks *blocks);
#endif /* block_h */
+171
View File
@@ -0,0 +1,171 @@
//
// cursor.h
// damus
//
// Created by William Casarin on 2023-04-09.
//
#ifndef cursor_h
#define cursor_h
#include <ctype.h>
#include <string.h>
typedef unsigned char u8;
struct cursor {
const u8 *p;
const u8 *start;
const u8 *end;
};
static inline int is_whitespace(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
}
static inline int is_boundary(char c) {
return !isalnum(c);
}
static inline int is_invalid_url_ending(char c) {
return c == '!' || c == '?' || c == ')' || c == '.' || c == ',' || c == ';';
}
static inline int is_alphanumeric(char c) {
return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
}
static inline void make_cursor(struct cursor *c, const u8 *content, size_t len)
{
c->start = content;
c->end = content + len;
c->p = content;
}
static inline int consume_until_boundary(struct cursor *cur) {
char c;
while (cur->p < cur->end) {
c = *cur->p;
if (is_boundary(c))
return 1;
cur->p++;
}
return 1;
}
static inline int consume_until_whitespace(struct cursor *cur, int or_end) {
char c;
int consumedAtLeastOne = 0;
while (cur->p < cur->end) {
c = *cur->p;
if (is_whitespace(c))
return consumedAtLeastOne;
cur->p++;
consumedAtLeastOne = 1;
}
return or_end;
}
static inline int consume_until_non_alphanumeric(struct cursor *cur, int or_end) {
char c;
int consumedAtLeastOne = 0;
while (cur->p < cur->end) {
c = *cur->p;
if (!is_alphanumeric(c))
return consumedAtLeastOne;
cur->p++;
consumedAtLeastOne = 1;
}
return or_end;
}
static inline int parse_char(struct cursor *cur, char c) {
if (cur->p >= cur->end)
return 0;
if (*cur->p == c) {
cur->p++;
return 1;
}
return 0;
}
static inline int peek_char(struct cursor *cur, int ind) {
if ((cur->p + ind < cur->start) || (cur->p + ind >= cur->end))
return -1;
return *(cur->p + ind);
}
static int parse_digit(struct cursor *cur, int *digit) {
int c;
if ((c = peek_char(cur, 0)) == -1)
return 0;
c -= '0';
if (c >= 0 && c <= 9) {
*digit = c;
cur->p++;
return 1;
}
return 0;
}
static inline int pull_byte(struct cursor *cur, u8 *byte) {
if (cur->p >= cur->end)
return 0;
*byte = *cur->p;
cur->p++;
return 1;
}
static inline int pull_bytes(struct cursor *cur, int count, const u8 **bytes) {
if (cur->p + count > cur->end)
return 0;
*bytes = cur->p;
cur->p += count;
return 1;
}
static inline int parse_str(struct cursor *cur, const char *str) {
int i;
char c, cs;
unsigned long len;
len = strlen(str);
if (cur->p + len >= cur->end)
return 0;
for (i = 0; i < len; i++) {
c = tolower(cur->p[i]);
cs = tolower(str[i]);
if (c != cs)
return 0;
}
cur->p += len;
return 1;
}
#endif /* cursor_h */
+43 -123
View File
@@ -6,127 +6,13 @@
//
#include "damus.h"
#include "cursor.h"
#include "bolt11.h"
#include "bech32.h"
#include <stdlib.h>
#include <string.h>
typedef unsigned char u8;
struct cursor {
const u8 *p;
const u8 *start;
const u8 *end;
};
static inline int is_whitespace(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
}
static inline int is_boundary(char c) {
return !isalnum(c);
}
static inline int is_invalid_url_ending(char c) {
return c == '!' || c == '?' || c == ')' || c == '.' || c == ',' || c == ';';
}
static void make_cursor(struct cursor *c, const u8 *content, size_t len)
{
c->start = content;
c->end = content + len;
c->p = content;
}
static int consume_until_boundary(struct cursor *cur) {
char c;
while (cur->p < cur->end) {
c = *cur->p;
if (is_boundary(c))
return 1;
cur->p++;
}
return 1;
}
static int consume_until_whitespace(struct cursor *cur, int or_end) {
char c;
bool consumedAtLeastOne = false;
while (cur->p < cur->end) {
c = *cur->p;
if (is_whitespace(c))
return consumedAtLeastOne;
cur->p++;
consumedAtLeastOne = true;
}
return or_end;
}
static int parse_char(struct cursor *cur, char c) {
if (cur->p >= cur->end)
return 0;
if (*cur->p == c) {
cur->p++;
return 1;
}
return 0;
}
static inline int peek_char(struct cursor *cur, int ind) {
if ((cur->p + ind < cur->start) || (cur->p + ind >= cur->end))
return -1;
return *(cur->p + ind);
}
static int parse_digit(struct cursor *cur, int *digit) {
int c;
if ((c = peek_char(cur, 0)) == -1)
return 0;
c -= '0';
if (c >= 0 && c <= 9) {
*digit = c;
cur->p++;
return 1;
}
return 0;
}
static int parse_str(struct cursor *cur, const char *str) {
int i;
char c, cs;
unsigned long len;
len = strlen(str);
if (cur->p + len >= cur->end)
return 0;
for (i = 0; i < len; i++) {
c = tolower(cur->p[i]);
cs = tolower(str[i]);
if (c != cs)
return 0;
}
cur->p += len;
return 1;
}
static int parse_mention(struct cursor *cur, struct block *block) {
static int parse_mention_index(struct cursor *cur, struct block *block) {
int d1, d2, d3, ind;
const u8 *start = cur->p;
@@ -151,8 +37,8 @@ static int parse_mention(struct cursor *cur, struct block *block) {
return 0;
}
block->type = BLOCK_MENTION;
block->block.mention = ind;
block->type = BLOCK_MENTION_INDEX;
block->block.mention_index = ind;
return 1;
}
@@ -274,6 +160,27 @@ static int parse_invoice(struct cursor *cur, struct block *block) {
return 1;
}
static int parse_mention_bech32(struct cursor *cur, struct block *block) {
const u8 *start = cur->p;
if (!parse_str(cur, "nostr:"))
return 0;
block->block.str.start = (const char *)cur->p;
if (!parse_nostr_bech32(cur, &block->block.mention_bech32.bech32)) {
cur->p = start;
return 0;
}
block->block.str.end = (const char *)cur->p;
block->type = BLOCK_MENTION_BECH32;
return 1;
}
static int add_text_then_block(struct cursor *cur, struct blocks *blocks, struct block block, const u8 **start, const u8 *pre_mention)
{
if (!add_text_block(blocks, *start, pre_mention))
@@ -303,7 +210,7 @@ int damus_parse_content(struct blocks *blocks, const char *content) {
pre_mention = cur.p;
if (cp == -1 || is_whitespace(cp)) {
if (c == '#' && (parse_mention(&cur, &block) || parse_hashtag(&cur, &block))) {
if (c == '#' && (parse_mention_index(&cur, &block) || parse_hashtag(&cur, &block))) {
if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
return 0;
continue;
@@ -315,6 +222,10 @@ int damus_parse_content(struct blocks *blocks, const char *content) {
if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
return 0;
continue;
} else if (c == 'n' && parse_mention_bech32(&cur, &block)) {
if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
return 0;
continue;
}
}
@@ -335,8 +246,17 @@ void blocks_init(struct blocks *blocks) {
}
void blocks_free(struct blocks *blocks) {
if (blocks->blocks) {
free(blocks->blocks);
blocks->num_blocks = 0;
if (!blocks->blocks) {
return;
}
for (int i = 0; i < blocks->num_blocks; ++i) {
if (blocks->blocks[i].type == BLOCK_MENTION_BECH32) {
free(blocks->blocks[i].block.mention_bech32.bech32.buffer);
blocks->blocks[i].block.mention_bech32.bech32.buffer = NULL;
}
}
free(blocks->blocks);
blocks->num_blocks = 0;
}
+3 -38
View File
@@ -9,45 +9,10 @@
#define damus_h
#include <stdio.h>
#include "nostr_bech32.h"
#include "block.h"
typedef unsigned char u8;
#define MAX_BLOCKS 1024
enum block_type {
BLOCK_HASHTAG = 1,
BLOCK_TEXT = 2,
BLOCK_MENTION = 3,
BLOCK_URL = 4,
BLOCK_INVOICE = 5,
};
typedef struct str_block {
const char *start;
const char *end;
} str_block_t;
typedef struct invoice_block {
struct str_block invstr;
union {
struct bolt11 *bolt11;
};
} invoice_block_t;
typedef struct block {
enum block_type type;
union {
struct str_block str;
struct invoice_block invoice;
int mention;
} block;
} block_t;
typedef struct blocks {
int num_blocks;
struct block *blocks;
} blocks_t;
void blocks_init(struct blocks *blocks);
void blocks_free(struct blocks *blocks);
int damus_parse_content(struct blocks *blocks, const char *content);
#endif /* damus_h */
+295
View File
@@ -0,0 +1,295 @@
//
// nostr_bech32.c
// damus
//
// Created by William Casarin on 2023-04-09.
//
#include "nostr_bech32.h"
#include <stdlib.h>
#include "cursor.h"
#include "bech32.h"
#define MAX_TLVS 16
#define TLV_SPECIAL 0
#define TLV_RELAY 1
#define TLV_AUTHOR 2
#define TLV_KIND 3
#define TLV_KNOWN_TLVS 4
struct nostr_tlv {
u8 type;
u8 len;
const u8 *value;
};
struct nostr_tlvs {
struct nostr_tlv tlvs[MAX_TLVS];
int num_tlvs;
};
static int parse_nostr_tlv(struct cursor *cur, struct nostr_tlv *tlv) {
// get the tlv tag
if (!pull_byte(cur, &tlv->type))
return 0;
// unknown, fail!
if (tlv->type >= TLV_KNOWN_TLVS)
return 0;
// get the length
if (!pull_byte(cur, &tlv->len))
return 0;
// is the reported length greater then our buffer? if so fail
if (cur->p + tlv->len > cur->end)
return 0;
tlv->value = cur->p;
cur->p += tlv->len;
return 1;
}
static int parse_nostr_tlvs(struct cursor *cur, struct nostr_tlvs *tlvs) {
int i;
tlvs->num_tlvs = 0;
for (i = 0; i < MAX_TLVS; i++) {
if (parse_nostr_tlv(cur, &tlvs->tlvs[i])) {
tlvs->num_tlvs++;
} else {
break;
}
}
if (tlvs->num_tlvs == 0)
return 0;
return 1;
}
static int find_tlv(struct nostr_tlvs *tlvs, u8 type, struct nostr_tlv **tlv) {
*tlv = NULL;
for (int i = 0; i < tlvs->num_tlvs; i++) {
if (tlvs->tlvs[i].type == type) {
*tlv = &tlvs->tlvs[i];
return 1;
}
}
return 0;
}
static int parse_nostr_bech32_type(const char *prefix, enum nostr_bech32_type *type) {
// Parse type
if (strcmp(prefix, "note") == 0) {
*type = NOSTR_BECH32_NOTE;
return 1;
} else if (strcmp(prefix, "npub") == 0) {
*type = NOSTR_BECH32_NPUB;
return 1;
} else if (strcmp(prefix, "nprofile") == 0) {
*type = NOSTR_BECH32_NPROFILE;
return 1;
} else if (strcmp(prefix, "nevent") == 0) {
*type = NOSTR_BECH32_NEVENT;
return 1;
} else if (strcmp(prefix, "nrelay") == 0) {
*type = NOSTR_BECH32_NRELAY;
return 1;
} else if (strcmp(prefix, "naddr") == 0) {
*type = NOSTR_BECH32_NADDR;
return 1;
}
return 0;
}
static int parse_nostr_bech32_note(struct cursor *cur, struct bech32_note *note) {
return pull_bytes(cur, 32, &note->event_id);
}
static int parse_nostr_bech32_npub(struct cursor *cur, struct bech32_npub *npub) {
return pull_bytes(cur, 32, &npub->pubkey);
}
static int tlvs_to_relays(struct nostr_tlvs *tlvs, struct relays *relays) {
struct nostr_tlv *tlv;
struct str_block *str;
relays->num_relays = 0;
for (int i = 0; i < tlvs->num_tlvs; i++) {
tlv = &tlvs->tlvs[i];
if (tlv->type != TLV_RELAY)
continue;
if (relays->num_relays + 1 > MAX_RELAYS)
break;
str = &relays->relays[relays->num_relays++];
str->start = (const char*)tlv->value;
str->end = (const char*)(tlv->value + tlv->len);
}
return 1;
}
static int parse_nostr_bech32_nevent(struct cursor *cur, struct bech32_nevent *nevent) {
struct nostr_tlvs tlvs;
struct nostr_tlv *tlv;
if (!parse_nostr_tlvs(cur, &tlvs))
return 0;
if (!find_tlv(&tlvs, TLV_SPECIAL, &tlv))
return 0;
if (tlv->len != 32)
return 0;
nevent->event_id = tlv->value;
if (find_tlv(&tlvs, TLV_AUTHOR, &tlv)) {
nevent->pubkey = tlv->value;
} else {
nevent->pubkey = NULL;
}
return tlvs_to_relays(&tlvs, &nevent->relays);
}
static int parse_nostr_bech32_naddr(struct cursor *cur, struct bech32_naddr *naddr) {
struct nostr_tlvs tlvs;
struct nostr_tlv *tlv;
if (!parse_nostr_tlvs(cur, &tlvs))
return 0;
if (!find_tlv(&tlvs, TLV_SPECIAL, &tlv))
return 0;
naddr->identifier.start = (const char*)tlv->value;
naddr->identifier.end = (const char*)tlv->value + tlv->len;
if (!find_tlv(&tlvs, TLV_AUTHOR, &tlv))
return 0;
naddr->pubkey = tlv->value;
return tlvs_to_relays(&tlvs, &naddr->relays);
}
static int parse_nostr_bech32_nprofile(struct cursor *cur, struct bech32_nprofile *nprofile) {
struct nostr_tlvs tlvs;
struct nostr_tlv *tlv;
if (!parse_nostr_tlvs(cur, &tlvs))
return 0;
if (!find_tlv(&tlvs, TLV_SPECIAL, &tlv))
return 0;
if (tlv->len != 32)
return 0;
nprofile->pubkey = tlv->value;
return tlvs_to_relays(&tlvs, &nprofile->relays);
}
static int parse_nostr_bech32_nrelay(struct cursor *cur, struct bech32_nrelay *nrelay) {
struct nostr_tlvs tlvs;
struct nostr_tlv *tlv;
if (!parse_nostr_tlvs(cur, &tlvs))
return 0;
if (!find_tlv(&tlvs, TLV_SPECIAL, &tlv))
return 0;
nrelay->relay.start = (const char*)tlv->value;
nrelay->relay.end = (const char*)tlv->value + tlv->len;
return 1;
}
int parse_nostr_bech32(struct cursor *cur, struct nostr_bech32 *obj) {
const u8 *start, *end;
start = cur->p;
if (!consume_until_non_alphanumeric(cur, 1)) {
cur->p = start;
return 0;
}
end = cur->p;
size_t data_len;
size_t input_len = end - start;
if (input_len < 10 || input_len > 10000) {
return 0;
}
obj->buffer = malloc(input_len * 2);
if (!obj->buffer)
return 0;
u8 data[input_len];
char prefix[input_len];
if (bech32_decode_len(prefix, data, &data_len, (const char*)start, input_len) == BECH32_ENCODING_NONE) {
cur->p = start;
return 0;
}
obj->buflen = 0;
if (!bech32_convert_bits(obj->buffer, &obj->buflen, 8, data, data_len, 5, 0)) {
goto fail;
}
if (!parse_nostr_bech32_type(prefix, &obj->type)) {
goto fail;
}
struct cursor bcur;
make_cursor(&bcur, obj->buffer, obj->buflen);
switch (obj->type) {
case NOSTR_BECH32_NOTE:
if (!parse_nostr_bech32_note(&bcur, &obj->data.note))
goto fail;
break;
case NOSTR_BECH32_NPUB:
if (!parse_nostr_bech32_npub(&bcur, &obj->data.npub))
goto fail;
break;
case NOSTR_BECH32_NEVENT:
if (!parse_nostr_bech32_nevent(&bcur, &obj->data.nevent))
goto fail;
break;
case NOSTR_BECH32_NADDR:
if (!parse_nostr_bech32_naddr(&bcur, &obj->data.naddr))
goto fail;
break;
case NOSTR_BECH32_NPROFILE:
if (!parse_nostr_bech32_nprofile(&bcur, &obj->data.nprofile))
goto fail;
break;
case NOSTR_BECH32_NRELAY:
if (!parse_nostr_bech32_nrelay(&bcur, &obj->data.nrelay))
goto fail;
break;
}
return 1;
fail:
free(obj->buffer);
cur->p = start;
return 0;
}
+78
View File
@@ -0,0 +1,78 @@
//
// nostr_bech32.h
// damus
//
// Created by William Casarin on 2023-04-09.
//
#ifndef nostr_bech32_h
#define nostr_bech32_h
#include <stdio.h>
#include "str_block.h"
#include "cursor.h"
typedef unsigned char u8;
#define MAX_RELAYS 10
struct relays {
struct str_block relays[MAX_RELAYS];
int num_relays;
};
enum nostr_bech32_type {
NOSTR_BECH32_NOTE = 1,
NOSTR_BECH32_NPUB = 2,
NOSTR_BECH32_NPROFILE = 3,
NOSTR_BECH32_NEVENT = 4,
NOSTR_BECH32_NRELAY = 5,
NOSTR_BECH32_NADDR = 6,
};
struct bech32_note {
const u8 *event_id;
};
struct bech32_npub {
const u8 *pubkey;
};
struct bech32_nevent {
struct relays relays;
const u8 *event_id;
const u8 *pubkey; // optional
};
struct bech32_nprofile {
struct relays relays;
const u8 *pubkey;
};
struct bech32_naddr {
struct relays relays;
struct str_block identifier;
const u8 *pubkey;
};
struct bech32_nrelay {
struct str_block relay;
};
typedef struct nostr_bech32 {
enum nostr_bech32_type type;
u8 *buffer; // holds strings and tlv stuff
size_t buflen;
union {
struct bech32_note note;
struct bech32_npub npub;
struct bech32_nevent nevent;
struct bech32_nprofile nprofile;
struct bech32_naddr naddr;
struct bech32_nrelay nrelay;
} data;
} nostr_bech32_t;
int parse_nostr_bech32(struct cursor *cur, struct nostr_bech32 *obj);
#endif /* nostr_bech32_h */
+16
View File
@@ -0,0 +1,16 @@
//
// str_block.h
// damus
//
// Created by William Casarin on 2023-04-09.
//
#ifndef str_block_h
#define str_block_h
typedef struct str_block {
const char *start;
const char *end;
} str_block_t;
#endif /* str_block_h */
+57 -22
View File
@@ -17,7 +17,7 @@
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */; };
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */; };
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; };
3A48E23B29D518F000BA313D /* Translations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E23A29D518F000BA313D /* Translations.swift */; };
3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */; };
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
@@ -137,6 +137,11 @@
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFBA2804A34C0006080F /* ProofOfWork.swift */; };
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FF7D42823313F009601DB /* Mentions.swift */; };
4C8682872814DE470026224F /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8682862814DE470026224F /* ProfileView.swift */; };
4C8D00C829DF791C0036AF10 /* CompatibleAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */; };
4C8D00CA29DF80350036AF10 /* TruncatedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00C929DF80350036AF10 /* TruncatedText.swift */; };
4C8D00CC29DF92DF0036AF10 /* Hashtags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */; };
4C8D00CF29E38B950036AF10 /* nostr_bech32.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00CE29E38B950036AF10 /* nostr_bech32.c */; };
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */; };
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */; };
4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD152839DB54008EE7EF /* NostrMetadata.swift */; };
4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD17283A9EE5008EE7EF /* LoginView.swift */; };
@@ -187,6 +192,8 @@
4CCEB7B029B5415A0078AA28 /* SearchingProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */; };
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */; };
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; };
4CDA128A29E9D10C0006FA5A /* SignalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA128929E9D10C0006FA5A /* SignalView.swift */; };
4CDA128C29EB19C40006FA5A /* LocalNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA128B29EB19C40006FA5A /* LocalNotification.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 */; };
@@ -324,10 +331,7 @@
3A41E559299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A48E23A29D518F000BA313D /* Translations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Translations.swift; sourceTree = "<group>"; };
3A4F3320297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A4F3321297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A4F3322297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-FR"; path = "fr-FR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutedThreadsManager.swift; sourceTree = "<group>"; };
3A5C4575296A879E0032D398 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-419"; path = "es-419.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A5CAE1E298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/Localizable.strings"; sourceTree = "<group>"; };
@@ -335,6 +339,9 @@
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>"; };
3A821C3E29E819D500B4BCA7 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
3A821C3F29E819D500B4BCA7 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A821C4029E819D500B4BCA7 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fr; path = fr.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>"; };
@@ -381,9 +388,6 @@
3AD14EB829C40F3F009D2D9C /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "sv-SE"; path = "sv-SE.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3AD14EB929C40F3F009D2D9C /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sv-SE"; path = "sv-SE.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AD14EBA29C40F3F009D2D9C /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sv-SE"; path = "sv-SE.lproj/Localizable.strings"; sourceTree = "<group>"; };
3AD14EBB29C40F47009D2D9C /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AD14EBC29C40F47009D2D9C /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-CA"; path = "fr-CA.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3AD14EBD29C40F47009D2D9C /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/Localizable.strings"; sourceTree = "<group>"; };
3AD5662B29BD2F5300BF77C5 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3AD5662C29BD2F5300BF77C5 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fa; path = fa.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3AD5662D29BD2F5300BF77C5 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = "<group>"; };
@@ -537,6 +541,15 @@
4C75EFBA2804A34C0006080F /* ProofOfWork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProofOfWork.swift; sourceTree = "<group>"; };
4C7FF7D42823313F009601DB /* Mentions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mentions.swift; sourceTree = "<group>"; };
4C8682862814DE470026224F /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompatibleAttribute.swift; sourceTree = "<group>"; };
4C8D00C929DF80350036AF10 /* TruncatedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TruncatedText.swift; sourceTree = "<group>"; };
4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hashtags.swift; sourceTree = "<group>"; };
4C8D00CD29E38B950036AF10 /* nostr_bech32.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = nostr_bech32.h; sourceTree = "<group>"; };
4C8D00CE29E38B950036AF10 /* nostr_bech32.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = nostr_bech32.c; sourceTree = "<group>"; };
4C8D00D029E38E4C0036AF10 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = "<group>"; };
4C8D00D129E397AD0036AF10 /* block.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = block.h; sourceTree = "<group>"; };
4C8D00D229E3C19F0036AF10 /* str_block.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = str_block.h; sourceTree = "<group>"; };
4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP19Tests.swift; sourceTree = "<group>"; };
4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusColors.swift; sourceTree = "<group>"; };
4C90BD152839DB54008EE7EF /* NostrMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrMetadata.swift; sourceTree = "<group>"; };
4C90BD17283A9EE5008EE7EF /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
@@ -587,6 +600,8 @@
4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingProfileView.swift; sourceTree = "<group>"; };
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploadModel.swift; sourceTree = "<group>"; };
4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; };
4CDA128929E9D10C0006FA5A /* SignalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalView.swift; sourceTree = "<group>"; };
4CDA128B29EB19C40006FA5A /* LocalNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotification.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>"; };
@@ -773,6 +788,11 @@
4C3EA67428FF7A5A00C48A62 /* take.c */,
4C3EA67628FF7A9800C48A62 /* talstr.c */,
4C3EA67828FF7ABF00C48A62 /* list.c */,
4C8D00CD29E38B950036AF10 /* nostr_bech32.h */,
4C8D00CE29E38B950036AF10 /* nostr_bech32.c */,
4C8D00D029E38E4C0036AF10 /* cursor.h */,
4C8D00D129E397AD0036AF10 /* block.h */,
4C8D00D229E3C19F0036AF10 /* str_block.h */,
);
path = "damus-c";
sourceTree = "<group>";
@@ -823,7 +843,7 @@
3AA59D1C2999B0400061C48E /* DraftsModel.swift */,
4C54AA0629A540BA003E4487 /* NotificationsModel.swift */,
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */,
3A48E23A29D518F000BA313D /* Translations.swift */,
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -986,6 +1006,9 @@
4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */,
4CE4F0F129D4FCFA005914DB /* DebouncedOnChange.swift */,
4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */,
4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */,
4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */,
4CDA128B29EB19C40006FA5A /* LocalNotification.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -1001,6 +1024,7 @@
4CE8794D2996B16A00F758CC /* RelayToggle.swift */,
4CE8794F2996B2BD00F758CC /* RelayStatus.swift */,
4CE879512996B68900F758CC /* RelayType.swift */,
4CDA128929E9D10C0006FA5A /* SignalView.swift */,
);
path = Relays;
sourceTree = "<group>";
@@ -1105,6 +1129,7 @@
4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */,
4CE4F0F729DB7399005914DB /* ThiccDivider.swift */,
4C1A9A2229DDDB8100516EAC /* IconLabel.swift */,
4C8D00C929DF80350036AF10 /* TruncatedText.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -1180,6 +1205,7 @@
3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */,
3A3040F229A91366008A0F29 /* ProfileViewTests.swift */,
3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */,
4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */,
);
path = damusTests;
sourceTree = "<group>";
@@ -1383,8 +1409,7 @@
"es-419",
"es-ES",
fa,
"fr-CA",
"fr-FR",
fr,
"hu-HU",
id,
"it-IT",
@@ -1472,6 +1497,7 @@
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */,
4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */,
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */,
4CDA128A29E9D10C0006FA5A /* SignalView.swift in Sources */,
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */,
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
@@ -1490,6 +1516,7 @@
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */,
4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */,
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
4C8D00C829DF791C0036AF10 /* CompatibleAttribute.swift in Sources */,
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */,
F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */,
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */,
@@ -1514,8 +1541,10 @@
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */,
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */,
3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */,
4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */,
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */,
4C8D00CA29DF80350036AF10 /* TruncatedText.swift in Sources */,
4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */,
4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */,
4C649844285A952100EAE2B3 /* LocalUserConfig.swift in Sources */,
@@ -1624,12 +1653,15 @@
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
6439E014296790CF0020672B /* ProfilePicImageView.swift in Sources */,
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */,
4CDA128C29EB19C40006FA5A /* LocalNotification.swift in Sources */,
4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */,
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */,
4C8D00CC29DF92DF0036AF10 /* Hashtags.swift in Sources */,
4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */,
4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */,
4CEE2AF9280B2EAC00AB5EEF /* PowView.swift in Sources */,
3165648B295B70D500C64604 /* LinkView.swift in Sources */,
4C8D00CF29E38B950036AF10 /* nostr_bech32.c in Sources */,
4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */,
4C3EA66028FF5E7700C48A62 /* node_id.c in Sources */,
4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */,
@@ -1666,7 +1698,6 @@
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */,
3A48E23B29D518F000BA313D /* Translations.swift in Sources */,
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
4C06670B28FDE64700038D2A /* damus.c in Sources */,
@@ -1702,6 +1733,7 @@
buildActionMask = 2147483647;
files = (
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */,
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */,
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */,
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */,
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */,
@@ -1751,7 +1783,6 @@
3A5C4575296A879E0032D398 /* es-419 */,
3A2B8B0A296A8982009CC16D /* en-US */,
3AEB8005297CCEA900713A25 /* tr-TR */,
3A4F3322297CCFEE004B5F72 /* fr-FR */,
3A185A06297F2C3800F4BDC0 /* lv-LV */,
3A929C22297F2CF80090925E /* it-IT */,
3AB5B86C2986D8A3006599D2 /* de */,
@@ -1773,10 +1804,10 @@
3AD5663229C0DA4B00BF77C5 /* ko */,
3AD14EB529C40F38009D2D9C /* hu-HU */,
3AD14EB829C40F3F009D2D9C /* sv-SE */,
3AD14EBC29C40F47009D2D9C /* fr-CA */,
3A325AC629C9E0B8002BE7ED /* vi */,
3A325AC929C9E0CF002BE7ED /* es-ES */,
3AC59CA929CDDB78007E04A6 /* pt-BR */,
3A821C4029E819D500B4BCA7 /* fr */,
);
name = Localizable.stringsdict;
sourceTree = "<group>";
@@ -1786,7 +1817,6 @@
children = (
3ACB685B297633BC00C46468 /* es-419 */,
3AEB8003297CCEA800713A25 /* tr-TR */,
3A4F3320297CCFEE004B5F72 /* fr-FR */,
3A185A04297F2C3800F4BDC0 /* lv-LV */,
3A929C20297F2CF80090925E /* it-IT */,
3AB5B86A2986D8A3006599D2 /* de */,
@@ -1808,10 +1838,10 @@
3AD5663329C0DA4B00BF77C5 /* ko */,
3AD14EB629C40F38009D2D9C /* hu-HU */,
3AD14EB929C40F3F009D2D9C /* sv-SE */,
3AD14EBB29C40F47009D2D9C /* fr-CA */,
3A325AC529C9E0B8002BE7ED /* vi */,
3A325AC829C9E0CF002BE7ED /* es-ES */,
3AC59CA829CDDB78007E04A6 /* pt-BR */,
3A821C3F29E819D500B4BCA7 /* fr */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
@@ -1821,7 +1851,6 @@
children = (
3ACB685E297633BC00C46468 /* es-419 */,
3AEB8004297CCEA800713A25 /* tr-TR */,
3A4F3321297CCFEE004B5F72 /* fr-FR */,
3A185A05297F2C3800F4BDC0 /* lv-LV */,
3A929C21297F2CF80090925E /* it-IT */,
3AB5B86B2986D8A3006599D2 /* de */,
@@ -1844,10 +1873,10 @@
3AD5663129C0DA4B00BF77C5 /* ko */,
3AD14EB729C40F38009D2D9C /* hu-HU */,
3AD14EBA29C40F3F009D2D9C /* sv-SE */,
3AD14EBD29C40F47009D2D9C /* fr-CA */,
3A325AC429C9E0B8002BE7ED /* vi */,
3A325AC729C9E0CF002BE7ED /* es-ES */,
3AC59CA729CDDB78007E04A6 /* pt-BR */,
3A821C3E29E819D500B4BCA7 /* fr */,
);
name = Localizable.strings;
sourceTree = "<group>";
@@ -1983,7 +2012,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -2008,9 +2037,12 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.4.1;
MARKETING_VERSION = 1.4.3;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "damus-c/damus-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -2027,7 +2059,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -2052,9 +2084,12 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.4.1;
MARKETING_VERSION = 1.4.3;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "damus-c/damus-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"red" : "0xBE",
"green" : "0x5F",
"blue" : "0x00"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xED",
"green" : "0x26",
"red" : "0xBF"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x05",
"green" : "0xDF",
"red" : "0xFA"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "bitcoin-hashtag.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "bitcoin-hashtag.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "bitcoin-hashtag.svg",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="12.843903"
height="17"
viewBox="0 0 12.843902 16.999999"
version="1.1"
id="svg2"
sodipodi:docname="bitcoin-hashtag.svg"
inkscape:version="1.3-dev (77bc73e, 2022-05-18)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="31.12"
inkscape:cx="4.2577121"
inkscape:cy="7.535347"
inkscape:window-width="1526"
inkscape:window-height="957"
inkscape:window-x="1637"
inkscape:window-y="10"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" />
<g
id="surface1"
transform="matrix(0.94507527,0,0,0.94507527,-4.5943665,-3.2875042)">
<path
style="fill:#f59119;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40637"
d="M 18.388175,10.742602 C 18.668352,8.874761 17.240002,7.8694225 15.295251,7.193703 L 15.927019,4.6611305 14.383304,4.2765743 13.768015,6.7432244 C 13.361486,6.644338 12.943967,6.545453 12.531944,6.4520613 L 13.152726,3.9689298 11.609011,3.584375 10.977241,6.1169476 C 10.642129,6.0400371 10.312509,5.9686201 9.988384,5.886215 L 9.993834,5.880715 7.8623408,5.3478364 7.4558114,6.9959316 c 0,0 1.1426793,0.2636953 1.1207046,0.2801765 0.6207823,0.1538223 0.7361485,0.5658464 0.714174,0.8954659 l -0.7196673,2.889659 c 0.043949,0.01099 0.098885,0.02747 0.1648089,0.04944 L 8.5710227,11.072223 7.5601911,15.11555 c -0.076912,0.192277 -0.26919,0.477948 -0.7031873,0.368073 0.010984,0.02198 -1.1261994,-0.280174 -1.1261994,-0.280174 l -0.7636164,1.76346 2.0106754,0.505417 c 0.3735682,0.0934 0.7361485,0.186784 1.0987301,0.280176 l -0.6427568,2.565534 1.5437155,0.384556 0.6317701,-2.538067 c 0.4230107,0.115364 0.8295407,0.219746 1.2305777,0.318632 l -0.63177,2.527079 1.543716,0.384557 0.637263,-2.560042 c 2.631459,0.499922 4.614667,0.296656 5.444208,-2.082094 0.670226,-1.917284 -0.03296,-3.021507 -1.417363,-3.741176 1.010832,-0.236227 1.768957,-0.895465 1.972221,-2.268877 z m -3.526922,4.944286 c -0.477949,1.917283 -3.702721,0.884477 -4.752008,0.620782 l 0.851514,-3.395075 c 1.043795,0.2582 4.394922,0.774604 3.900494,2.774293 z M 15.3392,10.715134 c -0.433999,1.74698 -3.120394,0.857008 -3.993884,0.642757 l 0.769111,-3.081939 c 0.87349,0.219746 3.675252,0.620784 3.224773,2.439182 z m 0,0"
id="path2" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "coffee.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "coffee.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "coffee.svg",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon -->
<svg
version="1.1"
x="0px"
y="0px"
viewBox="0 0 13.999999 18"
enable-background="new 0 0 1000 1000"
xml:space="preserve"
id="svg4"
sodipodi:docname="coffee.svg"
width="14"
height="18"
inkscape:version="1.3-dev (77bc73e, 2022-05-18)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs4" /><sodipodi:namedview
id="namedview4"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="10.680141"
inkscape:cx="-17.181421"
inkscape:cy="4.07298"
inkscape:window-width="1368"
inkscape:window-height="947"
inkscape:window-x="1764"
inkscape:window-y="58"
inkscape:window-maximized="0"
inkscape:current-layer="svg4" />
<metadata
id="metadata1"> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
<g
id="g4"
transform="matrix(0.01779387,0,0,0.01779387,-1.8340539,0.04465199)"><g
transform="matrix(0.1,0,0,-0.1,0,511)"
id="g3"><path
d="m 4302.6,4870.6 c 149.5,-177.8 240.5,-319.3 347.6,-545.6 119.2,-254.6 169.7,-448.6 183.9,-699.2 22.2,-462.7 -137.4,-778 -539.5,-1060.9 -474.9,-335.4 -685,-739.6 -687,-1315.5 0,-260.7 38.4,-501.1 115.2,-739.6 50.5,-149.5 56.6,-159.6 60.6,-97 18.2,234.4 56.6,476.9 101,626.4 121.2,422.3 305.1,622.4 885.1,959.8 424.3,248.5 575.9,487 575.9,905.3 -2,501.1 -359.7,1295.3 -798.2,1768.1 -82.8,88.9 -198,202.1 -256.6,250.6 l -105.1,86.9 z"
id="path1"
style="fill:#be5f00;fill-opacity:1" /><path
d="m 5981.8,3577.3 c 272.8,-369.8 309.2,-846.7 90.9,-1192.2 -147.5,-232.4 -373.8,-406.2 -822.4,-638.5 -592,-303.1 -854.7,-683 -854.7,-1232.6 0,-276.8 14.2,-343.5 72.7,-343.5 38.4,0 48.5,16.2 68.7,111.1 34.4,167.7 135.4,349.6 262.7,476.9 147.5,145.5 349.6,244.5 838.6,412.2 503.2,171.8 725.4,280.9 846.7,416.3 210.1,232.4 276.8,535.5 202.1,903.2 -76.8,373.8 -216.2,618.3 -537.5,943.7 -155.7,155.6 -214.3,206.1 -167.8,143.4 z"
id="path2"
style="fill:#be5f00;fill-opacity:1" /><path
d="M 2748.7,592.8 C 2158.7,507.9 1732.3,352.3 1542.4,156.3 1415.1,25 1409,-11.4 1427.2,-445.8 c 34.3,-832.5 181.9,-1729.7 462.7,-2829 153.6,-600.1 309.2,-1113.4 351.6,-1159.9 56.6,-62.6 272.8,-157.6 476.9,-210.1 668.8,-173.8 2172.2,-196 2960.3,-42.4 357.7,68.7 604.2,163.7 731.5,278.9 68.7,60.6 84.9,92.9 107.1,198 64.7,335.4 56.6,319.3 131.3,319.3 107.1,0 438.5,92.9 602.2,167.7 220.3,103.1 363.7,202.1 567.8,398.1 470.8,452.6 759.8,1149.8 761.8,1840.8 0,398.1 -72.7,614.3 -274.8,818.4 -220.3,222.3 -466.8,309.2 -937.6,325.4 l -309.2,12.1 14.2,218.2 14.2,218.2 -56.6,52.5 C 6866.9,314 6274.9,499.9 5696.9,582.7 L 5614,594.8 5533.2,457.4 5450.4,322 5565.6,305.8 c 588,-84.9 868.9,-159.6 978,-256.6 56.6,-50.5 60.6,-62.6 44.5,-113.2 -80.8,-226.3 -1000.2,-371.8 -2329.8,-371.8 -1220.5,0 -2097.5,123.3 -2295.5,321.3 -52.5,54.5 -56.6,66.7 -36.4,111.1 48.5,107.1 341.5,206.1 822.4,276.8 143.5,20.2 301.1,38.4 349.6,38.4 46.5,0 84.9,4 84.9,8.1 0,8.1 -175.8,295 -185.9,305.1 -4.2,2.1 -115.3,-12 -248.7,-32.2 z m 4797.1,-1614.5 c 153.6,-26.3 272.8,-82.8 317.2,-153.6 56.6,-84.9 60.6,-400.1 8.1,-652.7 -111.1,-545.6 -404.1,-996.2 -788.1,-1220.5 -139.4,-80.8 -333.4,-151.5 -355.6,-129.3 -8.1,8.1 18.2,216.2 58.6,462.7 78.8,482.9 159.6,1073 202.1,1450.8 l 24.2,232.4 70.7,12.1 c 115.3,20.3 325.4,20.3 462.8,-1.9 z"
id="path3"
style="fill:#be5f00;fill-opacity:1" /></g></g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "nostr-hashtag.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "nostr-hashtag.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "nostr-hashtag.svg",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.0"
width="18pt"
height="18.199053pt"
viewBox="0 0 18 18.199053"
preserveAspectRatio="xMidYMid"
id="svg1"
sodipodi:docname="nostr-hashtag.svg"
inkscape:version="1.3-dev (77bc73e, 2022-05-18)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="pt"
showgrid="false"
inkscape:zoom="5.4017383"
inkscape:cx="50.354161"
inkscape:cy="-13.514168"
inkscape:window-width="1497"
inkscape:window-height="866"
inkscape:window-x="1747"
inkscape:window-y="96"
inkscape:window-maximized="0"
inkscape:current-layer="svg1" />
<metadata
id="metadata1">
Created by potrace 1.15, written by Peter Selinger 2001-2017
</metadata>
<g
transform="matrix(0.00138509,0,0,-0.00138509,0.3,17.927982)"
fill="#000000"
stroke="none"
id="g1">
<path
d="m 11315,12756 c -49,-23 -135,-71 -190,-106 -128,-81 -170,-100 -222,-100 -37,0 -43,-3 -43,-22 0,-11 12,-36 26,-55 l 26,-34 -20,-37 c -128,-248 -171,-359 -212,-547 -56,-262 -43,-645 36,-1045 48,-244 104,-451 234,-865 158,-503 280,-942 320,-1145 27,-140 60,-448 60,-559 0,-164 -39,-366 -92,-478 -57,-123 -176,-222 -286,-239 -49,-8 -50,-23 -5,-38 18,-6 34,-14 38,-18 13,-13 -25,-66 -97,-134 -121,-115 -267,-174 -432,-174 -200,0 -327,87 -732,507 -233,240 -541,529 -604,567 -31,18 -32,18 -27,0 3,-11 9,-36 13,-56 8,-49 -5,-49 -65,1 -75,62 -168,118 -250,150 -134,51 -214,63 -481,70 l -245,6 -65,41 c -100,63 -125,68 -295,66 -186,-3 -262,7 -484,64 -279,71 -341,67 -780,-61 -334,-97 -442,-118 -761,-150 -200,-19 -365,-19 -496,1 -58,9 -107,14 -111,11 -3,-4 6,-22 21,-41 l 27,-35 -23,-5 c -13,-3 -61,-8 -106,-11 -113,-9 -157,-26 -219,-87 -50,-49 -54,-51 -140,-64 -60,-10 -108,-25 -153,-48 -304,-157 -331,-166 -471,-166 -48,0 -120,7 -160,15 -40,9 -74,13 -78,10 -3,-3 0,-20 7,-37 6,-18 10,-34 7,-36 -2,-3 -24,0 -47,7 -24,6 -103,16 -176,23 -163,15 -350,1 -521,-40 l -114,-26 51,-23 c 53,-23 76,-50 66,-77 -3,-8 -51,-38 -105,-66 -251,-129 -515,-384 -727,-703 -110,-166 -208,-325 -217,-355 -6,-21 -5,-22 34,-16 36,6 41,4 41,-12 0,-11 -15,-45 -34,-76 -46,-75 -76,-161 -127,-359 -78,-307 -137,-444 -243,-569 -38,-44 -51,-67 -43,-72 7,-4 40,-8 75,-8 37,0 62,-4 62,-11 0,-6 -12,-26 -27,-45 -32,-43 -24,-53 48,-60 108,-11 103,-6 95,-82 -4,-37 -11,-128 -15,-202 -5,-74 -16,-164 -26,-200 -22,-85 -75,-190 -136,-269 -27,-35 -49,-67 -49,-72 0,-4 17,-11 38,-14 20,-3 49,-8 64,-10 64,-10 285,31 408,76 101,37 249,117 348,188 54,38 110,75 126,80 15,6 70,11 123,11 267,0 582,98 766,239 34,26 72,53 83,59 25,13 31,8 58,-46 21,-40 38,-32 57,26 30,93 97,200 141,223 25,14 38,3 98,-86 27,-38 73,-95 104,-125 31,-30 56,-63 56,-75 0,-11 -20,-60 -45,-110 -42,-84 -81,-202 -70,-213 3,-3 27,16 54,41 50,47 81,59 81,34 0,-8 -10,-49 -21,-93 -26,-97 -32,-275 -10,-313 l 14,-25 36,41 c 42,48 118,88 251,131 102,33 185,47 345,57 55,4 159,13 230,21 292,34 285,35 368,-56 66,-72 122,-115 170,-130 20,-6 37,-15 37,-19 0,-12 -22,-35 -75,-77 -27,-21 -151,-133 -275,-248 -424,-394 -462,-415 -864,-486 -65,-12 -145,-29 -177,-39 -191,-60 -348,-213 -554,-541 -103,-165 -162,-241 -292,-383 -176,-190 -332,-333 -778,-711 -497,-422 -750,-737 -895,-1113 -64,-165 -104,-203 -216,-203 -38,0 -94,7 -124,15 -74,20 -244,20 -301,0 -27,-10 -61,-33 -87,-62 -41,-45 -43,-46 -89,-39 -25,3 -67,18 -92,33 C 457,781 369,812 175,813 49,814 0,786 0,713 0,669 31,628 122,557 204,491 254,434 469,153 527,77 549,57 597,33 652,7 662,5 780,6 c 69,0 150,6 180,14 30,7 132,32 225,56 281,72 308,76 511,84 181,6 195,8 247,34 76,37 137,107 231,267 230,392 287,481 394,626 219,293 469,581 756,869 284,284 368,342 641,439 164,59 295,123 410,199 101,68 257,219 295,286 14,24 56,76 93,116 100,106 177,142 437,203 165,40 229,62 390,133 191,85 220,94 460,143 118,24 239,49 267,56 29,7 56,10 59,6 4,-3 15,-50 26,-104 14,-73 21,-159 25,-331 6,-265 -3,-385 -47,-605 -50,-247 -105,-393 -222,-592 -59,-98 -74,-167 -58,-256 23,-126 61,-177 174,-232 95,-47 169,-52 357,-28 125,16 244,22 505,26 667,12 1011,-35 1799,-241 277,-73 308,-86 376,-161 72,-79 113,-170 175,-388 19,-66 42,-130 50,-143 42,-63 155,-112 261,-112 109,0 124,57 54,206 -48,102 -53,149 -17,165 31,15 73,-5 102,-48 12,-18 45,-87 75,-153 57,-132 106,-197 173,-233 35,-18 56,-21 136,-20 73,2 110,-3 159,-19 89,-29 333,-32 403,-5 114,44 140,145 60,231 -23,24 -112,72 -272,143 -71,32 -199,124 -250,179 -71,78 -111,140 -224,343 -132,236 -172,297 -262,391 -58,61 -94,89 -148,115 -113,56 -85,53 -866,85 -223,9 -616,36 -656,45 -11,2 -86,12 -165,20 -179,20 -595,95 -712,130 -145,42 -319,139 -371,208 -32,42 -53,102 -67,187 -22,144 48,316 261,640 125,190 193,317 230,428 31,95 60,282 60,392 0,76 4,102 16,114 20,20 25,18 192,-102 73,-52 168,-114 210,-137 83,-47 132,-82 132,-95 0,-4 -14,-17 -32,-28 l -32,-21 29,-11 c 108,-41 305,-52 707,-39 186,6 345,9 353,5 8,-3 15,-15 15,-27 0,-20 3,-21 54,-15 110,13 266,81 426,187 89,58 99,63 170,69 161,14 229,78 523,495 88,124 143,185 169,185 2,0 2,-27 0,-60 -2,-33 0,-60 5,-60 23,0 140,145 212,263 117,192 165,317 271,712 106,394 111,410 135,410 15,0 22,-10 29,-40 13,-53 12,-52 42,-24 40,36 103,133 154,234 25,50 50,90 55,90 6,0 11,-17 13,-37 2,-21 7,-38 12,-38 19,0 93,101 139,190 29,56 87,206 140,363 50,147 114,318 142,380 132,288 199,526 246,867 25,190 25,756 0,975 -23,195 -53,379 -78,485 -46,194 -152,515 -233,706 -130,306 -218,564 -260,764 -48,228 -48,572 1,770 31,128 82,204 166,246 64,33 118,83 187,174 120,158 184,199 503,321 250,96 347,153 347,204 0,41 -42,57 -174,68 -112,9 -385,70 -439,99 -10,5 -27,29 -37,54 -23,53 -99,137 -159,176 -56,35 -72,36 -40,1 24,-26 23,-26 -13,3 -21,16 -42,41 -47,56 -9,26 -14,28 -60,28 -50,0 -101,16 -101,32 0,4 -42,8 -92,7 -88,0 -98,-3 -183,-43 z m 550,-126 c 10,-11 16,-20 13,-20 -3,0 -13,9 -23,20 -10,11 -16,20 -13,20 3,0 13,-9 23,-20 z"
id="path1"
style="fill:#cc43c5;fill-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "plebchain.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "plebchain.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "plebchain.svg",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 21.315096 18"
width="21.315096"
height="18"
version="1.1"
id="svg21"
sodipodi:docname="plebchain.svg"
inkscape:version="1.3-dev (77bc73e, 2022-05-18)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs21" />
<sodipodi:namedview
id="namedview21"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="19.666667"
inkscape:cx="12.762712"
inkscape:cy="12.991525"
inkscape:window-width="1418"
inkscape:window-height="883"
inkscape:window-x="1745"
inkscape:window-y="10"
inkscape:window-maximized="0"
inkscape:current-layer="svg21" />
<path
d="M 18.625339,11.886754 C 17.668676,9.87076 15.553749,8.748431 12.298294,7.531368 12.258627,7.5164347 11.514296,7.2747022 10.649566,6.9942364 9.6532356,6.6713042 8.5682391,6.7520372 7.6316422,7.220569 L 5.7999816,8.136166 13.616623,17.9338 h 4.316652 l 0.703264,-1.662727 c 0.593598,-1.401862 0.641198,-3.009057 -0.0112,-4.384319 z"
id="path5"
style="fill:#bf26ed;fill-opacity:1;stroke-width:0.466665" />
<path
d="M 16.627546,5.1089093 C 16.710146,4.5484447 16.797412,3.95578 16.838479,3.6790475 17.083011,2.0219197 15.937348,0.48005806 14.28022,0.23552546 12.623092,-0.00900704 11.081231,1.1366559 10.836698,2.7937838 c -0.0406,0.2762657 -0.128333,0.8689305 -0.210932,1.4298619 -0.282799,0.058333 -0.513332,0.2851324 -0.558132,0.5870648 -0.0658,0.441465 0.084,1.0145298 0.370999,1.2072625 0.09707,1.4303286 1.118596,2.778524 2.547992,2.989457 1.429395,0.210933 2.796257,-0.785398 3.302589,-2.1265932 0.330399,-0.1021996 0.639331,-0.6071313 0.703731,-1.0490632 0.0434,-0.302399 -0.111533,-0.5856647 -0.365399,-0.7228643 z"
id="path10"
style="fill:#bf26ed;fill-opacity:1;stroke-width:0.466665" />
<path
d="m 21.193864,14.872477 c -0.0392,-0.501665 -0.310799,-0.956663 -0.684131,-1.294529 l -1.900727,-1.723394 c 0,0 0.172666,0.346732 0.332732,1.276796 L 15.53415,11.550288 c -0.878731,-0.405999 -1.835861,-0.616465 -2.804191,-0.616465 h -0.807331 c -0.87733,0 -1.681394,-0.553464 -1.9338602,-1.393928 C 9.8926348,9.220696 9.819835,8.832897 9.7904351,8.363432 V 8.362965 C 10.503966,7.7651672 10.944031,6.8285703 10.968765,5.8700401 11.247364,5.6651742 11.372897,5.0865094 11.28843,4.648311 11.230564,4.3482453 10.990698,4.1317127 10.706032,4.0850462 10.599632,3.5283147 10.487633,2.93985 10.435366,2.6654509 10.121301,1.0199896 8.5327726,-0.05940684 6.8873113,0.25419216 5.2418501,0.56779106 4.1624536,2.1567859 4.4760526,3.8022471 4.5283191,4.0766462 4.6407854,4.665111 4.7467184,5.2218423 4.4989192,5.3697751 4.355653,5.6595742 4.4125861,5.9596399 4.4956525,6.3983051 4.8255848,6.8901701 5.159717,6.9779032 5.2950499,7.2952355 5.4798493,7.5892345 5.7015152,7.8510336 L 5.7999816,8.136166 5.2171168,11.699621 3.2925898,12.11822 C 3.041524,12.177953 2.8109914,12.278752 2.6061254,12.411752 1.8342613,12.912484 1.5458622,13.865414 1.7731282,14.725478 1.8309946,14.943877 2.8833245,17.9338 2.8833245,17.9338 H 15.133284 l 0.408332,-1.691661 3.480855,0.605265 c 1.184397,0.211399 2.267993,-0.748064 2.171393,-1.974927 z"
id="path16"
style="fill:#f59119;fill-opacity:1;stroke-width:0.466665" />
<path
d="m 12.589026,15.06801 -2.251659,-0.776531 c 0,0 0.96693,-0.1106 0.95853,-0.625798 -4.67e-4,-0.03687 -0.01213,-0.240332 -0.307999,-0.210932 -0.0812,0.0079 -0.734064,0.07327 -1.1927962,0.118999 -0.3028657,0.03033 -0.6015314,0.09007 -0.8927304,0.178733 l -0.1591328,0.04853 -3.5256551,-2.101393 c 0,0 0,0 0.3495322,-0.763464 C 6.1779803,9.600561 5.8004482,8.136166 5.8004482,8.136166 L 1.324663,10.373825 C 0.63539857,10.718224 0.2,11.422889 0.2,12.193819 c 0,0.939397 0.64306455,1.756528 1.5558615,1.977727 l 6.4357789,1.371062 2.5834586,0.955731 c 0.167999,0.0532 0.329932,0.0602 0.517065,0.0033 0.187599,-0.05693 0.246399,-0.09847 0.337398,-0.165199 0.2464,-0.180133 0.706065,-0.536665 1.037864,-0.795664 0.170799,-0.133933 0.125533,-0.402266 -0.0784,-0.472732 z"
id="path21"
style="fill:#bf26ed;fill-opacity:1;stroke-width:0.466665" />
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "zapathon.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "zapathon.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "zapathon.svg",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="11.106925"
height="18"
viewBox="0 0 2.9387073 4.7624999"
version="1.1"
id="svg1"
inkscape:version="1.3-dev (77bc73e, 2022-05-18)"
sodipodi:docname="zapathon.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="16"
inkscape:cx="16.96875"
inkscape:cy="9.96875"
inkscape:window-width="1406"
inkscape:window-height="767"
inkscape:window-x="1741"
inkscape:window-y="214"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-10.993855,-8.058313)">
<g
transform="matrix(0.01604881,0,0,-0.01604881,10.573102,13.422443)"
id="g10"
inkscape:label="Bolt"
style="display:inline">
<path
id="path14"
style="fill:#c98f19;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1"
d="m 94.833712,155.13322 -8.129016,18.00903 -55.167069,-3.59001 18.868958,-24.19503 44.825648,2.77303 z m 83.691438,80.98476 -8.89814,-11.93085 -11.755,-16.519 -1.505,-2.114 -43.191,-60.691 L 63.081111,74.14633 79.705774,38.853232 C 121.34411,95.870625 162.2814,153.39008 203.27601,210.87013 Z m -22.57414,11.14115 12.32197,61.15287 -21.22997,22.33399 -17.432,-69.53486 -7.0256,-30.70796 27.52164,-9.9823 5.84396,26.73826"
sodipodi:nodetypes="cccccccccccccccccccccc" />
<path
id="path16"
style="fill:#fadf05;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1"
d="m 178.78514,236.43412 -5.679,-0.377 -8.065,-0.533 -5.9,-0.389 -33.394,-2.212 6.162,29.388 6.223,29.667 8.95029,38.92409 -54.426393,-75.90909 -61.5969,-86.55 44.1332,2.921 8.9137,0.589 -6.1723,-29.396 -0.9867,-4.71 -1.5106,-7.209 -11.7273,-55.917704 16.3074,22.9175 37.169603,52.234204 43.189,60.693 1.505,2.113 16.907,23.756 h -0.002"
sodipodi:nodetypes="cccccccccccccccccccccc" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+3
View File
@@ -12,11 +12,14 @@ class DamusColors {
static let adaptableGrey = Color("DamusAdaptableGrey")
static let white = Color("DamusWhite")
static let black = Color("DamusBlack")
static let brown = Color("DamusBrown")
static let yellow = Color("DamusYellow")
static let lightGrey = Color("DamusLightGrey")
static let mediumGrey = Color("DamusMediumGrey")
static let darkGrey = Color("DamusDarkGrey")
static let green = Color("DamusGreen")
static let purple = Color("DamusPurple")
static let deepPurple = Color("DamusDeepPurple")
static let blue = Color("DamusBlue")
}
+133 -83
View File
@@ -8,119 +8,169 @@
import SwiftUI
import NaturalLanguage
struct Translated: Equatable {
let artifacts: NoteArtifacts
let language: String
}
enum TranslateStatus: Equatable {
case havent_tried
case trying
case translating
case translated(Translated)
case not_needed
}
struct TranslateView: View {
let damus_state: DamusState
let event: NostrEvent
let size: EventViewKind
let currentLanguage: String
@State var translated: TranslateStatus
@State var checkingTranslationStatus: Bool = false
@State var translatable: Bool = true
@State var noteLanguage: String?
@State var show_translated_note: Bool
@State var translated_artifacts: NoteArtifacts?
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind) {
self.damus_state = damus_state
self.event = event
self.size = size
self._noteLanguage = State(initialValue: damus_state.translations.detectLanguage(event, state: damus_state))
if let translationWithLanguage = damus_state.translations.cachedTranslation(event) {
self._noteLanguage = State(initialValue: translationWithLanguage.language)
let translatedBlocks = event.get_blocks(content: translationWithLanguage.translation)
self._translated_artifacts = State.init(initialValue: render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey))
if #available(iOS 16, *) {
self.currentLanguage = Locale.current.language.languageCode?.identifier ?? "en"
} else {
self._translated_artifacts = State(initialValue: nil)
self.currentLanguage = Locale.current.languageCode ?? "en"
}
self._show_translated_note = State(initialValue: damus_state.settings.auto_translate)
if damus_state.pubkey == event.pubkey && damus_state.is_privkey_user {
// Do not translate self-authored notes if logged in with a private key
// as we can assume the user can understand their own notes.
// The detected language prediction could be incorrect and not in the list of preferred languages.
// Offering a translation in this case is definitely incorrect so let's avoid it altogether.
self._translated = State(initialValue: .not_needed)
} else if let cached = damus_state.events.lookup_translated_artifacts(evid: event.id) {
self._translated = State(initialValue: cached)
} else {
let initval: TranslateStatus = self.damus_state.settings.auto_translate ? .trying : .havent_tried
self._translated = State(initialValue: initval)
}
}
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
var TranslateButton: some View {
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
show_translated_note = true
processTranslation()
self.translated = .trying
}
.translate_button_style()
}
func processTranslation() {
guard noteLanguage != nil && !checkingTranslationStatus && translatable else {
func TranslatedView(lang: String?, artifacts: NoteArtifacts) -> some View {
return VStack(alignment: .leading) {
Text(String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang ?? "ja"))
.foregroundColor(.gray)
.font(.footnote)
.padding([.top, .bottom], 10)
if self.size == .selected {
SelectableText(attributedString: artifacts.content.attributed, size: self.size)
} else {
artifacts.content.text
.font(eventviewsize_to_font(self.size))
}
}
}
func failed_attempt() {
DispatchQueue.main.async {
self.translated = .not_needed
damus_state.events.store_translation_artifacts(evid: event.id, translated: .not_needed)
}
}
func attempt_translation() async {
guard case .trying = translated else {
return
}
guard damus_state.settings.can_translate(damus_state.pubkey) else {
return
}
let note_lang = event.note_language(damus_state.keypair.privkey) ?? currentLanguage
// Don't translate if its in our preferred languages
guard !preferredLanguages.contains(note_lang) else {
failed_attempt()
return
}
DispatchQueue.main.async {
self.translated = .translating
}
// If the note language is different from our preferred languages, send a translation request.
let translator = Translator(damus_state.settings)
let originalContent = event.get_content(damus_state.keypair.privkey)
let translated_note = try? await translator.translate(originalContent, from: note_lang, to: currentLanguage)
guard let translated_note else {
// if its the same, give up and don't retry
failed_attempt()
return
}
guard originalContent != translated_note else {
// if its the same, give up and don't retry
failed_attempt()
return
}
checkingTranslationStatus = true
show_translated_note = true
Task {
let translationWithLanguage = await damus_state.translations.translate(event, state: damus_state)
DispatchQueue.main.async {
guard translationWithLanguage != nil else {
noteLanguage = damus_state.translations.targetLanguage
checkingTranslationStatus = false
show_translated_note = false
translatable = false
return
}
noteLanguage = translationWithLanguage!.language
// Render translated note.
let translatedBlocks = event.get_blocks(content: translationWithLanguage!.translation)
translated_artifacts = render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
translatable = true
checkingTranslationStatus = false
}
}
}
func Translated(lang: String, artifacts: NoteArtifacts) -> some View {
return Group {
Button(String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang)) {
show_translated_note = false
}
.translate_button_style()
SelectableText(attributedString: artifacts.content, size: self.size)
}
}
func MainContent(note_lang: String) -> some View {
return Group {
if translatable {
let languageName = Locale.current.localizedString(forLanguageCode: note_lang)
if let languageName, let translated_artifacts, show_translated_note {
Translated(lang: languageName, artifacts: translated_artifacts)
} else if !damus_state.settings.auto_translate {
TranslateButton
} else {
EmptyView()
}
} else {
EmptyView()
}
// Render translated note
let translated_blocks = event.get_blocks(content: translated_note)
let artifacts = render_blocks(blocks: translated_blocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
// and cache it
DispatchQueue.main.async {
self.translated = .translated(Translated(artifacts: artifacts, language: note_lang))
damus_state.events.store_translation_artifacts(evid: event.id, translated: self.translated)
}
}
var body: some View {
Group {
if let note_lang = noteLanguage, note_lang != damus_state.translations.targetLanguage {
MainContent(note_lang: note_lang)
.task {
if show_translated_note {
processTranslation()
}
}
} else {
switch translated {
case .havent_tried:
if damus_state.settings.auto_translate {
Text("")
} else {
TranslateButton
}
case .trying:
Text("")
case .translating:
Text("Translating...", comment: "Text to display when waiting for the translation of a note to finish processing before showing it.")
.foregroundColor(.gray)
.font(.footnote)
.padding([.top, .bottom], 10)
case .translated(let translated):
let languageName = Locale.current.localizedString(forLanguageCode: translated.language)
TranslatedView(lang: languageName, artifacts: translated.artifacts)
case .not_needed:
Text("")
}
}
.onChange(of: translated) { val in
guard case .trying = translated else {
return
}
Task {
await attempt_translation()
}
}
.task {
await attempt_translation()
}
}
}
+53
View File
@@ -0,0 +1,53 @@
//
// TruncatedText.swift
// damus
//
// Created by William Casarin on 2023-04-06.
//
import SwiftUI
struct TruncatedText: View {
let text: CompatibleText
let maxChars: Int = 280
var body: some View {
let truncatedAttributedString: AttributedString? = getTruncatedString()
if let truncatedAttributedString {
Text(truncatedAttributedString)
.fixedSize(horizontal: false, vertical: true)
} else {
text.text
.fixedSize(horizontal: false, vertical: true)
}
if truncatedAttributedString != nil {
Spacer()
Button(NSLocalizedString("Show more", comment: "Button to show entire note.")) { }
.allowsHitTesting(false)
}
}
func getTruncatedString() -> AttributedString? {
let nsAttributedString = NSAttributedString(text.attributed)
if nsAttributedString.length < maxChars { return nil }
let range = NSRange(location: 0, length: maxChars)
let truncatedAttributedString = nsAttributedString.attributedSubstring(from: range)
return AttributedString(truncatedAttributedString) + "..."
}
}
struct TruncatedText_Previews: PreviewProvider {
static var previews: some View {
VStack(spacing: 100) {
TruncatedText(text: CompatibleText(stringLiteral: "hello\nthere\none\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven"))
.frame(width: 200, height: 200)
TruncatedText(text: CompatibleText(stringLiteral: "hello\nthere\none\ntwo\nthree\nfour"))
.frame(width: 200, height: 200)
}
}
}
+37 -13
View File
@@ -7,27 +7,51 @@
import SwiftUI
struct UserViewRow: View {
let damus_state: DamusState
let pubkey: String
@State var navigating: Bool = false
var body: some View {
let dest = ProfileView(damus_state: damus_state, pubkey: pubkey)
UserView(damus_state: damus_state, pubkey: pubkey)
.contentShape(Rectangle())
.background(
NavigationLink(destination: dest, isActive: $navigating) {
EmptyView()
}
)
.onTapGesture {
navigating = true
}
}
}
struct UserView: View {
let damus_state: DamusState
let pubkey: String
var body: some View {
NavigationLink(destination: ProfileView(damus_state: damus_state, pubkey: pubkey)) {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey)
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false)
if let about = profile?.about {
Text(about)
.lineLimit(3)
.font(.footnote)
}
}
VStack {
HStack {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
Spacer()
VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey)
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false)
if let about = profile?.about {
Text(about)
.lineLimit(3)
.font(.footnote)
}
}
Spacer()
}
}
.buttonStyle(PlainButtonStyle())
}
}
+52 -24
View File
@@ -66,7 +66,6 @@ struct ContentView: View {
@State var active_sheet: Sheets? = nil
@State var damus_state: DamusState? = nil
@State var selected_timeline: Timeline? = .home
@State var is_thread_open: Bool = false
@State var is_deleted_account: Bool = false
@State var is_profile_open: Bool = false
@State var event: NostrEvent? = nil
@@ -91,11 +90,19 @@ struct ContentView: View {
let sub_id = UUID().description
@Environment(\.colorScheme) var colorScheme
var mystery: some View {
Text("Are you lost?", comment: "Text asking the user if they are lost in the app.")
.id("what")
}
var PostingTimelineView: some View {
VStack {
ZStack {
TabView(selection: $filter_state) {
// This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why.
mystery
contentTimelineView(filter: FilterState.posts.filter)
.tag(FilterState.posts)
.id(FilterState.posts)
@@ -176,8 +183,7 @@ struct ContentView: View {
NotificationsView(state: damus, notifications: home.notifications)
case .dms:
DirectMessagesView(damus_state: damus_state!)
.environmentObject(home.dms)
DirectMessagesView(damus_state: damus_state!, model: damus_state!.dms)
case .none:
EmptyView()
@@ -240,6 +246,11 @@ struct ContentView: View {
}
}
func open_event(ev: NostrEvent) {
self.active_event = ev
self.thread_open = true
}
var body: some View {
VStack(alignment: .leading, spacing: 0) {
if let damus = self.damus_state {
@@ -260,13 +271,7 @@ struct ContentView: View {
ToolbarItem(placement: .navigationBarTrailing) {
HStack(alignment: .center) {
if home.signal.signal != home.signal.max_signal {
NavigationLink(destination: RelayConfigView(state: damus_state!)) {
Text("\(home.signal.signal)/\(home.signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
.font(.callout)
.foregroundColor(.gray)
}
}
SignalView(state: damus_state!, signal: home.signal)
// maybe expand this to other timelines in the future
if selected_timeline == .search {
@@ -291,7 +296,7 @@ struct ContentView: View {
}
.navigationViewStyle(.stack)
TabBar(new_events: $home.new_events, selected: $selected_timeline, isSidebarVisible: $isSideBarOpened, action: switch_timeline)
TabBar(new_events: $home.new_events, selected: $selected_timeline, settings: damus.settings, action: switch_timeline)
.padding([.bottom], 8)
.background(Color(uiColor: .systemBackground).ignoresSafeArea())
}
@@ -335,10 +340,9 @@ struct ContentView: View {
} else if ref.key == "e" {
find_event(state: damus_state!, evid: ref.ref_id, search_type: .event, find_from: nil) { ev in
if let ev {
active_event = ev
open_event(ev: ev)
}
}
thread_open = true
}
case .filter(let filt):
active_search = filt
@@ -351,11 +355,6 @@ struct ContentView: View {
.onReceive(handle_notify(.boost)) { notif in
current_boost = (notif.object as? NostrEvent)
}
.onReceive(handle_notify(.open_thread)) { obj in
//let ev = obj.object as! NostrEvent
//thread.set_active_event(ev)
//is_thread_open = true
}
.onReceive(handle_notify(.reply)) { notif in
let ev = notif.object as! NostrEvent
self.active_sheet = .reply(ev)
@@ -468,6 +467,35 @@ struct ContentView: View {
.onReceive(handle_notify(.new_mutes)) { notif in
home.filter_muted()
}
.onReceive(handle_notify(.mute_thread)) { notif in
home.filter_muted()
}
.onReceive(handle_notify(.unmute_thread)) { notif in
home.filter_muted()
}
.onReceive(handle_notify(.local_notification)) { notif in
let local = notif.object as! LossyLocalNotification
guard let damus_state else {
return
}
guard let target = damus_state.events.lookup(local.event_id) else {
return
}
switch local.type {
case .dm:
selected_timeline = .dms
damus_state.dms.open_dm_by_pk(target.pubkey)
case .like: fallthrough
case .zap: fallthrough
case .mention: fallthrough
case .repost:
open_event(ev: target)
}
}
.alert(NSLocalizedString("Deleted Account", comment: "Alert message to indicate this is a deleted account"), isPresented: $is_deleted_account) {
Button(NSLocalizedString("Logout", comment: "Button to close the alert that informs that the current account has been deleted.")) {
is_deleted_account = false
@@ -561,7 +589,7 @@ struct ContentView: View {
}
Button(NSLocalizedString("Repost", comment: "Button to confirm reposting a post.")) {
if let current_boost {
self.damus_state?.pool.send(.event(current_boost))
self.damus_state?.postbox.send(current_boost)
}
}
} message: {
@@ -570,6 +598,8 @@ struct ContentView: View {
}
func switch_timeline(_ timeline: Timeline) {
self.isSideBarOpened = false
self.popToRoot()
NotificationCenter.default.post(name: .switched_timeline, object: timeline)
@@ -609,8 +639,6 @@ struct ContentView: View {
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
let settings = UserSettingsStore()
self.damus_state = DamusState(pool: pool,
keypair: keypair,
likes: EventCounter(our_pubkey: pubkey),
@@ -622,7 +650,7 @@ struct ContentView: View {
previews: PreviewCache(),
zaps: Zaps(our_pubkey: pubkey),
lnurls: LNUrls(),
settings: settings,
settings: UserSettingsStore(),
relay_filters: relay_filters,
relay_metadata: metadatas,
drafts: Drafts(),
@@ -631,7 +659,7 @@ struct ContentView: View {
postbox: PostBox(pool: pool),
bootstrap_relays: bootstrap_relays,
replies: ReplyCounter(our_pubkey: pubkey),
translations: Translations(settings)
muted_threads: MutedThreadsManager(keypair: keypair)
)
home.damus_state = self.damus_state!
+1 -1
View File
@@ -61,7 +61,7 @@ class BookmarksManager: ObservableObject {
if isBookmarked(ev) {
bookmarks = bookmarks.filter { $0 != ev }
} else {
bookmarks.append(ev)
bookmarks.insert(ev, at: 0)
}
}
+3 -4
View File
@@ -29,7 +29,8 @@ struct DamusState {
let postbox: PostBox
let bootstrap_relays: [String]
let replies: ReplyCounter
let translations: Translations
let muted_threads: MutedThreadsManager
var pubkey: String {
return keypair.pubkey
}
@@ -39,7 +40,5 @@ struct DamusState {
}
static var empty: DamusState {
let settings = UserSettingsStore()
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), translations: Translations(settings))
}
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil))) }
}
+6 -2
View File
@@ -16,6 +16,8 @@ class DirectMessageModel: ObservableObject {
@Published var draft: String
let pubkey: String
var is_request: Bool
var our_pubkey: String
@@ -29,17 +31,19 @@ class DirectMessageModel: ObservableObject {
return true
}
init(events: [NostrEvent], our_pubkey: String) {
init(events: [NostrEvent], our_pubkey: String, pubkey: String) {
self.events = events
self.is_request = false
self.our_pubkey = our_pubkey
self.draft = ""
self.pubkey = pubkey
}
init(our_pubkey: String) {
init(our_pubkey: String, pubkey: String) {
self.events = []
self.is_request = false
self.our_pubkey = our_pubkey
self.draft = ""
self.pubkey = pubkey
}
}
+32 -9
View File
@@ -8,20 +8,43 @@
import Foundation
class DirectMessagesModel: ObservableObject {
@Published var dms: [(String, DirectMessageModel)] = []
@Published var dms: [DirectMessageModel] = []
@Published var loading: Bool = false
@Published var open_dm: Bool = false
@Published private(set) var active_model: DirectMessageModel = DirectMessageModel(our_pubkey: "", pubkey: "")
let our_pubkey: String
init(our_pubkey: String) {
self.our_pubkey = our_pubkey
}
var message_requests: [(String, DirectMessageModel)] {
return dms.filter { dm in dm.1.is_request }
var message_requests: [DirectMessageModel] {
return dms.filter { dm in dm.is_request }
}
var friend_dms: [(String, DirectMessageModel)] {
return dms.filter { dm in !dm.1.is_request }
var friend_dms: [DirectMessageModel] {
return dms.filter { dm in !dm.is_request }
}
func set_active_dm_model(_ model: DirectMessageModel) {
self.active_model = model
}
func open_dm_by_pk(_ pubkey: String) {
self.set_active_dm(pubkey)
self.open_dm = true
}
func open_dm_by_model(_ model: DirectMessageModel) {
self.set_active_dm_model(model)
self.open_dm = true
}
func set_active_dm(_ pubkey: String) {
for model in self.dms where model.pubkey == pubkey {
self.set_active_dm_model(model)
break
}
}
func lookup_or_create(_ pubkey: String) -> DirectMessageModel {
@@ -29,15 +52,15 @@ class DirectMessagesModel: ObservableObject {
return dm
}
let new = DirectMessageModel(our_pubkey: our_pubkey)
dms.append((pubkey, new))
let new = DirectMessageModel(our_pubkey: our_pubkey, pubkey: pubkey)
dms.append(new)
return new
}
func lookup(_ pubkey: String) -> DirectMessageModel? {
for dm in dms {
if pubkey == dm.0 {
return dm.1
if pubkey == dm.pubkey {
return dm
}
}
+2 -1
View File
@@ -1,5 +1,5 @@
//
// DraftModel.swift
// DraftsModel.swift
// damus
//
// Created by Terry Yiu on 2/12/23.
@@ -10,4 +10,5 @@ import Foundation
class Drafts: ObservableObject {
@Published var post: NSMutableAttributedString = NSMutableAttributedString(string: "")
@Published var replies: [NostrEvent: NSMutableAttributedString] = [:]
@Published var medias: [UploadedMedia] = []
}
+5 -1
View File
@@ -74,8 +74,12 @@ func build_mention_indices(_ blocks: [Block], type: MentionType) -> Set<Int> {
switch block {
case .mention(let m):
if m.type == type {
acc.insert(m.index)
if let idx = m.index {
acc.insert(idx)
}
}
case .relay:
return
case .text:
return
case .hashtag:
+1 -1
View File
@@ -82,7 +82,7 @@ class FollowersModel: ObservableObject {
if ev.known_kind == .contacts {
handle_contact_event(ev)
} else if ev.known_kind == .metadata {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
}
case .notice(let msg):
+1 -1
View File
@@ -62,7 +62,7 @@ class FollowingModel {
break
case .event(_, let ev):
if ev.kind == 0 {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
}
case .notice(let msg):
print("followingmodel notice: \(msg)")
+186 -103
View File
@@ -8,26 +8,19 @@
import Foundation
import UIKit
struct NewEventsBits {
let bits: Int
init() {
bits = 0
}
init (prev: NewEventsBits, setting: Timeline) {
self.bits = prev.bits | timeline_bit(setting)
}
init (prev: NewEventsBits, unsetting: Timeline) {
self.bits = prev.bits & ~timeline_bit(unsetting)
}
func is_set(_ timeline: Timeline) -> Bool {
let notification_bit = timeline_bit(timeline)
return (bits & notification_bit) == notification_bit
}
struct NewEventsBits: OptionSet {
let rawValue: Int
static let home = NewEventsBits(rawValue: 1 << 0)
static let zaps = NewEventsBits(rawValue: 1 << 1)
static let mentions = NewEventsBits(rawValue: 1 << 2)
static let reposts = NewEventsBits(rawValue: 1 << 3)
static let likes = NewEventsBits(rawValue: 1 << 4)
static let search = NewEventsBits(rawValue: 1 << 5)
static let dms = NewEventsBits(rawValue: 1 << 6)
static let all = NewEventsBits(rawValue: 0xFFFFFFFF)
static let notifications: NewEventsBits = [.zaps, .likes, .reposts, .mentions]
}
class HomeModel: ObservableObject {
@@ -48,34 +41,27 @@ class HomeModel: ObservableObject {
let dms_subid = UUID().description
let init_subid = UUID().description
let profiles_subid = UUID().description
var loading: Bool = false
var signal = SignalModel()
@Published var new_events: NewEventsBits = NewEventsBits()
@Published var notifications = NotificationsModel()
@Published var dms: DirectMessagesModel
@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: "")
}
init(damus_state: DamusState) {
self.damus_state = damus_state
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
filter_muted()
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
}
var dms: DirectMessagesModel {
return damus_state.dms
}
func has_sub_id_event(sub_id: String, ev_id: String) -> Bool {
@@ -86,6 +72,13 @@ class HomeModel: ObservableObject {
return has_event[sub_id]!.contains(ev_id)
}
func setup_debouncer() {
// turn off debouncer after initial load
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
self.should_debounce_dms = false
}
}
func process_event(sub_id: String, relay_id: String, ev: NostrEvent) {
if has_sub_id_event(sub_id: sub_id, ev_id: ev.id) {
@@ -141,7 +134,7 @@ class HomeModel: ObservableObject {
return
}
if !notifications.insert_zap(zap) {
if !notifications.insert_zap(zap, damus_state: damus_state) {
return
}
@@ -152,7 +145,7 @@ class HomeModel: ObservableObject {
}
if damus_state.settings.zap_notification {
// Create in-app local notification for zap received.
create_in_app_zap_notification(profiles: profiles, zap: zap)
create_in_app_zap_notification(profiles: profiles, zap: zap, evId: ev.referenced_ids.first?.id ?? "")
}
}
@@ -193,10 +186,6 @@ class HomeModel: ObservableObject {
}
func handle_channel_create(_ ev: NostrEvent) {
guard ev.is_valid else {
return
}
self.channels[ev.id] = ev
}
@@ -204,16 +193,21 @@ class HomeModel: ObservableObject {
}
func filter_muted() {
events.filter { !damus_state.contacts.is_muted($0.pubkey) }
self.dms.dms = dms.dms.filter { !damus_state.contacts.is_muted($0.0) }
notifications.filter { !damus_state.contacts.is_muted($0.pubkey) }
events.filter { ev in
!damus_state.contacts.is_muted(ev.pubkey)
}
self.dms.dms = dms.dms.filter { ev in
!damus_state.contacts.is_muted(ev.pubkey)
}
notifications.filter { ev in
!damus_state.contacts.is_muted(ev.pubkey) &&
!damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey)
}
}
func handle_delete_event(_ ev: NostrEvent) {
guard ev.is_valid else {
return
}
self.deleted_events.insert(ev.id)
}
@@ -235,7 +229,7 @@ class HomeModel: ObservableObject {
if let inner_ev = ev.inner_event {
boost_ev_id = inner_ev.id
guard inner_ev.is_valid else {
guard validate_event(ev: inner_ev) == .ok else {
return
}
@@ -304,10 +298,7 @@ class HomeModel: ObservableObject {
break
}
update_signal_from_pool(signal: signal, pool: damus_state.pool)
print("ws_event \(ev)")
update_signal_from_pool(signal: self.signal, pool: damus_state.pool)
case .nostr_event(let ev):
switch ev {
case .event(let sub_id, let ev):
@@ -326,7 +317,7 @@ class HomeModel: ObservableObject {
case .eose(let sub_id):
if sub_id == dms_subid {
var dms = dms.dms.flatMap { $0.1.events }
var dms = dms.dms.flatMap { $0.events }
dms.append(contentsOf: incoming_dms)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(dms), damus_state: damus_state)
} else if sub_id == notifications_subid {
@@ -454,7 +445,7 @@ class HomeModel: ObservableObject {
}
func handle_metadata_event(_ ev: NostrEvent) {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
}
func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? {
@@ -476,16 +467,17 @@ class HomeModel: ObservableObject {
return
}
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
guard should_show_event(contacts: damus_state.contacts, ev: ev) && !damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey) 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) {
if !notifications.insert_event(ev, damus_state: damus_state) {
return
}
@@ -527,15 +519,26 @@ class HomeModel: ObservableObject {
}
}
func got_new_dm(notifs: NewEventsBits, ev: NostrEvent) {
self.new_events = notifs
if damus_state.settings.dm_notification {
let convo = ev.decrypted(privkey: self.damus_state.keypair.privkey) ?? NSLocalizedString("New encrypted direct message", comment: "Notification that the user has received a new direct message")
let notify = LocalNotification(type: .dm, event: ev, target: ev, content: convo)
create_local_notification(profiles: damus_state.profiles, notify: notify)
}
}
func handle_dm(_ ev: NostrEvent) {
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
return
}
damus_state.events.insert(ev)
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
got_new_dm(notifs: notifs, ev: ev)
}
self.incoming_dms = []
return
@@ -545,11 +548,7 @@ class HomeModel: ObservableObject {
dm_debouncer.debounce { [self] in
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
self.new_events = notifs
if damus_state.settings.dm_notification,
let displayName = damus_state.profiles.lookup(id: self.incoming_dms.last!.pubkey)?.display_name {
create_local_notification(displayName: displayName, conversation: "You have received a direct message", type: .dm)
}
got_new_dm(notifs: notifs, ev: ev)
}
self.incoming_dms = []
}
@@ -562,8 +561,8 @@ func update_signal_from_pool(signal: SignalModel, pool: RelayPool) {
signal.max_signal = pool.relays.count
}
if signal.signal != pool.num_connecting {
signal.signal = signal.max_signal - pool.num_connecting
if signal.signal != pool.num_connected {
signal.signal = pool.num_connected
}
}
@@ -657,11 +656,7 @@ func print_filters(relay_id: String?, filters groups: [[NostrFilter]]) {
print("-----")
}
func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEvent) {
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
return
}
func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: Profile, ev: NostrEvent) {
if our_pubkey == ev.pubkey && (profile.deleted ?? false) {
DispatchQueue.main.async {
notify(.deleted_account, ())
@@ -712,6 +707,47 @@ func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEve
}
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
}
func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping () -> Void) {
let validated = events.is_event_valid(ev.id)
switch validated {
case .unknown:
Task {
let result = validate_event(ev: ev)
DispatchQueue.main.async {
events.validation[ev.id] = result
guard result == .ok else {
return
}
callback()
}
}
case .ok:
callback()
case .bad_id: fallthrough
case .bad_sig:
break
}
}
func process_metadata_event(events: EventCache, our_pubkey: String, profiles: Profiles, ev: NostrEvent) {
guard_valid_event(events: events, ev: ev) {
DispatchQueue.global(qos: .background).async {
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
return
}
DispatchQueue.main.async {
process_metadata_profile(our_pubkey: our_pubkey, profiles: profiles, profile: profile, ev: ev)
}
}
}
}
func robohash(_ pk: String) -> String {
@@ -852,10 +888,10 @@ func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesM
}
}
for (pk, _) in dms.dms {
if pk == the_pk {
for model in dms.dms {
if model.pubkey == the_pk {
found = true
inserted = insert_uniq_sorted_event(events: &(dms.dms[i].1.events), new_ev: ev) {
inserted = insert_uniq_sorted_event(events: &(dms.dms[i].events), new_ev: ev) {
$0.created_at < $1.created_at
}
@@ -865,8 +901,8 @@ func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesM
}
if !found {
let model = DirectMessageModel(events: [ev], our_pubkey: our_pubkey)
dms.dms.append((the_pk, model))
let model = DirectMessageModel(events: [ev], our_pubkey: our_pubkey, pubkey: the_pk)
dms.dms.append(model)
inserted = true
}
@@ -893,14 +929,53 @@ func handle_incoming_dms(prev_events: NewEventsBits, dms: DirectMessagesModel, o
}
if inserted {
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
dms.dms = dms.dms.filter({ $0.events.count > 0 }).sorted { a, b in
return a.events.last!.created_at > b.events.last!.created_at
}
}
return new_events
}
func determine_event_notifications(_ ev: NostrEvent) -> NewEventsBits {
guard let kind = ev.known_kind else {
return []
}
if kind == .zap {
return [.zaps]
}
if kind == .boost {
return [.reposts]
}
if kind == .text {
return [.mentions]
}
if kind == .like {
return [.likes]
}
return []
}
func timeline_to_notification_bits(_ timeline: Timeline, ev: NostrEvent?) -> NewEventsBits {
switch timeline {
case .home:
return [.home]
case .notifications:
if let ev {
return determine_event_notifications(ev)
}
return [.notifications]
case .search:
return [.search]
case .dms:
return [.dms]
}
}
/// A helper to determine if we need to notify the user of new events
func handle_last_events(new_events: NewEventsBits, ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) -> NewEventsBits? {
@@ -909,7 +984,7 @@ func handle_last_events(new_events: NewEventsBits, ev: NostrEvent, timeline: Tim
if last_ev == nil || last_ev!.created_at < ev.created_at {
save_last_event(ev, timeline: timeline)
if shouldNotify {
return NewEventsBits(prev: new_events, setting: timeline)
return new_events.union(timeline_to_notification_bits(timeline, ev: ev))
}
}
@@ -975,12 +1050,13 @@ func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale
}
}
func create_in_app_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) {
func create_in_app_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, evId: String) {
let content = UNMutableNotificationContent()
content.title = zap_notification_title(zap)
content.body = zap_notification_body(profiles: profiles, zap: zap, locale: locale)
content.sound = UNNotificationSound.default
content.userInfo = LossyLocalNotification(type: .zap, event_id: evId).to_user_info()
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
@@ -1007,53 +1083,60 @@ func process_local_notification(damus_state: DamusState, event ev: NostrEvent) {
return
}
if type == .text && damus_state.settings.mention_notification {
for block in ev.blocks(damus_state.keypair.privkey) {
if case .mention(let mention) = block, mention.ref.ref_id == damus_state.keypair.pubkey,
let displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name {
let justContent = NSAttributedString(render_note_content(ev: ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey).content).string
create_local_notification(displayName: displayName, conversation: justContent, type: type)
}
}
} else if type == .boost && damus_state.settings.repost_notification,
let displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name {
// Don't show notifications from muted threads.
if damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey) {
return
}
if let inner_ev = ev.inner_event {
create_local_notification(displayName: displayName, conversation: inner_ev.content, type: type)
if type == .text && damus_state.settings.mention_notification {
let blocks = ev.blocks(damus_state.keypair.privkey)
for case .mention(let mention) in blocks where mention.ref.ref_id == damus_state.keypair.pubkey {
let content = NSAttributedString(render_note_content(ev: ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey).content.attributed).string
let notify = LocalNotification(type: .mention, event: ev, target: ev, content: content)
create_local_notification(profiles: damus_state.profiles, notify: notify )
}
} else if type == .boost && damus_state.settings.repost_notification, let inner_ev = ev.inner_event {
let notify = LocalNotification(type: .repost, event: ev, target: inner_ev, content: inner_ev.content)
create_local_notification(profiles: damus_state.profiles, notify: notify)
} else if type == .like && damus_state.settings.like_notification,
let displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name,
let e_ref = ev.referenced_ids.first?.ref_id,
let content = damus_state.events.lookup(e_ref)?.content {
create_local_notification(displayName: displayName, conversation: content, type: type)
let evid = ev.referenced_ids.first?.ref_id,
let liked_event = damus_state.events.lookup(evid)
{
let notify = LocalNotification(type: .like, event: ev, target: liked_event, content: liked_event.content)
create_local_notification(profiles: damus_state.profiles, notify: notify)
}
}
func create_local_notification(displayName: String, conversation: String, type: NostrKind) {
func create_local_notification(profiles: Profiles, notify: LocalNotification) {
let content = UNMutableNotificationContent()
var title = ""
var identifier = ""
switch type {
case .text:
let displayName = event_author_name(profiles: profiles, pubkey: notify.event.pubkey)
switch notify.type {
case .mention:
title = String(format: NSLocalizedString("Mentioned by %@", comment: "Mentioned by heading in local notification"), displayName)
identifier = "myMentionNotification"
case .boost:
case .repost:
title = String(format: NSLocalizedString("Reposted by %@", comment: "Reposted by heading in local notification"), displayName)
identifier = "myBoostNotification"
case .like:
title = String(format: NSLocalizedString("Liked by %@", comment: "Liked by heading in local notification"), displayName)
identifier = "myLikeNotification"
case .dm:
title = String(format: NSLocalizedString("DM by %@", comment: "DM by heading in local notification"), displayName)
title = String(format: NSLocalizedString("%@", comment: "DM by heading in local notification"), displayName)
identifier = "myDMNotification"
default:
case .zap:
// not handled here
break
}
content.title = title
content.body = conversation
content.body = notify.content
content.sound = UNNotificationSound.default
content.userInfo = notify.to_lossy().to_user_info()
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
+9
View File
@@ -25,6 +25,15 @@ enum MediaUpload {
return url.pathExtension
}
}
var localURL: URL {
switch self {
case .image(let url):
return url
case .video(let url):
return url
}
}
var is_image: Bool {
if case .image = self {
+102 -10
View File
@@ -21,8 +21,8 @@ enum MentionType {
}
}
struct Mention {
let index: Int
struct Mention: Equatable {
let index: Int?
let type: MentionType
let ref: ReferencedId
}
@@ -58,12 +58,30 @@ struct LightningInvoice<T> {
}
}
enum Block {
enum Block: Equatable {
static func == (lhs: Block, rhs: Block) -> Bool {
switch (lhs, rhs) {
case (.text(let a), .text(let b)):
return a == b
case (.mention(let a), .mention(let b)):
return a == b
case (.hashtag(let a), .hashtag(let b)):
return a == b
case (.url(let a), .url(let b)):
return a == b
case (.invoice(let a), .invoice(let b)):
return a.string == b.string
case (_, _):
return false
}
}
case text(String)
case mention(Mention)
case hashtag(String)
case url(URL)
case invoice(Invoice)
case relay(String)
var is_invoice: Invoice? {
if case .invoice(let invoice) = self {
@@ -114,7 +132,17 @@ func render_blocks(blocks: [Block]) -> String {
return blocks.reduce("") { str, block in
switch block {
case .mention(let m):
return str + "#[\(m.index)]"
if let idx = m.index {
return str + "#[\(idx)]"
} else if m.type == .pubkey, let pk = bech32_pubkey(m.ref.ref_id) {
return str + "nostr:\(pk)"
} else if let note_id = bech32_note_id(m.ref.ref_id) {
return str + "nostr:\(note_id)"
} else {
return str + m.ref.ref_id
}
case .relay(let relay):
return str + relay
case .text(let txt):
return str + txt
case .hashtag(let htag):
@@ -177,14 +205,16 @@ func convert_block(_ b: block_t, tags: [[String]]) -> Block? {
return nil
}
return .text(str)
} else if b.type == BLOCK_MENTION {
return convert_mention_block(ind: b.block.mention, tags: tags)
} else if b.type == BLOCK_MENTION_INDEX {
return convert_mention_index_block(ind: b.block.mention_index, tags: tags)
} else if b.type == BLOCK_URL {
return convert_url_block(b.block.str)
} else if b.type == BLOCK_INVOICE {
return convert_invoice_block(b.block.invoice)
} else if b.type == BLOCK_MENTION_BECH32 {
return convert_mention_bech32_block(b.block.mention_bech32)
}
return nil
}
@@ -307,6 +337,60 @@ func convert_invoice_block(_ b: invoice_block) -> Block? {
return .invoice(Invoice(description: description, amount: amount, string: invstr, expiry: b11.expiry, payment_hash: payment_hash, created_at: created_at))
}
func convert_mention_bech32_block(_ b: mention_bech32_block) -> Block?
{
switch b.bech32.type {
case NOSTR_BECH32_NOTE:
let note = b.bech32.data.note;
let event_id = hex_encode(Data(bytes: note.event_id, count: 32))
let event_id_ref = ReferencedId(ref_id: event_id, relay_id: nil, key: "e")
return .mention(Mention(index: nil, type: .event, ref: event_id_ref))
case NOSTR_BECH32_NEVENT:
let nevent = b.bech32.data.nevent;
let event_id = hex_encode(Data(bytes: nevent.event_id, count: 32))
var relay_id: String? = nil
if nevent.relays.num_relays > 0 {
relay_id = strblock_to_string(nevent.relays.relays.0)
}
let event_id_ref = ReferencedId(ref_id: event_id, relay_id: relay_id, key: "e")
return .mention(Mention(index: nil, type: .event, ref: event_id_ref))
case NOSTR_BECH32_NPUB:
let npub = b.bech32.data.npub
let pubkey = hex_encode(Data(bytes: npub.pubkey, count: 32))
let pubkey_ref = ReferencedId(ref_id: pubkey, relay_id: nil, key: "p")
return .mention(Mention(index: nil, type: .pubkey, ref: pubkey_ref))
case NOSTR_BECH32_NPROFILE:
let nprofile = b.bech32.data.nprofile
let pubkey = hex_encode(Data(bytes: nprofile.pubkey, count: 32))
var relay_id: String? = nil
if nprofile.relays.num_relays > 0 {
relay_id = strblock_to_string(nprofile.relays.relays.0)
}
let pubkey_ref = ReferencedId(ref_id: pubkey, relay_id: relay_id, key: "p")
return .mention(Mention(index: nil, type: .pubkey, ref: pubkey_ref))
case NOSTR_BECH32_NRELAY:
let nrelay = b.bech32.data.nrelay
guard let relay_str = strblock_to_string(nrelay.relay) else {
return nil
}
return .relay(relay_str)
case NOSTR_BECH32_NADDR:
// TODO: wtf do I do with this
guard let naddr = strblock_to_string(b.str) else {
return nil
}
return .text("nostr:" + naddr)
default:
return nil
}
}
func convert_invoice_description(b11: bolt11) -> InvoiceDescription? {
if let desc = b11.description {
return .description(String(cString: desc))
@@ -319,7 +403,7 @@ func convert_invoice_description(b11: bolt11) -> InvoiceDescription? {
return nil
}
func convert_mention_block(ind: Int32, tags: [[String]]) -> Block?
func convert_mention_index_block(ind: Int32, tags: [[String]]) -> Block?
{
let ind = Int(ind)
@@ -557,7 +641,7 @@ func parse_mention_type(_ c: String) -> MentionType? {
}
/// Convert
func make_post_tags(post_blocks: [PostBlock], tags: [[String]]) -> PostTags {
func make_post_tags(post_blocks: [PostBlock], tags: [[String]], silent_mentions: Bool) -> PostTags {
var new_tags = tags
var blocks: [Block] = []
@@ -567,6 +651,14 @@ func make_post_tags(post_blocks: [PostBlock], tags: [[String]]) -> PostTags {
guard let mention_type = parse_mention_type(ref.key) else {
continue
}
if silent_mentions || mention_type == .event {
let mention = Mention(index: nil, type: mention_type, ref: ref)
let block = Block.mention(mention)
blocks.append(block)
continue
}
if let ind = find_tag_ref(type: ref.key, id: ref.ref_id, tags: tags) {
let mention = Mention(index: ind, type: mention_type, ref: ref)
let block = Block.mention(mention)
@@ -592,7 +684,7 @@ func make_post_tags(post_blocks: [PostBlock], tags: [[String]]) -> PostTags {
func post_to_event(post: NostrPost, privkey: String, pubkey: String) -> NostrEvent {
let tags = post.references.map(refid_to_tag)
let post_blocks = parse_post_blocks(content: post.content)
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags)
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags, silent_mentions: false)
let content = render_blocks(blocks: post_tags.blocks)
let new_ev = NostrEvent(content: content, pubkey: pubkey, kind: post.kind.rawValue, tags: post_tags.tags)
new_ev.calculate_id()
+76
View File
@@ -0,0 +1,76 @@
//
// MutedThreadsManager.swift
// damus
//
// Created by Terry Yiu on 4/6/23.
//
import Foundation
fileprivate func getMutedThreadsKey(pubkey: String) -> String {
pk_setting_key(pubkey, key: "muted_threads")
}
func loadMutedThreads(pubkey: String) -> [String] {
let key = getMutedThreadsKey(pubkey: pubkey)
return UserDefaults.standard.stringArray(forKey: key) ?? []
}
func saveMutedThreads(pubkey: String, currentValue: [String], value: [String]) -> Bool {
let uniqueMutedThreads = Array(Set(value))
if uniqueMutedThreads != currentValue {
UserDefaults.standard.set(uniqueMutedThreads, forKey: getMutedThreadsKey(pubkey: pubkey))
return true
}
return false
}
class MutedThreadsManager: ObservableObject {
private let userDefaults = UserDefaults.standard
private let keypair: Keypair
private var _mutedThreadsSet: Set<String>
private var _mutedThreads: [String]
var mutedThreads: [String] {
get {
return _mutedThreads
}
set {
if saveMutedThreads(pubkey: keypair.pubkey, currentValue: _mutedThreads, value: newValue) {
self._mutedThreads = newValue
self.objectWillChange.send()
}
}
}
init(keypair: Keypair) {
self._mutedThreads = loadMutedThreads(pubkey: keypair.pubkey)
self._mutedThreadsSet = Set(_mutedThreads)
self.keypair = keypair
}
func isMutedThread(_ ev: NostrEvent, privkey: String?) -> Bool {
return _mutedThreadsSet.contains(ev.thread_id(privkey: privkey))
}
func updateMutedThread(_ ev: NostrEvent) {
let threadId = ev.thread_id(privkey: nil)
if isMutedThread(ev, privkey: keypair.privkey) {
mutedThreads = mutedThreads.filter { $0 != threadId }
_mutedThreadsSet.remove(threadId)
notify(.unmute_thread, ev)
} else {
mutedThreads.append(threadId)
_mutedThreadsSet.insert(threadId)
notify(.mute_thread, ev)
}
}
func clearAll() {
mutedThreads = []
_mutedThreadsSet.removeAll()
}
}
+4 -4
View File
@@ -129,7 +129,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
for el in zaps {
let evid = el.key
let zapgrp = el.value
let notif: NotificationItem = .event_zap(evid, zapgrp)
notifs.append(notif)
}
@@ -233,7 +233,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
}
}
func insert_event(_ ev: NostrEvent) -> Bool {
func insert_event(_ ev: NostrEvent, damus_state: DamusState) -> Bool {
if should_queue {
return insert_uniq_sorted_event_created(events: &incoming_events, new_ev: ev)
}
@@ -246,7 +246,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
return false
}
func insert_zap(_ zap: Zap) -> Bool {
func insert_zap(_ zap: Zap, damus_state: DamusState) -> Bool {
if should_queue {
return insert_uniq_sorted_zap_by_created(zaps: &incoming_zaps, new_zap: zap)
}
@@ -300,7 +300,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
}
}
func flush() -> Bool {
func flush(_ damus_state: DamusState) -> Bool {
var inserted = false
for zap in incoming_zaps {
+1 -1
View File
@@ -119,7 +119,7 @@ class ProfileModel: ObservableObject, Equatable {
} else if ev.known_kind == .contacts {
handle_profile_contact_event(ev)
} else if ev.known_kind == .metadata {
process_metadata_event(our_pubkey: damus.pubkey, profiles: damus.profiles, ev: ev)
process_metadata_event(events: damus.events, our_pubkey: damus.pubkey, profiles: damus.profiles, ev: ev)
}
seen_event.insert(ev.id)
}
+1 -1
View File
@@ -161,7 +161,7 @@ func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad
}
if ev.known_kind == .metadata {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
}
}
+1 -1
View File
@@ -129,7 +129,7 @@ class ThreadModel: ObservableObject {
}
if ev.known_kind == .metadata {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
} else if ev.is_textlike {
self.add_event(ev, privkey: self.damus_state.keypair.privkey)
}
-150
View File
@@ -1,150 +0,0 @@
//
// Translations.swift
// damus
//
// Created by Terry Yiu on 3/29/23.
//
import Foundation
import NaturalLanguage
class Translations: ObservableObject {
private static let languageDetectionMinConfidence = 0.5
@Published var translations: [NostrEvent: String] = [:]
@Published var languages: [NostrEvent: String] = [:]
let settings: UserSettingsStore
let translator: Translator
let targetLanguage = currentLanguage()
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
init(_ settings: UserSettingsStore) {
self.settings = settings
self.translator = Translator(settings)
}
/**
Attempts to detect the language of the content of a given nostr event using Apple's offline NaturalLanguage API.
The detected language will be returned only if it has a 50% or more confidence.
This is a best effort guess and could be incorrect.
*/
func detectLanguage(_ event: NostrEvent, state: DamusState) -> String? {
if let cachedLanguage = languages[event] {
return cachedLanguage
}
// Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in
// and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer.
let originalBlocks = event.blocks(state.keypair.privkey)
let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ")
// Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate.
let languageRecognizer = NLLanguageRecognizer()
languageRecognizer.processString(originalOnlyText)
guard let locale = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= Translations.languageDetectionMinConfidence })?.key.rawValue else {
return nil
}
// Remove the variant component and just take the language part as translation services typically only supports the variant-less language.
// Moreover, speakers of one variant can generally understand other variants.
let language = localeToLanguage(locale)
languages[event] = language
return language
}
/**
Returns true if the given translation is effectively the same as the original note, ignoring whitespaces and new lines.
*/
private func translationSameAsOriginal(_ translation: String, event: NostrEvent, state: DamusState) -> Bool {
return translation.trimmingCharacters(in: .whitespacesAndNewlines) == event.get_content(state.keypair.privkey).trimmingCharacters(in: .whitespacesAndNewlines)
}
func hasCachedTranslation(_ event: NostrEvent) -> Bool {
return languages[event] != nil
}
func cachedTranslation(_ event: NostrEvent) -> TranslationWithLanguage? {
if let cachedLanguage = languages[event] {
if let cachedTranslation = translations[event] {
return TranslationWithLanguage(translation: cachedTranslation, language: cachedLanguage)
} else {
return nil
}
} else {
return nil
}
}
func translate(_ event: NostrEvent, state: DamusState) async -> TranslationWithLanguage? {
guard shouldTranslate(event, state: state) else {
return nil
}
guard let noteLanguage = detectLanguage(event, state: state) else {
return nil
}
if languages[event] != nil {
return cachedTranslation(event)
}
do {
guard let translationWithLanguage = try await translator.translate(event.get_content(state.keypair.privkey), from: noteLanguage, to: targetLanguage) else {
return nil
}
// If the translated content is identical to the original content, don't return the translation.
if translationSameAsOriginal(translationWithLanguage.translation, event: event, state: state) {
// Nil out the translation as it's the same as the original.
translations[event] = nil
// Leave an entry so that we don't attempt to translate it again in the future.
languages[event] = targetLanguage
return nil
} else {
translations[event] = translationWithLanguage.translation
languages[event] = translationWithLanguage.language
return translationWithLanguage
}
} catch {
return nil
}
}
func shouldTranslate(_ event: NostrEvent, state: DamusState) -> Bool {
// Do not translate self-authored content because if the language recognizer guesses the wrong language for your own note,
// it's annoying and unexpected for the translation to show up.
if event.pubkey == state.pubkey && state.is_privkey_user {
return false
}
// Avoid translating if no translation service is configured.
switch settings.translation_service {
case .none:
return false
case .libretranslate:
if URLComponents(string: settings.libretranslate_url) == nil {
return false
}
case .deepl:
if settings.deepl_api_key == "" {
return false
}
}
// If translation was attempted before, use the results of the cached translation to determine if it should be shown.
if languages[event] != nil {
return translations[event] != nil
}
// Avoid translating notes if language cannot be detected or if it is in one of the user's preferred languages.
guard let noteLanguage = detectLanguage(event, state: state), !preferredLanguages.contains(noteLanguage) else {
return false
}
return true
}
}
+25
View File
@@ -163,6 +163,12 @@ class UserSettingsStore: ObservableObject {
UserDefaults.standard.set(notification_only_from_following, forKey: "notification_only_from_following")
}
}
@Published var translate_dms: Bool {
didSet {
UserDefaults.standard.set(translate_dms, forKey: "translate_dms")
}
}
@Published var truncate_timeline_text: Bool {
didSet {
@@ -170,6 +176,12 @@ class UserSettingsStore: ObservableObject {
}
}
@Published var notification_indicators: Int {
didSet {
UserDefaults.standard.set(notification_indicators, forKey: "notification_indicators")
}
}
@Published var truncate_mention_text: Bool {
didSet {
UserDefaults.standard.set(truncate_mention_text, forKey: "truncate_mention_text")
@@ -274,7 +286,9 @@ class UserSettingsStore: ObservableObject {
repost_notification = UserDefaults.standard.object(forKey: "repost_notification") as? Bool ?? true
like_notification = UserDefaults.standard.object(forKey: "like_notification") as? Bool ?? true
dm_notification = UserDefaults.standard.object(forKey: "dm_notification") as? Bool ?? true
notification_indicators = UserDefaults.standard.object(forKey: "notification_indicators") as? Int ?? NewEventsBits.all.rawValue
notification_only_from_following = UserDefaults.standard.object(forKey: "notification_only_from_following") as? Bool ?? false
translate_dms = UserDefaults.standard.object(forKey: "translate_dms") as? Bool ?? false
truncate_timeline_text = UserDefaults.standard.object(forKey: "truncate_timeline_text") as? Bool ?? false
truncate_mention_text = UserDefaults.standard.object(forKey: "truncate_mention_text") as? Bool ?? false
disable_animation = should_disable_image_animation()
@@ -336,6 +350,17 @@ class UserSettingsStore: ObservableObject {
private func clearDeepLApiKey() throws {
try Vault.deletePrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration())
}
func can_translate(_ pubkey: String) -> Bool {
switch translation_service {
case .none:
return false
case .libretranslate:
return URLComponents(string: libretranslate_url) != nil
case .deepl:
return deepl_api_key != ""
}
}
}
struct DamusLibreTranslateKeychainConfiguration: KeychainConfiguration {
+13 -5
View File
@@ -7,7 +7,7 @@
import Foundation
struct Profile: Codable {
class Profile: Codable {
var value: [String: AnyCodable]
init (name: String?, display_name: String?, about: String?, picture: String?, banner: String?, website: String?, lud06: String?, lud16: String?, nip05: String?) {
@@ -39,7 +39,7 @@ struct Profile: Codable {
return s
}
private mutating func set_val<T>(_ key: String, _ val: T?) {
private func set_val<T>(_ key: String, _ val: T?) {
if val == nil {
self.value.removeValue(forKey: key)
return
@@ -48,7 +48,7 @@ struct Profile: Codable {
self.value[key] = AnyCodable.init(val)
}
private mutating func set_str(_ key: String, _ val: String?) {
private func set_str(_ key: String, _ val: String?) {
set_val(key, val)
}
@@ -110,13 +110,21 @@ struct Profile: Codable {
}
}
private var _lnurl: String? = nil
var lnurl: String? {
if let _lnurl {
return _lnurl
}
guard let addr = lud16 ?? lud06 else {
return nil;
}
if addr.contains("@") {
return lnaddress_to_lnurl(addr);
// this is a heavy op and is used a lot in views, cache it!
let addr = lnaddress_to_lnurl(addr);
self._lnurl = addr
return addr
}
if !addr.lowercased().hasPrefix("lnurl") {
@@ -139,7 +147,7 @@ struct Profile: Codable {
self.value = [:]
}
init(from decoder: Decoder) throws {
required init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.value = try container.decode([String: AnyCodable].self)
}
+25 -10
View File
@@ -13,11 +13,15 @@ import CryptoKit
import NaturalLanguage
enum ValidationResult: Decodable {
case unknown
case ok
case bad_id
case bad_sig
var is_bad: Bool {
return self == .bad_id || self == .bad_sig
}
}
struct OtherEvent {
@@ -82,7 +86,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
}
var too_big: Bool {
return self.content.count > 16000
return self.content.utf8.count > 16000
}
var should_show_event: Bool {
@@ -93,14 +97,6 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
return calculate_event_id(ev: self) == self.id
}
var is_valid: Bool {
return validity == .ok
}
lazy var validity: ValidationResult = {
return .ok //validate_event(ev: self)
}()
private var _blocks: [Block]? = nil
func blocks(_ privkey: String?) -> [Block] {
if let bs = _blocks {
@@ -260,6 +256,25 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
return event_is_reply(self, privkey: privkey)
}
func note_language(_ privkey: String?) -> String? {
// Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in
// and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer.
let originalBlocks = blocks(privkey)
let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ")
// Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate.
let languageRecognizer = NLLanguageRecognizer()
languageRecognizer.processString(originalOnlyText)
guard let locale = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= 0.5 })?.key.rawValue else {
return nil
}
// Remove the variant component and just take the language part as translation services typically only supports the variant-less language.
// Moreover, speakers of one variant can generally understand other variants.
return localeToLanguage(locale)
}
public var referenced_ids: [ReferencedId] {
return get_referenced_ids(key: "e")
}
+1 -3
View File
@@ -89,7 +89,7 @@ final class RelayConnection: WebSocketDelegate {
self.isConnected = false
case .text(let txt):
if txt.count > 2000 {
if txt.utf8.count > 2000 {
DispatchQueue.global(qos: .default).async {
if let ev = decode_nostr_event(txt: txt) {
DispatchQueue.main.async {
@@ -105,8 +105,6 @@ final class RelayConnection: WebSocketDelegate {
}
}
print("decode failed for \(txt)")
// TODO: trigger event error
default:
break
+4
View File
@@ -52,6 +52,10 @@ class RelayPool {
var num_connecting: Int {
return relays.reduce(0) { n, r in n + (r.connection.isConnecting ? 1 : 0) }
}
var num_connected: Int {
return relays.reduce(0) { n, r in n + (r.connection.isConnected ? 1 : 0) }
}
func remove_handler(sub_id: String) {
self.handlers = handlers.filter { $0.sub_id != sub_id }
+1 -1
View File
@@ -9,7 +9,7 @@ import Foundation
func created_deleted_account_profile(keypair: FullKeypair) -> NostrEvent {
var profile = Profile()
let profile = Profile()
profile.deleted = true
profile.about = "account deleted"
profile.name = "nobody"
+44
View File
@@ -0,0 +1,44 @@
//
// CompatibleAttribute.swift
// damus
//
// Created by William Casarin on 2023-04-06.
//
import Foundation
import SwiftUI
class CompatibleText: Equatable {
var text: Text
var attributed: AttributedString
init() {
self.text = Text("")
self.attributed = AttributedString(stringLiteral: "")
}
init(stringLiteral: String) {
self.text = Text(stringLiteral)
self.attributed = AttributedString(stringLiteral: stringLiteral)
}
init(text: Text, attributed: AttributedString) {
self.text = text
self.attributed = attributed
}
init(attributed: AttributedString) {
self.text = Text(attributed)
self.attributed = attributed
}
static func == (lhs: CompatibleText, rhs: CompatibleText) -> Bool {
return lhs.attributed == rhs.attributed
}
static func +(lhs: CompatibleText, rhs: CompatibleText) -> CompatibleText {
let combinedText = lhs.text + rhs.text
let combinedAttributes = lhs.attributed + rhs.attributed
return CompatibleText(text: combinedText, attributed: combinedAttributes)
}
}
+29
View File
@@ -13,6 +13,9 @@ class EventCache {
private var events: [String: NostrEvent] = [:]
private var replies = ReplyMap()
private var cancellable: AnyCancellable?
private var translations: [String: TranslateStatus] = [:]
private var artifacts: [String: NoteArtifacts] = [:]
var validation: [String: ValidationResult] = [:]
//private var thread_latest: [String: Int64]
@@ -24,6 +27,30 @@ class EventCache {
}
}
func is_event_valid(_ evid: String) -> ValidationResult {
guard let result = validation[evid] else {
return .unknown
}
return result
}
func store_translation_artifacts(evid: String, translated: TranslateStatus) {
self.translations[evid] = translated
}
func store_artifacts(evid: String, artifacts: NoteArtifacts) {
self.artifacts[evid] = artifacts
}
func lookup_artifacts(evid: String) -> NoteArtifacts? {
return self.artifacts[evid]
}
func lookup_translated_artifacts(evid: String) -> TranslateStatus? {
return self.translations[evid]
}
func parent_events(event: NostrEvent) -> [NostrEvent] {
var parents: [NostrEvent] = []
@@ -87,6 +114,8 @@ class EventCache {
private func prune() {
events = [:]
translations = [:]
artifacts = [:]
replies.replies = [:]
}
}
@@ -22,6 +22,7 @@ extension KFOptionSetter {
maxSize: imageContext.maxMebibyteSize(),
downsampleSize: imageContext.downsampleSize()
)
options.loadDiskFileSynchronously = false
options.backgroundDecode = true
options.cacheOriginalImage = true
options.scaleFactor = UIScreen.main.scale
+68
View File
@@ -0,0 +1,68 @@
//
// Hashtags.swift
// damus
//
// Created by William Casarin on 2023-04-06.
//
import Foundation
import SwiftUI
struct CustomHashtag {
let name: String
let offset: CGFloat?
let color: Color?
init(name: String, color: Color? = nil, offset: CGFloat? = nil) {
self.name = name
self.color = color
self.offset = offset
}
static let coffee = CustomHashtag(name: "coffee", color: DamusColors.brown, offset: -1.0)
static let bitcoin = CustomHashtag(name: "bitcoin", color: Color.orange, offset: -3.0)
static let nostr = CustomHashtag(name: "nostr", color: DamusColors.purple, offset: -2.0)
static let plebchain = CustomHashtag(name: "plebchain", color: DamusColors.deepPurple, offset: -3.0)
static let zap = CustomHashtag(name: "zap", color: DamusColors.yellow, offset: -4.0)
}
let custom_hashtags: [String: CustomHashtag] = [
"bitcoin": CustomHashtag.bitcoin,
"btc": CustomHashtag.bitcoin,
"nostr": CustomHashtag.nostr,
"coffee": CustomHashtag.coffee,
"coffeechain": CustomHashtag.coffee,
"plebchain": CustomHashtag.plebchain,
"zap": CustomHashtag.zap,
"zapathon": CustomHashtag.zap,
]
func hashtag_str(_ htag: String) -> CompatibleText {
var attributedString = AttributedString(stringLiteral: "#\(htag)")
attributedString.link = URL(string: "damus:t:\(htag)")
let lowertag = htag.lowercased()
var text = Text(attributedString)
if let custom_hashtag = custom_hashtags[lowertag] {
if let col = custom_hashtag.color {
attributedString.foregroundColor = col
}
let name = custom_hashtag.name
if let img = UIImage(named: "\(name)-hashtag") {
attributedString = attributedString + " "
attributed_string_attach_icon(&attributedString, img: img)
}
text = Text(attributedString)
let img = Image("\(name)-hashtag")
text = text + Text("\(img)").baselineOffset(custom_hashtag.offset ?? 0.0)
} else {
attributedString.foregroundColor = DamusColors.purple
}
return CompatibleText(text: text, attributed: attributedString)
}
+47
View File
@@ -0,0 +1,47 @@
//
// LocalNotification.swift
// damus
//
// Created by William Casarin on 2023-04-15.
//
import Foundation
struct LossyLocalNotification {
let type: LocalNotificationType
let event_id: String
func to_user_info() -> [AnyHashable: Any] {
return [
"type": self.type.rawValue,
"evid": self.event_id
]
}
static func from_user_info(user_info: [AnyHashable: Any]) -> LossyLocalNotification {
let target_id = user_info["evid"] as! String
let typestr = user_info["type"] as! String
let type = LocalNotificationType(rawValue: typestr)!
return LossyLocalNotification(type: type, event_id: target_id)
}
}
struct LocalNotification {
let type: LocalNotificationType
let event: NostrEvent
let target: NostrEvent
let content: String
func to_lossy() -> LossyLocalNotification {
return LossyLocalNotification(type: self.type, event_id: self.target.id)
}
}
enum LocalNotificationType: String {
case dm
case like
case mention
case repost
case zap
}
-8
View File
@@ -22,14 +22,6 @@ func localizedStringFormat(key: String, locale: Locale?) -> String {
return bundle.localizedString(forKey: key, value: fallback, table: nil)
}
func currentLanguage() -> String {
if #available(iOS 16, *) {
return Locale.current.language.languageCode?.identifier ?? "en"
} else {
return Locale.current.languageCode ?? "en"
}
}
/**
Removes the variant part of a locale code so that it contains only the language code.
*/
+9
View File
@@ -104,6 +104,15 @@ extension Notification.Name {
static var zapping: Notification.Name {
return Notification.Name("zapping")
}
static var mute_thread: Notification.Name {
return Notification.Name("mute_thread")
}
static var unmute_thread: Notification.Name {
return Notification.Name("unmute_thread")
}
static var local_notification: Notification.Name {
return Notification.Name("local_notification")
}
}
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {
+9 -34
View File
@@ -20,28 +20,18 @@ public struct Translator {
self.userSettingsStore = userSettingsStore
}
/**
Translates a string from source language to target language.
If the translation provider supports its own language detection, it may determine the source language by itself that could be
different from what is passed in as the sourceLanguage argument.
The source language that is actually used in the translation will be returned as part of the TranslationWithLanguage object.
If the translation was unable to be fetched for whatever reason, nil is returned.
*/
public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> TranslationWithLanguage? {
public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
switch userSettingsStore.translation_service {
case .libretranslate:
return try await translateWithLibreTranslate(text, from: sourceLanguage, to: targetLanguage)
case .deepl:
return try await translateWithDeepL(text, to: targetLanguage)
return try await translateWithDeepL(text, from: sourceLanguage, to: targetLanguage)
case .none:
return nil
return text
}
}
/**
Translates a string from sourceLanguage to targetLanguage using LibreTranslate. We do not rely on LibreTranslate's language detection API as it requires a separate API call. Instead, we rely on the passed in sourceLanguage argument.
*/
private func translateWithLibreTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> TranslationWithLanguage? {
private func translateWithLibreTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
let url = try makeURL(userSettingsStore.libretranslate_url, path: "/translate")
var request = URLRequest(url: url)
@@ -61,15 +51,10 @@ public struct Translator {
let translatedText: String
}
let response: Response = try await decodedData(for: request)
let translation = response.translatedText
return TranslationWithLanguage(translation: translation, language: targetLanguage)
return response.translatedText
}
/**
Translates a string to targetLanguage using DeepL. We do not accept a sourceLanguage as an argument as DeepL performs language detection within the translate API, its models are generally fairly accurate, and does not require a separate API call like LibreTranslate.
*/
private func translateWithDeepL(_ text: String, to targetLanguage: String) async throws -> TranslationWithLanguage? {
private func translateWithDeepL(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
if userSettingsStore.deepl_api_key == "" {
return nil
}
@@ -83,9 +68,10 @@ public struct Translator {
struct RequestBody: Encodable {
let text: [String]
let source_lang: String
let target_lang: String
}
let body = RequestBody(text: [text], target_lang: targetLanguage.uppercased())
let body = RequestBody(text: [text], source_lang: sourceLanguage.uppercased(), target_lang: targetLanguage.uppercased())
request.httpBody = try encoder.encode(body)
struct Response: Decodable {
@@ -97,13 +83,7 @@ public struct Translator {
}
let response: Response = try await decodedData(for: request)
if response.translations.isEmpty {
return nil
}
let translation = response.translations.map { $0.text }.joined(separator: " ")
return TranslationWithLanguage(translation: translation, language: response.translations.first!.detected_source_language)
return response.translations.map { $0.text }.joined(separator: " ")
}
private func makeURL(_ baseUrl: String, path: String) throws -> URL {
@@ -124,11 +104,6 @@ public struct Translator {
}
}
public struct TranslationWithLanguage {
let translation: String
let language: String
}
private extension URLSession {
func data(for request: URLRequest) async throws -> Data {
var task: URLSessionDataTask?
+2 -2
View File
@@ -48,13 +48,13 @@ struct EventActionBar: View {
HStack {
if damus_state.keypair.privkey != nil {
HStack(spacing: 4) {
EventActionButton(img: "bubble.left", col: bar.replied ? Color.blue : Color.gray) {
EventActionButton(img: "bubble.left", col: bar.replied ? DamusColors.purple : Color.gray) {
notify(.reply, event)
}
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
Text(verbatim: "\(bar.replies > 0 ? "\(bar.replies)" : "")")
.font(.footnote.weight(.medium))
.foregroundColor(bar.replied ? Color.blue : Color.gray)
.foregroundColor(bar.replied ? DamusColors.purple : Color.gray)
}
}
Spacer()
+2 -2
View File
@@ -122,9 +122,9 @@ enum MediaUploader: String, CaseIterable, Identifiable {
var model: Model {
switch self {
case .nostrBuild:
return .init(index: -1, tag: "nostrBuild", displayName: NSLocalizedString("NostrBuild", comment: "Dropdown option label for system default for NostrBuild image uploader."))
return .init(index: -1, tag: "nostrBuild", displayName: "nostr.build")
case .nostrImg:
return .init(index: 0, tag: "nostrImg", displayName: NSLocalizedString("NostrImg", comment: "Dropdown option label for system default for NostrImg image uploader."))
return .init(index: 0, tag: "nostrImg", displayName: "nostrimg.com")
}
}
+10 -8
View File
@@ -9,17 +9,20 @@ import SwiftUI
struct DMChatView: View {
let damus_state: DamusState
let pubkey: String
@EnvironmentObject var dms: DirectMessageModel
@ObservedObject var dms: DirectMessageModel
@State var showPrivateKeyWarning: Bool = false
var pubkey: String {
dms.pubkey
}
var Messages: some View {
ScrollViewReader { scroller in
ScrollView {
VStack(alignment: .leading) {
ForEach(Array(zip(dms.events, dms.events.indices)), id: \.0.id) { (ev, ind) in
DMView(event: dms.events[ind], damus_state: damus_state)
.contextMenu{MenuItems(event: ev, keypair: damus_state.keypair, target_pubkey: ev.pubkey, bookmarks: damus_state.bookmarks)}
.contextMenu{MenuItems(event: ev, keypair: damus_state.keypair, target_pubkey: ev.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads)}
}
EndBlock(height: 80)
}
@@ -120,7 +123,7 @@ struct DMChatView: View {
func send_message() {
let tags = [["p", pubkey]]
let post_blocks = parse_post_blocks(content: dms.draft)
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags)
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags, silent_mentions: true)
let content = render_blocks(blocks: post_tags.blocks)
guard let dm = create_dm(content, to_pk: pubkey, tags: post_tags.tags, keypair: damus_state.keypair) else {
@@ -177,10 +180,9 @@ struct DMChatView_Previews: PreviewProvider {
static var previews: some View {
let ev = NostrEvent(content: "hi", pubkey: "pubkey", kind: 1, tags: [])
let model = DirectMessageModel(events: [ev], our_pubkey: "pubkey")
let model = DirectMessageModel(events: [ev], our_pubkey: "pubkey", pubkey: "the_pk")
DMChatView(damus_state: test_damus_state(), pubkey: "pubkey")
.environmentObject(model)
DMChatView(damus_state: test_damus_state(), dms: model)
}
}
+9 -1
View File
@@ -25,6 +25,14 @@ struct DMView: View {
}
}
var dm_options: EventViewOptions {
if self.damus_state.settings.translate_dms {
return []
}
return [.no_translate]
}
var DM: some View {
HStack {
if is_ours {
@@ -33,7 +41,7 @@ struct DMView: View {
let should_show_img = should_show_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: .normal, artifacts: .just_content(event.get_content(damus_state.keypair.privkey)), options: [])
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: .normal, artifacts: .just_content(event.get_content(damus_state.keypair.privkey)), options: dm_options)
.padding([.top, .leading, .trailing], 10)
.padding([.bottom], 25)
.background(VisualEffectView(effect: UIBlurEffect(style: .prominent))
+17 -22
View File
@@ -16,21 +16,12 @@ struct DirectMessagesView: View {
let damus_state: DamusState
@State var dm_type: DMType = .friend
@State var open_dm: Bool = false
@State var pubkey: String = ""
@EnvironmentObject var model: DirectMessagesModel
@State var active_model: DirectMessageModel
init(damus_state: DamusState) {
self.damus_state = damus_state
self._active_model = State(initialValue: DirectMessageModel(our_pubkey: damus_state.pubkey))
}
@ObservedObject var model: DirectMessagesModel
func MainContent(requests: Bool) -> some View {
ScrollView {
let chat = DMChatView(damus_state: damus_state, pubkey: pubkey)
.environmentObject(active_model)
NavigationLink(destination: chat, isActive: $open_dm) {
let chat = DMChatView(damus_state: damus_state, dms: model.active_model)
NavigationLink(destination: chat, isActive: $model.open_dm) {
EmptyView()
}
LazyVStack(spacing: 0) {
@@ -38,7 +29,7 @@ struct DirectMessagesView: View {
EmptyTimelineView()
} else {
let dms = requests ? model.message_requests : model.friend_dms
ForEach(dms, id: \.0) { tup in
ForEach(dms, id: \.pubkey) { tup in
MaybeEvent(tup)
.padding(.top, 10)
@@ -51,14 +42,20 @@ struct DirectMessagesView: View {
}
}
func MaybeEvent(_ tup: (String, DirectMessageModel)) -> some View {
var options: EventViewOptions {
if self.damus_state.settings.translate_dms {
return [.truncate_content, .no_action_bar]
}
return [.truncate_content, .no_action_bar, .no_translate]
}
func MaybeEvent(_ model: DirectMessageModel) -> some View {
Group {
if let ev = tup.1.events.last {
EventView(damus: damus_state, event: ev, pubkey: tup.0)
if let ev = model.events.last {
EventView(damus: damus_state, event: ev, pubkey: model.pubkey, options: options)
.onTapGesture {
pubkey = tup.0
active_model = tup.1
open_dm = true
self.model.open_dm_by_model(model)
}
} else {
EmptyView()
@@ -98,8 +95,6 @@ struct DirectMessagesView_Previews: PreviewProvider {
kind: 4,
tags: [])
let ds = test_damus_state()
let model = DirectMessageModel(events: [ev], our_pubkey: ds.pubkey)
DirectMessagesView(damus_state: ds)
.environmentObject(model)
DirectMessagesView(damus_state: ds, model: ds.dms)
}
}
+5
View File
@@ -6,6 +6,7 @@
//
import SwiftUI
import Combine
let PPM_SIZE: CGFloat = 80.0
let BANNER_HEIGHT: CGFloat = 150.0;
@@ -197,6 +198,9 @@ struct EditMetadataView: View {
TextField(NSLocalizedString("jb55@jb55.com", comment: "Placeholder example text for identifier used for NIP-05 verification."), text: $nip05)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
.onReceive(Just(nip05)) { newValue in
self.nip05 = newValue.trimmingCharacters(in: .whitespaces)
}
}, header: {
Text("NIP-05 Verification", comment: "Label for NIP-05 Verification section of user profile form.")
}, footer: {
@@ -227,6 +231,7 @@ struct EditMetadataView: View {
}
}
.ignoresSafeArea(edges: .top)
.background(Color(.systemGroupedBackground))
}
func uploadedProfilePicture(image_url: URL?) {
+26 -53
View File
@@ -14,28 +14,6 @@ enum EventViewKind {
case selected
}
func eventviewsize_to_font(_ size: EventViewKind) -> Font {
switch size {
case .small:
return .body
case .normal:
return .body
case .selected:
return .custom("selected", size: 21.0)
}
}
func eventviewsize_to_uifont(_ size: EventViewKind) -> UIFont {
switch size {
case .small:
return .preferredFont(forTextStyle: .body)
case .normal:
return .preferredFont(forTextStyle: .body)
case .selected:
return .preferredFont(forTextStyle: .title2)
}
}
struct EventView: View {
let event: NostrEvent
let options: EventViewOptions
@@ -44,25 +22,11 @@ struct EventView: View {
@EnvironmentObject var action_bar: ActionBarModel
init(damus: DamusState, event: NostrEvent, options: EventViewOptions) {
init(damus: DamusState, event: NostrEvent, pubkey: String? = nil, options: EventViewOptions = []) {
self.event = event
self.options = options
self.damus = damus
self.pubkey = event.pubkey
}
init(damus: DamusState, event: NostrEvent) {
self.event = event
self.options = []
self.damus = damus
self.pubkey = event.pubkey
}
init(damus: DamusState, event: NostrEvent, pubkey: String) {
self.event = event
self.options = [.no_action_bar]
self.damus = damus
self.pubkey = pubkey
self.pubkey = pubkey ?? event.pubkey
}
var body: some View {
@@ -105,19 +69,6 @@ func should_show_images(settings: UserSettingsStore, contacts: Contacts, ev: Nos
return false
}
func event_validity_color(_ validation: ValidationResult) -> some View {
Group {
switch validation {
case .ok:
EmptyView()
case .bad_id:
Color.orange.opacity(0.4)
case .bad_sig:
Color.red.opacity(0.4)
}
}
}
extension View {
func pubkey_context_menu(bech32_pubkey: String) -> some View {
return self.contextMenu {
@@ -129,9 +80,9 @@ extension View {
}
}
func event_context_menu(_ event: NostrEvent, keypair: Keypair, target_pubkey: String, bookmarks: BookmarksManager) -> some View {
func event_context_menu(_ event: NostrEvent, keypair: Keypair, target_pubkey: String, bookmarks: BookmarksManager, muted_threads: MutedThreadsManager) -> some View {
return self.contextMenu {
EventMenuContext(event: event, keypair: keypair, target_pubkey: target_pubkey, bookmarks: bookmarks)
EventMenuContext(event: event, keypair: keypair, target_pubkey: target_pubkey, bookmarks: bookmarks, muted_threads: muted_threads)
}
}
@@ -156,6 +107,28 @@ func make_actionbar_model(ev: String, damus: DamusState) -> ActionBarModel {
return model
}
func eventviewsize_to_font(_ size: EventViewKind) -> Font {
switch size {
case .small:
return .body
case .normal:
return .body
case .selected:
return .custom("selected", size: 21.0)
}
}
func eventviewsize_to_uifont(_ size: EventViewKind) -> UIFont {
switch size {
case .small:
return .preferredFont(forTextStyle: .body)
case .normal:
return .preferredFont(forTextStyle: .body)
case .selected:
return .preferredFont(forTextStyle: .title2)
}
}
struct EventView_Previews: PreviewProvider {
static var previews: some View {
+1 -1
View File
@@ -73,7 +73,7 @@ struct BuilderEventView: View {
var body: some View {
VStack {
if let event = event {
if let event {
let ev = event.inner_event ?? event
let thread = ThreadModel(event: ev, damus_state: damus)
let dest = ThreadView(state: damus, thread: thread)
+1 -1
View File
@@ -23,7 +23,7 @@ struct EmbeddedEventView: View {
Spacer()
EventMenuContext(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks)
EventMenuContext(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads)
.padding([.bottom], 4)
}
+23 -3
View File
@@ -12,6 +12,7 @@ struct EventMenuContext: View {
let keypair: Keypair
let target_pubkey: String
let bookmarks: BookmarksManager
let muted_threads: MutedThreadsManager
@Environment(\.colorScheme) var colorScheme
@@ -19,7 +20,7 @@ struct EventMenuContext: View {
HStack {
Menu {
MenuItems(event: event, keypair: keypair, target_pubkey: target_pubkey, bookmarks: bookmarks)
MenuItems(event: event, keypair: keypair, target_pubkey: target_pubkey, bookmarks: bookmarks, muted_threads: muted_threads)
} label: {
Label("", systemImage: "ellipsis")
@@ -36,14 +37,20 @@ struct MenuItems: View {
let keypair: Keypair
let target_pubkey: String
let bookmarks: BookmarksManager
let muted_threads: MutedThreadsManager
@State private var isBookmarked: Bool = false
@State private var isMutedThread: Bool = false
init(event: NostrEvent, keypair: Keypair, target_pubkey: String, bookmarks: BookmarksManager) {
init(event: NostrEvent, keypair: Keypair, target_pubkey: String, bookmarks: BookmarksManager, muted_threads: MutedThreadsManager) {
let bookmarked = bookmarks.isBookmarked(event)
self._isBookmarked = State(initialValue: bookmarked)
let muted_thread = muted_threads.isMutedThread(event, privkey: keypair.privkey)
self._isMutedThread = State(initialValue: muted_thread)
self.bookmarks = bookmarks
self.muted_threads = muted_threads
self.event = event
self.keypair = keypair
self.target_pubkey = target_pubkey
@@ -86,6 +93,19 @@ struct MenuItems: View {
Label(isBookmarked ? removeBookmarkString : addBookmarkString, systemImage: imageName)
}
if event.known_kind != .dm {
Button {
self.muted_threads.updateMutedThread(event)
let muted = self.muted_threads.isMutedThread(event, privkey: self.keypair.privkey)
isMutedThread = muted
} label: {
let imageName = isMutedThread ? "speaker" : "speaker.slash"
let unmuteThreadString = NSLocalizedString("Unmute conversation", comment: "Context menu option for unmuting a conversation.")
let muteThreadString = NSLocalizedString("Mute conversation", comment: "Context menu option for muting a conversation.")
Label(isMutedThread ? unmuteThreadString : muteThreadString, systemImage: imageName)
}
}
Button {
NotificationCenter.default.post(name: .broadcast_event, object: event)
} label: {
@@ -104,7 +124,7 @@ struct MenuItems: View {
Button(role: .destructive) {
notify(.mute, target_pubkey)
} label: {
Label(NSLocalizedString("Mute", comment: "Context menu option for muting users."), systemImage: "exclamationmark.octagon")
Label(NSLocalizedString("Mute User", comment: "Context menu option for muting users."), systemImage: "exclamationmark.octagon")
}
}
}
+1 -1
View File
@@ -35,7 +35,7 @@ struct SelectedEventView: View {
Spacer()
EventMenuContext(event: event, keypair: damus.keypair, target_pubkey: event.pubkey, bookmarks: damus.bookmarks)
EventMenuContext(event: event, keypair: damus.keypair, target_pubkey: event.pubkey, bookmarks: damus.bookmarks, muted_threads: damus.muted_threads)
.padding([.bottom], 4)
}
+2 -2
View File
@@ -15,6 +15,7 @@ struct EventViewOptions: OptionSet {
static let wide = EventViewOptions(rawValue: 1 << 3)
static let truncate_content = EventViewOptions(rawValue: 1 << 4)
static let pad_content = EventViewOptions(rawValue: 1 << 5)
static let no_translate = EventViewOptions(rawValue: 1 << 6)
}
struct TextEvent: View {
@@ -36,7 +37,6 @@ struct TextEvent: View {
}
}
.contentShape(Rectangle())
.background(event_validity_color(event.validity))
.id(event.id)
.frame(maxWidth: .infinity, minHeight: PFP_SIZE)
.padding([.bottom], 2)
@@ -108,7 +108,7 @@ struct TextEvent: View {
}
var ContextButton: some View {
EventMenuContext(event: event, keypair: damus.keypair, target_pubkey: event.pubkey, bookmarks: damus.bookmarks)
EventMenuContext(event: event, keypair: damus.keypair, target_pubkey: event.pubkey, bookmarks: damus.bookmarks, muted_threads: damus.muted_threads)
.padding([.bottom], 4)
}
+10 -4
View File
@@ -12,13 +12,20 @@ struct FollowUserView: View {
let damus_state: DamusState
static let markdown = Markdown()
@State var navigating: Bool = false
var body: some View {
let dest = ProfileView(damus_state: damus_state, pubkey: target.pubkey)
NavigationLink(destination: dest, isActive: $navigating) {
EmptyView()
}
HStack {
UserView(damus_state: damus_state, pubkey: target.pubkey)
UserViewRow(damus_state: damus_state, pubkey: target.pubkey)
FollowButtonView(target: target, follows_you: false, follow_state: damus_state.contacts.follow_state(target.pubkey))
}
Spacer()
}
}
@@ -33,10 +40,9 @@ struct FollowersView: View {
LazyVStack(alignment: .leading) {
ForEach(followers.contacts ?? [], id: \.self) { pk in
FollowUserView(target: .pubkey(pk), damus_state: damus_state)
Divider()
}
}
.padding()
.padding(.horizontal)
}
.navigationBarTitle(NSLocalizedString("Followers", comment: "Navigation bar title for view that shows who is following a user."))
.onAppear {
@@ -57,7 +63,7 @@ struct FollowingView: View {
var body: some View {
ScrollView {
LazyVStack(alignment: .leading) {
ForEach(following.contacts, id: \.self) { pk in
ForEach(following.contacts.reversed(), id: \.self) { pk in
FollowUserView(target: .pubkey(pk), damus_state: damus_state)
}
}
+8 -4
View File
@@ -15,6 +15,7 @@ struct ImagePicker: UIViewControllerRepresentable {
let sourceType: UIImagePickerController.SourceType
let pubkey: String
@Binding var image_upload_confirm: Bool
var imagesOnly: Bool = false
let onImagePicked: (URL) -> Void
let onVideoPicked: (URL) -> Void
@@ -24,15 +25,18 @@ struct ImagePicker: UIViewControllerRepresentable {
private let sourceType: UIImagePickerController.SourceType
private let onImagePicked: (URL) -> Void
private let onVideoPicked: (URL) -> Void
@Binding var image_upload_confirm: Bool
init(presentationMode: Binding<PresentationMode>,
sourceType: UIImagePickerController.SourceType,
onImagePicked: @escaping (URL) -> Void,
onVideoPicked: @escaping (URL) -> Void) {
onVideoPicked: @escaping (URL) -> Void,
image_upload_confirm: Binding<Bool>) {
_presentationMode = presentationMode
self.sourceType = sourceType
self.onImagePicked = onImagePicked
self.onVideoPicked = onVideoPicked
self._image_upload_confirm = image_upload_confirm
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
@@ -51,9 +55,9 @@ struct ImagePicker: UIViewControllerRepresentable {
onImagePicked(editedImageURL)
}
}
presentationMode.dismiss()
image_upload_confirm = true
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
presentationMode.dismiss()
}
@@ -98,7 +102,7 @@ struct ImagePicker: UIViewControllerRepresentable {
onVideoPicked: { videoURL in
// Handle the selected video URL
onVideoPicked(videoURL)
})
}, image_upload_confirm: $image_upload_confirm)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
+1 -1
View File
@@ -9,7 +9,7 @@ import SwiftUI
struct ImageView: View {
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
Text(verbatim: /*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
}
}
+15 -9
View File
@@ -27,21 +27,27 @@ func timeline_bit(_ timeline: Timeline) -> Int {
}
}
func show_indicator(timeline: Timeline, current: NewEventsBits, indicator_setting: Int) -> Bool {
if timeline == .notifications {
return (current.rawValue & indicator_setting & NewEventsBits.notifications.rawValue) > 0
}
return (current.rawValue & indicator_setting) == timeline_to_notification_bits(timeline, ev: nil).rawValue
}
struct TabButton: View {
let timeline: Timeline
let img: String
@Binding var selected: Timeline?
@Binding var new_events: NewEventsBits
@Binding var isSidebarVisible: Bool
let settings: UserSettingsStore
let action: (Timeline) -> ()
var body: some View {
ZStack(alignment: .center) {
Tab
if new_events.is_set(timeline) {
if show_indicator(timeline: timeline, current: new_events, indicator_setting: settings.notification_indicators) {
Circle()
.size(CGSize(width: 8, height: 8))
.frame(width: 10, height: 10, alignment: .topTrailing)
@@ -55,8 +61,8 @@ struct TabButton: View {
var Tab: some View {
Button(action: {
action(timeline)
new_events = NewEventsBits(prev: new_events, unsetting: timeline)
isSidebarVisible = false
let bits = timeline_to_notification_bits(timeline, ev: nil)
new_events = NewEventsBits(rawValue: new_events.rawValue & ~bits.rawValue)
}) {
Label("", systemImage: selected == timeline ? "\(img).fill" : img)
.contentShape(Rectangle())
@@ -70,18 +76,18 @@ struct TabButton: View {
struct TabBar: View {
@Binding var new_events: NewEventsBits
@Binding var selected: Timeline?
@Binding var isSidebarVisible: Bool
let settings: UserSettingsStore
let action: (Timeline) -> ()
var body: some View {
VStack {
Divider()
HStack {
TabButton(timeline: .home, img: "house", selected: $selected, new_events: $new_events, isSidebarVisible: $isSidebarVisible, action: action).keyboardShortcut("1")
TabButton(timeline: .dms, img: "bubble.left.and.bubble.right", selected: $selected, new_events: $new_events, isSidebarVisible: $isSidebarVisible, action: action).keyboardShortcut("2")
TabButton(timeline: .search, img: "magnifyingglass.circle", selected: $selected, new_events: $new_events, isSidebarVisible: $isSidebarVisible, action: action).keyboardShortcut("3")
TabButton(timeline: .notifications, img: "bell", selected: $selected, new_events: $new_events, isSidebarVisible: $isSidebarVisible, action: action).keyboardShortcut("4")
TabButton(timeline: .home, img: "house", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("1")
TabButton(timeline: .dms, img: "bubble.left.and.bubble.right", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("2")
TabButton(timeline: .search, img: "magnifyingglass.circle", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("3")
TabButton(timeline: .notifications, img: "bell", selected: $selected, new_events: $new_events, settings: settings, action: action).keyboardShortcut("4")
}
}
}
+1 -1
View File
@@ -37,7 +37,7 @@ struct MutelistView: View {
var body: some View {
List(users, id: \.self) { pubkey in
UserView(damus_state: damus_state, pubkey: pubkey)
UserViewRow(damus_state: damus_state, pubkey: pubkey)
.id(pubkey)
.swipeActions {
RemoveAction(pubkey: pubkey)
+63 -57
View File
@@ -29,7 +29,6 @@ struct NoteContentView: View {
let size: EventViewKind
let preview_height: CGFloat?
let options: EventViewOptions
let translatable: Bool
@State var artifacts: NoteArtifacts
@State var preview: LinkViewRepresentable?
@@ -40,11 +39,16 @@ struct NoteContentView: View {
self.show_images = show_images
self.size = size
self.options = options
self.translatable = damus_state.translations.shouldTranslate(event, state: damus_state)
self._artifacts = State(initialValue: artifacts)
self.preview_height = lookup_cached_preview_size(previews: damus_state.previews, evid: event.id)
self._preview = State(initialValue: load_cached_preview(previews: damus_state.previews, evid: event.id))
self._artifacts = State(initialValue: render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey))
if let cache = damus_state.events.lookup_artifacts(evid: event.id) {
self._artifacts = State(initialValue: cache)
} else {
let artifacts = render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
damus_state.events.store_artifacts(evid: event.id, artifacts: artifacts)
self._artifacts = State(initialValue: artifacts)
}
}
var truncate: Bool {
@@ -56,8 +60,15 @@ struct NoteContentView: View {
}
var truncatedText: some View {
TruncatedText(text: artifacts.content, maxChars: (truncate ? 280 : nil))
.font(eventviewsize_to_font(size))
Group {
if truncate {
TruncatedText(text: artifacts.content)
.font(eventviewsize_to_font(size))
} else {
artifacts.content.text
.font(eventviewsize_to_font(size))
}
}
}
var invoicesView: some View {
@@ -88,10 +99,10 @@ struct NoteContentView: View {
VStack(alignment: .leading) {
if size == .selected {
if with_padding {
SelectableText(attributedString: artifacts.content, size: self.size)
SelectableText(attributedString: artifacts.content.attributed, size: self.size)
.padding(.horizontal)
} else {
SelectableText(attributedString: artifacts.content, size: self.size)
SelectableText(attributedString: artifacts.content.attributed, size: self.size)
}
} else {
if with_padding {
@@ -102,7 +113,7 @@ struct NoteContentView: View {
}
}
if translatable {
if !options.contains(.no_translate) && (size == .selected || damus_state.settings.auto_translate) {
if with_padding {
translateView
.padding(.horizontal)
@@ -151,6 +162,7 @@ struct NoteContentView: View {
if m.type == .pubkey && m.ref.ref_id == profile.pubkey {
self.artifacts = render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
}
case .relay: return
case .text: return
case .hashtag: return
case .url: return
@@ -194,21 +206,28 @@ struct NoteContentView: View {
}
}
func hashtag_str(_ htag: String) -> AttributedString {
var attributedString = AttributedString(stringLiteral: "#\(htag)")
attributedString.link = URL(string: "damus:t:\(htag)")
attributedString.foregroundColor = DamusColors.purple
return attributedString
}
enum ImageName {
case systemImage(String)
case image(String)
}
func url_str(_ url: URL) -> AttributedString {
func attributed_string_attach_icon(_ astr: inout AttributedString, img: UIImage) {
let attachment = NSTextAttachment()
attachment.image = img
let attachmentString = NSAttributedString(attachment: attachment)
let wrapped = AttributedString(attachmentString)
astr.append(wrapped)
}
func url_str(_ url: URL) -> CompatibleText {
var attributedString = AttributedString(stringLiteral: url.absoluteString)
attributedString.link = url
attributedString.foregroundColor = DamusColors.purple
return attributedString
return CompatibleText(attributed: attributedString)
}
func mention_str(_ m: Mention, profiles: Profiles) -> AttributedString {
func mention_str(_ m: Mention, profiles: Profiles) -> CompatibleText {
switch m.type {
case .pubkey:
let pk = m.ref.ref_id
@@ -217,13 +236,15 @@ func mention_str(_ m: Mention, profiles: Profiles) -> AttributedString {
var attributedString = AttributedString(stringLiteral: "@\(disp)")
attributedString.link = URL(string: "damus:\(encode_pubkey_uri(m.ref))")
attributedString.foregroundColor = DamusColors.purple
return attributedString
return CompatibleText(attributed: attributedString)
case .event:
let bevid = bech32_note_id(m.ref.ref_id) ?? m.ref.ref_id
var attributedString = AttributedString(stringLiteral: "@\(abbrev_pubkey(bevid))")
attributedString.link = URL(string: "damus:\(encode_event_id_uri(m.ref))")
attributedString.foregroundColor = DamusColors.purple
return attributedString
return CompatibleText(attributed: attributedString)
}
}
@@ -231,19 +252,25 @@ struct NoteContentView_Previews: PreviewProvider {
static var previews: some View {
let state = test_damus_state()
let content = "hi there ¯\\_(ツ)_/¯ https://jb55.com/s/Oct12-150217.png 5739a762ef6124dd.jpg"
let artifacts = NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: [])
let txt = CompatibleText(attributed: AttributedString(stringLiteral: content))
let artifacts = NoteArtifacts(content: txt, images: [], invoices: [], links: [])
NoteContentView(damus_state: state, event: NostrEvent(content: content, pubkey: "pk"), show_images: true, size: .normal, artifacts: artifacts, options: [])
}
}
struct NoteArtifacts {
let content: AttributedString
struct NoteArtifacts: Equatable {
static func == (lhs: NoteArtifacts, rhs: NoteArtifacts) -> Bool {
return lhs.content == rhs.content
}
let content: CompatibleText
let images: [URL]
let invoices: [Invoice]
let links: [URL]
static func just_content(_ content: String) -> NoteArtifacts {
NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: [])
let txt = CompatibleText(attributed: AttributedString(stringLiteral: content))
return NoteArtifacts(content: txt, images: [], invoices: [], links: [])
}
}
@@ -263,7 +290,7 @@ func render_blocks(blocks: [Block], profiles: Profiles, privkey: String?) -> Not
.count == 1
var ind: Int = -1
let txt: AttributedString = blocks.reduce("") { str, block in
let txt: CompatibleText = blocks.reduce(CompatibleText()) { str, block in
ind = ind + 1
switch block {
@@ -277,10 +304,19 @@ func render_blocks(blocks: [Block], profiles: Profiles, privkey: String?) -> Not
if let prev = blocks[safe: ind-1], case .url(let u) = prev, is_image_url(u) {
trimmed = " " + trim_prefix(trimmed)
}
if let next = blocks[safe: ind+1], case .url(let u) = next, is_image_url(u) {
trimmed = trim_suffix(trimmed)
if let next = blocks[safe: ind+1] {
if case .url(let u) = next, is_image_url(u) {
trimmed = trim_suffix(trimmed)
} else if case .mention(let m) = next, m.type == .event, one_note_ref {
trimmed = trim_suffix(trimmed)
}
}
return str + AttributedString(stringLiteral: trimmed)
return str + CompatibleText(stringLiteral: trimmed)
case .relay(let relay):
return str + CompatibleText(stringLiteral: relay)
case .hashtag(let htag):
return str + hashtag_str(htag)
case .invoice(let invoice):
@@ -329,36 +365,6 @@ func load_cached_preview(previews: PreviewCache, evid: String) -> LinkViewRepres
return LinkViewRepresentable(meta: .linkmeta(meta))
}
struct TruncatedText: View {
let text: AttributedString
let maxChars: Int?
var body: some View {
let truncatedAttributedString: AttributedString? = getTruncatedString()
Text(truncatedAttributedString ?? text)
.fixedSize(horizontal: false, vertical: true)
if truncatedAttributedString != nil {
Spacer()
Button(NSLocalizedString("Show more", comment: "Button to show entire note.")) { }
.allowsHitTesting(false)
}
}
func getTruncatedString() -> AttributedString? {
guard let maxChars = maxChars else { return nil }
let nsAttributedString = NSAttributedString(text)
if nsAttributedString.length < maxChars { return nil }
let range = NSRange(location: 0, length: maxChars)
let truncatedAttributedString = nsAttributedString.attributedSubstring(from: range)
return AttributedString(truncatedAttributedString) + "..."
}
}
// trim suffix whitespace and newlines
func trim_suffix(_ str: String) -> String {
@@ -12,6 +12,10 @@ enum NotificationFilterState: String {
case zaps
case replies
func is_other( item: NotificationItem) -> Bool {
return item.is_zap == nil && item.is_reply == nil
}
func filter(_ item: NotificationItem) -> Bool {
switch self {
case .all:
@@ -27,33 +31,38 @@ enum NotificationFilterState: String {
struct NotificationsView: View {
let state: DamusState
@ObservedObject var notifications: NotificationsModel
@State var filter_state: NotificationFilterState
@State var filter_state: NotificationFilterState = .all
@Environment(\.colorScheme) var colorScheme
init(state: DamusState, notifications: NotificationsModel) {
self.state = state
self._notifications = ObservedObject(initialValue: notifications)
self._filter_state = State(initialValue: load_notification_filter_state(pubkey: state.pubkey))
var mystery: some View {
VStack(spacing: 20) {
Text("Wake up, \(Profile.displayName(profile: state.profiles.lookup(id: state.pubkey), pubkey: state.pubkey).display_name)", comment: "Text telling the user to wake up, where the argument is their display name.")
Text("You are dreaming...", comment: "Text telling the user that they are dreaming.")
}
.id("what")
}
var body: some View {
TabView(selection: $filter_state) {
mystery
// This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why.
NotificationTab(NotificationFilterState.all)
.tag(NotificationFilterState.all)
.id(NotificationFilterState.all)
NotificationTab(NotificationFilterState.zaps)
.tag(NotificationFilterState.zaps)
.id(NotificationFilterState.zaps)
NotificationTab(NotificationFilterState.replies)
.tag(NotificationFilterState.replies)
.id(NotificationFilterState.replies)
}
.onChange(of: filter_state) { val in
save_notification_filter_state(pubkey: state.pubkey, state: val)
}
.onAppear {
self.filter_state = load_notification_filter_state(pubkey: state.pubkey)
}
.safeAreaInset(edge: .top, spacing: 0) {
VStack(spacing: 0) {
CustomPicker(selection: $filter_state, content: {
@@ -94,20 +103,20 @@ struct NotificationsView: View {
}
.coordinateSpace(name: "scroll")
.onReceive(handle_notify(.scroll_to_top)) { notif in
let _ = notifications.flush()
let _ = notifications.flush(state)
self.notifications.should_queue = false
scroll_to_event(scroller: scroller, id: "startblock", delay: 0.0, animate: true, anchor: .top)
}
}
.onAppear {
let _ = notifications.flush()
let _ = notifications.flush(state)
}
}
}
struct NotificationsView_Previews: PreviewProvider {
static var previews: some View {
NotificationsView(state: test_damus_state(), notifications: NotificationsModel())
NotificationsView(state: test_damus_state(), notifications: NotificationsModel(), filter_state: NotificationFilterState.all)
}
}
+124 -30
View File
@@ -6,6 +6,7 @@
//
import SwiftUI
import AVFoundation
enum NostrPostResult {
case post(NostrPost)
@@ -21,9 +22,12 @@ struct PostView: View {
@State var attach_media: Bool = false
@State var attach_camera: Bool = false
@State var error: String? = nil
@State var uploadedMedias: [UploadedMedia] = []
@State var image_upload_confirm: Bool = false
@State var originalReferences: [ReferencedId] = []
@State var references: [ReferencedId] = []
@State var mediaToUpload: MediaUpload? = nil
@StateObject var image_upload: ImageUploadModel = ImageUploadModel()
@@ -57,7 +61,14 @@ struct PostView: View {
}
}
let content = self.post.string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
var content = self.post.string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
let imagesString = uploadedMedias.map { $0.uploadedURL.absoluteString }.joined(separator: " ")
content.append(" " + imagesString + " ")
let new_post = NostrPost(content: content, references: references, kind: kind)
NotificationCenter.default.post(name: .post, object: NostrPostResult.post(new_post))
@@ -66,13 +77,15 @@ struct PostView: View {
damus_state.drafts.replies.removeValue(forKey: replying_to)
} else {
damus_state.drafts.post = NSMutableAttributedString(string: "")
uploadedMedias = []
damus_state.drafts.medias = []
}
dismiss()
}
var is_post_empty: Bool {
return post.string.allSatisfy { $0.isWhitespace }
return post.string.allSatisfy { $0.isWhitespace } && uploadedMedias.isEmpty
}
var ImageButton: some View {
@@ -165,32 +178,23 @@ struct PostView: View {
}
}
.frame(height: 30)
.padding([.bottom], 10)
}
func append_url(_ url: String) {
let uploadedImageURL = NSMutableAttributedString(string: url)
let combinedAttributedString = NSMutableAttributedString()
combinedAttributedString.append(post)
if !post.string.hasSuffix(" ") {
combinedAttributedString.append(NSAttributedString(string: " "))
}
combinedAttributedString.append(uploadedImageURL)
// make sure we have a space at the end
combinedAttributedString.append(NSAttributedString(string: " "))
post = combinedAttributedString
.padding()
}
func handle_upload(media: MediaUpload) {
let uploader = get_media_uploader(damus_state.pubkey)
Task.init {
let img = getImage(media: media)
let res = await image_upload.start(media: media, uploader: uploader)
switch res {
case .success(let url):
append_url(url)
guard let url = URL(string: url) else {
self.error = "Error uploading image :("
return
}
let uploadedMedia = UploadedMedia(localURL: media.localURL, uploadedURL: url, representingImage: img)
uploadedMedias.append(uploadedMedia)
case .failed(let error):
if let error {
@@ -206,7 +210,7 @@ struct PostView: View {
var body: some View {
GeometryReader { (deviceSize: GeometryProxy) in
VStack(alignment: .leading, spacing: 0) {
let searching = get_searching_string(post.string)
TopBar
@@ -219,13 +223,19 @@ struct PostView: View {
VStack(alignment: .leading, spacing: 0) {
HStack(alignment: .top) {
ProfilePicView(pubkey: damus_state.pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
.padding(.leading, replying_to != nil ? 15 : 0)
TextEntry
}
.frame(height: deviceSize.size.height*0.78)
.frame(height: uploadedMedias.isEmpty ? deviceSize.size.height*0.78 : deviceSize.size.height*0.2)
.id("post")
PVImageCarouselView(media: $uploadedMedias, deviceWidth: deviceSize.size.width)
.onChange(of: uploadedMedias) { _ in
damus_state.drafts.medias = uploadedMedias
}
}
.padding(.horizontal)
}
.frame(maxHeight: searching == nil ? .infinity : 70)
.onAppear {
@@ -236,26 +246,35 @@ struct PostView: View {
// This if-block observes @ for tagging
if let searching {
UserSearch(damus_state: damus_state, search: searching, post: $post)
.padding(.leading, replying_to != nil ? 15 : 0)
.frame(maxHeight: .infinity)
} else {
Divider()
.padding([.bottom], 10)
VStack(alignment: .leading) {
AttachmentBar
.padding(.vertical, 5)
.padding(.horizontal)
}
}
}
.padding()
.sheet(isPresented: $attach_media) {
ImagePicker(sourceType: .photoLibrary, pubkey: damus_state.pubkey) { img in
handle_upload(media: .image(img))
ImagePicker(sourceType: .photoLibrary, pubkey: damus_state.pubkey, image_upload_confirm: $image_upload_confirm) { img in
self.mediaToUpload = .image(img)
} onVideoPicked: { url in
handle_upload(media: .video(url))
self.mediaToUpload = .video(url)
}
.alert("Are you sure you want to upload this image?", isPresented: $image_upload_confirm) {
Button(NSLocalizedString("Upload", comment: "Button to proceed with uploading."), role: .none) {
if let mediaToUpload {
self.handle_upload(media: mediaToUpload)
self.attach_media = false
}
}
Button(NSLocalizedString("Cancel", comment: "Button to cancel the upload."), role: .cancel) {}
}
}
.sheet(isPresented: $attach_camera) {
ImagePicker(sourceType: .camera, pubkey: damus_state.pubkey) { img in
// image_upload_confirm isn't handled here, I don't know we need to display it here too tbh
ImagePicker(sourceType: .camera, pubkey: damus_state.pubkey, image_upload_confirm: $image_upload_confirm) { img in
handle_upload(media: .image(img))
} onVideoPicked: { url in
handle_upload(media: .video(url))
@@ -273,6 +292,7 @@ struct PostView: View {
}
} else {
post = damus_state.drafts.post
uploadedMedias = damus_state.drafts.medias
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
@@ -284,6 +304,7 @@ struct PostView: View {
damus_state.drafts.replies.removeValue(forKey: replying_to)
} else if replying_to == nil && damus_state.drafts.post.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
damus_state.drafts.post = NSMutableAttributedString(string : "")
damus_state.drafts.medias = uploadedMedias
}
}
.alert(NSLocalizedString("Note contains \"nsec1\" private key. Are you sure?", comment: "Alert user that they might be attempting to paste a private key and ask them to confirm."), isPresented: $showPrivateKeyWarning, actions: {
@@ -324,3 +345,76 @@ struct PostView_Previews: PreviewProvider {
PostView(replying_to: nil, damus_state: test_damus_state())
}
}
struct PVImageCarouselView: View {
@Binding var media: [UploadedMedia]
let deviceWidth: CGFloat
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(media.map({$0.representingImage}), id: \.self) { image in
ZStack(alignment: .topTrailing) {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: media.count == 1 ? deviceWidth*0.8 : 250, height: media.count == 1 ? 400 : 250)
.cornerRadius(10)
.padding()
Image(systemName: "xmark.circle.fill")
.foregroundColor(.white)
.padding(20)
.onTapGesture {
if let index = media.map({$0.representingImage}).firstIndex(of: image) {
media.remove(at: index)
}
}
}
}
}
.padding()
}
}
}
fileprivate func getImage(media: MediaUpload) -> UIImage {
var uiimage: UIImage = UIImage()
if media.is_image {
// fetch the image data
if let data = try? Data(contentsOf: media.localURL) {
uiimage = UIImage(data: data) ?? UIImage()
}
} else {
let asset = AVURLAsset(url: media.localURL)
let generator = AVAssetImageGenerator(asset: asset)
generator.appliesPreferredTrackTransform = true
let time = CMTimeMake(value: 1, timescale: 60) // get the thumbnail image at the 1st second
do {
let cgImage = try generator.copyCGImage(at: time, actualTime: nil)
uiimage = UIImage(cgImage: cgImage)
} catch {
print("No thumbnail: \(error)")
}
// create a play icon on the top to differentiate if media upload is image or a video, gif is an image
let playIcon = UIImage(systemName: "play.fill")?.withTintColor(.white, renderingMode: .alwaysOriginal)
let size = uiimage.size
let scale = UIScreen.main.scale
UIGraphicsBeginImageContextWithOptions(size, false, scale)
uiimage.draw(at: .zero)
let playIconSize = CGSize(width: 60, height: 60)
let playIconOrigin = CGPoint(x: (size.width - playIconSize.width) / 2, y: (size.height - playIconSize.height) / 2)
playIcon?.draw(in: CGRect(origin: playIconOrigin, size: playIconSize))
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
uiimage = newImage ?? UIImage()
}
return uiimage
}
struct UploadedMedia: Equatable {
let localURL: URL
let uploadedURL: URL
let representingImage: UIImage
}
+15 -11
View File
@@ -72,19 +72,23 @@ struct UserSearch: View {
}
var body: some View {
ScrollView {
LazyVStack {
Divider()
if users.count == 0 {
EmptyUserSearchView()
} else {
ForEach(users) { user in
UserView(damus_state: damus_state, pubkey: user.pubkey)
.onTapGesture {
on_user_tapped(user: user)
}
VStack(spacing: 0) {
Divider()
ScrollView {
LazyVStack {
if users.count == 0 {
EmptyUserSearchView()
} else {
ForEach(users) { user in
UserView(damus_state: damus_state, pubkey: user.pubkey)
.contentShape(Rectangle())
.onTapGesture {
on_user_tapped(user: user)
}
}
}
}
.padding()
}
}
}
@@ -18,7 +18,8 @@ struct EditProfilePictureControl: View {
@State private var show_camera = false
@State private var show_library = false
@State var image_upload_confirm: Bool = false
var body: some View {
Menu {
Button(action: {
@@ -44,14 +45,16 @@ struct EditProfilePictureControl: View {
}
}
.sheet(isPresented: $show_camera) {
ImagePicker(sourceType: .camera, pubkey: pubkey, imagesOnly: true) { img in
// The alert may not be required for the profile pic upload case. Not showing the confirm check alert for this scenario
ImagePicker(sourceType: .camera, pubkey: pubkey, image_upload_confirm: $image_upload_confirm, imagesOnly: true) { img in
handle_upload(media: .image(img))
} onVideoPicked: { url in
print("Cannot upload videos as profile image")
}
}
.sheet(isPresented: $show_library) {
ImagePicker(sourceType: .photoLibrary, pubkey: pubkey, imagesOnly: true) { img in
// The alert may not be required for the profile pic upload case. Not showing the confirm check alert for this scenario
ImagePicker(sourceType: .photoLibrary, pubkey: pubkey, image_upload_confirm: $image_upload_confirm, imagesOnly: true) { img in
handle_upload(media: .image(img))
} onVideoPicked: { url in
print("Cannot upload videos as profile image")
+9 -2
View File
@@ -119,6 +119,7 @@ struct ProfileView: View {
@State var showing_select_wallet: Bool = false
@State var is_zoomed: Bool = false
@State var show_share_sheet: Bool = false
@State var show_qr_code: Bool = false
@State var action_sheet_presented: Bool = false
@State var filter_state : FilterState = .posts
@State var yOffset: CGFloat = 0
@@ -213,6 +214,10 @@ struct ProfileView: View {
Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) {
show_share_sheet = true
}
Button(NSLocalizedString("QR Code", comment: "Button to view profile's qr code.")) {
show_qr_code = true
}
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
if profile.pubkey != damus_state.pubkey && damus_state.is_privkey_user {
@@ -266,8 +271,7 @@ struct ProfileView: View {
var dmButton: some View {
let dm_model = damus_state.dms.lookup_or_create(profile.pubkey)
let dmview = DMChatView(damus_state: damus_state, pubkey: profile.pubkey)
.environmentObject(dm_model)
let dmview = DMChatView(damus_state: damus_state, dms: dm_model)
return NavigationLink(destination: dmview) {
Image(systemName: "bubble.left.circle")
.profile_button_style(scheme: colorScheme)
@@ -465,6 +469,9 @@ struct ProfileView: View {
}
}
}
.fullScreenCover(isPresented: $show_qr_code) {
QRCodeView(damus_state: damus_state, pubkey: profile.pubkey)
}
}
}
+7 -5
View File
@@ -10,12 +10,13 @@ import CoreImage.CIFilterBuiltins
struct QRCodeView: View {
let damus_state: DamusState
@State var pubkey: String
@Environment(\.dismiss) var dismiss
@Environment(\.presentationMode) var presentationMode
var maybe_key: String? {
guard let key = bech32_pubkey(damus_state.pubkey) else {
guard let key = bech32_pubkey(pubkey) else {
return nil
}
@@ -39,10 +40,11 @@ struct QRCodeView: View {
}
VStack(alignment: .center) {
let profile = damus_state.profiles.lookup(id: damus_state.pubkey)
if (damus_state.profiles.lookup(id: damus_state.pubkey)?.picture) != nil {
ProfilePicView(pubkey: damus_state.pubkey, size: 90.0, highlight: .custom(DamusColors.white, 4.0), profiles: damus_state.profiles)
let profile = damus_state.profiles.lookup(id: pubkey)
if (damus_state.profiles.lookup(id: pubkey)?.picture) != nil {
ProfilePicView(pubkey: pubkey, size: 90.0, highlight: .custom(DamusColors.white, 4.0), profiles: damus_state.profiles)
.padding(.top, 50)
} else {
Image(systemName: "person.fill")
@@ -119,6 +121,6 @@ struct QRCodeView: View {
struct QRCodeView_Previews: PreviewProvider {
static var previews: some View {
QRCodeView(damus_state: test_damus_state())
QRCodeView(damus_state: test_damus_state(), pubkey: test_event.pubkey)
}
}
+1 -1
View File
@@ -70,7 +70,7 @@ struct RelayDetailView: View {
if let pubkey = nip11.pubkey {
Section(NSLocalizedString("Admin", comment: "Label to display relay contact user.")) {
UserView(damus_state: state, pubkey: pubkey)
UserViewRow(damus_state: state, pubkey: pubkey)
}
}
Section(NSLocalizedString("Relay", comment: "Label to display relay address.")) {
+34
View File
@@ -0,0 +1,34 @@
//
// SignalView.swift
// damus
//
// Created by William Casarin on 2023-04-14.
//
import SwiftUI
struct SignalView: View {
let state: DamusState
@ObservedObject var signal: SignalModel
var body: some View {
Group {
if signal.signal != signal.max_signal {
NavigationLink(destination: RelayConfigView(state: state)) {
Text("\(signal.signal)/\(signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
.font(.callout)
.foregroundColor(.gray)
}
} else {
Text("")
}
}
}
}
struct SignalView_Previews: PreviewProvider {
static var previews: some View {
SignalView(state: test_damus_state(), signal: SignalModel(signal: 5, max_signal: 10))
}
}
+8 -7
View File
@@ -15,6 +15,8 @@ struct SearchHomeView: View {
@State var search: String = ""
@FocusState private var isFocused: Bool
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
var SearchInput: some View {
HStack {
HStack{
@@ -48,21 +50,20 @@ struct SearchHomeView: View {
damus: damus_state,
show_friend_icon: true,
filter: {
if damus_state.muted_threads.isMutedThread($0, privkey: self.damus_state.keypair.privkey) {
return false
}
if damus_state.settings.show_only_preferred_languages == false {
return true
}
// Always show your own posts.
if $0.pubkey == damus_state.pubkey {
return true
}
// If we can't determine the note's language with 50%+ confidence, lean on the side of caution and show it anyway.
guard let noteLanguage = damus_state.translations.detectLanguage($0, state: damus_state) else {
guard let noteLanguage = $0.note_language(damus_state.keypair.privkey) else {
return true
}
return damus_state.translations.preferredLanguages.contains(noteLanguage)
return preferredLanguages.contains(noteLanguage)
}
)
.refreshable {
@@ -11,7 +11,19 @@ struct NotificationSettingsView: View {
@ObservedObject var settings: UserSettingsStore
@Environment(\.dismiss) var dismiss
func indicator_binding(_ val: NewEventsBits) -> Binding<Bool> {
return Binding.init(get: {
(settings.notification_indicators & val.rawValue) > 0
}, set: { v in
if v {
settings.notification_indicators |= val.rawValue
} else {
settings.notification_indicators &= ~val.rawValue
}
})
}
var body: some View {
Form {
Section(header: Text(NSLocalizedString("Local Notifications", comment: "Section header for damus local notifications user configuration"))) {
@@ -31,6 +43,17 @@ struct NotificationSettingsView: View {
Toggle(NSLocalizedString("Show only from users you follow", comment: "Setting to Show notifications only associated to users your follow"), isOn: $settings.notification_only_from_following)
.toggleStyle(.switch)
}
Section(header: Text(NSLocalizedString("Notification Dots", comment: "Section header for notification indicator dot settings"))) {
Toggle(NSLocalizedString("Zaps", comment: "Setting to enable Zap Local Notification"), isOn: indicator_binding(.zaps))
.toggleStyle(.switch)
Toggle(NSLocalizedString("Mentions", comment: "Setting to enable Mention Local Notification"), isOn: indicator_binding(.mentions))
.toggleStyle(.switch)
Toggle(NSLocalizedString("Reposts", comment: "Setting to enable Repost Local Notification"), isOn: indicator_binding(.reposts))
.toggleStyle(.switch)
Toggle(NSLocalizedString("Likes", comment: "Setting to enable Like Local Notification"), isOn: indicator_binding(.likes))
.toggleStyle(.switch)
}
}
.navigationTitle("Notifications")
.onReceive(handle_notify(.switched_timeline)) { _ in
@@ -69,6 +69,11 @@ struct TranslationSettingsView: View {
Toggle(NSLocalizedString("Automatically translate notes", comment: "Toggle to automatically translate notes."), isOn: $settings.auto_translate)
.toggleStyle(.switch)
}
if settings.translation_service != .none {
Toggle(NSLocalizedString("Translate DMs", comment: "Toggle to translate direct messages."), isOn: $settings.translate_dms)
.toggleStyle(.switch)
}
}
}
.navigationTitle("Translation")
+1 -1
View File
@@ -142,7 +142,7 @@ struct SideMenuView: View {
.font(.title)
.foregroundColor(textColor())
}).fullScreenCover(isPresented: $showQRCode) {
QRCodeView(damus_state: damus_state)
QRCodeView(damus_state: damus_state, pubkey: damus_state.pubkey)
}
}
.padding(.top, verticalSpacing)
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -39,7 +39,7 @@
<key>many</key>
<string>Followers</string>
<key>other</key>
<string>Sledují</string>
<string>Sledují</string>
</dict>
</dict>
<key>following_count</key>
+10
View File
@@ -9,6 +9,16 @@
<string>applinks:damus.io</string>
<string>webcredentials:damus.io</string>
</array>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.personal-information.photos-library</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.jb55.damus2</string>
+7
View File
@@ -55,6 +55,13 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
// Display the notification in the foreground
completionHandler([.banner, .list, .sound, .badge])
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
let notification = LossyLocalNotification.from_user_info(user_info: userInfo)
notify(.local_notification, notification)
completionHandler()
}
}
func needs_setup() -> Keypair? {
Binary file not shown.
+7 -7
View File
@@ -61,7 +61,7 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ und %1$d andere:r reagierten auf einen Beitrag in dem Du markiert warst</string>
<string>%2$@ und %1$d andere*r reagierten auf einen Beitrag in dem Du markiert warst</string>
<key>other</key>
<string>%2$@ und %1$d andere reagierten auf einen Beitrag in dem Du markiert warst</string>
</dict>
@@ -77,7 +77,7 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ und %1$d andere:r reagierten auf deinen Beitrag</string>
<string>%2$@ und %1$d andere*r reagierten auf deinen Beitrag</string>
<key>other</key>
<string>%2$@ und %1$d andere reagierten auf deinen Beitrag</string>
</dict>
@@ -93,7 +93,7 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ und %1$d andere:r reagierten auf dein Profil</string>
<string>%2$@ und %1$d andere*r reagierten auf dein Profil</string>
<key>other</key>
<string>%2$@ und %1$d andere reagierten auf dein Profil</string>
</dict>
@@ -141,7 +141,7 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Antwort an %2$@, %3$@ &amp; %1$d andere:r</string>
<string>Antwort an %2$@, %3$@ &amp; %1$d andere*r</string>
<key>other</key>
<string>Antwort an %2$@, %3$@ &amp; %1$d andere</string>
</dict>
@@ -157,7 +157,7 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ und %1$d andere:r teilten einen Beitrag in dem Du markiert warst</string>
<string>%2$@ und %1$d andere*r teilten einen Beitrag in dem Du markiert warst</string>
<key>other</key>
<string>%2$@ und %1$d andere teilten ein Beitrag in dem Du markiert warst</string>
</dict>
@@ -173,7 +173,7 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ und %1$d andere:r teilten deinen Beitrag</string>
<string>%2$@ und %1$d andere*r teilten deinen Beitrag</string>
<key>other</key>
<string>%2$@ und %1$d andere teilten deinen Beitrag</string>
</dict>
@@ -189,7 +189,7 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ und %1$d andere:r teilten dein Profil</string>
<string>%2$@ und %1$d andere*r teilten dein Profil</string>
<key>other</key>
<string>%2$@ und %1$d andere teilten dein Profil</string>
</dict>
Binary file not shown.

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