Compare commits

...

24 Commits

Author SHA1 Message Date
d02678751d Allow profile edit button text to scale down when translation is too long 2023-01-30 00:03:16 -05:00
William Casarin
7bcc345038 Fix build 2023-01-28 15:54:52 -08:00
William Casarin
bf0f879d66 revert dubious change 2023-01-28 15:53:04 -08:00
William Casarin
3af9131afe autocomplete: add space after replacing occurances
This is more intuitive
2023-01-28 15:50:42 -08:00
Swift
b6b6d033a8 User tagging and autocompletion
Co-authored-by: William Casarin <jb55@jb55.com>
Changelog-Added: User tagging and autocompletion in posts
Closes: #347
Fixes: #411, #63
2023-01-28 15:43:45 -08:00
William Casarin
819d7496b2 v1.0.0-12 changelog 2023-01-28 10:54:49 -08:00
William Casarin
4c58e73e18 v1.0.0-12 2023-01-28 10:52:34 -08:00
William Casarin
6e38707aaa Merge remote-tracking branches 'ar' and 'pt_PT' 2023-01-28 09:55:02 -08:00
William Casarin
0f08612b79 Added arabic and portugese translations
Changelog-Added: Added arabic and portugese translations
2023-01-28 09:42:32 -08:00
transifex-integration[bot]
ef89c4b33b Apply translations in pt_PT
translated for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'pt_PT' language.
2023-01-28 17:41:01 +00:00
transifex-integration[bot]
5c9bc02ac6 Apply translations in ar
translated for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'ar' language.
2023-01-28 17:40:50 +00:00
Joel Klabo
b57d2a3a6e Use AttributedString in NoteContentView
Changelog-Changed: Remove markdown link support from posts
Closes: #398
2023-01-28 09:38:38 -08:00
OlegAba
0e8c94b668 Replace SVGKit package with CoreSVG
Changelog-Fixed: Fixed crash on some SVG profile pictures
Closes: #416
2023-01-28 09:38:22 -08:00
transifex-integration[bot]
3e6c8c47a7 Apply translations in pt_PT
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'pt_PT' language.
2023-01-28 17:00:11 +00:00
ericholguin
e4beb872a5 Add QRCode view
Changelog-Added: Add QRCode view for sharing your pubkey
Closes: #418
2023-01-28 08:36:53 -08:00
William Casarin
552bd9cae5 Implement NIP-21 URI handling
Changelog-Added: Added nostr: uri handling
2023-01-28 08:31:11 -08:00
transifex-integration[bot]
059a16a8dc Apply translations in ar
translation completed for the source file '/damus Localizations/en-US.xcloc/Localized Contents/en-US.xliff'
on the 'ar' language.
2023-01-28 11:15:51 +00:00
William Casarin
b6ea17a0eb Merge remote-tracking branches 'tyiu/tyiu/string-comments' and 'tyiu/tyiu/remove-it-CH'
Changelog-Fixed: Localization fixes
Changelog-Fixed: Don't allow blocking yourself
2023-01-27 12:49:24 -08:00
William Casarin
a9e9f0dc8f Hide muted users from global
Changelog-Fixed: Hide muted users from global
2023-01-27 12:16:41 -08:00
William Casarin
5edb7df5c4 Mute events in threads
Changlog-Added: Mute events in threads
2023-01-27 12:11:57 -08:00
William Casarin
d559dd3a13 Allow non-string values in profiles
Profiles were not loading from other clients because some fields were
not strings

Changelog-Fixed: Fixed profiles sometimes not loading from other clients
2023-01-27 10:19:29 -08:00
William Casarin
b9c2473a2d reporting: don't use spam for every report
Changelog-Fixed: Fixed bug where `spam` was always the report type
2023-01-27 10:18:48 -08:00
William Casarin
196081cd38 mutelists: #d must be an array
Not sure how this was working before
2023-01-27 09:36:04 -08:00
a20fa08030 Remove it-CH locale as there is no difference with it-IT yet 2023-01-26 10:01:42 -05:00
37 changed files with 1922 additions and 1026 deletions

View File

@@ -1,3 +1,28 @@
## [1.0.0-12] - 2023-01-28
### Added
- Added arabic and portugese translations (William Casarin)
- Add QRCode view for sharing your pubkey (ericholguin)
- Added nostr: uri handling (William Casarin)
### Changed
- Remove markdown link support from posts (Joel Klabo)
### Fixed
- Fixed crash on some SVG profile pictures (OlegAba)
- Localization fixes
- Don't allow blocking yourself (Terry)
- Hide muted users from global (William Casarin)
- Fixed profiles sometimes not loading from other clients (William Casarin)
- Fixed bug where `spam` was always the report type (William Casarin)
[1.0.0-12]: https://github.com/damus-io/damus/releases/tag/v1.0.0-12
## [1.0.0-11] - 2023-01-25

View File

