Compare commits

..

75 Commits

Author SHA1 Message Date
tyiu 8d7df8f5f8 Fix typo on ParticipantsView.swift file name
Changelog-Fixed: Fix typo on ParticipantsView.swift file name
2023-04-17 03:18:49 +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
65 changed files with 697 additions and 662 deletions
+46
View File
@@ -1,3 +1,48 @@
## [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
@@ -987,3 +1032,4 @@
[0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2
+4 -5
View File
@@ -10,7 +10,6 @@
#include <ctype.h>
#include <string.h>
#include "bech32.h"
typedef unsigned char u8;
@@ -32,8 +31,8 @@ static inline int is_invalid_url_ending(char c) {
return c == '!' || c == '?' || c == ')' || c == '.' || c == ',' || c == ';';
}
static inline int is_bech32_character(char c) {
return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || bech32_charset_rev[c] != -1;
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)
@@ -75,14 +74,14 @@ static inline int consume_until_whitespace(struct cursor *cur, int or_end) {
return or_end;
}
static inline int consume_until_non_bech32_character(struct cursor *cur, int 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_bech32_character(c))
if (!is_alphanumeric(c))
return consumedAtLeastOne;
cur->p++;
+1 -1
View File
@@ -222,7 +222,7 @@ int parse_nostr_bech32(struct cursor *cur, struct nostr_bech32 *obj) {
start = cur->p;
if (!consume_until_non_bech32_character(cur, 1)) {
if (!consume_until_non_alphanumeric(cur, 1)) {
cur->p = start;
return 0;
}
+27 -20
View File
@@ -192,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 */; };
@@ -276,7 +278,7 @@
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */; };
F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */; };
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */; };
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParicipantsView.swift */; };
F7F0BA272978E54D009531F3 /* ParticipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParticipantsView.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -330,9 +332,6 @@
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>"; };
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutedThreadsManager.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>"; };
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>"; };
@@ -340,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>"; };
@@ -386,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>"; };
@@ -601,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>"; };
@@ -687,7 +688,7 @@
F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIPURLBuilder.swift; sourceTree = "<group>"; };
F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfilePictureControl.swift; sourceTree = "<group>"; };
F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToDismiss.swift; sourceTree = "<group>"; };
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParicipantsView.swift; sourceTree = "<group>"; };
F7F0BA262978E54D009531F3 /* ParticipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantsView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -925,7 +926,7 @@
4C3AC7A42836987600E1F516 /* MainTabView.swift */,
4C363A8B28236B92006E126D /* PubkeyView.swift */,
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */,
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */,
F7F0BA262978E54D009531F3 /* ParticipantsView.swift */,
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */,
4C3AC7A628369BA200E1F516 /* SearchHomeView.swift */,
4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */,
@@ -1007,6 +1008,7 @@
4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */,
4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */,
4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */,
4CDA128B29EB19C40006FA5A /* LocalNotification.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -1022,6 +1024,7 @@
4CE8794D2996B16A00F758CC /* RelayToggle.swift */,
4CE8794F2996B2BD00F758CC /* RelayStatus.swift */,
4CE879512996B68900F758CC /* RelayType.swift */,
4CDA128929E9D10C0006FA5A /* SignalView.swift */,
);
path = Relays;
sourceTree = "<group>";
@@ -1406,8 +1409,7 @@
"es-419",
"es-ES",
fa,
"fr-CA",
"fr-FR",
fr,
"hu-HU",
id,
"it-IT",
@@ -1495,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 */,
@@ -1526,7 +1529,7 @@
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */,
4C3D52B6298DB4E6001C5831 /* ZapEvent.swift in Sources */,
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */,
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */,
F7F0BA272978E54D009531F3 /* ParticipantsView.swift in Sources */,
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */,
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */,
@@ -1650,6 +1653,7 @@
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 */,
@@ -1779,7 +1783,6 @@
3A5C4575296A879E0032D398 /* es-419 */,
3A2B8B0A296A8982009CC16D /* en-US */,
3AEB8005297CCEA900713A25 /* tr-TR */,
3A4F3322297CCFEE004B5F72 /* fr-FR */,
3A185A06297F2C3800F4BDC0 /* lv-LV */,
3A929C22297F2CF80090925E /* it-IT */,
3AB5B86C2986D8A3006599D2 /* de */,
@@ -1801,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>";
@@ -1814,7 +1817,6 @@
children = (
3ACB685B297633BC00C46468 /* es-419 */,
3AEB8003297CCEA800713A25 /* tr-TR */,
3A4F3320297CCFEE004B5F72 /* fr-FR */,
3A185A04297F2C3800F4BDC0 /* lv-LV */,
3A929C20297F2CF80090925E /* it-IT */,
3AB5B86A2986D8A3006599D2 /* de */,
@@ -1836,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>";
@@ -1849,7 +1851,6 @@
children = (
3ACB685E297633BC00C46468 /* es-419 */,
3AEB8004297CCEA800713A25 /* tr-TR */,
3A4F3321297CCFEE004B5F72 /* fr-FR */,
3A185A05297F2C3800F4BDC0 /* lv-LV */,
3A929C21297F2CF80090925E /* it-IT */,
3AB5B86B2986D8A3006599D2 /* de */,
@@ -1872,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>";
@@ -2036,9 +2037,12 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.4.2;
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";
@@ -2080,9 +2084,12 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.4.2;
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;
+1 -2
View File
@@ -37,8 +37,6 @@ struct CustomPicker<SelectionValue: Hashable, Content: View>: View {
text
.padding(EdgeInsets(top: 15, leading: 0, bottom: 10, trailing: 0))
.font(.system(size: 14, weight: .heavy))
.contentShape(Rectangle())
.frame(maxWidth: .infinity)
}
.background(
Group {
@@ -50,6 +48,7 @@ struct CustomPicker<SelectionValue: Hashable, Content: View>: View {
},
alignment: .bottom
)
.frame(maxWidth: .infinity)
.accentColor(tag == selection ? textColor() : .gray)
}
}
+23 -1
View File
@@ -7,11 +7,34 @@
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 {
VStack {
HStack {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
@@ -28,7 +51,6 @@ struct UserView: View {
Spacer()
}
Spacer()
}
}
}
+41 -22
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,14 +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.
Text("")
.id("what")
mystery
contentTimelineView(filter: FilterState.posts.filter)
.tag(FilterState.posts)
.id(FilterState.posts)
@@ -179,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()
@@ -243,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 {
@@ -263,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 {
@@ -338,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
@@ -354,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)
@@ -477,6 +473,29 @@ struct ContentView: View {
.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
@@ -570,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: {
@@ -640,7 +659,7 @@ struct ContentView: View {
postbox: PostBox(pool: pool),
bootstrap_relays: bootstrap_relays,
replies: ReplyCounter(our_pubkey: pubkey),
muted_threads: MutedThreadsManager(pubkey: pubkey)
muted_threads: MutedThreadsManager(keypair: keypair)
)
home.damus_state = self.damus_state!
+1 -2
View File
@@ -40,6 +40,5 @@ struct DamusState {
}
static var empty: DamusState {
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(pubkey: ""))
}
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), 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
}
}
+1
View File
@@ -10,4 +10,5 @@ import Foundation
class Drafts: ObservableObject {
@Published var post: NSMutableAttributedString = NSMutableAttributedString(string: "")
@Published var replies: [NostrEvent: NSMutableAttributedString] = [:]
@Published var medias: [UploadedMedia] = []
}
+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)")
+126 -82
View File
@@ -41,36 +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: "")
filter_muted()
self.setup_debouncer()
}
init(damus_state: DamusState) {
self.damus_state = damus_state
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
self.setup_debouncer()
filter_muted()
}
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 {
@@ -81,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) {
@@ -147,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 ?? "")
}
}
@@ -188,10 +186,6 @@ class HomeModel: ObservableObject {
}
func handle_channel_create(_ ev: NostrEvent) {
guard ev.is_valid else {
return
}
self.channels[ev.id] = ev
}
@@ -199,16 +193,21 @@ class HomeModel: ObservableObject {
}
func filter_muted() {
events.filter { !damus_state.contacts.is_muted($0.pubkey) && !damus_state.muted_threads.isMutedThread($0) }
self.dms.dms = dms.dms.filter { !damus_state.contacts.is_muted($0.0) }
notifications.filter_and_build_notifications(damus_state)
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)
}
@@ -230,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
}
@@ -299,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):
@@ -321,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 {
@@ -449,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? {
@@ -471,11 +467,12 @@ 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)
}
@@ -522,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
@@ -540,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 = []
}
@@ -557,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
}
}
@@ -652,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, ())
@@ -707,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 {
@@ -847,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
}
@@ -860,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
}
@@ -888,8 +929,8 @@ 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
}
}
@@ -1009,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)
@@ -1042,57 +1084,59 @@ func process_local_notification(damus_state: DamusState, event ev: NostrEvent) {
}
// Don't show notifications from muted threads.
if damus_state.muted_threads.isMutedThread(ev) {
if damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey) {
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.attributed).string
create_local_notification(displayName: displayName, conversation: justContent, type: type)
}
}
} else if type == .boost && damus_state.settings.repost_notification,
let displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name {
if let inner_ev = ev.inner_event {
create_local_notification(displayName: displayName, conversation: inner_ev.content, type: type)
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 {
+8 -8
View File
@@ -30,7 +30,7 @@ func saveMutedThreads(pubkey: String, currentValue: [String], value: [String]) -
class MutedThreadsManager: ObservableObject {
private let userDefaults = UserDefaults.standard
private let pubkey: String
private let keypair: Keypair
private var _mutedThreadsSet: Set<String>
private var _mutedThreads: [String]
@@ -39,26 +39,26 @@ class MutedThreadsManager: ObservableObject {
return _mutedThreads
}
set {
if saveMutedThreads(pubkey: pubkey, currentValue: _mutedThreads, value: newValue) {
if saveMutedThreads(pubkey: keypair.pubkey, currentValue: _mutedThreads, value: newValue) {
self._mutedThreads = newValue
self.objectWillChange.send()
}
}
}
init(pubkey: String) {
self._mutedThreads = loadMutedThreads(pubkey: pubkey)
init(keypair: Keypair) {
self._mutedThreads = loadMutedThreads(pubkey: keypair.pubkey)
self._mutedThreadsSet = Set(_mutedThreads)
self.pubkey = pubkey
self.keypair = keypair
}
func isMutedThread(_ ev: NostrEvent) -> Bool {
return _mutedThreadsSet.contains(ev.thread_id(privkey: nil))
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) {
if isMutedThread(ev, privkey: keypair.privkey) {
mutedThreads = mutedThreads.filter { $0 != threadId }
_mutedThreadsSet.remove(threadId)
notify(.unmute_thread, ev)
+10 -14
View File
@@ -239,7 +239,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
}
if insert_event_immediate(ev) {
filter_and_build_notifications(damus_state)
self.notifications = build_notifications()
return true
}
@@ -252,47 +252,47 @@ class NotificationsModel: ObservableObject, ScrollQueue {
}
if insert_zap_immediate(zap) {
filter_and_build_notifications(damus_state)
self.notifications = build_notifications()
return true
}
return false
}
func filter_and_build_notifications(_ damus_state: DamusState) {
func filter(_ isIncluded: (NostrEvent) -> Bool) {
var changed = false
var count = 0
count = incoming_events.count
incoming_events = incoming_events.filter { include_event($0, damus_state: damus_state) }
incoming_events = incoming_events.filter(isIncluded)
changed = changed || incoming_events.count != count
count = profile_zaps.zaps.count
profile_zaps.zaps = profile_zaps.zaps.filter { zap in include_event(zap.request.ev, damus_state: damus_state) }
profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request.ev) }
changed = changed || profile_zaps.zaps.count != count
for el in reactions {
count = el.value.events.count
el.value.events = el.value.events.filter { include_event($0, damus_state: damus_state) }
el.value.events = el.value.events.filter(isIncluded)
changed = changed || el.value.events.count != count
}
for el in reposts {
count = el.value.events.count
el.value.events = el.value.events.filter { include_event($0, damus_state: damus_state) }
el.value.events = el.value.events.filter(isIncluded)
changed = changed || el.value.events.count != count
}
for el in zaps {
count = el.value.zaps.count
el.value.zaps = el.value.zaps.filter {
include_event($0.request.ev, damus_state: damus_state)
isIncluded($0.request.ev)
}
changed = changed || el.value.zaps.count != count
}
count = replies.count
replies = replies.filter { include_event($0, damus_state: damus_state) }
replies = replies.filter(isIncluded)
changed = changed || replies.count != count
if changed {
@@ -312,13 +312,9 @@ class NotificationsModel: ObservableObject, ScrollQueue {
}
if inserted {
filter_and_build_notifications(damus_state)
self.notifications = build_notifications()
}
return inserted
}
func include_event(_ event: NostrEvent, damus_state: DamusState) -> Bool {
return !damus_state.contacts.is_muted(event.pubkey) && !damus_state.muted_threads.isMutedThread(event)
}
}
+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)
}
+6 -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 {
+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 }
+9
View File
@@ -15,6 +15,7 @@ class EventCache {
private var cancellable: AnyCancellable?
private var translations: [String: TranslateStatus] = [:]
private var artifacts: [String: NoteArtifacts] = [:]
var validation: [String: ValidationResult] = [:]
//private var thread_latest: [String: Int64]
@@ -26,6 +27,14 @@ 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
}
+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
}
+3
View File
@@ -110,6 +110,9 @@ extension Notification.Name {
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 {
+8 -6
View File
@@ -9,10 +9,13 @@ 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 {
@@ -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 -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)
@@ -59,14 +50,12 @@ struct DirectMessagesView: View {
return [.truncate_content, .no_action_bar, .no_translate]
}
func MaybeEvent(_ tup: (String, DirectMessageModel)) -> some View {
func MaybeEvent(_ model: DirectMessageModel) -> some View {
Group {
if let ev = tup.1.events.last {
EventView(damus: damus_state, event: ev, pubkey: tup.0, options: options)
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()
@@ -106,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)
}
}
-13
View File
@@ -69,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 {
+2 -2
View File
@@ -46,7 +46,7 @@ struct MenuItems: View {
let bookmarked = bookmarks.isBookmarked(event)
self._isBookmarked = State(initialValue: bookmarked)
let muted_thread = muted_threads.isMutedThread(event)
let muted_thread = muted_threads.isMutedThread(event, privkey: keypair.privkey)
self._isMutedThread = State(initialValue: muted_thread)
self.bookmarks = bookmarks
@@ -96,7 +96,7 @@ struct MenuItems: View {
if event.known_kind != .dm {
Button {
self.muted_threads.updateMutedThread(event)
let muted = self.muted_threads.isMutedThread(event)
let muted = self.muted_threads.isMutedThread(event, privkey: self.keypair.privkey)
isMutedThread = muted
} label: {
let imageName = isMutedThread ? "speaker" : "speaker.slash"
-1
View File
@@ -37,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)
+2 -5
View File
@@ -21,14 +21,11 @@ struct FollowUserView: View {
}
HStack {
UserView(damus_state: damus_state, pubkey: target.pubkey)
.contentShape(Rectangle())
.onTapGesture {
navigating = true
}
UserViewRow(damus_state: damus_state, pubkey: target.pubkey)
FollowButtonView(target: target, follows_you: false, follow_state: damus_state.contacts.follow_state(target.pubkey))
}
Spacer()
}
}
+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
@@ -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)
@@ -35,12 +35,19 @@ struct NotificationsView: View {
@Environment(\.colorScheme) var colorScheme
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.
Text("")
.id("what")
NotificationTab(NotificationFilterState.all)
.tag(NotificationFilterState.all)
@@ -1,5 +1,5 @@
//
// ParicipantsView.swift
// ParticipantsView.swift
// damus
//
// Created by Joel Klabo on 1/18/23.
+120 -25
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 {
@@ -168,29 +181,20 @@ struct PostView: View {
.padding()
}
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
}
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
@@ -222,8 +226,14 @@ struct PostView: View {
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)
}
@@ -247,14 +257,24 @@ struct PostView: View {
}
}
.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))
@@ -272,6 +292,7 @@ struct PostView: View {
}
} else {
post = damus_state.drafts.post
uploadedMedias = damus_state.drafts.medias
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
@@ -283,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: {
@@ -323,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
}
@@ -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))
}
}
+1 -1
View File
@@ -50,7 +50,7 @@ struct SearchHomeView: View {
damus: damus_state,
show_friend_icon: true,
filter: {
if damus_state.muted_threads.isMutedThread($0) {
if damus_state.muted_threads.isMutedThread($0, privkey: self.damus_state.keypair.privkey) {
return false
}
+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.
@@ -190,6 +190,11 @@ Sentence composed of 2 variables to describe how many people are following a use
<target>Appearance</target>
<note>Section header for text and appearance settings</note>
</trans-unit>
<trans-unit id="Are you lost?" xml:space="preserve">
<source>Are you lost?</source>
<target>Are you lost?</target>
<note>Text asking the user if they are lost in the app.</note>
</trans-unit>
<trans-unit id="Are you sure you want to repost this?" xml:space="preserve">
<source>Are you sure you want to repost this?</source>
<target>Are you sure you want to repost this?</target>
@@ -721,8 +726,7 @@ Sentence composed of 2 variables to describe how many people are following a use
<source>Mute</source>
<target>Mute</target>
<note>Alert button to mute a user.
Button to mute a profile.
Context menu option for muting users.</note>
Button to mute a profile.</note>
</trans-unit>
<trans-unit id="Mute %@?" xml:space="preserve">
<source>Mute %@?</source>
@@ -732,7 +736,13 @@ Sentence composed of 2 variables to describe how many people are following a use
<trans-unit id="Mute User" xml:space="preserve">
<source>Mute User</source>
<target>Mute User</target>
<note>Title of alert for muting a user.</note>
<note>Context menu option for muting users.
Title of alert for muting a user.</note>
</trans-unit>
<trans-unit id="Mute conversation" xml:space="preserve">
<source>Mute conversation</source>
<target>Mute conversation</target>
<note>Context menu option for muting a conversation.</note>
</trans-unit>
<trans-unit id="Muted" xml:space="preserve">
<source>Muted</source>
@@ -779,6 +789,11 @@ Sentence composed of 2 variables to describe how many people are following a use
<target>Nothing to see here. Check back later!</target>
<note>Indicates that there are no notes in the timeline to view.</note>
</trans-unit>
<trans-unit id="Notification Dots" xml:space="preserve">
<source>Notification Dots</source>
<target>Notification Dots</target>
<note>Section header for notification indicator dot settings</note>
</trans-unit>
<trans-unit id="Notification Preference" xml:space="preserve">
<source>Notification Preference</source>
<target>Notification Preference</target>
@@ -913,6 +928,11 @@ Picker option to indicate that a zap should be sent privately and not identify t
<target>Public key</target>
<note>Label indicating that the text is a user's public account key.</note>
</trans-unit>
<trans-unit id="QR Code" xml:space="preserve">
<source>QR Code</source>
<target>QR Code</target>
<note>Button to view profile's qr code.</note>
</trans-unit>
<trans-unit id="Reactions" xml:space="preserve">
<source>Reactions</source>
<target>Reactions</target>
@@ -1300,6 +1320,11 @@ Picker option to indicate that a zap should be sent privately and not identify t
<target>Universe 🛸</target>
<note>Toolbar label for the universal view where posts from all connected relay servers appear.</note>
</trans-unit>
<trans-unit id="Unmute conversation" xml:space="preserve">
<source>Unmute conversation</source>
<target>Unmute conversation</target>
<note>Context menu option for unmuting a conversation.</note>
</trans-unit>
<trans-unit id="User has been muted" xml:space="preserve">
<source>User has been muted</source>
<target>User has been muted</target>
@@ -1339,6 +1364,11 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
ARE YOU SURE YOU WANT TO CONTINUE?</target>
<note>Alert for deleting the users account.</note>
</trans-unit>
<trans-unit id="Wake up, %@" xml:space="preserve">
<source>Wake up, %@</source>
<target>Wake up, %@</target>
<note>Text telling the user to wake up, where the argument is their display name.</note>
</trans-unit>
<trans-unit id="Wallet" xml:space="preserve">
<source>Wallet</source>
<target>Wallet</target>
@@ -1374,6 +1404,11 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
<target>Yes, Post with Private Key</target>
<note>Button to proceed with posting a note even though it looks like they might be posting a private key.</note>
</trans-unit>
<trans-unit id="You are dreaming..." xml:space="preserve">
<source>You are dreaming...</source>
<target>You are dreaming...</target>
<note>Text telling the user that they are dreaming.</note>
</trans-unit>
<trans-unit id="You have no bookmarks yet, add them in the context menu" xml:space="preserve">
<source>You have no bookmarks yet, add them in the context menu</source>
<target>You have no bookmarks yet, add them in the context menu</target>
Binary file not shown.
Binary file not shown.
-366
View File
@@ -1,366 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>collapsed_event_view_other_notes</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@NOTES@</string>
<key>NOTES</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>... %d autre note ...</string>
<key>many</key>
<string>... %d autres notes ...</string>
<key>other</key>
<string>... %d autres notes ...</string>
</dict>
</dict>
<key>followers_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@FOLLOWERS@</string>
<key>FOLLOWERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Abonné</string>
<key>many</key>
<string>Abonnés</string>
<key>other</key>
<string>Abonnés</string>
</dict>
</dict>
<key>following_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@FOLLOWING@</string>
<key>FOLLOWING</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Abonnement</string>
<key>many</key>
<string>Abonnements</string>
<key>other</key>
<string>Abonnements</string>
</dict>
</dict>
<key>reacted_tagged_in_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REACTED@</string>
<key>REACTED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ et %1$d autre ont réagi à une note dans laquelle vous apparaissez</string>
<key>many</key>
<string>%2$@ et %1$d autres ont réagi à une note dans laquelle vous apparaissez</string>
<key>other</key>
<string>%2$@ et %1$d autres ont réagi à une note dans laquelle vous apparaissez</string>
</dict>
</dict>
<key>reacted_your_post_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REACTED@</string>
<key>REACTED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ et %1$d autre ont réagi à votre note</string>
<key>many</key>
<string>%2$@ et %1$d autres ont réagi à votre note</string>
<key>other</key>
<string>%2$@ et %1$d autres ont réagi à votre note</string>
</dict>
</dict>
<key>reacted_your_profile_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REACTED@</string>
<key>REACTED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ et %1$d autre ont réagi à votre profil</string>
<key>many</key>
<string>%2$@ et %1$d autres ont réagi à votre profil</string>
<key>other</key>
<string>%2$@ et %1$d autres ont réagi à votre profil</string>
</dict>
</dict>
<key>reactions_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REACTIONS@</string>
<key>REACTIONS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Réaction</string>
<key>many</key>
<string>Réactions</string>
<key>other</key>
<string>Réactions</string>
</dict>
</dict>
<key>relays_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@RELAYS@</string>
<key>RELAYS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Relai</string>
<key>many</key>
<string>Relais</string>
<key>other</key>
<string>Relais</string>
</dict>
</dict>
<key>replying_to_two_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Réponse à %2$@, %3$@ &amp; %1$d autre</string>
<key>many</key>
<string>Réponse à %2$@, %3$@ &amp; %1$d autres</string>
<key>other</key>
<string>Réponse à %2$@, %3$@ &amp; %1$d autres</string>
</dict>
</dict>
<key>reposted_tagged_in_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REPOSTED@</string>
<key>REPOSTED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ et %1$d autre ont cité une note dans laquelle vous apparaissez</string>
<key>many</key>
<string>%2$@ et %1$d autres ont cité une note dans laquelle vous apparaissez</string>
<key>other</key>
<string>%2$@ et %1$d autres ont republié une note dans laquelle vous apparaissez</string>
</dict>
</dict>
<key>reposted_your_post_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REPOSTED@</string>
<key>REPOSTED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ et %1$d autre ont cité votre note</string>
<key>many</key>
<string>%2$@ et %1$d autres ont cité votre note</string>
<key>other</key>
<string>%2$@ et %1$d autres ont republié votre note</string>
</dict>
</dict>
<key>reposted_your_profile_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REPOSTED@</string>
<key>REPOSTED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ et %1$d autre ont republié votre profile</string>
<key>many</key>
<string>%2$@ et %1$d autres ont republié votre profile</string>
<key>other</key>
<string>%2$@ et %1$d autres ont republié votre profile</string>
</dict>
</dict>
<key>reposts_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REPOSTS@</string>
<key>REPOSTS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Republication</string>
<key>many</key>
<string>Republications</string>
<key>other</key>
<string>Republications</string>
</dict>
</dict>
<key>sats_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%1$#@SATS@</string>
<key>SATS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>@</string>
<key>one</key>
<string>%2$@ sat</string>
<key>many</key>
<string>%2$@ sats</string>
<key>other</key>
<string>%2$@ sats</string>
</dict>
</dict>
<key>zap_notification_no_message</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%1$#@NOTIFICATION@</string>
<key>NOTIFICATION</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>@</string>
<key>one</key>
<string>Vous avez reçu %2$@ sat de %3$@</string>
<key>many</key>
<string>Vous avez reçu %2$@ sats de %3$@</string>
<key>other</key>
<string>Vous avez reçu %2$@ sats de %3$@</string>
</dict>
</dict>
<key>zap_notification_with_message</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%1$#@NOTIFICATION@</string>
<key>NOTIFICATION</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>@</string>
<key>one</key>
<string>You received %2$@ sat from %3$@: "%4$@"</string>
<key>many</key>
<string>You received %2$@ sats from %3$@: "%4$@"</string>
<key>other</key>
<string>You received %2$@ sats from %3$@: "%4$@"</string>
</dict>
</dict>
<key>zapped_tagged_in_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@ZAPPED@</string>
<key>ZAPPED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ et %1$d autre ont zappé une note dans laquelle vous apparaissez</string>
<key>many</key>
<string>%2$@ et %1$d autres ont zappé une note dans laquelle vous apparaissez</string>
<key>other</key>
<string>%2$@ et %1$d autres ont zappé une note dans laquelle vous apparaissez</string>
</dict>
</dict>
<key>zapped_your_post_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@ZAPPED@</string>
<key>ZAPPED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ et %1$d autre ont zappé votre note</string>
<key>many</key>
<string>%2$@ et %1$d autres ont zappé votre note</string>
<key>other</key>
<string>%2$@ et %1$d autres ont zappé votre note</string>
</dict>
</dict>
<key>zapped_your_profile_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@ZAPPED@</string>
<key>ZAPPED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ et %1$d autre ont zappé votre profile</string>
<key>many</key>
<string>%2$@ et %1$d autres ont zappé votre profile</string>
<key>other</key>
<string>%2$@ et %1$d autres ont zappé votre profile</string>
</dict>
</dict>
<key>zaps_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@ZAPS@</string>
<key>ZAPS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Zap</string>
<key>many</key>
<string>Zaps</string>
<key>other</key>
<string>Zaps</string>
</dict>
</dict>
</dict>
</plist>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -15,7 +15,7 @@
<key>one</key>
<string>...%d andra inlägg...</string>
<key>other</key>
<string>...%d andra inlägg...</string>
<string>...%d andra anteckningar...</string>
</dict>
</dict>
<key>followers_count</key>
+9 -9
View File
@@ -51,55 +51,55 @@ final class DMTests: XCTestCase {
handle_incoming_dms(prev_events: notif, dms: model, our_pubkey: alice.pubkey, evs: [alice_to_bob])
XCTAssertEqual(model.dms.count, 1)
XCTAssertEqual(model.dms[0].0, bob.pubkey)
XCTAssertEqual(model.dms[0].pubkey, bob.pubkey)
let bob_to_alice = create_dm("hi alice", to_pk: alice.pubkey, tags: [["p", alice.pubkey]], keypair: bob, created_at: now + 1)!
handle_incoming_dms(prev_events: notif, dms: model, our_pubkey: alice.pubkey, evs: [bob_to_alice])
XCTAssertEqual(model.dms.count, 1)
XCTAssertEqual(model.dms[0].0, bob.pubkey)
XCTAssertEqual(model.dms[0].pubkey, bob.pubkey)
let alice_to_bob_2 = create_dm("hi bob", to_pk: bob.pubkey, tags: [["p", bob.pubkey]], keypair: alice, created_at: now + 2)!
handle_incoming_dms(prev_events: notif, dms: model, our_pubkey: alice.pubkey, evs: [alice_to_bob_2])
XCTAssertEqual(model.dms.count, 1)
XCTAssertEqual(model.dms[0].0, bob.pubkey)
XCTAssertEqual(model.dms[0].pubkey, bob.pubkey)
let fiatjaf_to_alice = create_dm("hi alice", to_pk: alice.pubkey, tags: [["p", alice.pubkey]], keypair: fiatjaf, created_at: now+5)!
handle_incoming_dms(prev_events: notif, dms: model, our_pubkey: alice.pubkey, evs: [fiatjaf_to_alice])
XCTAssertEqual(model.dms.count, 2)
XCTAssertEqual(model.dms[0].0, fiatjaf.pubkey)
XCTAssertEqual(model.dms[0].pubkey, fiatjaf.pubkey)
let dave_to_alice = create_dm("hi alice", to_pk: alice.pubkey, tags: [["p", alice.pubkey]], keypair: dave, created_at: now + 10)!
handle_incoming_dms(prev_events: notif, dms: model, our_pubkey: alice.pubkey, evs: [dave_to_alice])
XCTAssertEqual(model.dms.count, 3)
XCTAssertEqual(model.dms[0].0, dave.pubkey)
XCTAssertEqual(model.dms[0].pubkey, dave.pubkey)
let bob_to_alice_2 = create_dm("hi alice 2", to_pk: alice.pubkey, tags: [["p", alice.pubkey]], keypair: bob, created_at: now + 15)!
handle_incoming_dms(prev_events: notif, dms: model, our_pubkey: alice.pubkey, evs: [bob_to_alice_2])
XCTAssertEqual(model.dms.count, 3)
XCTAssertEqual(model.dms[0].0, bob.pubkey)
XCTAssertEqual(model.dms[0].pubkey, bob.pubkey)
let charlie_to_alice = create_dm("hi alice", to_pk: alice.pubkey, tags: [["p", alice.pubkey]], keypair: charlie, created_at: now + 20)!
handle_incoming_dms(prev_events: notif, dms: model, our_pubkey: alice.pubkey, evs: [charlie_to_alice])
XCTAssertEqual(model.dms.count, 4)
XCTAssertEqual(model.dms[0].0, charlie.pubkey)
XCTAssertEqual(model.dms[0].pubkey, charlie.pubkey)
let bob_to_alice_3 = create_dm("hi alice 3", to_pk: alice.pubkey, tags: [["p", alice.pubkey]], keypair: bob, created_at: now + 25)!
handle_incoming_dms(prev_events: notif, dms: model, our_pubkey: alice.pubkey, evs: [bob_to_alice_3])
XCTAssertEqual(model.dms.count, 4)
XCTAssertEqual(model.dms[0].0, bob.pubkey)
XCTAssertEqual(model.dms[0].pubkey, bob.pubkey)
let charlie_to_alice_2 = create_dm("hi alice 2", to_pk: alice.pubkey, tags: [["p", alice.pubkey]], keypair: charlie, created_at: now + 30)!
handle_incoming_dms(prev_events: notif, dms: model, our_pubkey: alice.pubkey, evs: [charlie_to_alice_2])
XCTAssertEqual(model.dms.count, 4)
XCTAssertEqual(model.dms[0].0, charlie.pubkey)
XCTAssertEqual(model.dms[0].pubkey, charlie.pubkey)
}
}