@@ -45,12 +45,18 @@ Abbreviated version of a nostr public key.</note>
<source>%@ %@</source>
<target>%@ %@</target>
<note>Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.
<note>Sentence composed of 2 variables to describe how many tip payments there are on a post. In source English, the first variable is the number of tip payments, and the second variable is 'Tip' or 'Tips'.
Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.</note>
</trans-unit>
<trans-unit id="%@ has been blocked" xml:space="preserve">
<source>%@ has been blocked</source>
<target>تم حظر %@</target>
<note>Alert message that informs a user was blocked.</note>
</trans-unit>
<trans-unit id="%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction." xml:space="preserve">
<source>%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction.</source>
<target>انشاء حسابك لايتطلب رقم جوال أو بريد الكتروني أو معلومات شخصية. احصل على حسابك الخاص خلال ثواني.</target>
<target>انشاء حسابك لايتطلب رقم جوال أو بريد الكتروني أو معلومات شخصية. احصل على حسابك الخاص في ثواني.</target>
<note>Explanation of what is done to keep personally identifiable information private. There is a heading that precedes this explanation which is a variable to this string.</note>
</trans-unit>
@@ -62,7 +68,7 @@ Sentence composed of 2 variables to describe how many profiles a user is followi
</trans-unit>
<trans-unit id="%@. Tip your friend's posts and stack sats with Bitcoin⚡, the native currency of the internet." xml:space="preserve">
<source>%@. Tip your friend's posts and stack sats with Bitcoin⚡, the native currency of the internet.</source>
<target>%@. بسهولة مطلقة، أرسل و استقبل برقيات البتكوين ⚡️، عملة الانترنت الحقيقية.</target>
<target>%@. بسهولة مطلقة، أرسل و استقبل برقيات البتكوين ⚡️عملة الانترنت العالمية.</target>
<note>Explanation of what can be done by users to earn money. There is a heading that precedes this explanation which is a variable to this string.</note>
</trans-unit>
@@ -70,7 +76,7 @@ Sentence composed of 2 variables to describe how many profiles a user is followi
<source>%lld</source>
<target>%lld</target>
<note>Number of reposts.
<note>Number of tip payments on a post.
Number of profiles a user is following.</note>
</trans-unit>
<trans-unit id="%lld/%lld" xml:space="preserve">
@@ -93,7 +99,7 @@ Number of profiles a user is following.</note>
</trans-unit>
<trans-unit id="(Profile.displayName(profile: profile, pubkey: whos))'s Followers" xml:space="preserve">
<source>(Profile.displayName(profile: profile, pubkey: whos))'s Followers</source>
<target>(Profile.displayName(profile: profile, pubkey: whos))متابعي</target>
<target>متابعي (Profile.displayName(profile: profile, pubkey: whos))</target>
<note>Navigation bar title for view that shows who is following a user.</note>
</trans-unit>
@@ -133,12 +139,24 @@ Number of profiles a user is following.</note>
<note>Placeholder text for About Me description.</note>
</trans-unit>
<trans-unit id="Accept" xml:space="preserve">
<source>Accept</source>
<target>موافق</target>
<note>Button to accept the end user license agreement before being allowed into the app.</note>
</trans-unit>
<trans-unit id="Account ID" xml:space="preserve">
<source>Account ID</source>
<target>معرف الحساب</target>
<note>Label to indicate the public ID of the account.</note>
</trans-unit>
<trans-unit id="Actions" xml:space="preserve">
<source>Actions</source>
<target>خيارات</target>
<note>Title for confirmation dialog to either share, report, or block a profile.</note>
</trans-unit>
<trans-unit id="Add" xml:space="preserve">
<source>Add</source>
<target>اضافة</target>
@@ -200,6 +218,38 @@ Number of profiles a user is following.</note>
<note>Dropdown option label for Lightning wallet, Blixt Wallet</note>
</trans-unit>
<trans-unit id="Block" xml:space="preserve">
<source>Block</source>
<target>حظر</target>
<note>Alert button to block a user.
Button to block a profile.
Context menu option for blocking users.</note>
</trans-unit>
<trans-unit id="Block %@?" xml:space="preserve">
<source>Block %@?</source>
<target>حظر %@؟</target>
<note>Alert message prompt to ask if a user should be blocked.</note>
</trans-unit>
<trans-unit id="Block User" xml:space="preserve">
<source>Block User</source>
<target>حظر المستخدم</target>
<note>Title of alert for blocking a user.</note>
</trans-unit>
<trans-unit id="Blocked" xml:space="preserve">
<source>Blocked</source>
<target>قائمة الحظر</target>
<note>Sidebar menu label for Profile view.</note>
</trans-unit>
<trans-unit id="Blocked Users" xml:space="preserve">
<source>Blocked Users</source>
<target>المحظورون</target>
<note>Navigation title of view to see list of blocked users.</note>
</trans-unit>
<trans-unit id="Blue Wallet" xml:space="preserve">
<source>Blue Wallet</source>
<target>Blue Wallet</target>
@@ -222,7 +272,9 @@ Number of profiles a user is following.</note>
<source>Cancel</source>
<target>الغاء</target>
<note>Button to cancel out of posting a note.
<note>Alert button to cancel out of alert for blocking a user.
Button to cancel out of alert that creates a new mutelist.
Button to cancel out of posting a note.
Button to cancel out of reposting a post.
Button to cancel out of view adding user inputted relay.
Cancel out of logging out the user.</note>
@@ -300,15 +352,21 @@ Number of profiles a user is following.</note>
<note>Context menu option for copying the JSON text from the note.</note>
</trans-unit>
<trans-unit id="Copy Report ID" xml:space="preserve">
<source>Copy Report ID</source>
<target>نسخ معرف البلاغ</target>
<note>Button to copy report ID.</note>
</trans-unit>
<trans-unit id="Copy Text" xml:space="preserve">
<source>Copy Text</source>
<target>نسخ النص</target>
<note>Context menu option for copying the text from an note.</note>
</trans-unit>
<trans-unit id="Copy User ID" xml:space="preserve">
<source>Copy User ID</source>
<target>نسخ معرف المستخدم</target>
<trans-unit id="Copy User Pubkey" xml:space="preserve">
<source>Copy User Pubkey</source>
<target>نسخ معرف الحساب</target>
<note>Context menu option for copying the ID of the user who created the note.</note>
</trans-unit>
@@ -318,6 +376,12 @@ Number of profiles a user is following.</note>
<note>Title of section for copying a Lightning invoice identifier.</note>
</trans-unit>
<trans-unit id="Could not find user to block..." xml:space="preserve">
<source>Could not find user to block...</source>
<target>لم يتم العثور حساب لحظره</target>
<note>Alert message to indicate that the blocked user could not be found.</note>
</trans-unit>
<trans-unit id="Create" xml:space="preserve">
<source>Create</source>
<target>انشاء</target>
@@ -330,6 +394,12 @@ Number of profiles a user is following.</note>
<note>Button to create an account.</note>
</trans-unit>
<trans-unit id="Create new mutelist" xml:space="preserve">
<source>Create new mutelist</source>
<target>أنشئ قائمة حظر جديدة</target>
<note>Title of alert prompting the user to create a new mutelist.</note>
</trans-unit>
<trans-unit id="Creator(s) of Bitcoin. Absolute legend." xml:space="preserve">
<source>Creator(s) of Bitcoin. Absolute legend.</source>
<target>مبتكر البتكوين. اسطورة لن تتكرر.</target>
@@ -365,7 +435,8 @@ Number of profiles a user is following.</note>
<source>Delete</source>
<target>حذف</target>
<note>Button to delete a relay server that the user connects to.</note>
<note>Button to delete a relay server that the user connects to.
Button to remove a user from their blocklist.</note>
</trans-unit>
<trans-unit id="Dismiss" xml:space="preserve">
<source>Dismiss</source>
@@ -385,6 +456,12 @@ Number of profiles a user is following.</note>
<note>Button to dismiss wallet selection view for paying Lightning invoice.</note>
</trans-unit>
<trans-unit id="EULA" xml:space="preserve">
<source>EULA</source>
<target>اتفاقية الاستخدام</target>
<note>Label indicating that the below text is the EULA, an acronym for End User License Agreement.</note>
</trans-unit>
<trans-unit id="Earn Money" xml:space="preserve">
<source>Earn Money</source>
<target>اكسب المال.</target>
@@ -441,7 +518,7 @@ Number of profiles a user is following.</note>
</trans-unit>
<trans-unit id="Following" xml:space="preserve">
<source>Following</source>
<target>يتابع</target>
<target>المتابَعين</target>
<note>Text to indicate that the button next to it is in a state that indicates that it is in the process of following a profile.
Part of a larger sentence to describe how many profiles a user is following.</note>
@@ -482,12 +559,24 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<note>Navigation bar title for Home view where posts and replies appear from those who the user is following.</note>
</trans-unit>
<trans-unit id="Illegal content" xml:space="preserve">
<source>Illegal content</source>
<target>محتوى غير قانوني</target>
<note>Button for user to report that the account or content has illegal content.</note>
</trans-unit>
<trans-unit id="Invalid key" xml:space="preserve">
<source>Invalid key</source>
<target>المفتاح غير صالح</target>
<note>Error message indicating that an invalid account key was entered for login.</note>
</trans-unit>
<trans-unit id="It's spam" xml:space="preserve">
<source>It's spam</source>
<target>سبام</target>
<note>Button for user to report that the account or content has spam.</note>
</trans-unit>
<trans-unit id="LNLink" xml:space="preserve">
<source>LNLink</source>
<target>LNLink</target>
@@ -557,9 +646,15 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<note>Label for NIP-05 Verification section of user profile form.</note>
</trans-unit>
<trans-unit id="No block list found, create a new one? This will overwrite any previous block lists." xml:space="preserve">
<source>No block list found, create a new one? This will overwrite any previous block lists.</source>
<target>لم نعثر على قائمة حظر. هل تريد انشاء قائمة جديدة؟ سيتم استبدال أي قوائم سابقة ان وجدت</target>
<note>Alert message prompt that asks if the user wants to create a new block list, overwriting previous block lists.</note>
</trans-unit>
<trans-unit id="Nothing to see here. Check back later!" xml:space="preserve">
<source>Nothing to see here. Check back later!</source>
<target>لا جديد في هذه اللحظة. يرجى المحاولة لاحقا!</target>
<target>لا جديد في هذه اللحظة. يرجى المعاودة لاحقا!</target>
<note>Indicates that there are no notes in the timeline to view.</note>
</trans-unit>
@@ -569,6 +664,12 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<note>Navigation title for notifications.</note>
</trans-unit>
<trans-unit id="Nudity or explicit content" xml:space="preserve">
<source>Nudity or explicit content</source>
<target>عري أو محتوى فاضح</target>
<note>Button for user to report that the account or content has nudity or explicit content.</note>
</trans-unit>
<trans-unit id="Pay" xml:space="preserve">
<source>Pay</source>
<target>ادفع</target>
@@ -595,7 +696,7 @@ Part of a larger sentence to describe how many profiles a user is following.</no
</trans-unit>
<trans-unit id="Posts" xml:space="preserve">
<source>Posts</source>
<target>منشورات</target>
<target>المنشورات</target>
<note>Label for filter for seeing only posts (instead of posts and replies).</note>
</trans-unit>
@@ -665,6 +766,12 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<note>Section title for recommend relay servers that could be added as part of configuration</note>
</trans-unit>
<trans-unit id="Reject" xml:space="preserve">
<source>Reject</source>
<target>رفض</target>
<note>Button to reject the end user license agreement, which disallows the user from being let into the app.</note>
</trans-unit>
<trans-unit id="Relay" xml:space="preserve">
<source>Relay</source>
<target>موصّل</target>
@@ -677,6 +784,12 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<note>Sidebar menu label for Relay servers view</note>
</trans-unit>
<trans-unit id="Relays have been notified and clients will be able to use this information to filter content. Thank you!" xml:space="preserve">
<source>Relays have been notified and clients will be able to use this information to filter content. Thank you!</source>
<target>تم ابلاغ الموصّلات وسيتم الاستفادة من هذا البلاغ لتصفية المحتوى. شكرا لك!</target>
<note>Description of what was done as a result of sending a report to relay servers.</note>
</trans-unit>
<trans-unit id="Remove all" xml:space="preserve">
<source>Remove all</source>
<target>حذف المشاركين</target>
@@ -685,7 +798,7 @@ Part of a larger sentence to describe how many profiles a user is following.</no
</trans-unit>
<trans-unit id="Reply to self" xml:space="preserve">
<source>Reply to self</source>
<target>رد على منشور</target>
<target>رد على منشوره السابق</target>
<note>Label to indicate that the user is replying to themself.</note>
</trans-unit>
@@ -701,6 +814,25 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<note>Indicating that the user is replying to the following listed people.</note>
</trans-unit>
<trans-unit id="Report" xml:space="preserve">
<source>Report</source>
<target>ابلاغ</target>
<note>Button to report a profile.
Context menu option for reporting content.</note>
</trans-unit>
<trans-unit id="Report ID:" xml:space="preserve">
<source>Report ID:</source>
<target>معرف البلاغ</target>
<note>Label indicating that the text underneath is the identifier of the report that was sent to relay servers.</note>
</trans-unit>
<trans-unit id="Report sent!" xml:space="preserve">
<source>Report sent!</source>
<target>تم الابلاغ!</target>
<note>Message indicating that a report was successfully sent to relay servers.</note>
</trans-unit>
<trans-unit id="Repost" xml:space="preserve">
<source>Repost</source>
<target>إعادة نشر</target>
@@ -710,10 +842,16 @@ Part of a larger sentence to describe how many profiles a user is following.</no
</trans-unit>
<trans-unit id="Reposted" xml:space="preserve">
<source>Reposted</source>
<target>منشور معاد</target>
<target>منشور مُعاد</target>
<note>Text indicating that the post was reposted (i.e. re-shared).</note>
</trans-unit>
<trans-unit id="Reposts" xml:space="preserve">
<source>Reposts</source>
<target>اعادات النشر</target>
<note>Navigation bar title for Reposts view.</note>
</trans-unit>
<trans-unit id="Requests" xml:space="preserve">
<source>Requests</source>
<target>طلبات</target>
@@ -803,7 +941,8 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<source>Share</source>
<target>مشاركة</target>
<note>Button to share an image.</note>
<note>Button to share an image.
Button to share the link to a profile.</note>
</trans-unit>
<trans-unit id="Show" xml:space="preserve">
<source>Show</source>
@@ -829,6 +968,18 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<note>Dropdown option label for Lightning wallet, Strike.</note>
</trans-unit>
<trans-unit id="Thanks!" xml:space="preserve">
<source>Thanks!</source>
<target>شكرا!</target>
<note>Button to close out of alert that informs that the action to block a user was successful.</note>
</trans-unit>
<trans-unit id="They are impersonating someone" xml:space="preserve">
<source>They are impersonating someone</source>
<target>انتحال صفة شخص آخر</target>
<note>Button for user to report that the account is impersonating someone.</note>
</trans-unit>
<trans-unit id="This is a public key, you will not be able to make posts or interact in any way. This is used for viewing accounts from their perspective." xml:space="preserve">
<source>This is a public key, you will not be able to make posts or interact in any way. This is used for viewing accounts from their perspective.</source>
<target>هذا مفتاح عام. لن تستطيع النشر أو التفاعل بهذا الحساب بأي طريقة. تستطيع فقط مشاهدة المحتوى العام من منظور صاحب الحساب.</target>
@@ -837,7 +988,7 @@ Part of a larger sentence to describe how many profiles a user is following.</no
</trans-unit>
<trans-unit id="This is an old-style nostr key. We're not sure if it's a pubkey or private key. Please toggle the button below if this a public key." xml:space="preserve">
<source>This is an old-style nostr key. We're not sure if it's a pubkey or private key. Please toggle the button below if this a public key.</source>
<target>صيغة المفتاح قديمة. لا نستطيع التحديد إذا ما كان المفتاح خاصا أو عاما. الرجاء نقر الزر بالأسفل إذا كان المفتاح عاما.</target>
<target>صيغة المفتاح قديمة. لا نستطيع التحديد إذا ما كان المفتاح خاصا أو عاما. الرجاء تفعيل الخانة بالأسفل إذا كان المفتاح عاما.</target>
<note>Warning that the inputted account key for login is an old-style and asking user to verify if it is a public key.</note>
</trans-unit>
@@ -855,7 +1006,7 @@ Part of a larger sentence to describe how many profiles a user is following.</no
</trans-unit>
<trans-unit id="Thread" xml:space="preserve">
<source>Thread</source>
<target>سلسلة</target>
<target>منشور</target>
<note>Navigation bar title for note thread.
Navigation bar title for threaded event detail view.</note>
@@ -890,6 +1041,18 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<note>Text to indicate that the button next to it is in a state that will unfollow a profile when tapped.</note>
</trans-unit>
<trans-unit id="User blocked" xml:space="preserve">
<source>User blocked</source>
<target>الحساب محظور</target>
<note>Alert message to indicate</note>
</trans-unit>
<trans-unit id="User has been blocked" xml:space="preserve">
<source>User has been blocked</source>
<target>تم الحظر</target>
<note>Alert message that informs a user was blocked.</note>
</trans-unit>
<trans-unit id="Username" xml:space="preserve">
<source>Username</source>
<target>اسم المستخدم</target>
@@ -933,12 +1096,30 @@ Part of a larger sentence to describe how many profiles a user is following.</no
<note>Text to welcome user.</note>
</trans-unit>
<trans-unit id="What do you want to report?" xml:space="preserve">
<source>What do you want to report?</source>
<target>عن ماذا تريد الابلاغ</target>
<note>Header text to prompt user what issue they want to report.</note>
</trans-unit>
<trans-unit id="Yes, Overwrite" xml:space="preserve">
<source>Yes, Overwrite</source>
<target>نعم، استبدل</target>
<note>Text of button that confirms to overwrite the existing mutelist.</note>
</trans-unit>
<trans-unit id="Your Name" xml:space="preserve">
<source>Your Name</source>
<target>الاسم</target>
<note>Label for Your Name section of user profile form.</note>
</trans-unit>
<trans-unit id="Your report will be sent to the relays you are connected to" xml:space="preserve">
<source>Your report will be sent to the relays you are connected to</source>
<target>سيتم ارسال بلاغك للموصّلات المتصلة بحسابك</target>
<note>Footer text to inform user what will happen when the report is submitted.</note>
</trans-unit>
<trans-unit id="Zebedee" xml:space="preserve">
<source>Zebedee</source>
<target>Zebedee</target>
@@ -1001,7 +1182,7 @@ Part of a larger sentence to describe how many profiles a user is following.</no
</trans-unit>
<trans-unit id="optional" xml:space="preserve">
<source>optional</source>
<target>خياري</target>
<target>غير الزامي</target>
<note>Label indicating that a form input is optional.</note>
</trans-unit>
@@ -1092,7 +1273,7 @@ Part of a larger sentence to describe how many profiles a user is following.</no
</trans-unit>
<trans-unit id="/collapsed_event_view_other_notes:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
<source>··· %#@NOTES@ ···</source>
<target>··· %#@منشورات@ ···</target>
<target>··· %#@NOTES@ ···</target>
<note>Text to indicate that the thread was collapsed and that there are other notes to view if tapped.</note>
</trans-unit>
@@ -1104,19 +1285,19 @@ Part of a larger sentence to describe how many profiles a user is following.</no
</trans-unit>
<trans-unit id="/followers_count:dict/FOLLOWERS:dict/other:dict/:string" xml:space="preserve">
<source>Followers</source>
<target>متابعين</target>
<target>المتابعون</target>
<note>Part of a larger sentence to describe how many people are following a user.</note>
</trans-unit>
<trans-unit id="/followers_count:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
<source>%#@FOLLOWERS@</source>
<target>%#@متابعين@</target>
<target>%#@FOLLOWERS@</target>
<note>Part of a larger sentence to describe how many people are following a user.</note>
</trans-unit>
<trans-unit id="/reactions_count:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
<source>%#@REACTIONS@</source>
<target>%#@تفاعل@</target>
<target>%#@REACTIONS@</target>
<note>Part of a larger sentence to describe how many reactions there are on a post.</note>
</trans-unit>
@@ -1134,7 +1315,7 @@ Part of a larger sentence to describe how many profiles a user is following.</no
</trans-unit>
<trans-unit id="/relays_count:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
<source>%#@RELAYS@</source>
<target>%#@موصّل@</target>
<target>%#@RELAYS@</target>
<note>Part of a larger sentence to describe how many relay servers a user is connected.</note>
</trans-unit>
@@ -1152,7 +1333,7 @@ Part of a larger sentence to describe how many profiles a user is following.</no
</trans-unit>
<trans-unit id="/replying_to_one_and_others:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
<source>Replying to %@%#@OTHERS@</source>
<target>رد على %@%#@آخرين@</target>
<target>رد على %@%#@OTHERS@</target>
<note>Label to indicate that the user is replying to 1 user and others.</note>
</trans-unit>
@@ -1174,7 +1355,7 @@ Part of a larger sentence to describe how many profiles a user is following.</no
</trans-unit>
<trans-unit id="/replying_to_two_and_others:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
<source>Replying to %@, %@%#@OTHERS@</source>
<target>رد على %@, %@%#@آخرين@</target>
<target>رد على%@, %@%#@OTHERS@</target>
<note>Label to indicate that the user is replying to 2 users and others.</note>
</trans-unit>
@@ -1196,7 +1377,7 @@ Part of a larger sentence to describe how many profiles a user is following.</no
</trans-unit>
<trans-unit id="/reposts_count:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
<source>%#@REPOSTS@</source>
<target>%#@اعادات نشر@</target>
<target>%#@REPOSTS@</target>
<note>Part of a larger sentence to describe how many reposts there are.</note>
</trans-unit>
@@ -1220,19 +1401,19 @@ Part of a larger sentence to describe how many profiles a user is following.</no
</trans-unit>
<trans-unit id="/sats_count:dict/SATS:dict/one:dict/:string" xml:space="preserve">
<source>%2$@ sat</source>
<target>%2$@ sat</target>
<target>%2$@ ساتوشي</target>
<note>Amount of sats.</note>
</trans-unit>
<trans-unit id="/sats_count:dict/SATS:dict/other:dict/:string" xml:space="preserve">
<source>%2$@ sats</source>
<target>%2$@ sats</target>
<target>%2$@ ساتوشي</target>
<note>Amount of sats.</note>
</trans-unit>
<trans-unit id="/tips_count:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
<source>%#@TIPS@</source>
<target>%#@اكراميات@</target>
<target>%#@TIPS@</target>
<note>Part of a larger sentence to describe how many tip payments there are on a post.</note>
</trans-unit>

View File

@@ -166,14 +166,20 @@
4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABE02981A83900D66079 /* MutelistView.swift */; };
4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABE22981BC7D00D66079 /* UserView.swift */; };
4CF0ABE52981EE0C00D66079 /* EULAView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABE42981EE0C00D66079 /* EULAView.swift */; };
4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABE6298444FC00D66079 /* MutedEventView.swift */; };
4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABE829844AF100D66079 /* AnyCodable.swift */; };
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEB29844B4700D66079 /* AnyDecodable.swift */; };
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABED29844B5500D66079 /* AnyEncodable.swift */; };
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */; };
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABF52985CD5500D66079 /* UserSearch.swift */; };
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfileZoomView.swift */; };
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; };
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FBD06E296255C400D9D3B2 /* Theme.swift */; };
6C7DE41F2955169800E66263 /* Vault in Frameworks */ = {isa = PBXBuildFile; productRef = 6C7DE41E2955169800E66263 /* Vault */; };
7C45AE6D297352F90031D7BC /* SVGKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7C45AE6C297352F90031D7BC /* SVGKit */; };
7C45AE6F297352F90031D7BC /* SVGKitSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7C45AE6E297352F90031D7BC /* SVGKitSwift */; };
7C45AE71297353390031D7BC /* KFImageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C45AE70297353390031D7BC /* KFImageModel.swift */; };
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C60CAEE298471A1009C80D6 /* CoreSVG.swift */; };
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */; };
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; };
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
@@ -227,9 +233,6 @@
3ACB685B297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3ACB685E297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = "<group>"; };
3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeAgoTests.swift; sourceTree = "<group>"; };
3ACD32D0297F2D1E002F68B9 /* it-CH */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-CH"; path = "it-CH.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3ACD32D1297F2D1E002F68B9 /* it-CH */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-CH"; path = "it-CH.lproj/Localizable.strings"; sourceTree = "<group>"; };
3ACD32D2297F2D1E002F68B9 /* it-CH */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "it-CH"; path = "it-CH.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3AEABD20297CCFA8003F2975 /* de-DE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "de-DE"; path = "de-DE.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AEABD21297CCFA8003F2975 /* de-DE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "de-DE"; path = "de-DE.lproj/Localizable.strings"; sourceTree = "<group>"; };
3AEABD22297CCFA8003F2975 /* de-DE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "de-DE"; path = "de-DE.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -419,11 +422,19 @@
4CF0ABE02981A83900D66079 /* MutelistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutelistView.swift; sourceTree = "<group>"; };
4CF0ABE22981BC7D00D66079 /* UserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserView.swift; sourceTree = "<group>"; };
4CF0ABE42981EE0C00D66079 /* EULAView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EULAView.swift; sourceTree = "<group>"; };
4CF0ABE6298444FC00D66079 /* MutedEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutedEventView.swift; sourceTree = "<group>"; };
4CF0ABE829844AF100D66079 /* AnyCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyCodable.swift; sourceTree = "<group>"; };
4CF0ABEB29844B4700D66079 /* AnyDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyDecodable.swift; sourceTree = "<group>"; };
4CF0ABED29844B5500D66079 /* AnyEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = "<group>"; };
4CF0ABEF29857E9200D66079 /* Bech32Object.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Object.swift; sourceTree = "<group>"; };
4CF0ABF52985CD5500D66079 /* UserSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSearch.swift; sourceTree = "<group>"; };
4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; };
5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
6439E013296790CF0020672B /* ProfileZoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZoomView.swift; sourceTree = "<group>"; };
647D9A8C2968520300A295DE /* SideMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = "<group>"; };
64FBD06E296255C400D9D3B2 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
7C45AE70297353390031D7BC /* KFImageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFImageModel.swift; sourceTree = "<group>"; };
7C60CAEE298471A1009C80D6 /* CoreSVG.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreSVG.swift; sourceTree = "<group>"; };
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomableScrollView.swift; sourceTree = "<group>"; };
9609F057296E220800069BF3 /* BannerImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerImageView.swift; sourceTree = "<group>"; };
BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; };
@@ -440,8 +451,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
7C45AE6F297352F90031D7BC /* SVGKitSwift in Frameworks */,
7C45AE6D297352F90031D7BC /* SVGKit in Frameworks */,
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
6C7DE41F2955169800E66263 /* Vault in Frameworks */,
4CE6DF1227F7A2B300C66700 /* Starscream in Frameworks */,
@@ -580,6 +589,7 @@
4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup;
children = (
4CF0ABF42985CD4200D66079 /* Posting */,
4CF0ABDF2981A83000D66079 /* Muting */,
4CC7AAEE297F11B300430951 /* Events */,
3AA24800297E3DAE0090C62D /* Reposts */,
@@ -635,6 +645,7 @@
4CF0ABD529817F5B00D66079 /* ReportView.swift */,
4CF0ABE42981EE0C00D66079 /* EULAView.swift */,
3AA247FE297E3D900090C62D /* RepostsView.swift */,
5C513FCB2984ACA60072348F /* QRCodeView.swift */,
);
path = Views;
sourceTree = "<group>";
@@ -662,6 +673,7 @@
4C7FF7D628233637009601DB /* Util */ = {
isa = PBXGroup;
children = (
4CF0ABEA29844B2F00D66079 /* AnyCodable */,
4C3A1D322960DB0500558C0F /* Markdown.swift */,
4CEE2AF4280B29E600AB5EEF /* TimeAgo.swift */,
4CE4F8CC281352B30009DFBB /* Notifications.swift */,
@@ -677,6 +689,8 @@
64FBD06E296255C400D9D3B2 /* Theme.swift */,
4CB8838529656C8B00DC99E7 /* NIP05.swift */,
4CF0ABD72981980C00D66079 /* Lists.swift */,
4CF0ABEF29857E9200D66079 /* Bech32Object.swift */,
7C60CAEE298471A1009C80D6 /* CoreSVG.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -708,6 +722,7 @@
4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */,
4CC7AAF7297F1CEE00430951 /* EventProfile.swift */,
4CC7AAF9297F64AC00430951 /* EventMenu.swift */,
4CF0ABE6298444FC00D66079 /* MutedEventView.swift */,
);
path = Events;
sourceTree = "<group>";
@@ -824,6 +839,24 @@
path = Muting;
sourceTree = "<group>";
};
4CF0ABEA29844B2F00D66079 /* AnyCodable */ = {
isa = PBXGroup;
children = (
4CF0ABE829844AF100D66079 /* AnyCodable.swift */,
4CF0ABEB29844B4700D66079 /* AnyDecodable.swift */,
4CF0ABED29844B5500D66079 /* AnyEncodable.swift */,
);
path = AnyCodable;
sourceTree = "<group>";
};
4CF0ABF42985CD4200D66079 /* Posting */ = {
isa = PBXGroup;
children = (
4CF0ABF52985CD5500D66079 /* UserSearch.swift */,
);
path = Posting;
sourceTree = "<group>";
};
F7F0BA23297892AE009531F3 /* Modifiers */ = {
isa = PBXGroup;
children = (
@@ -853,8 +886,6 @@
4C649880286E0EE300EAE2B3 /* secp256k1 */,
4C06670328FC7EC500038D2A /* Kingfisher */,
6C7DE41E2955169800E66263 /* Vault */,
7C45AE6C297352F90031D7BC /* SVGKit */,
7C45AE6E297352F90031D7BC /* SVGKitSwift */,
);
productName = damus;
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
@@ -933,7 +964,6 @@
"tr-TR",
"fr-FR",
"lv-LV",
"it-CH",
"it-IT",
);
mainGroup = 4CE6DEDA27F7A08100C66700;
@@ -942,7 +972,6 @@
4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */,
4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */,
6C7DE41D2955169800E66263 /* XCRemoteSwiftPackageReference "Vault" */,
7C45AE6B297352F90031D7BC /* XCRemoteSwiftPackageReference "SVGKit" */,
);
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
projectDirPath = "";
@@ -996,6 +1025,7 @@
4C216F34286F5ACD00040376 /* DMView.swift in Sources */,
4C3EA64428FF558100C48A62 /* sha256.c in Sources */,
4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */,
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */,
4C363AA828297703006E126D /* InsertSort.swift in Sources */,
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */,
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */,
@@ -1003,6 +1033,7 @@
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */,
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */,
4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */,
4C3EA67728FF7A9800C48A62 /* talstr.c in Sources */,
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
@@ -1024,6 +1055,7 @@
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */,
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */,
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */,
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */,
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
@@ -1043,6 +1075,7 @@
4C3EA66828FF5F9900C48A62 /* hex.c in Sources */,
E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */,
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */,
4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */,
4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */,
4C75EFB128049D510006080F /* NostrResponse.swift in Sources */,
4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */,
@@ -1053,6 +1086,7 @@
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */,
7C45AE71297353390031D7BC /* KFImageModel.swift in Sources */,
4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */,
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
4CC7AAF2297F129C00430951 /* EmbeddedEventView.swift in Sources */,
4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */,
@@ -1088,6 +1122,7 @@
4CF0ABE52981EE0C00D66079 /* EULAView.swift in Sources */,
4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */,
4C3EA64128FF553900C48A62 /* hash_u5.c in Sources */,
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */,
4C3EA64F28FF59F200C48A62 /* tal.c in Sources */,
4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */,
4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */,
@@ -1097,6 +1132,7 @@
4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */,
4CF0ABD82981980C00D66079 /* Lists.swift in Sources */,
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */,
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */,
4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */,
@@ -1122,6 +1158,7 @@
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */,
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */,
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
@@ -1197,7 +1234,6 @@
3AEB8005297CCEA900713A25 /* tr-TR */,
3A4F3322297CCFEE004B5F72 /* fr-FR */,
3A185A06297F2C3800F4BDC0 /* lv-LV */,
3ACD32D2297F2D1E002F68B9 /* it-CH */,
3A929C22297F2CF80090925E /* it-IT */,
);
name = Localizable.stringsdict;
@@ -1212,7 +1248,6 @@
3AEB8003297CCEA800713A25 /* tr-TR */,
3A4F3320297CCFEE004B5F72 /* fr-FR */,
3A185A04297F2C3800F4BDC0 /* lv-LV */,
3ACD32D0297F2D1E002F68B9 /* it-CH */,
3A929C20297F2CF80090925E /* it-IT */,
);
name = InfoPlist.strings;
@@ -1227,7 +1262,6 @@
3AEB8004297CCEA800713A25 /* tr-TR */,
3A4F3321297CCFEE004B5F72 /* fr-FR */,
3A185A05297F2C3800F4BDC0 /* lv-LV */,
3ACD32D1297F2D1E002F68B9 /* it-CH */,
3A929C21297F2CF80090925E /* it-IT */,
);
name = Localizable.strings;
@@ -1364,7 +1398,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 11;
CURRENT_PROJECT_VERSION = 12;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -1405,7 +1439,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 11;
CURRENT_PROJECT_VERSION = 12;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -1587,14 +1621,6 @@
minimumVersion = 1.0.0;
};
};
7C45AE6B297352F90031D7BC /* XCRemoteSwiftPackageReference "SVGKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SVGKit/SVGKit";
requirement = {
kind = revision;
revision = e1f13e27b1e4c0ffe20e7d8d3984bf49c2a584d0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@@ -1618,16 +1644,6 @@
package = 6C7DE41D2955169800E66263 /* XCRemoteSwiftPackageReference "Vault" */;
productName = Vault;
};
7C45AE6C297352F90031D7BC /* SVGKit */ = {
isa = XCSwiftPackageProductDependency;
package = 7C45AE6B297352F90031D7BC /* XCRemoteSwiftPackageReference "SVGKit" */;
productName = SVGKit;
};
7C45AE6E297352F90031D7BC /* SVGKitSwift */ = {
isa = XCSwiftPackageProductDependency;
package = 7C45AE6B297352F90031D7BC /* XCRemoteSwiftPackageReference "SVGKit" */;
productName = SVGKitSwift;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 4CE6DEDB27F7A08100C66700 /* Project object */;

View File

@@ -26,14 +26,6 @@
"version" : "4.0.4"
}
},
{
"identity" : "svgkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SVGKit/SVGKit",
"state" : {
"revision" : "e1f13e27b1e4c0ffe20e7d8d3984bf49c2a584d0"
}
},
{
"identity" : "vault",
"kind" : "remoteSourceControl",

View File

@@ -302,7 +302,7 @@ struct ContentView: View {
case .report(let target):
MaybeReportView(target: target)
case .post:
PostView(replying_to: nil, references: [])
PostView(replying_to: nil, references: [], damus_state: damus_state!)
case .reply(let event):
ReplyView(replying_to: event, damus: damus_state!)
}

View File

@@ -284,7 +284,7 @@ class HomeModel: ObservableObject {
our_contacts_filter.authors = [damus_state.pubkey]
var our_blocklist_filter = NostrFilter.filter_kinds([30000])
our_blocklist_filter.parameter = "mute"
our_blocklist_filter.parameter = ["mute"]
our_blocklist_filter.authors = [damus_state.pubkey]
var dms_filter = NostrFilter.filter_kinds([
@@ -410,15 +410,8 @@ class HomeModel: ObservableObject {
return ok
}
func should_hide_event(_ ev: NostrEvent) -> Bool {
if damus_state.contacts.is_muted(ev.pubkey) {
return true
}
return !ev.should_show_event
}
func handle_text_event(sub_id: String, _ ev: NostrEvent) {
if should_hide_event(ev) {
if should_hide_event(contacts: damus_state.contacts, ev: ev) {
return
}
@@ -726,3 +719,11 @@ func event_has_our_pubkey(_ ev: NostrEvent, our_pubkey: String) -> Bool {
return false
}
func should_hide_event(contacts: Contacts, ev: NostrEvent) -> Bool {
if contacts.is_muted(ev.pubkey) {
return true
}
return !ev.should_show_event
}

View File

@@ -5,9 +5,8 @@
// Created by Oleg Abalonski on 1/11/23.
//
import Foundation
import UIKit
import Kingfisher
import SVGKit
class KFImageModel: ObservableObject {
@@ -80,8 +79,15 @@ struct CustomImageProcessor: ImageProcessor {
}
// Handle SVG image
if let svgImage = SVGKImage(data: data), let image = svgImage.uiImage {
return image.kf.scaled(to: options.scaleFactor)
if let dataString = String(data: data, encoding: .utf8),
let svg = SVG(dataString) {
let render = UIGraphicsImageRenderer(size: svg.size)
let image = render.image { context in
svg.draw(in: context.cgContext)
}
return image.kf.scaled(to: options.scaleFactor)
}
return DefaultImageProcessor.default.process(item: item, options: options)

View File

@@ -30,6 +30,10 @@ class SearchHomeModel: ObservableObject {
return filter
}
func filter_muted() {
events = events.filter { !should_hide_event(contacts: damus_state.contacts, ev: $0) }
}
func subscribe() {
loading = true
damus_state.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event)
@@ -50,7 +54,7 @@ class SearchHomeModel: ObservableObject {
guard sub_id == self.base_subid || sub_id == self.profiles_subid else {
return
}
if ev.is_textlike && ev.should_show_event && !ev.is_reply(nil) {
if ev.is_textlike && !should_hide_event(contacts: damus_state.contacts, ev: ev) && !ev.is_reply(nil) {
if seen_pubkey.contains(ev.pubkey) {
return
}

View File

@@ -8,7 +8,7 @@
import Foundation
struct Profile: Codable {
var value: [String: String]
var value: [String: AnyCodable]
init (name: String?, display_name: String?, about: String?, picture: String?, banner: String?, website: String?, lud06: String?, lud16: String?, nip05: String?) {
self.value = [:]
@@ -23,50 +23,71 @@ struct Profile: Codable {
self.nip05 = nip05
}
private func str(_ str: String) -> String? {
guard let val = self.value[str] else{
return nil
}
guard let s = val.value as? String else {
return nil
}
return s
}
private mutating func set_str(_ key: String, _ val: String?) {
if val == nil {
self.value.removeValue(forKey: key)
return
}
self.value[key] = AnyCodable.init(val)
}
var display_name: String? {
get { return value["display_name"]; }
set(s) { value["display_name"] = s }
get { return str("display_name"); }
set(s) { set_str("display_name", s) }
}
var name: String? {
get { return value["name"]; }
set(s) { value["name"] = s }
get { return str("name"); }
set(s) { set_str("name", s) }
}
var about: String? {
get { return value["about"]; }
set(s) { value["about"] = s }
get { return str("about"); }
set(s) { set_str("about", s) }
}
var picture: String? {
get { return value["picture"]; }
set(s) { value["picture"] = s }
get { return str("picture"); }
set(s) { set_str("picture", s) }
}
var banner: String? {
get { return value["banner"]; }
set(s) { value["banner"] = s }
get { return str("banner"); }
set(s) { set_str("banner", s) }
}
var website: String? {
get { return value["website"]; }
set(s) { value["website"] = s }
get { return str("website"); }
set(s) { set_str("website", s) }
}
var lud06: String? {
get { return str("lud06"); }
set(s) { set_str("lud06", s) }
}
var lud16: String? {
get { return str("lud16"); }
set(s) { set_str("lud16", s) }
}
var website_url: URL? {
return self.website.flatMap { URL(string: $0) }
}
var lud06: String? {
get { return value["lud06"]; }
set(s) { value["lud06"] = s }
}
var lud16: String? {
get { return value["lud16"]; }
set(s) { value["lud16"] = s }
}
var lnurl: String? {
guard let addr = lud06 ?? lud16 else {
return nil;
@@ -80,8 +101,8 @@ struct Profile: Codable {
}
var nip05: String? {
get { return value["nip05"]; }
set(s) { value["nip05"] = s }
get { return str("nip05"); }
set(s) { set_str("nip05", s) }
}
var lightning_uri: URL? {
@@ -90,7 +111,7 @@ struct Profile: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.value = try container.decode([String: String].self)
self.value = try container.decode([String: AnyCodable].self)
}
func encode(to encoder: Encoder) throws {

View File

@@ -27,7 +27,7 @@ struct KeyEvent {
let relay_url: String
}
struct ReferencedId: Identifiable, Hashable {
struct ReferencedId: Identifiable, Hashable, Equatable {
let ref_id: String
let relay_id: String?
let key: String

View File

@@ -7,7 +7,7 @@
import Foundation
struct NostrFilter: Codable {
struct NostrFilter: Codable, Equatable {
var ids: [String]?
var kinds: [Int]?
var referenced_ids: [String]?
@@ -17,7 +17,7 @@ struct NostrFilter: Codable {
var limit: UInt32?
var authors: [String]?
var hashtag: [String]? = nil
var parameter: String? = nil
var parameter: [String]? = nil
private enum CodingKeys : String, CodingKey {
case ids

View File

@@ -8,7 +8,7 @@
import Foundation
enum NostrLink {
enum NostrLink: Equatable {
case ref(ReferencedId)
case filter(NostrFilter)
}
@@ -101,6 +101,24 @@ func decode_universal_link(_ s: String) -> NostrLink? {
return nil
}
func decode_nostr_bech32_uri(_ s: String) -> NostrLink? {
guard let obj = Bech32Object.parse(s) else {
return nil
}
switch obj {
case .nsec(let privkey):
guard let pubkey = privkey_to_pubkey(privkey: privkey) else {
return nil
}
return .ref(ReferencedId(ref_id: pubkey, relay_id: nil, key: "p"))
case .npub(let pubkey):
return .ref(ReferencedId(ref_id: pubkey, relay_id: nil, key: "p"))
case .note(let id):
return .ref(ReferencedId(ref_id: id, relay_id: nil, key: "e"))
}
}
func decode_nostr_uri(_ s: String) -> NostrLink? {
if s.starts(with: "https://damus.io/") {
return decode_universal_link(s)
@@ -122,5 +140,15 @@ func decode_nostr_uri(_ s: String) -> NostrLink? {
return .filter(NostrFilter.filter_hashtag([parts[1].lowercased()]))
}
return tag_to_refid(parts).map { .ref($0) }
if let rid = tag_to_refid(parts) {
return .ref(rid)
}
guard parts.count == 1 else {
return nil
}
let part = parts[0]
return decode_nostr_bech32_uri(part)
}

View File

@@ -0,0 +1,147 @@
import Foundation
/**
A type-erased `Codable` value.
The `AnyCodable` type forwards encoding and decoding responsibilities
to an underlying value, hiding its specific underlying type.
You can encode or decode mixed-type values in dictionaries
and other collections that require `Encodable` or `Decodable` conformance
by declaring their contained type to be `AnyCodable`.
- SeeAlso: `AnyEncodable`
- SeeAlso: `AnyDecodable`
*/
@frozen public struct AnyCodable: Codable {
public let value: Any
public init<T>(_ value: T?) {
self.value = value ?? ()
}
}
extension AnyCodable: _AnyEncodable, _AnyDecodable {}
extension AnyCodable: Equatable {
public static func == (lhs: AnyCodable, rhs: AnyCodable) -> Bool {
switch (lhs.value, rhs.value) {
case is (Void, Void):
return true
case let (lhs as Bool, rhs as Bool):
return lhs == rhs
case let (lhs as Int, rhs as Int):
return lhs == rhs
case let (lhs as Int8, rhs as Int8):
return lhs == rhs
case let (lhs as Int16, rhs as Int16):
return lhs == rhs
case let (lhs as Int32, rhs as Int32):
return lhs == rhs
case let (lhs as Int64, rhs as Int64):
return lhs == rhs
case let (lhs as UInt, rhs as UInt):
return lhs == rhs
case let (lhs as UInt8, rhs as UInt8):
return lhs == rhs
case let (lhs as UInt16, rhs as UInt16):
return lhs == rhs
case let (lhs as UInt32, rhs as UInt32):
return lhs == rhs
case let (lhs as UInt64, rhs as UInt64):
return lhs == rhs
case let (lhs as Float, rhs as Float):
return lhs == rhs
case let (lhs as Double, rhs as Double):
return lhs == rhs
case let (lhs as String, rhs as String):
return lhs == rhs
case let (lhs as [String: AnyCodable], rhs as [String: AnyCodable]):
return lhs == rhs
case let (lhs as [AnyCodable], rhs as [AnyCodable]):
return lhs == rhs
case let (lhs as [String: Any], rhs as [String: Any]):
return NSDictionary(dictionary: lhs) == NSDictionary(dictionary: rhs)
case let (lhs as [Any], rhs as [Any]):
return NSArray(array: lhs) == NSArray(array: rhs)
case is (NSNull, NSNull):
return true
default:
return false
}
}
}
extension AnyCodable: CustomStringConvertible {
public var description: String {
switch value {
case is Void:
return String(describing: nil as Any?)
case let value as CustomStringConvertible:
return value.description
default:
return String(describing: value)
}
}
}
extension AnyCodable: CustomDebugStringConvertible {
public var debugDescription: String {
switch value {
case let value as CustomDebugStringConvertible:
return "AnyCodable(\(value.debugDescription))"
default:
return "AnyCodable(\(description))"
}
}
}
extension AnyCodable: ExpressibleByNilLiteral {}
extension AnyCodable: ExpressibleByBooleanLiteral {}
extension AnyCodable: ExpressibleByIntegerLiteral {}
extension AnyCodable: ExpressibleByFloatLiteral {}
extension AnyCodable: ExpressibleByStringLiteral {}
extension AnyCodable: ExpressibleByStringInterpolation {}
extension AnyCodable: ExpressibleByArrayLiteral {}
extension AnyCodable: ExpressibleByDictionaryLiteral {}
extension AnyCodable: Hashable {
public func hash(into hasher: inout Hasher) {
switch value {
case let value as Bool:
hasher.combine(value)
case let value as Int:
hasher.combine(value)
case let value as Int8:
hasher.combine(value)
case let value as Int16:
hasher.combine(value)
case let value as Int32:
hasher.combine(value)
case let value as Int64:
hasher.combine(value)
case let value as UInt:
hasher.combine(value)
case let value as UInt8:
hasher.combine(value)
case let value as UInt16:
hasher.combine(value)
case let value as UInt32:
hasher.combine(value)
case let value as UInt64:
hasher.combine(value)
case let value as Float:
hasher.combine(value)
case let value as Double:
hasher.combine(value)
case let value as String:
hasher.combine(value)
case let value as [String: AnyCodable]:
hasher.combine(value)
case let value as [AnyCodable]:
hasher.combine(value)
default:
break
}
}
}

View File

@@ -0,0 +1,188 @@
#if canImport(Foundation)
import Foundation
#endif
/**
A type-erased `Decodable` value.
The `AnyDecodable` type forwards decoding responsibilities
to an underlying value, hiding its specific underlying type.
You can decode mixed-type values in dictionaries
and other collections that require `Decodable` conformance
by declaring their contained type to be `AnyDecodable`:
let json = """
{
"boolean": true,
"integer": 42,
"double": 3.141592653589793,
"string": "string",
"array": [1, 2, 3],
"nested": {
"a": "alpha",
"b": "bravo",
"c": "charlie"
},
"null": null
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
let dictionary = try! decoder.decode([String: AnyDecodable].self, from: json)
*/
@frozen public struct AnyDecodable: Decodable {
public let value: Any
public init<T>(_ value: T?) {
self.value = value ?? ()
}
}
@usableFromInline
protocol _AnyDecodable {
var value: Any { get }
init<T>(_ value: T?)
}
extension AnyDecodable: _AnyDecodable {}
extension _AnyDecodable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {
#if canImport(Foundation)
self.init(NSNull())
#else
self.init(Optional<Self>.none)
#endif
} else if let bool = try? container.decode(Bool.self) {
self.init(bool)
} else if let int = try? container.decode(Int.self) {
self.init(int)
} else if let uint = try? container.decode(UInt.self) {
self.init(uint)
} else if let double = try? container.decode(Double.self) {
self.init(double)
} else if let string = try? container.decode(String.self) {
self.init(string)
} else if let array = try? container.decode([AnyDecodable].self) {
self.init(array.map { $0.value })
} else if let dictionary = try? container.decode([String: AnyDecodable].self) {
self.init(dictionary.mapValues { $0.value })
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "AnyDecodable value cannot be decoded")
}
}
}
extension AnyDecodable: Equatable {
public static func == (lhs: AnyDecodable, rhs: AnyDecodable) -> Bool {
switch (lhs.value, rhs.value) {
#if canImport(Foundation)
case is (NSNull, NSNull), is (Void, Void):
return true
#endif
case let (lhs as Bool, rhs as Bool):
return lhs == rhs
case let (lhs as Int, rhs as Int):
return lhs == rhs
case let (lhs as Int8, rhs as Int8):
return lhs == rhs
case let (lhs as Int16, rhs as Int16):
return lhs == rhs
case let (lhs as Int32, rhs as Int32):
return lhs == rhs
case let (lhs as Int64, rhs as Int64):
return lhs == rhs
case let (lhs as UInt, rhs as UInt):
return lhs == rhs
case let (lhs as UInt8, rhs as UInt8):
return lhs == rhs
case let (lhs as UInt16, rhs as UInt16):
return lhs == rhs
case let (lhs as UInt32, rhs as UInt32):
return lhs == rhs
case let (lhs as UInt64, rhs as UInt64):
return lhs == rhs
case let (lhs as Float, rhs as Float):
return lhs == rhs
case let (lhs as Double, rhs as Double):
return lhs == rhs
case let (lhs as String, rhs as String):
return lhs == rhs
case let (lhs as [String: AnyDecodable], rhs as [String: AnyDecodable]):
return lhs == rhs
case let (lhs as [AnyDecodable], rhs as [AnyDecodable]):
return lhs == rhs
default:
return false
}
}
}
extension AnyDecodable: CustomStringConvertible {
public var description: String {
switch value {
case is Void:
return String(describing: nil as Any?)
case let value as CustomStringConvertible:
return value.description
default:
return String(describing: value)
}
}
}
extension AnyDecodable: CustomDebugStringConvertible {
public var debugDescription: String {
switch value {
case let value as CustomDebugStringConvertible:
return "AnyDecodable(\(value.debugDescription))"
default:
return "AnyDecodable(\(description))"
}
}
}
extension AnyDecodable: Hashable {
public func hash(into hasher: inout Hasher) {
switch value {
case let value as Bool:
hasher.combine(value)
case let value as Int:
hasher.combine(value)
case let value as Int8:
hasher.combine(value)
case let value as Int16:
hasher.combine(value)
case let value as Int32:
hasher.combine(value)
case let value as Int64:
hasher.combine(value)
case let value as UInt:
hasher.combine(value)
case let value as UInt8:
hasher.combine(value)
case let value as UInt16:
hasher.combine(value)
case let value as UInt32:
hasher.combine(value)
case let value as UInt64:
hasher.combine(value)
case let value as Float:
hasher.combine(value)
case let value as Double:
hasher.combine(value)
case let value as String:
hasher.combine(value)
case let value as [String: AnyDecodable]:
hasher.combine(value)
case let value as [AnyDecodable]:
hasher.combine(value)
default:
break
}
}
}

View File

@@ -0,0 +1,291 @@
#if canImport(Foundation)
import Foundation
#endif
/**
A type-erased `Encodable` value.
The `AnyEncodable` type forwards encoding responsibilities
to an underlying value, hiding its specific underlying type.
You can encode mixed-type values in dictionaries
and other collections that require `Encodable` conformance
by declaring their contained type to be `AnyEncodable`:
let dictionary: [String: AnyEncodable] = [
"boolean": true,
"integer": 42,
"double": 3.141592653589793,
"string": "string",
"array": [1, 2, 3],
"nested": [
"a": "alpha",
"b": "bravo",
"c": "charlie"
],
"null": nil
]
let encoder = JSONEncoder()
let json = try! encoder.encode(dictionary)
*/
@frozen public struct AnyEncodable: Encodable {
public let value: Any
public init<T>(_ value: T?) {
self.value = value ?? ()
}
}
@usableFromInline
protocol _AnyEncodable {
var value: Any { get }
init<T>(_ value: T?)
}
extension AnyEncodable: _AnyEncodable {}
// MARK: - Encodable
extension _AnyEncodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch value {
#if canImport(Foundation)
case is NSNull:
try container.encodeNil()
#endif
case is Void:
try container.encodeNil()
case let bool as Bool:
try container.encode(bool)
case let int as Int:
try container.encode(int)
case let int8 as Int8:
try container.encode(int8)
case let int16 as Int16:
try container.encode(int16)
case let int32 as Int32:
try container.encode(int32)
case let int64 as Int64:
try container.encode(int64)
case let uint as UInt:
try container.encode(uint)
case let uint8 as UInt8:
try container.encode(uint8)
case let uint16 as UInt16:
try container.encode(uint16)
case let uint32 as UInt32:
try container.encode(uint32)
case let uint64 as UInt64:
try container.encode(uint64)
case let float as Float:
try container.encode(float)
case let double as Double:
try container.encode(double)
case let string as String:
try container.encode(string)
#if canImport(Foundation)
case let number as NSNumber:
try encode(nsnumber: number, into: &container)
case let date as Date:
try container.encode(date)
case let url as URL:
try container.encode(url)
#endif
case let array as [Any?]:
try container.encode(array.map { AnyEncodable($0) })
case let dictionary as [String: Any?]:
try container.encode(dictionary.mapValues { AnyEncodable($0) })
case let encodable as Encodable:
try encodable.encode(to: encoder)
default:
let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "AnyEncodable value cannot be encoded")
throw EncodingError.invalidValue(value, context)
}
}
#if canImport(Foundation)
private func encode(nsnumber: NSNumber, into container: inout SingleValueEncodingContainer) throws {
switch Character(Unicode.Scalar(UInt8(nsnumber.objCType.pointee))) {
case "B":
try container.encode(nsnumber.boolValue)
case "c":
try container.encode(nsnumber.int8Value)
case "s":
try container.encode(nsnumber.int16Value)
case "i", "l":
try container.encode(nsnumber.int32Value)
case "q":
try container.encode(nsnumber.int64Value)
case "C":
try container.encode(nsnumber.uint8Value)
case "S":
try container.encode(nsnumber.uint16Value)
case "I", "L":
try container.encode(nsnumber.uint32Value)
case "Q":
try container.encode(nsnumber.uint64Value)
case "f":
try container.encode(nsnumber.floatValue)
case "d":
try container.encode(nsnumber.doubleValue)
default:
let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "NSNumber cannot be encoded because its type is not handled")
throw EncodingError.invalidValue(nsnumber, context)
}
}
#endif
}
extension AnyEncodable: Equatable {
public static func == (lhs: AnyEncodable, rhs: AnyEncodable) -> Bool {
switch (lhs.value, rhs.value) {
case is (Void, Void):
return true
case let (lhs as Bool, rhs as Bool):
return lhs == rhs
case let (lhs as Int, rhs as Int):
return lhs == rhs
case let (lhs as Int8, rhs as Int8):
return lhs == rhs
case let (lhs as Int16, rhs as Int16):
return lhs == rhs
case let (lhs as Int32, rhs as Int32):
return lhs == rhs
case let (lhs as Int64, rhs as Int64):
return lhs == rhs
case let (lhs as UInt, rhs as UInt):
return lhs == rhs
case let (lhs as UInt8, rhs as UInt8):
return lhs == rhs
case let (lhs as UInt16, rhs as UInt16):
return lhs == rhs
case let (lhs as UInt32, rhs as UInt32):
return lhs == rhs
case let (lhs as UInt64, rhs as UInt64):
return lhs == rhs
case let (lhs as Float, rhs as Float):
return lhs == rhs
case let (lhs as Double, rhs as Double):
return lhs == rhs
case let (lhs as String, rhs as String):
return lhs == rhs
case let (lhs as [String: AnyEncodable], rhs as [String: AnyEncodable]):
return lhs == rhs
case let (lhs as [AnyEncodable], rhs as [AnyEncodable]):
return lhs == rhs
default:
return false
}
}
}
extension AnyEncodable: CustomStringConvertible {
public var description: String {
switch value {
case is Void:
return String(describing: nil as Any?)
case let value as CustomStringConvertible:
return value.description
default:
return String(describing: value)
}
}
}
extension AnyEncodable: CustomDebugStringConvertible {
public var debugDescription: String {
switch value {
case let value as CustomDebugStringConvertible:
return "AnyEncodable(\(value.debugDescription))"
default:
return "AnyEncodable(\(description))"
}
}
}
extension AnyEncodable: ExpressibleByNilLiteral {}
extension AnyEncodable: ExpressibleByBooleanLiteral {}
extension AnyEncodable: ExpressibleByIntegerLiteral {}
extension AnyEncodable: ExpressibleByFloatLiteral {}
extension AnyEncodable: ExpressibleByStringLiteral {}
extension AnyEncodable: ExpressibleByStringInterpolation {}
extension AnyEncodable: ExpressibleByArrayLiteral {}
extension AnyEncodable: ExpressibleByDictionaryLiteral {}
extension _AnyEncodable {
public init(nilLiteral _: ()) {
self.init(nil as Any?)
}
public init(booleanLiteral value: Bool) {
self.init(value)
}
public init(integerLiteral value: Int) {
self.init(value)
}
public init(floatLiteral value: Double) {
self.init(value)
}
public init(extendedGraphemeClusterLiteral value: String) {
self.init(value)
}
public init(stringLiteral value: String) {
self.init(value)
}
public init(arrayLiteral elements: Any...) {
self.init(elements)
}
public init(dictionaryLiteral elements: (AnyHashable, Any)...) {
self.init([AnyHashable: Any](elements, uniquingKeysWith: { first, _ in first }))
}
}
extension AnyEncodable: Hashable {
public func hash(into hasher: inout Hasher) {
switch value {
case let value as Bool:
hasher.combine(value)
case let value as Int:
hasher.combine(value)
case let value as Int8:
hasher.combine(value)
case let value as Int16:
hasher.combine(value)
case let value as Int32:
hasher.combine(value)
case let value as Int64:
hasher.combine(value)
case let value as UInt:
hasher.combine(value)
case let value as UInt8:
hasher.combine(value)
case let value as UInt16:
hasher.combine(value)
case let value as UInt32:
hasher.combine(value)
case let value as UInt64:
hasher.combine(value)
case let value as Float:
hasher.combine(value)
case let value as Double:
hasher.combine(value)
case let value as String:
hasher.combine(value)
case let value as [String: AnyEncodable]:
hasher.combine(value)
case let value as [AnyEncodable]:
hasher.combine(value)
default:
break
}
}
}

View File

@@ -0,0 +1,31 @@
//
// Bech32Object.swift
// damus
//
// Created by William Casarin on 2023-01-28.
//
import Foundation
enum Bech32Object {
case nsec(String)
case npub(String)
case note(String)
static func parse(_ str: String) -> Bech32Object? {
guard let decoded = try? bech32_decode(str) else {
return nil
}
if decoded.hrp == "npub" {
return .npub(hex_encode(decoded.data))
} else if decoded.hrp == "nsec" {
return .nsec(hex_encode(decoded.data))
} else if decoded.hrp == "note" {
return .note(hex_encode(decoded.data))
}
return nil
}
}

101
damus/Util/CoreSVG.swift Normal file
View File

@@ -0,0 +1,101 @@
//
// CoreSVG.swift
// damus
//
// Created by Oleg Abalonski on 1/27/23.
// Ref: https://gist.github.com/ollieatkinson/eb87a82fcb5500d5561fed8b0900a9f7
import Darwin
import Foundation
import UIKit
@objc
class CGSVGDocument: NSObject { }
var CGSVGDocumentRetain: (@convention(c) (CGSVGDocument?) -> Unmanaged<CGSVGDocument>?) = load("CGSVGDocumentRetain")
var CGSVGDocumentRelease: (@convention(c) (CGSVGDocument?) -> Void) = load("CGSVGDocumentRelease")
var CGSVGDocumentCreateFromData: (@convention(c) (CFData?, CFDictionary?) -> Unmanaged<CGSVGDocument>?) = load("CGSVGDocumentCreateFromData")
var CGContextDrawSVGDocument: (@convention(c) (CGContext?, CGSVGDocument?) -> Void) = load("CGContextDrawSVGDocument")
var CGSVGDocumentGetCanvasSize: (@convention(c) (CGSVGDocument?) -> CGSize) = load("CGSVGDocumentGetCanvasSize")
typealias ImageWithCGSVGDocument = @convention(c) (AnyObject, Selector, CGSVGDocument) -> UIImage
var ImageWithCGSVGDocumentSEL: Selector = NSSelectorFromString("_imageWithCGSVGDocument:")
let CoreSVG = dlopen("/System/Library/PrivateFrameworks/CoreSVG.framework/CoreSVG", RTLD_NOW)
func load<T>(_ name: String) -> T {
unsafeBitCast(dlsym(CoreSVG, name), to: T.self)
}
public class SVG {
deinit { CGSVGDocumentRelease(document) }
let document: CGSVGDocument
public convenience init?(_ value: String) {
guard let data = value.data(using: .utf8) else { return nil }
self.init(data)
}
public init?(_ data: Data) {
guard let document = CGSVGDocumentCreateFromData(data as CFData, nil)?.takeUnretainedValue() else { return nil }
guard CGSVGDocumentGetCanvasSize(document) != .zero else { return nil }
self.document = document
}
public var size: CGSize {
CGSVGDocumentGetCanvasSize(document)
}
public func image() -> UIImage? {
let ImageWithCGSVGDocument = unsafeBitCast(UIImage.self.method(for: ImageWithCGSVGDocumentSEL), to: ImageWithCGSVGDocument.self)
let image = ImageWithCGSVGDocument(UIImage.self, ImageWithCGSVGDocumentSEL, document)
return image
}
public func draw(in context: CGContext) {
draw(in: context, size: size)
}
public func draw(in context: CGContext, size target: CGSize) {
var target = target
let ratio = (
x: target.width / size.width,
y: target.height / size.height
)
let rect = (
document: CGRect(origin: .zero, size: size), ()
)
let scale: (x: CGFloat, y: CGFloat)
if target.width <= 0 {
scale = (ratio.y, ratio.y)
target.width = size.width * scale.x
} else if target.height <= 0 {
scale = (ratio.x, ratio.x)
target.width = size.width * scale.y
} else {
let min = min(ratio.x, ratio.y)
scale = (min, min)
target.width = size.width * scale.x
target.height = size.height * scale.y
}
let transform = (
scale: CGAffineTransform(scaleX: scale.x, y: scale.y),
aspect: CGAffineTransform(translationX: (target.width / scale.x - rect.document.width) / 2, y: (target.height / scale.y - rect.document.height) / 2)
)
context.translateBy(x: 0, y: target.height)
context.scaleBy(x: 1, y: -1)
context.concatenate(transform.scale)
context.concatenate(transform.aspect)
CGContextDrawSVGDocument(context, document)
}
}

View File

@@ -24,7 +24,7 @@ func create_or_update_list_event(keypair: FullKeypair, mprev: NostrEvent?, to_ad
}
}
var tags = [["d", list_name], [list_type, to_add]]
let tags = [["d", list_name], [list_type, to_add]]
let ev = NostrEvent(content: "", pubkey: pubkey, kind: 30000, tags: tags)
ev.tags = tags

View File

@@ -71,8 +71,8 @@ struct EventDetailView: View {
}
toggle_thread_view()
}
case .event(let ev, let highlight):
EventView(event: ev, has_action_bar: true, damus: damus)
case .event(let ev, _):
EventView(damus: damus, event: ev, has_action_bar: true)
.onTapGesture {
if thread.initial_event.id == ev.id {
toggle_thread_view()

View File

@@ -35,7 +35,7 @@ struct EventView: View {
@EnvironmentObject var action_bar: ActionBarModel
init(event: NostrEvent, has_action_bar: Bool, damus: DamusState) {
init(damus: DamusState, event: NostrEvent, has_action_bar: Bool) {
self.event = event
self.has_action_bar = has_action_bar
self.damus = damus
@@ -222,9 +222,9 @@ struct EventView_Previews: PreviewProvider {
*/
EventView(
damus: test_damus_state(),
event: test_event,
has_action_bar: true,
damus: test_damus_state()
has_action_bar: true
)
}
.padding()

View File

@@ -0,0 +1,112 @@
//
// MutedEventView.swift
// damus
//
// Created by William Casarin on 2023-01-27.
//
import SwiftUI
struct MutedEventView: View {
let damus_state: DamusState
let event: NostrEvent
let scroller: ScrollViewProxy?
let selected: Bool
@Binding var nav_target: String?
@Binding var navigating: Bool
@State var shown: Bool
@Environment(\.colorScheme) var colorScheme
init(damus_state: DamusState, event: NostrEvent, scroller: ScrollViewProxy?, nav_target: Binding<String?>, navigating: Binding<Bool>, selected: Bool) {
self.damus_state = damus_state
self.event = event
self.scroller = scroller
self.selected = selected
self._nav_target = nav_target
self._navigating = navigating
self._shown = State(initialValue: !should_hide_event(contacts: damus_state.contacts, ev: event))
}
var should_mute: Bool {
return should_hide_event(contacts: damus_state.contacts, ev: event)
}
var FillColor: Color {
colorScheme == .light ? Color("DamusLightGrey") : Color("DamusDarkGrey")
}
var MutedBox: some View {
ZStack {
RoundedRectangle(cornerRadius: 20)
.foregroundColor(FillColor)
HStack {
Text("Post from a user you've blocked")
Spacer()
Button(shown ? "Hide" : "Show") {
shown.toggle()
}
}
.padding(10)
}
}
var Event: some View {
Group {
if selected {
SelectedEventView(damus: damus_state, event: event)
} else {
EventView(damus: damus_state, event: event, has_action_bar: true)
.onTapGesture {
nav_target = event.id
navigating = true
}
.onAppear {
// TODO: find another solution to prevent layout shifting and layout blocking on large responses
scroller?.scrollTo("main", anchor: .bottom)
}
}
}
}
var body: some View {
Group {
if should_mute {
MutedBox
}
if shown {
Event
}
}
.onReceive(handle_notify(.new_mutes)) { notif in
guard let mutes = notif.object as? [String] else {
return
}
if mutes.contains(event.pubkey) {
shown = false
}
}
.onReceive(handle_notify(.new_unmutes)) { notif in
guard let unmutes = notif.object as? [String] else {
return
}
if unmutes.contains(event.pubkey) {
shown = true
}
}
}
}
struct MutedEventView_Previews: PreviewProvider {
@State static var nav_target: String? = nil
@State static var navigating: Bool = false
static var previews: some View {
MutedEventView(damus_state: test_damus_state(), event: test_event, scroller: nil, nav_target: $nav_target, navigating: $navigating, selected: false)
.frame(width: .infinity, height: 50)
}
}

View File

@@ -9,13 +9,13 @@ import SwiftUI
import LinkPresentation
struct NoteArtifacts {
let content: String
let content: AttributedString
let images: [URL]
let invoices: [Invoice]
let links: [URL]
static func just_content(_ content: String) -> NoteArtifacts {
NoteArtifacts(content: content, images: [], invoices: [], links: [])
NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: [])
}
}
@@ -24,19 +24,18 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -
var invoices: [Invoice] = []
var img_urls: [URL] = []
var link_urls: [URL] = []
let txt = blocks.reduce("") { str, block in
let txt: AttributedString = blocks.reduce("") { str, block in
switch block {
case .mention(let m):
return str + mention_str(m, profiles: profiles)
case .text(let txt):
return str + txt
return str + AttributedString(stringLiteral: txt)
case .hashtag(let htag):
return str + hashtag_str(htag)
case .invoice(let invoice):
invoices.append(invoice)
return str
case .url(let url):
// Handle Image URLs
if is_image_url(url) {
// Append Image
@@ -44,7 +43,7 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -
return str
} else {
link_urls.append(url)
return str + url.absoluteString
return str + url_str(url)
}
}
}
@@ -72,7 +71,7 @@ struct NoteContentView: View {
func MainContent() -> some View {
return VStack(alignment: .leading) {
Text(Markdown.parse(content: artifacts.content))
Text(artifacts.content)
.font(eventviewsize_to_font(size))
.fixedSize(horizontal: false, vertical: true)
@@ -163,20 +162,36 @@ struct NoteContentView: View {
}
}
func hashtag_str(_ htag: String) -> String {
return "[#\(htag)](nostr:t:\(htag))"
}
func hashtag_str(_ htag: String) -> AttributedString {
var attributedString = AttributedString(stringLiteral: "#\(htag)")
attributedString.link = URL(string: "nostr:t:\(htag)")
attributedString.foregroundColor = .purple
return attributedString
}
func mention_str(_ m: Mention, profiles: Profiles) -> String {
func url_str(_ url: URL) -> AttributedString {
var attributedString = AttributedString(stringLiteral: url.absoluteString)
attributedString.link = url
attributedString.foregroundColor = .purple
return attributedString
}
func mention_str(_ m: Mention, profiles: Profiles) -> AttributedString {
switch m.type {
case .pubkey:
let pk = m.ref.ref_id
let profile = profiles.lookup(id: pk)
let disp = Profile.displayName(profile: profile, pubkey: pk)
return "[@\(disp)](nostr:\(encode_pubkey_uri(m.ref)))"
var attributedString = AttributedString(stringLiteral: "@\(disp)")
attributedString.link = URL(string: "nostr:\(encode_pubkey_uri(m.ref))")
attributedString.foregroundColor = .purple
return attributedString
case .event:
let bevid = bech32_note_id(m.ref.ref_id) ?? m.ref.ref_id
return "[@\(abbrev_pubkey(bevid))](nostr:\(encode_event_id_uri(m.ref)))"
var attributedString = AttributedString(stringLiteral: "@\(abbrev_pubkey(bevid))")
attributedString.link = URL(string: "nostr:\(encode_event_id_uri(m.ref))")
attributedString.foregroundColor = .purple
return attributedString
}
}
@@ -185,7 +200,7 @@ struct NoteContentView_Previews: PreviewProvider {
static var previews: some View {
let state = test_damus_state()
let content = "hi there ¯\\_(ツ)_/¯ https://jb55.com/s/Oct12-150217.png 5739a762ef6124dd.jpg"
let artifacts = NoteArtifacts(content: content, images: [], invoices: [], links: [])
let artifacts = NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: [])
NoteContentView(privkey: "", event: NostrEvent(content: content, pubkey: "pk"), profiles: state.profiles, previews: PreviewCache(), show_images: true, artifacts: artifacts, size: .normal)
}
}

View File

@@ -16,10 +16,11 @@ let POST_PLACEHOLDER = NSLocalizedString("Type your post here...", comment: "Tex
struct PostView: View {
@State var post: String = ""
let replying_to: NostrEvent?
@FocusState var focus: Bool
let replying_to: NostrEvent?
let references: [ReferencedId]
let damus_state: DamusState
@Environment(\.presentationMode) var presentationMode
@@ -74,6 +75,7 @@ struct PostView: View {
TextEditor(text: $post)
.focused($focus)
.textInputAutocapitalization(.sentences)
if post.isEmpty {
Text(POST_PLACEHOLDER)
.padding(.top, 8)
@@ -82,6 +84,14 @@ struct PostView: View {
.allowsHitTesting(false)
}
}
// This if-block observes @ for tagging
if let searching = get_searching_string(post) {
VStack {
Spacer()
UserSearch(damus_state: damus_state, search: searching, post: $post)
}.zIndex(1)
}
}
.onAppear() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
@@ -92,3 +102,23 @@ struct PostView: View {
}
}
func get_searching_string(_ post: String) -> String? {
guard let last_word = post.components(separatedBy: .whitespacesAndNewlines).last else {
return nil
}
guard last_word.count >= 2 else {
return nil
}
guard last_word.first! == "@" else {
return nil
}
// don't include @npub... strings
guard last_word.count != 64 else {
return nil
}
return String(last_word.dropFirst())
}

View File

@@ -0,0 +1,87 @@
//
// UserAutocompletion.swift
// damus
//
// Created by William Casarin on 2023-01-28.
//
import SwiftUI
struct SearchedUser: Identifiable {
let petname: String?
let profile: Profile?
let pubkey: String
var id: String {
return pubkey
}
}
struct UserSearch: View {
let damus_state: DamusState
let search: String
@Binding var post: String
var users: [SearchedUser] {
guard let contacts = damus_state.contacts.event else {
return []
}
return search_users(profiles: damus_state.profiles, tags: contacts.tags, search: search)
}
var body: some View {
ScrollView {
LazyVStack {
ForEach(users) { user in
UserView(damus_state: damus_state, pubkey: user.pubkey)
.onTapGesture {
guard let pk = bech32_pubkey(user.pubkey) else {
return
}
post = post.replacingOccurrences(of: "@"+search, with: "@"+pk+" ")
}
}
}
}
}
}
struct UserSearch_Previews: PreviewProvider {
static let search: String = "jb55"
@State static var post: String = "some @jb55"
static var previews: some View {
UserSearch(damus_state: test_damus_state(), search: search, post: $post)
}
}
func search_users(profiles: Profiles, tags: [[String]], search: String) -> [SearchedUser] {
var seen_user = Set<String>()
return tags.reduce(into: Array<SearchedUser>()) { arr, tag in
guard tag.count >= 2 && tag[0] == "p" else {
return
}
let pubkey = tag[1]
guard !seen_user.contains(pubkey) else {
return
}
seen_user.insert(pubkey)
var petname: String? = nil
if tag.count >= 4 {
petname = tag[3]
}
let profile = profiles.lookup(id: pubkey)
guard ((petname?.hasPrefix(search) ?? false) || (profile?.name?.hasPrefix(search) ?? false)) else {
return
}
let searched_user = SearchedUser(petname: petname, profile: profile, pubkey: pubkey)
arr.append(searched_user)
}
}

View File

@@ -91,6 +91,7 @@ struct EditButton: View {
RoundedRectangle(cornerRadius: 24)
.stroke(borderColor(), lineWidth: 1)
}
.minimumScaleFactor(0.5)
}
}

View File

@@ -0,0 +1,90 @@
//
// QRCodeView.swift
// damus
//
// Created by eric on 1/27/23.
//
import SwiftUI
import CoreImage.CIFilterBuiltins
struct QRCodeView: View {
let damus_state: DamusState
@Environment(\.dismiss) var dismiss
@Environment(\.presentationMode) var presentationMode
var maybe_key: String? {
guard let key = bech32_pubkey(damus_state.pubkey) else {
return nil
}
return key
}
var body: some View {
ZStack(alignment: .topLeading) {
DamusGradient()
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "xmark")
.foregroundColor(.white)
.font(.subheadline)
.padding(.leading, 20)
}
.zIndex(1)
VStack(alignment: .center) {
Spacer()
if let key = maybe_key {
Image(uiImage: generateQRCode(pubkey: "nostr:" + key))
.interpolation(.none)
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
.padding()
Text(key)
.font(.headline)
.foregroundColor(Color(.white))
.padding()
}
Spacer()
}
}
.modifier(SwipeToDismissModifier(minDistance: nil, onDismiss: {
presentationMode.wrappedValue.dismiss()
}))
}
func generateQRCode(pubkey: String) -> UIImage {
let data = pubkey.data(using: String.Encoding.ascii)
let qrFilter = CIFilter(name: "CIQRCodeGenerator")
qrFilter?.setValue(data, forKey: "inputMessage")
let qrImage = qrFilter?.outputImage
let colorInvertFilter = CIFilter(name: "CIColorInvert")
colorInvertFilter?.setValue(qrImage, forKey: "inputImage")
let outputInvertedImage = colorInvertFilter?.outputImage
let maskToAlphaFilter = CIFilter(name: "CIMaskToAlpha")
maskToAlphaFilter?.setValue(outputInvertedImage, forKey: "inputImage")
let outputCIImage = maskToAlphaFilter?.outputImage
let context = CIContext()
let cgImage = context.createCGImage(outputCIImage!, from: outputCIImage!.extent)!
return UIImage(cgImage: cgImage)
}
}
struct QRCodeView_Previews: PreviewProvider {
static var previews: some View {
QRCodeView(damus_state: test_damus_state())
}
}

View File

@@ -45,9 +45,9 @@ struct ReplyView: View {
ParticipantsView(damus_state: damus, references: $references, originalReferences: $originalReferences)
}
ScrollView {
EventView(event: replying_to, has_action_bar: false, damus: damus)
EventView(damus: damus, event: replying_to, has_action_bar: false)
}
PostView(replying_to: replying_to, references: references)
PostView(replying_to: replying_to, references: references, damus_state: damus)
}
.onAppear {
references = gather_reply_ids(our_pubkey: damus.pubkey, from: replying_to)

View File

@@ -44,7 +44,7 @@ struct ReportView: View {
}
func do_send_report(type: ReportType) {
guard let ev = send_report(privkey: privkey, pool: pool, target: target, type: .spam) else {
guard let ev = send_report(privkey: privkey, pool: pool, target: target, type: type) else {
return
}

View File

@@ -87,6 +87,9 @@ struct SearchHomeView: View {
.onChange(of: search) { s in
print("search change 1")
}
.onReceive(handle_notify(.new_mutes)) { _ in
self.model.filter_muted()
}
.onAppear {
if model.events.isEmpty {
model.subscribe()

View File

@@ -14,6 +14,8 @@ struct SideMenuView: View {
@State var confirm_logout: Bool = false
@StateObject var user_settings = UserSettingsStore()
@State private var showQRCode = false
@Environment(\.colorScheme) var colorScheme
var sideBarWidth = min(UIScreen.main.bounds.size.width * 0.65, 400.0)
@@ -124,18 +126,33 @@ struct SideMenuView: View {
Spacer()
Button(action: {
//ConfigView(state: damus_state)
if damus_state.keypair.privkey == nil {
notify(.logout, ())
} else {
confirm_logout = true
HStack(alignment: .center) {
Button(action: {
//ConfigView(state: damus_state)
if damus_state.keypair.privkey == nil {
notify(.logout, ())
} else {
confirm_logout = true
}
}, label: {
Label(NSLocalizedString("Sign out", comment: "Sidebar menu label to sign out of the account."), systemImage: "pip.exit")
.font(.title3)
.foregroundColor(textColor())
})
Spacer()
Button(action: {
showQRCode.toggle()
}, label: {
Label(NSLocalizedString("", comment: "Sidebar menu label for accessing QRCode view"), systemImage: "qrcode")
.font(.title)
.foregroundColor(textColor())
.padding(.trailing, 20)
}).fullScreenCover(isPresented: $showQRCode) {
QRCodeView(damus_state: damus_state)
}
}, label: {
Label(NSLocalizedString("Sign out", comment: "Sidebar menu label to sign out of the account."), systemImage: "pip.exit")
.font(.title3)
.foregroundColor(textColor())
})
}
}
.padding(.top, 60)
.padding(.bottom, 40)

View File

@@ -255,15 +255,13 @@ struct ThreadV2View: View {
// MARK: - Parents events view
VStack {
ForEach(thread.parentEvents, id: \.id) { event in
EventView(event: event, has_action_bar: true, damus: damus)
.onTapGesture {
nav_target = event.id
navigating = true
}
.onAppear {
// TODO: find another solution to prevent layout shifting and layout blocking on large responses
reader.scrollTo("main", anchor: .bottom)
}
MutedEventView(damus_state: damus,
event: event,
scroller: reader,
nav_target: $nav_target,
navigating: $navigating,
selected: false
)
}
}.background(GeometryReader { geometry in
// get the height and width of the EventView view
@@ -278,22 +276,25 @@ struct ThreadV2View: View {
})
// MARK: - Actual event view
SelectedEventView(
damus: damus,
event: thread.current
MutedEventView(
damus_state: damus,
event: thread.current,
scroller: reader,
nav_target: $nav_target,
navigating: $navigating,
selected: true
).id("main")
// MARK: - Responses of the actual event view
ForEach(thread.childEvents, id: \.id) { event in
EventView(
MutedEventView(
damus_state: damus,
event: event,
has_action_bar: true,
damus: damus
scroller: reader,
nav_target: $nav_target,
navigating: $navigating,
selected: false
)
.onTapGesture {
nav_target = event.id
navigating = true
}
}
}.padding()
}.navigationBarTitle(NSLocalizedString("Thread", comment: "Navigation bar title for note thread."))

View File

@@ -39,11 +39,7 @@ struct InnerTimelineView: View {
EmptyTimelineView()
} else {
ForEach(events.filter(filter), id: \.id) { (ev: NostrEvent) in
//let tm = ThreadModel(event: inner_event_or_self(ev: ev), damus_state: damus)
//let is_chatroom = should_show_chatroom(ev)
//let tv = ThreadView(thread: tm, damus: damus, is_chatroom: is_chatroom)
EventView(event: ev, has_action_bar: true, damus: damus)
EventView(damus: damus, event: ev, has_action_bar: true)
.onTapGesture {
nav_target = ev
navigating = true

View File

@@ -1,9 +0,0 @@
/* Bundle display name */
"CFBundleDisplayName" = "Damus";
/* Bundle name */
"CFBundleName" = "damus";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "Dai il permesso a Damus di accedere alle tue Foto per salvare immagini";

View File

@@ -1,503 +0,0 @@
/* Blank space to separate profile picture from profile editor form. */
" " = "61b6edf1108e6f396680a33b02486a70_tr";
/* Description of how the nip05 identifier would be used for verification. */
"'%@' at '%@' will be used for verification" = "'%@' at '%@' sarà usato per la verifica";
/* Description of why the nip05 identifier is invalid. */
"'%@' is an invalid nip05 identifier. It should look like an email." = "'%@' non è valido. Dovrebbe essere simile ad un indirizzo email.";
/* Navigation bar title for view that shows who is following a user. */
"(Profile.displayName(profile: profile, pubkey: whos))'s Followers" = "Seguaci di (Profile.displayName(profile: profile, pubkey: whos))'";
/* Navigation bar title for view that shows who a user is following. */
"(who) following" = "(who) segui già";
/* Prefix character to username. */
"@" = "@";
/* Amount of time that has passed since reply quote event occurred.
Abbreviated version of a nostr public key. */
"%@" = "%@";
/* Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.
Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'. */
"%@ %@" = "%@ %@";
/* Explanation of what is done to keep personally identifiable information private. There is a heading that precedes this explanation which is a variable to this string. */
"%@. Creating an account doesn't require a phone number, email or name. Get started right away with zero friction." = "%@. Per creare un account non hai bisogno di un numero di telefono, un indirizzo email o del tuo nome. Inizia ora senza impegni.";
/* Explanation of what is done to keep private data encrypted. There is a heading that precedes this explanation which is a variable to this string. */
"%@. End-to-End encrypted private messaging. Keep Big Tech out of your DMs" = "%@. I messaggi sono criptati utilizzando la crittografia end-to-end. Mantieni i colossi della tecnologia lontani dai tuoi messaggi";
/* Explanation of what can be done by users to earn money. There is a heading that precedes this explanation which is a variable to this string. */
"%@. Tip your friend's posts and stack sats with Bitcoin⚡, the native currency of the internet." = "%@. Paga i tuoi amici e accumula sats con Bitcoin⚡, la moneta di internet.";
/* Number of reposts.
Number of profiles a user is following. */
"%lld" = "%lld";
/* Fraction of how many of the user's relay servers that are operational. */
"%lld/%lld" = "%lld/%lld";
/* Placeholder for event mention. */
"< e >" = "< e >";
/* Label to prompt for about text entry for user to describe about themself. */
"About" = "Informazioni";
/* Label for About Me section of user profile form. */
"About Me" = "Io";
/* Placeholder text for About Me description. */
"Absolute Boss" = "Capo supremo";
/* Label to indicate the public ID of the account. */
"Account ID" = "ID dell'account";
/* Button to add recommended relay server.
Button to confirm adding user inputted relay. */
"Add" = "Aggiungi";
/* Button label to re-add all original participants as profiles to reply to in a note */
"Add all" = "Aggiungi tutto";
/* Label for section for adding a relay server. */
"Add Relay" = "Aggiungi relè";
/* Any amount of sats */
"Any" = "Qualsiasi";
/* Alert message to ask if user wants to repost a post. */
"Are you sure you want to repost this?" = "Sei sicuro di voler segnalare questo post?";
/* Label for Banner Image section of user profile form. */
"Banner Image" = "Immagine banner";
/* Reminder to user that they should save their account information. */
"Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus." = "Prima di iniziare, dovrai salvare le informazioni del tuo account altrimenti non sarai in grado di accedere in futuro se dovessi disinstallare Damus.";
/* Dropdown option label for Lightning wallet, Bitcoin Beach. */
"Bitcoin Beach" = "Bitcoin Beach";
/* Label for Bitcoin Lightning Tips section of user profile form. */
"Bitcoin Lightning Tips" = "Mancia con Bitcoin Lightning";
/* Dropdown option label for Lightning wallet, Blixt Wallet */
"Blixt Wallet" = "Blixt Wallet";
/* Dropdown option label for Lightning wallet, Blue Wallet. */
"Blue Wallet" = "Blue Wallet";
/* Dropdown option label for Lightning wallet, Breez. */
"Breez" = "Breez";
/* Context menu option for broadcasting the user's note to all of the user's connected relay servers. */
"Broadcast" = "Trasmetti";
/* Button to cancel out of posting a note.
Button to cancel out of reposting a post.
Button to cancel out of view adding user inputted relay.
Cancel out of logging out the user. */
"Cancel" = "Annulla";
/* Dropdown option label for Lightning wallet, Cash App. */
"Cash App" = "Cash App";
/* Navigation bar title for Chatroom view. */
"Chat" = "Chat";
/* Button for clearing cached data. */
"Clear" = "Cancella";
/* Section title for clearing cached data. */
"Clear Cache" = "Cancella cache";
/* Label indicating that a user's key was copied. */
"Copied" = "Copiato";
/* Button to copy a relay server address. */
"Copy" = "Copia";
/* Context menu option for copying the ID of the account that created the note. */
"Copy Account ID" = "Copia ID dell'Account";
/* Context menu option to copy an image into clipboard.
Context menu option to copy an image to clipboard. */
"Copy Image" = "Copia Immagine";
/* Context menu option to copy the URL of an image into clipboard. */
"Copy Image URL" = "Copia URL dell'Immagine";
/* Title of section for copying a Lightning invoice identifier. */
"Copy invoice" = "Copia fattura";
/* Context menu option for copying a user's Lightning URL. */
"Copy LNURL" = "Copia LNURL";
/* Context menu option for copying the ID of the note. */
"Copy Note ID" = "Copia ID della Nota";
/* Context menu option for copying the JSON text from the note. */
"Copy Note JSON" = "Copia JSON della Nota";
/* Context menu option for copying the text from an note. */
"Copy Text" = "Copia Testo";
/* Context menu option for copying the ID of the user who created the note. */
"Copy User ID" = "Copia ID dell'Utente";
/* Button to create account. */
"Create" = "Crea";
/* Button to create an account. */
"Create Account" = "Crea Account";
/* Example description about Bitcoin creator(s), Satoshi Nakamoto. */
"Creator(s) of Bitcoin. Absolute legend." = "Creatore/i di Bitcoin. Leggenda assoluta";
/* Name of the app, shown on the first screen when user is not logged in. */
"Damus" = "Damus";
/* Button to pay a Lightning invoice with the user's default Lightning wallet. */
"Default Wallet" = "Portafoglio Principale";
/* Button to delete a relay server that the user connects to. */
"Delete" = "Cancella";
/* Button to dismiss a text field alert. */
"Dismiss" = "Lascia stare";
/* Label to prompt display name entry. */
"Display Name" = "Nome visualizzato";
/* DM selector for seeing either DMs or message requests, which are messages that have not been responded to yet. DM is the English abbreviation for Direct Message. */
"DM Type" = "Tipo DM";
/* Navigation title for DMs view, where DM is the English abbreviation for Direct Message.
Navigation title for view of DMs, where DM is an English abbreviation for Direct Message. */
"DMs" = "DM";
/* Button to dismiss wallet selection view for paying Lightning invoice. */
"Done" = "Finito";
/* Heading indicating that this application allows users to earn money. */
"Earn Money" = "Guadagna Soldi";
/* Button to edit user's profile. */
"Edit" = "Modifica";
/* Text indicating that the view is used for editing which participants are replied to in a note. */
"Edit participants" = "Modifica partecipanti";
/* Heading indicating that this application keeps private messaging end-to-end encrypted. */
"Encrypted" = "Criptato";
/* Prompt for user to enter an account key to login. */
"Enter your account key to login:" = "Inserisci la chiave del tuo account per accedere";
/* Error message indicating why saving keys failed. */
"Error: %@" = "Errore: %@";
/* Filter state for seeing either only posts, or posts & replies. */
"Filter State" = "Filtra";
/* Button to follow a user. */
"Follow" = "Segui";
/* Label describing followers of a user. */
"Followers" = "Seguaci";
/* Text to indicate that the button next to it is in a state that indicates that it is in the process of following a profile.
Part of a larger sentence to describe how many profiles a user is following. */
"Following" = "Seguiti";
/* Label to indicate that the user is in the process of following another user. */
"Following..." = "Segui già...";
/* Text to indicate that button next to it is in a state that will follow a profile when tapped. */
"Follows" = "Segui";
/* Navigation bar title for Global view where posts from all connected relay servers appear. */
"Global" = "Globale";
/* Navigation link to go to post referenced by hex code. */
"Goto post %@" = "Vai al post %@";
/* Navigation link to go to profile. */
"Goto profile %@" = "Vai al profilo %@";
/* Navigation bar title for Home view where posts and replies appear from those who the user is following. */
"Home" = "Home";
/* Placeholder example text for profile picture URL. */
"https://example.com/pic.jpg" = "https://esempio.com/foto.jpg";
/* Placeholder example text for website URL for user profile. */
"https://jb55.com" = "https://jb55.com";
/* Error message indicating that an invalid account key was entered for login. */
"Invalid key" = "Chiave non valida";
/* Placeholder example text for identifier used for NIP-05 verification. */
"jb55@jb55.com" = "jb55@jb55.com";
/* Moves the post button to the left side of the screen */
"Left Handed" = "Mancino";
/* Button to complete account creation and start using the app. */
"Let's go!" = "Andiamo!";
/* Placeholder text for entry of Lightning Address or LNURL. */
"Lightning Address or LNURL" = "Indirizzo Lightning o LNURL";
/* Indicates that the view is for paying a Lightning invoice. */
"Lightning Invoice" = "Fattura Lightning";
/* Dropdown option label for Lightning wallet, LNLink. */
"LNLink" = "LNLink";
/* Dropdown option label for system default for Lightning wallet. */
"Local default" = "Predefinito";
/* Button to log into account.
Button to log into an account. */
"Login" = "Entra";
/* Alert for logging out the user.
Button for logging out the user.
Button to logout the user. */
"Logout" = "Esci";
/* Reminder message in alert to get customer to verify that their private security account key is saved saved before logging out. */
"Make sure your nsec account key is saved before you logout or you will lose access to this account" = "Assicurati di aver salvato la chiave privata (nSEC) prima di uscire o perderai l'accesso a questo account";
/* Dropdown option label for Lightning wallet, Muun. */
"Muun" = "Muun";
/* Label for NIP-05 Verification section of user profile form. */
"NIP-05 Verification" = "Verifica NIP-05";
/* No search results. */
"none" = "Nessun risultato";
/* Indicates that there are no notes in the timeline to view. */
"Nothing to see here. Check back later!" = "Niente da vedere qui. Controlla dopo!";
/* Navigation title for notifications. */
"Notifications" = "Notifiche";
/* String indicating that a given timestamp just occurred */
"now" = "ora";
/* Prompt for user to enter in an account key to login. This text shows the characters the key could start with if it was a private key. */
"nsec1..." = "nsec1...";
/* Label indicating that a form input is optional. */
"optional" = "opzione1";
/* Button to pay a Lightning invoice. */
"Pay" = "Paga";
/* Navigation bar title for view to pay Lightning invoice. */
"Pay the Lightning invoice" = "Paga la fattura Lightning";
/* Dropdown option label for Lightning wallet, Phoenix. */
"Phoenix" = "Phoenix";
/* Button to post a note. */
"Post" = "Post";
/* Label for filter for seeing only posts (instead of posts and replies). */
"Posts" = "Post";
/* Label for filter for seeing posts and replies (instead of only posts). */
"Posts & Replies" = "Post & Risposte";
/* Heading indicating that this application keeps personally identifiable information private. A sentence describing what is done to keep data private comes after this heading. */
"Private" = "Privato";
/* Title of the secure field that holds the user's private key. */
"Private Key" = "Chiave Privata";
/* Sidebar menu label for Profile view. */
"Profile" = "Profilo1";
/* Label for Profile Picture section of user profile form. */
"Profile Picture" = "Foto Profilo";
/* Section title for the user's public account ID. */
"Public Account ID" = "ID Pubblico dell'Account";
/* Label indicating that the text is a user's public account key. */
"Public key" = "Chiave Pubblica";
/* Label indicating that the text is a user's public account key. */
"Public Key" = "Chiave Pubblica";
/* Prompt to ask user if the key they entered is a public key. */
"Public Key?" = "È la chiave pubblica?";
/* Navigation bar title for Reactions view. */
"Reactions" = "Reazioni";
/* Section title for recommend relay servers that could be added as part of configuration */
"Recommended Relays" = "Relè consigliati";
/* Text field for relay server. Used for testing purposes. */
"Relay" = "Relè";
/* Sidebar menu label for Relay servers view */
"Relays" = "Relè";
/* Button label to remove all participants from a note reply. */
"Remove all" = "Rimuovi tutto";
/* Label to indicate that the user is replying to themself. */
"Reply to self" = "Rispondi a te stesso";
/* Label to indicate that the user is replying to 2 users. */
"Replying to %@ & %@" = "Rispondi a %1$@ e %2$@";
/* Indicating that the user is replying to the following listed people. */
"Replying to:" = "Rispondi a:";
/* Button to confirm reposting a post.
Title of alert for confirming to repost a post. */
"Repost" = "Reposta";
/* Text indicating that the post was reposted (i.e. re-shared). */
"Reposted" = "Repostato";
/* Picker option for DM selector for seeing only message requests (DMs that someone else sent the user which has not been responded to yet). DM is the English abbreviation for Direct Message. */
"Requests" = "Richiesta";
/* Section title for resetting the user */
"Reset" = "Ricomincia";
/* Button to retry completing account creation after an error occurred. */
"Retry" = "Riprova";
/* Dropdown option label for Lightning wallet, River */
"River" = "River";
/* Example username of Bitcoin creator(s), Satoshi Nakamoto. */
"satoshi" = "satoshi";
/* Name of Bitcoin creator(s). */
"Satoshi Nakamoto" = "Satoshi Nakamoto";
/* Button for saving profile. */
"Save" = "Salva";
/* Context menu option to save an image. */
"Save Image" = "Salva Immagine";
/* Navigation link to search hashtag. */
"Search hashtag: #%@" = "Cerca hashtag: #%@";
/* Placeholder text to prompt entry of search query. */
"Search..." = "Cerca...";
/* Section title for user's secret account login key. */
"Secret Account Login Key" = "Chiave login segreta dell'Account";
/* Title of section for selecting a Lightning wallet to pay a Lightning invoice. */
"Select a Lightning wallet" = "Seleziona un portafoglio Lightning";
/* Prompt selection of user's default wallet */
"Select default wallet" = "Seleziona un wallet predefinito";
/* Text prompt for user to send a message to the other user. */
"Send a message to start the conversation..." = "Invia un messaggio e inizia la conversazione...";
/* Navigation title for Settings view.
Sidebar menu label for accessing the app settings */
"Settings" = "Impostazioni";
/* Button to share an image. */
"Share" = "Condividi";
/* Toggle to show or hide user's secret account login key. */
"Show" = "Mostra";
/* Toggle to show or hide selection of wallet. */
"Show wallet selector" = "Mostra wallet disponibili";
/* Sidebar menu label to sign out of the account. */
"Sign out" = "Esci";
/* Dropdown option label for Lightning wallet, Strike. */
"Strike" = "Strike";
/* Warning that the inputted account key is a public key and the result of what happens because of it. */
"This is a public key, you will not be able to make posts or interact in any way. This is used for viewing accounts from their perspective." = "Questa è una chiave pubblica, non potrai postare o interagire in alcun modo. Puoi utilizzarla solo per vedere gli account";
/* Warning that the inputted account key for login is an old-style and asking user to verify if it is a public key. */
"This is an old-style nostr key. We're not sure if it's a pubkey or private key. Please toggle the button below if this a public key." = "Questa è una chiave di vecchio tipo. Non siamo sicuri se si tratti di una chiave pubblica o privata. Utilizza il pulsante sottostante se si tratta di una chiave pubblica.";
/* Label to describe that a public key is the user's account ID and what they can do with it. */
"This is your account ID, you can give this to your friends so that they can follow you. Click to copy." = "Questo è l'ID del tuo account. Condividilo con i tuoi amici per farti seguire. Clicca per copiare";
/* Label to describe that a private key is the user's secret account key and what they should do with it. */
"This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!" = "Questa è la tua chiave privata. Ti serve ad accedere al tuo account. Non condividerla con nessuno! Salvala in un gestore password e tienila al sicuro";
/* Navigation bar title for note thread.
Navigation bar title for threaded event detail view. */
"Thread" = "Thread";
/* Text box prompt to ask user to type their post. */
"Type your post here..." = "Scrivi il tuo post qui...";
/* Non-breaking space character to fill in blank space next to event action button icons. */
"u{00A0}" = "u{00A0}";
/* Button to unfollow a user. */
"Unfollow" = "Smetti di seguire";
/* Text to indicate that the button next to it is in a state that indicates that it is in the process of unfollowing a profile. */
"Unfollowing" = "Smetti di seguire";
/* Label to indicate that the user is in the process of unfollowing another user. */
"Unfollowing..." = "Togliendo il segui...";
/* Text to indicate that the button next to it is in a state that will unfollow a profile when tapped. */
"Unfollows" = "Smetti di seguire";
/* Label for Username section of user profile form.
Label to prompt username entry. */
"Username" = "Nome utente";
/* Sidebar menu label for Wallet view. */
"Wallet" = "Portafoglio";
/* Dropdown option label for Lightning wallet, Wallet Of Satoshi. */
"Wallet Of Satoshi" = "Wallet Of Satoshi";
/* Section title for selection of wallet. */
"Wallet Selector" = "Seleziona un portafoglio";
/* Label for Website section of user profile form. */
"Website" = "Sito web";
/* Welcoming message to the reader. The variable is 'you', the reader. */
"Welcome to the social network %@ control." = "Benvenuto nel social network %@ controlla.";
/* Text to welcome user. */
"Welcome, %@!" = "Benvenuto, %@!";
/* Placeholder example for relay server address. */
"wss://some.relay.com" = "wss://un.relè.com";
/* You, in this context, is the person who controls their own social network. You is used in the context of a larger sentence that welcomes the reader to the social network that they control themself. */
"you" = "tu";
/* Label for Your Name section of user profile form. */
"Your Name" = "Nome";
/* Dropdown option label for Lightning wallet, Zebedee. */
"Zebedee" = "Zebedee";
/* Dropdown option label for Lightning wallet, Zeus LN. */
"Zeus LN" = "Zeus LN";

View File

@@ -1,154 +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>NOTES</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%d other note</string>
<key>other</key>
<string>%d other notes</string>
</dict>
<key>NSStringLocalizedFormatKey</key>
<string>··· %#@NOTES@ ···</string>
</dict>
<key>followers_count</key>
<dict>
<key>FOLLOWERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Follower</string>
<key>other</key>
<string>Followers</string>
</dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@FOLLOWERS@</string>
</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>Reaction</string>
<key>other</key>
<string>Reactions</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>Relay</string>
<key>other</key>
<string>Relays</string>
</dict>
</dict>
<key>replying_to_one_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>Replying to %@%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string> &amp; %d other</string>
<key>other</key>
<string> &amp; %d others</string>
<key>zero</key>
<string></string>
</dict>
</dict>
<key>replying_to_two_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>Replying to %@, %@%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string> &amp; %d other</string>
<key>other</key>
<string> &amp; %d others</string>
<key>zero</key>
<string></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>Repost</string>
<key>other</key>
<string>Reposts</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>other</key>
<string>%2$@ sats</string>
</dict>
</dict>
<key>tips_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@TIPS@</string>
<key>TIPS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Tip</string>
<key>other</key>
<string>Tips</string>
</dict>
</dict>
</dict>
</plist>

View File

@@ -79,6 +79,15 @@ class damusTests: XCTestCase {
XCTAssertEqual(parsed[1].is_url?.absoluteString, "HTTPS://jb55.COM")
}
func testBech32Url() {
let parsed = decode_nostr_uri("nostr:npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s")
let hexpk = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"
let expected: NostrLink = .ref(ReferencedId(ref_id: hexpk, relay_id: nil, key: "p"))
XCTAssertEqual(parsed, expected)
}
func testParseUrl() {
let parsed = parse_mentions(content: "a https://jb55.com b", tags: [])